Cloud Firestore vector search
The Firebase plugin provides integration with Firebase services for Genkit applications. It enables you to use Firebase Firestore as a vector database for retrieval-augmented generation (RAG) applications by defining retrievers.
Prerequisites
Section titled “Prerequisites”This plugin requires:
- A Firebase project - Create one at the Firebase Console
- Firestore database enabled in your Firebase project
- Firebase credentials configured for your application
Firebase Setup
Section titled “Firebase Setup”- Create a Firebase project at Firebase Console
- Enable Firestore in your project:
- Go to Firestore Database in the Firebase console
- Click “Create database”
- Choose your security rules and location
- Set up authentication using one of these methods:
- For local development:
firebase loginandfirebase use <project-id> - For production: Service account key or Application Default Credentials
- For local development:
Configuration
Section titled “Configuration”Basic Configuration
Section titled “Basic Configuration”To use this plugin, import the firebase package and initialize it with your project:
import "github.com/firebase/genkit/go/plugins/firebase"// Option 1: Using project ID (recommended)firebasePlugin := &firebase.Firebase{ ProjectId: "your-firebase-project-id",}
g := genkit.Init(context.Background(), genkit.WithPlugins(firebasePlugin))Environment Variable Configuration
Section titled “Environment Variable Configuration”You can also configure the project ID using environment variables:
export FIREBASE_PROJECT_ID=your-firebase-project-id// Plugin will automatically use FIREBASE_PROJECT_ID environment variablefirebasePlugin := &firebase.Firebase{}g := genkit.Init(context.Background(), genkit.WithPlugins(firebasePlugin))Advanced Configuration
Section titled “Advanced Configuration”For advanced use cases, you can provide a pre-configured Firebase app:
import firebasev4 "firebase.google.com/go/v4"
// Create Firebase app with custom configurationapp, err := firebasev4.NewApp(ctx, &firebasev4.Config{ ProjectID: "your-project-id", // Additional Firebase configuration options})if err != nil { log.Fatal(err)}
firebasePlugin := &firebase.Firebase{ App: app,}Defining Firestore Retrievers
Section titled “Defining Firestore Retrievers”The primary use case for the Firebase plugin is creating retrievers for RAG applications:
package main
import ( "context" "log" "os"
"cloud.google.com/go/firestore" "github.com/firebase/genkit/go/genkit" "github.com/firebase/genkit/go/plugins/compat_oai/openai" "github.com/firebase/genkit/go/plugins/firebase")
func main() { ctx := context.Background()
firebasePlugin := &firebase.Firebase{ProjectId: "your-firebase-project-id"} openaiPlugin := &openai.OpenAI{APIKey: os.Getenv("OPENAI_API_KEY")}
g := genkit.Init(ctx, genkit.WithPlugins(firebasePlugin, openaiPlugin))
retriever, err := firebase.DefineRetriever(ctx, g, firebase.RetrieverOptions{ Name: "my-documents", Collection: "documents", VectorField: "embedding", ContentField: "content", MetadataFields: []string{"title", "category"}, Embedder: openaiPlugin.Embedder(g, "text-embedding-3-small"), Limit: 10, DistanceMeasure: firestore.DistanceMeasureCosine, }) if err != nil { log.Fatal(err) }
_ = retriever}DistanceMeasure is firestore.DistanceMeasure from cloud.google.com/go/firestore, not from
firebase.google.com/go/v4. Set it explicitly: the plugin passes the field straight to Firestore
without substituting a default, so leaving it at its zero value sends an unspecified measure. It must
also match the measure the vector index was created with.
Using Retrievers in RAG Workflows
Section titled “Using Retrievers in RAG Workflows”Once defined, you can use the retriever in your RAG workflows:
// Retrieve relevant documentsresults, err := genkit.Retrieve(ctx, g, ai.WithRetriever(retriever), ai.WithTextDocs("What is machine learning?"),)if err != nil { log.Fatal(err)}
// Use retrieved documents in generationvar contextDocs []stringfor _, doc := range results.Documents { contextDocs = append(contextDocs, doc.Content[0].Text)}
context := strings.Join(contextDocs, "\n\n")resp, err := genkit.Generate(ctx, g, ai.WithPrompt(fmt.Sprintf("Context: %s\n\nQuestion: %s", context, "What is machine learning?")),)Complete RAG Example
Section titled “Complete RAG Example”Here’s a complete example showing how to set up a RAG system with Firebase:
package main
import ( "context" "fmt" "log" "strings"
"github.com/firebase/genkit/go/ai" "github.com/firebase/genkit/go/genkit" "github.com/firebase/genkit/go/plugins/firebase" "github.com/firebase/genkit/go/plugins/compat_oai/openai")
func main() { ctx := context.Background()
// Initialize plugins firebasePlugin := &firebase.Firebase{ ProjectId: "my-firebase-project", }
openaiPlugin := &openai.OpenAI{ APIKey: "your-openai-api-key", }
g := genkit.Init(ctx, genkit.WithPlugins(firebasePlugin, openaiPlugin))
// Define retriever for knowledge base retriever, err := firebase.DefineRetriever(ctx, g, firebase.RetrieverOptions{ Name: "knowledge-base", Collection: "documents", VectorField: "embedding", Embedder: openaiPlugin.Embedder(g, "text-embedding-3-small"), Limit: 5, }) if err != nil { log.Fatal(err) }
// RAG query function query := "How does machine learning work?"
// Step 1: Retrieve relevant documents retrievalResults, err := genkit.Retrieve(ctx, g, ai.WithRetriever(retriever), ai.WithTextDocs(query), ) if err != nil { log.Fatal(err) }
// Step 2: Prepare context from retrieved documents var contextParts []string for _, doc := range retrievalResults.Documents { contextParts = append(contextParts, doc.Content[0].Text) } context := strings.Join(contextParts, "\n\n")
// Step 3: Generate answer with context model := openaiPlugin.Model(g, "gpt-4o") response, err := genkit.Generate(ctx, g, ai.WithModel(model), ai.WithPrompt(fmt.Sprintf(`Based on the following context, answer the question:
Context:%s
Question: %s
Answer:`, context, query)), ) if err != nil { log.Fatal(err) }
fmt.Printf("Answer: %s\n", response.Text())}Firestore Data Structure
Section titled “Firestore Data Structure”Document Storage Format
Section titled “Document Storage Format”Your Firestore documents should follow this structure for optimal retrieval:
{ "content": "Your document text content here...", "embedding": [0.1, -0.2, 0.3, ...], "metadata": { "title": "Document Title", "author": "Author Name", "category": "Technology", "timestamp": "2024-01-15T10:30:00Z" }}The embedding field must hold a Firestore vector value, not a plain array of numbers. In Go,
write it with firestore.Vector32(...) from cloud.google.com/go/firestore.
Create the vector index
Section titled “Create the vector index”The retriever runs a Firestore FindNearest query against VectorField. That query needs a KNN
vector index on the field, and Firestore does not create one for you. Run this before you index any
documents:
gcloud firestore indexes composite create \ --project=your-firebase-project-id \ --collection-group=documents \ --query-scope=COLLECTION \ --field-config=field-path=embedding,vector-config='{"dimension":"1536","flat":"{}"}'dimension must equal the output size of the embedder you configure. text-embedding-3-small
produces 1536 values; change the number if you use a different model. Create the index with the same
distance measure you pass in RetrieverOptions.DistanceMeasure.
If the index is missing, the first genkit.Retrieve call fails with a FAILED_PRECONDITION error
from Firestore whose message contains a ready-to-run creation command.
Indexing Documents
Section titled “Indexing Documents”To add documents to your Firestore collection with embeddings:
// Example of adding documents with embeddingsembedder := openaiPlugin.Embedder(g, "text-embedding-3-small")
firestoreClient, err := firebasePlugin.Firestore(ctx)if err != nil { log.Fatal(err)}
documents := []struct { Content string Metadata map[string]any}{ { Content: "Machine learning is a subset of artificial intelligence...", Metadata: map[string]any{ "title": "Introduction to ML", "category": "Technology", }, }, // More documents...}
for _, doc := range documents { // Generate embedding embeddingResp, err := genkit.Embed(ctx, g, ai.WithEmbedder(embedder), ai.WithTextDocs(doc.Content), ) if err != nil { log.Fatal(err) }
// Store in Firestore. The embedding must be written as a Firestore vector // value; a bare []float32 is stored as a plain array and FindNearest will // never match it. _, err = firestoreClient.Collection("documents").NewDoc().Set(ctx, map[string]any{ "content": doc.Content, "embedding": firestore.Vector32(embeddingResp.Embeddings[0].Embedding), "metadata": doc.Metadata, }) if err != nil { log.Fatal(err) }}This snippet needs cloud.google.com/go/firestore in the import block shown above.
Configuration Options
Section titled “Configuration Options”Firebase struct
Section titled “Firebase struct”type Firebase struct { // ProjectId is your Firebase project ID // If empty, uses FIREBASE_PROJECT_ID environment variable ProjectId string
// App is a pre-configured Firebase app instance // Use either ProjectId or App, not both App *firebasev4.App}When you set ProjectId, the plugin builds the Firebase app during genkit.Init and stores it in
App, so App is safe to read afterwards. Prefer firebasePlugin.Firestore(ctx) over
firebasePlugin.App.Firestore(ctx): it caches one client and shares it with the retrievers.
RetrieverOptions
Section titled “RetrieverOptions”type RetrieverOptions struct { // Name is a unique identifier for the retriever Name string
// Label is an optional label for display in the Developer UI Label string
// Collection is the Firestore collection name containing documents Collection string
// Embedder is the embedder instance to use for query vectorization Embedder ai.Embedder
// VectorField is the field name containing the embedding vectors VectorField string
// MetadataFields is a list of metadata fields to retrieve MetadataFields []string
// ContentField is the field name containing the document content ContentField string
// Limit is the maximum number of documents to retrieve Limit int
// DistanceMeasure is the distance measure for vector similarity DistanceMeasure firestore.DistanceMeasure}firestore here is cloud.google.com/go/firestore. The legal values are
firestore.DistanceMeasureEuclidean, firestore.DistanceMeasureCosine and
firestore.DistanceMeasureDotProduct. The plugin applies no default, so the zero value reaches
FindNearest as an unspecified measure. Set the same measure the vector index was created with.
Authentication
Section titled “Authentication”Local Development
Section titled “Local Development”For local development, use the Firebase CLI:
# Install Firebase CLInpm install -g firebase-tools
# Login and set projectfirebase loginfirebase use your-project-idProduction Deployment
Section titled “Production Deployment”For production, use one of these authentication methods:
Service Account Key
Section titled “Service Account Key”import "google.golang.org/api/option"
app, err := firebasev4.NewApp(ctx, &firebasev4.Config{ ProjectID: "your-project-id",}, option.WithCredentialsFile("path/to/serviceAccountKey.json"))Application Default Credentials
Section titled “Application Default Credentials”Set the environment variable:
export GOOGLE_APPLICATION_CREDENTIALS="path/to/serviceAccountKey.json"Or use the metadata server on Google Cloud Platform.
Error Handling
Section titled “Error Handling”Genkit classifies its own failures with the sentinels in
github.com/firebase/genkit/go/core/status and you branch on them with errors.Is. Never branch on
the text of err.Error(). See Error types for the full set.
import ( "errors" "log"
"github.com/firebase/genkit/go/ai" "github.com/firebase/genkit/go/core/status" "github.com/firebase/genkit/go/genkit" "github.com/firebase/genkit/go/plugins/firebase")
retriever, err := firebase.DefineRetriever(ctx, g, options)if err != nil { // The plugin returns unclassified errors here. The two setup mistakes it // reports are "plugin not found" (the plugin never reached genkit.Init) // and a Firestore client that could not be built from your credentials. log.Fatalf("firebase.DefineRetriever: %v", err)}
// Handle retrieval errorsresults, err := genkit.Retrieve(ctx, g, ai.WithRetriever(retriever), ai.WithTextDocs(query),)if err != nil { if errors.Is(err, status.ErrInvalidArgument) { log.Fatalf("bad retrieval request: %v", err) } log.Printf("Retrieval failed: %v", err) // Implement fallback logic}The message still names the cause. The three you will hit:
| Cause | What the message says | Retry? |
|---|---|---|
No vector index on VectorField | FailedPrecondition, with a gcloud command to create the index | No. Create the index. |
| Credentials cannot read the collection | PermissionDenied | No. Fix IAM or the Firestore rules. |
| Query vector length differs from the index dimension | InvalidArgument | No. Re-index with the embedder you query with. |
Those three are configuration errors and fail the same way every time. DeadlineExceeded and
Unavailable from Firestore are the transient ones, and are worth a bounded retry with backoff.
Best Practices
Section titled “Best Practices”Performance Optimization
Section titled “Performance Optimization”- Batch Operations: Use Firestore batch writes when adding multiple documents
- Index Configuration: Create a KNN vector index on every field you query. See Create the vector index
- Caching: Implement caching for frequently accessed documents
- Pagination: Use pagination for large result sets
Security
Section titled “Security”- Firestore Rules: Configure proper security rules for your collections
- API Keys: Never expose Firebase configuration in client-side code
- Authentication: Implement proper user authentication for sensitive data
Cost Management
Section titled “Cost Management”- Document Size: Keep documents reasonably sized to minimize read costs
- Query Optimization: Design efficient queries to reduce operation costs
- Storage Management: Regularly clean up unused documents and embeddings
Integration Examples
Section titled “Integration Examples”With Multiple Embedders
Section titled “With Multiple Embedders”// Use different embedders for different types of contenttechnicalRetriever, err := firebase.DefineRetriever(ctx, g, firebase.RetrieverOptions{ Name: "technical-docs", Collection: "technical_documents", VectorField: "embedding", // More accurate for technical content Embedder: openaiPlugin.Embedder(g, "text-embedding-3-large"), Limit: 5,})if err != nil { log.Fatal(err)}
generalRetriever, err := firebase.DefineRetriever(ctx, g, firebase.RetrieverOptions{ Name: "general-knowledge", Collection: "general_documents", VectorField: "embedding", // Faster for general content Embedder: openaiPlugin.Embedder(g, "text-embedding-3-small"), Limit: 10,})if err != nil { log.Fatal(err)}Each collection needs its own vector index, and the dimension of each index must match the embedder
that writes to it: 3072 for text-embedding-3-large, 1536 for text-embedding-3-small.
With Flows
Section titled “With Flows”ragFlow := genkit.DefineFlow(g, "rag-qa", func(ctx context.Context, query string) (string, error) { // Retrieve context results, err := genkit.Retrieve(ctx, g, ai.WithRetriever(retriever), ai.WithTextDocs(query), ) if err != nil { return "", err }
// Generate response response, err := genkit.Generate(ctx, g, ai.WithPrompt(buildPromptWithContext(query, results)), ) if err != nil { return "", err }
return response.Text(), nil})