Chroma is an open-source embedding database designed for building AI applications that need fast, filterable similarity search. Unlike general-purpose databases that bolt on vector support as an afterthought, Chroma is purpose-built around the embedding workflow: store documents with their vector representations and arbitrary metadata, then query by semantic similarity with optional metadata filters—all through a simple API that feels native to Python and Go applications.
In a typical AI application stack, the search platform uses Chroma as its primary vector store via langchaingo abstractions. This integration layer provides a uniform interface for storing, indexing, and querying embeddings regardless of whether the underlying store is Chroma, Pinecone, or an in-memory index. The langchaingo vector store interface wraps Chroma’s collection API, handling serialization of metadata filters, pagination of large upsert batches, and automatic retry on transient failures.
Understanding how Chroma works under the hood—its HNSW indexing, metadata filtering pipeline, and persistence model—is essential for tuning query performance and making informed decisions about when Chroma is the right choice versus alternatives like pgvector or Pinecone.
A Chroma collection is analogous to a table in a relational database. Each collection stores documents as (id, embedding, metadata, document_text) tuples. Collections are the unit of index management—each collection maintains its own HNSW graph, and queries are scoped to a single collection. The search platform creates one collection per tenant to provide data isolation.
Chroma uses Hierarchical Navigable Small World (HNSW) graphs for approximate nearest neighbor search. HNSW builds a multi-layer graph where higher layers contain fewer nodes with longer-range connections, enabling logarithmic search time. Key tuning parameters are M (connections per node, affecting recall vs. memory) and ef_construction / ef_search (controlling build-time and query-time accuracy).
Chroma supports filtering query results by metadata fields using a where clause with operators like $eq, $ne, $gt, $in, and $contains. Filters are applied after the HNSW search narrows candidates, which means highly selective filters on large collections may return fewer results than requested. The langchaingo wrapper translates Go filter structs into Chroma’s JSON filter format.
Langchaingo defines a VectorStore interface with methods like AddDocuments, SimilaritySearch, and Delete. The Chroma implementation of this interface handles connection management, collection lifecycle, and translates langchaingo’s generic query options (top-k, score threshold, metadata filters) into Chroma-native API calls. This abstraction lets the search platform swap vector stores without changing application logic.
Chroma supports in-memory mode (for testing), persistent local storage (SQLite + parquet for metadata, memory-mapped HNSW files), and client-server mode (connecting to a remote Chroma server over HTTP/gRPC). The search platform uses client-server mode in production and in-memory mode in integration tests, configured through the langchaingo store options.
The integration follows a layered architecture: application code calls the langchaingo VectorStore interface, which delegates to a Chroma-specific adapter that communicates with the Chroma server over its REST or gRPC API. Documents flow through an embedding function (registered separately) before being upserted into a collection.
package search
import (
"context"
"log"
"github.com/tmc/langchaingo/embeddings"
"github.com/tmc/langchaingo/vectorstores/chroma"
)
// NewChromaStore creates a langchaingo-compatible vector store backed by Chroma.
func NewChromaStore(ctx context.Context, chromaURL string, collectionName string, embedder embeddings.Embedder) (*chroma.Store, error) {
store, err := chroma.New(
chroma.WithChromaURL(chromaURL),
chroma.WithNameSpace(collectionName),
chroma.WithEmbedder(embedder),
chroma.WithDistanceFunction("cosine"),
)
if err != nil {
return nil, fmt.Errorf("creating chroma store: %w", err)
}
log.Printf("Connected to Chroma collection %q at %s", collectionName, chromaURL)
return &store, nil
}
package search
import (
"context"
"fmt"
"github.com/tmc/langchaingo/schema"
"github.com/tmc/langchaingo/vectorstores"
)
// IndexDocuments embeds and stores documents in the Chroma collection.
func IndexDocuments(ctx context.Context, store vectorstores.VectorStore, docs []schema.Document) error {
// langchaingo handles batching internally based on the embedder's limits
ids, err := store.AddDocuments(ctx, docs)
if err != nil {
return fmt.Errorf("adding documents to chroma: %w", err)
}
fmt.Printf("Indexed %d documents, IDs: %v\n", len(ids), ids[:min(3, len(ids))])
return nil
}
// PrepareDocument creates a langchaingo Document with metadata for Chroma storage.
func PrepareDocument(content string, source string, docType string, tenantID string) schema.Document {
return schema.Document{
PageContent: content,
Metadata: map[string]any{
"source": source,
"doc_type": docType,
"tenant_id": tenantID,
"indexed_at": time.Now().UTC().Format(time.RFC3339),
},
}
}
package search
import (
"context"
"fmt"
"github.com/tmc/langchaingo/vectorstores"
)
// SemanticSearch queries the Chroma collection with optional metadata filtering.
func SemanticSearch(ctx context.Context, store vectorstores.VectorStore, query string, tenantID string, topK int) ([]schema.Document, error) {
// Build filter: restrict results to the requesting tenant
filter := map[string]any{
"tenant_id": map[string]any{
"$eq": tenantID,
},
}
results, err := store.SimilaritySearch(ctx, query, topK,
vectorstores.WithFilters(filter),
vectorstores.WithScoreThreshold(0.7),
)
if err != nil {
return nil, fmt.Errorf("chroma similarity search: %w", err)
}
fmt.Printf("Found %d results for query %q (tenant: %s)\n", len(results), query, tenantID)
return results, nil
}
The search platform in a production AI application stack uses the Chroma integration for its core semantic search pipeline. When a user submits a natural-language query, the service embeds the query using the same model that produced the stored embeddings, then calls SimilaritySearch on the tenant’s Chroma collection. Results are ranked by cosine similarity and filtered by metadata (document type, recency, access level) before being returned to the frontend.
During development and testing, the team runs Chroma in-memory mode, which avoids the need for a running Chroma server. Integration tests create ephemeral collections, index a known corpus, run queries, and verify that expected documents appear in the results with appropriate similarity scores. This pattern catches embedding model regressions and filter logic bugs before they reach production.
For multi-tenant deployments, each tenant gets a dedicated Chroma collection. This provides strong data isolation without the operational overhead of running separate Chroma instances. The collection-per-tenant model also allows per-tenant tuning of HNSW parameters based on corpus size and query patterns.
package search
import (
"context"
"fmt"
"github.com/tmc/langchaingo/schema"
"github.com/tmc/langchaingo/vectorstores"
)
const upsertBatchSize = 100
// BatchIndex indexes a large corpus in chunks, reporting progress.
func BatchIndex(ctx context.Context, store vectorstores.VectorStore, docs []schema.Document) error {
total := len(docs)
for i := 0; i < total; i += upsertBatchSize {
end := min(i+upsertBatchSize, total)
batch := docs[i:end]
if _, err := store.AddDocuments(ctx, batch); err != nil {
return fmt.Errorf("batch %d-%d failed: %w", i, end, err)
}
fmt.Printf("Indexed %d/%d documents (%.0f%%)\n", end, total, float64(end)/float64(total)*100)
}
return nil
}
package search
import (
"context"
"net/http"
"fmt"
"io"
)
// HealthCheck verifies the Chroma server is reachable and the collection exists.
func HealthCheck(ctx context.Context, chromaURL string) error {
resp, err := http.Get(chromaURL + "/api/v1/heartbeat")
if err != nil {
return fmt.Errorf("chroma unreachable: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("chroma health check failed (status %d): %s", resp.StatusCode, body)
}
return nil
}
topK values, you may get fewer results than expected. For strict access control, use separate collections per access boundary rather than metadata filters alone.M and ef_search accordingly.