Skip to content

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.

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
  1. Create a Firebase project at Firebase Console
  2. Enable Firestore in your project:
    • Go to Firestore Database in the Firebase console
    • Click “Create database”
    • Choose your security rules and location
  3. Set up authentication using one of these methods:
    • For local development: firebase login and firebase use <project-id>
    • For production: Service account key or Application Default Credentials

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))

You can also configure the project ID using environment variables:

Terminal window
export FIREBASE_PROJECT_ID=your-firebase-project-id
// Plugin will automatically use FIREBASE_PROJECT_ID environment variable
firebasePlugin := &firebase.Firebase{}
g := genkit.Init(context.Background(), genkit.WithPlugins(firebasePlugin))

For advanced use cases, you can provide a pre-configured Firebase app:

import firebasev4 "firebase.google.com/go/v4"
// Create Firebase app with custom configuration
app, 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,
}

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.

Once defined, you can use the retriever in your RAG workflows:

// Retrieve relevant documents
results, err := genkit.Retrieve(ctx, g,
ai.WithRetriever(retriever),
ai.WithTextDocs("What is machine learning?"),
)
if err != nil {
log.Fatal(err)
}
// Use retrieved documents in generation
var contextDocs []string
for _, 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?")),
)

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())
}

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.

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:

Terminal window
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.

To add documents to your Firestore collection with embeddings:

// Example of adding documents with embeddings
embedder := 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.

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.

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.

For local development, use the Firebase CLI:

Terminal window
# Install Firebase CLI
npm install -g firebase-tools
# Login and set project
firebase login
firebase use your-project-id

For production, use one of these authentication methods:

import "google.golang.org/api/option"
app, err := firebasev4.NewApp(ctx, &firebasev4.Config{
ProjectID: "your-project-id",
}, option.WithCredentialsFile("path/to/serviceAccountKey.json"))

Set the environment variable:

Terminal window
export GOOGLE_APPLICATION_CREDENTIALS="path/to/serviceAccountKey.json"

Or use the metadata server on Google Cloud Platform.

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 errors
results, 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:

CauseWhat the message saysRetry?
No vector index on VectorFieldFailedPrecondition, with a gcloud command to create the indexNo. Create the index.
Credentials cannot read the collectionPermissionDeniedNo. Fix IAM or the Firestore rules.
Query vector length differs from the index dimensionInvalidArgumentNo. 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.

  • 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
  • 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
  • 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
// Use different embedders for different types of content
technicalRetriever, 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.

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
})