Chroma
Developer-friendly vector database for local RAG — collections, metadata filters, upsert discipline, metrics, ops, debugging, and when to graduate end-to-end.
What Chroma is (plain English)
Chroma is a developer-friendly vector database aimed at local-first and small-app RAG: collections of embeddings + documents + metadata, with similarity query and filters. You can run it embedded in-process or as a client/server.
It shines for labs, notebooks, and early products. Many teams later graduate to pgvector, Weaviate, Pinecone, or another store — but the RAG mental model transfers.
Analogy: SQLite for vectors. Perfect for learning and early apps. You graduate when HA, multi-tenant scale, or hybrid search ops demand a heavier store — not because Chroma “isn’t real.”
flowchart LR
Chunks[Chunks + metadata] --> Emb[Embedding model]
Emb --> Col[(Chroma collection)]
Q[Query] --> QEmb[Embed query]
QEmb --> Search[Similarity + where filter]
Col --> Search
Search --> LLM[LLM context]
Interview cue: Chroma teaches the collection / id / embedding / metadata / query contract. Be ready to redraw that on any vector store.
The problem it solves for LLMs
Without a vector store you either scan all embeddings in memory or rely on keywords alone. Chroma gives you:
- Persist vectors across restarts (local path / server)
- Top-k semantic retrieval for RAG
- Metadata filters (
tenant_id,doc_type, …) - A gentle API for learning ANN retrieval before ops complexity
It does not generate answers — it retrieves candidates. The LLM still synthesizes (and can still hallucinate if retrieval is empty or wrong).
Deep foundation: Vector databases, Embeddings, RAG building blocks.
Architecture
| Concept | Meaning |
|---|---|
| Collection | Named set of items (like a table) |
| ID | Stable chunk id for upsert/delete |
| Embedding | Vector from your embedding model |
| Document | Text payload returned to the LLM |
| Metadata | Filterable fields (keep them typed/simple) |
| Query | Embed query → ANN top-k + where |
| Embedding function | Optional helper to embed on upsert/query |
flowchart TB
subgraph ingest [Ingest]
D[Doc] --> C[Chunk]
C --> E[Embed]
E --> U[Upsert ids]
end
subgraph query [Query]
Q[Question] --> EQ[Embed]
EQ --> ANN[Query collection]
ANN --> Top[Top-k docs]
end
U --> Store[(Chroma)]
Store --> ANN
Embedded vs client/server
| Mode | Good for | Risk |
|---|---|---|
Embedded (Client()) |
Notebooks, single-process labs | Multi-worker web apps each open their own DB |
| HttpClient / server | Local apps, small services | Still not “managed HA SaaS” |
Ship rule: if your web server forks workers, use client/server — not N in-process DBs fighting one path.
How to use
# Shape only — pin chromadb version in labs
import chromadb
client = chromadb.Client() # or HttpClient for server mode
col = client.get_or_create_collection("policies")
col.upsert(
ids=["policy-refund-001"],
documents=["Refunds within 30 days require order id ..."],
metadatas=[{"tenant_id": "acme", "doc_type": "policy"}],
embeddings=[[0.1, 0.2, 0.3]], # or use an embedding function
)
hits = col.query(
query_embeddings=[[0.11, 0.19, 0.29]],
n_results=5,
where={"tenant_id": "acme"},
)Ship rule: never invent new IDs on every ingest — upsert by stable chunk keys or you duplicate forever.
Collection naming that survives model swaps
policies_v3_text-embedding-3-small_1536_cosineInclude embed model + dim + metric. Changing any of those ⇒ new collection + full re-embed.
How Chroma fits LLM apps
| Surface | Role |
|---|---|
| Local RAG prototype | Default teaching store |
| Agent memory (early) | Collection of notes with metadata |
| Eval harness | Fast rebuild of indexes in CI |
| LlamaIndex / LangChain backend | Swap-friendly vector layer |
flowchart TB
App[RAG / agent service] --> Ch[(Chroma)]
Ingest[Ingest job] --> Ch
App --> LLM[LLM]
Ch --> App
Frameworks: LlamaIndex often sits above Chroma for readers/query engines — you can also call Chroma directly for clarity.
When to use Chroma vs graduate
| Use Chroma when | Graduate when |
|---|---|
| Local RAG labs, ShipAI RAG | Multi-tenant SaaS at scale |
| Single-node demos / teaching | Need mature HA, backups, SLOs |
| Notebook iteration speed | Team standard is pgvector / Weaviate / Pinecone |
| Embedded prototype in one process | Hybrid search + ops-heavy filtering |
See Data track: Choosing vector stores, Postgres + pgvector.
Walkthrough: 50-chunk policy lab
- Chunk policies with overlap; attach
tenant_id,doc_type,source_path. - Embed with a pinned model; upsert stable ids
path#chunk_i. - Query with
where={"tenant_id":"acme"}. - Delete one doc’s ids; re-upsert; confirm count unchanged.
- Measure recall@5 on a golden set before changing prompts.
sequenceDiagram
participant Dev
participant Ch as Chroma
participant Emb as Embed model
Dev->>Emb: embed chunks
Dev->>Ch: upsert ids + meta
Dev->>Emb: embed question
Dev->>Ch: query + where
Ch-->>Dev: top-k docs
Alternatives
| Store | Notes |
|---|---|
| pgvector | SQL + vectors; great if Postgres is already home |
| Weaviate / Qdrant / Milvus | Full vector DB features / scale |
| Pinecone | Managed; less ops, more vendor |
| FAISS (in-process) | Fast research; you own persistence |
| Redis vector | Cache-adjacent patterns — Redis for AI caching |
Production gotchas
- Embedding model swap without re-embed → garbage neighbors
- Missing
wherefilters → cross-tenant leaks - Huge documents in payload → context bloat; store pointers + tight chunks
- Embedded mode in multi-worker web servers — use client/server, not N in-process DBs
- No recall measurement — always keep a golden query set
- Duplicate ids from bad keying — index bloat + confusing hits
- Metric mismatch — cosine vs L2 vs IP vs how embeddings were trained
- Treating Chroma as long-term memory without retention policy
flowchart TD
Leak[Cross-tenant hit] --> F[where filter missing]
Dup[Duplicate chunks] --> I[Non-stable ids]
Junk[Random neighbors] --> E[Embed model changed]
Slow[Multi-worker corruption] --> M[Embedded mode misuse]
Failure modes checklist
querywithoutwherein a multi-tenant app- New UUID every ingest
- Documents = entire 50-page PDF
- Eval only on generated answers, never retrieval ids
- Prod still on laptop path
./chroma_datawith no backups
Hands-on next steps
- Ingest ≥50 chunks with metadata; query with a filter.
- Delete/re-upsert one doc; confirm no duplicates.
- Compare the same queries on pgvector later.
- Guided Vector DB.
Micro-project
- Build collection
policies_v1_<embed>_<dim>_cosine. - Upsert 50 chunks; run 10 golden queries; log ids.
- Change chunk size; rebuild; compare miss lists.
- Write “graduate when…” criteria for your app (QPS, tenants, HA).
Interview whiteboard: the five fields
Any vector store answer should name:
- Collection
- Stable id
- Embedding (+ model version)
- Document / payload
- Metadata + where filter
Then draw ingest and query as separate paths sharing the collection.
Distance metrics and embed models
| Rule | Detail |
|---|---|
| Same model for query + docs | Always |
| Same metric as training assumption | Cosine vs L2 vs IP |
| Normalize when required | Cosine/IP footguns |
| Name collection with model+dim+metric | Survive swaps |
Changing embed model without rebuild is a silent outage for RAG quality.
Hybrid search graduation path
Chroma is enough to learn dense retrieval. When keyword precision matters (IDs, error codes), plan hybrid:
- Dense top-k from Chroma
- BM25/keyword from your search stack
- Rerank → pack LLM context
Data track: Hybrid search and rerankers.
Backup and local ops
Even “just a lab store” needs:
- Path on durable disk
- Periodic copy of persistence dir
- Recreate-from-source ingest job (source of truth is docs, not only vectors)
If you cannot rebuild the index from documents + embed model id, you do not have a recovery plan.
Tradeoffs summary
| Stay on Chroma when… | Graduate when… |
|---|---|
| Labs, early product, teaching | HA, heavy multi-tenant, strict SLOs |
| Single-node simplicity | Team standardizes on Postgres/Weaviate/etc. |
| Fast iteration | Hybrid + complex filtering at scale |
Checklist
- Stable ids on upsert
- Mandatory tenant
wherein app wrappers - Collection name encodes embed model
- Golden recall@k suite
- Client/server if multi-worker
- Rebuild-from-source documented
Glossary
| Term | Meaning |
|---|---|
| Collection | Named vector namespace |
| Upsert | Insert or replace by id |
| Metadata filter / where | Restrict ANN candidates |
| Embedding function | Helper that embeds text for you |
| Recall@k | Relevant ids found in top-k |
| Graduate | Move to a heavier vector store |
Debugging playbook (first hour)
| Symptom | First checks | Fix direction |
|---|---|---|
| Cross-tenant hit | where missing on any path? |
Mandatory filter wrapper |
| Duplicate near-identical chunks | Unstable ids? | Stable path#chunk_i / hash |
| Random neighbors after “model upgrade” | Collection still old embed? | New collection + full re-embed |
| Empty results | Dim mismatch / wrong metric / empty col | Assert count, dim, metric |
| Multi-worker weirdness | Embedded Client() per worker? |
HttpClient / server mode |
| Good ANN, bad answers | Retrieval vs generate blame | Log hit ids before prompt work |
Anti-patterns
- New UUID every ingest — duplicates forever.
- Entire PDF as one document — retrieval unit too coarse.
- Optional ACL filters — security theater.
- Embedded mode in forked web servers — corruption / split-brain.
- Vectors as source of truth — you cannot rebuild without docs.
- Eval only on chat answers — miss retrieval failure.
- Cosine collection + L2-trained embed assumptions — silent quality loss.
Security and privacy threat note
- Metadata is searchable and often PII — minimize fields; encrypt at rest if required.
- Document payloads in the store are readable by anyone with DB access — treat like a datastore.
- Delete means delete ids and stop serving soft-deleted flags if you use them.
- Lab paths on laptops are not backups — and not HIPAA/SOC2 “controls.”
Interview prompts you should be able to answer
- Name the five fields of a vector item (collection, id, embedding, document, metadata).
- Why does changing the embed model require a new collection?
- When do you graduate from Chroma to pgvector / a dedicated vector DB?
- How do you prove upsert did not duplicate?
- How does Chroma fit under LlamaIndex without becoming a black box?
End-to-end: lab → small product
flowchart TB
Docs[Source docs] --> Chunk[Chunk + metadata]
Chunk --> Emb[Pinned embed model]
Emb --> Upsert[Upsert stable ids]
Upsert --> Ch[(Chroma server)]
API[RAG API] --> Wrap[where tenant mandatory]
Wrap --> Ch
Ch --> Pack[Top-k excerpts]
Pack --> LLM[LLM + citations]
CI[Golden recall@k] --> Ch
Graduation trigger examples: need HA replicas, heavy hybrid ops, multi-region, or the team already runs Postgres as home (Postgres + pgvector).
Production readiness checklist
- Client/server if multi-worker
- Collection name: embed model + dim + metric
- Stable ids + rebuild-from-source job
- Mandatory tenant
wherein wrappers - Golden recall@k suite
- Persistence path on durable disk + backup copy
- Retention / soft-delete policy
- Documented graduate-when criteria
How it works end-to-end (request lifecycle)
- Create/open a collection (name encodes embed model + dim).
- Embed chunks; upsert with stable ids + metadata.
- Query: embed question → similarity search +
wherefilters. - Return documents/metadatas/distances to the packer.
- LLM answers with citations bound to ids.
flowchart TD
Upsert[upsert ids+emb+meta] --> Col[(Collection)]
Q[Query text] --> QE[Query embed]
QE --> Search[query + where]
Col --> Search
Search --> Pack[Pack top-k]
Pack --> LLM
Collection naming and versioning
wiki_emb-bge-small-v1_dim384
tickets_emb-bge-small-v1_dim384When the embedding model changes, new collection + backfill — do not mix dimensions or spaces. Record embed model revision in ops docs.
Metadata filters and multi-tenancy
Always filter tenant_id (and often acl / space). Similarity without filters is a data leak waiting to happen.
| Metadata | Purpose |
|---|---|
tenant_id |
Isolation |
doc_id / source |
Citations |
doc_type |
Routing chunk strategies |
updated_at |
Freshness boosts / filters |
chunk_index |
Pack neighbors |
Upsert discipline
- Stable ids from
hash(doc_id, chunker_version, span) - Upsert > delete+add for most updates
- Tombstone or delete on source removal
- Prove no duplicates in the micro-project
Local vs client/server
| Mode | When |
|---|---|
| Embedded / local path | Labs, single developer |
| Client/server | Small team sharing one index |
Neither replaces HA multi-region product stores. Graduate criteria belong in the design doc.
Hybrid and graduation path
Chroma is primarily dense retrieval. When SKUs/error codes dominate failures, add BM25 (hybrid article) or graduate to Weaviate/Elastic/OpenSearch/pgvector+tsvector. Keep chunk ids stable across the migration.
Observability
Log collection, top_k, filter, hit_ids, distances, embed_model, query_ms. Empty hits should be a first-class metric — silent empty retrieval → hallucinated answers.
Worked walkthrough: 50-chunk policy lab
- Chunk 10 Markdown policies with heading splitter.
- Upsert to Chroma with
doc_type=policy. - 15 golden questions; measure recall@5.
- Ablate chunk size; keep winner.
- Write graduate-when: “>X vectors or need HA/hybrid.”
FAQ (Chroma)
Is Chroma production-ready?
For many early products yes; for regulated multi-tenant HA maybe not. Decide with criteria, not vibes.
Chroma vs pgvector?
Chroma: fastest learning loop. pgvector: SQL/ACL joins with existing Postgres.
Do I need LlamaIndex?
Optional — see LlamaIndex.
Deep dive: distance metrics
Cosine vs L2 vs IP must match how embeddings were trained/normalized. Mismatch quietly tanks recall. Pin metric next to embed model in the collection name docs.
Putting what / why / how together
| Lens | Chroma |
|---|---|
| What | Dev-friendly vector DB for embeddings + metadata |
| Why | Learn/ship RAG without ops mountain |
| How | Collections + upsert ids + filters + eval + graduate plan |
Architecture that survives early production
flowchart TB
subgraph writers [Writers]
Ingest[Ingest job]
end
subgraph readers [Readers]
API[Chat API]
end
Ingest --> Ch[(Chroma server)]
API --> Ch
API --> Emb[Embed service]
Ingest --> Emb
Use one embed service for write and read. Divergent embed models are the classic silent RAG break.
Capacity intuition
| Scale | Chroma fit |
|---|---|
| <100k vectors, single region | Often fine |
| Multi-tenant noisy neighbors | Watch memory; plan graduate |
| Strict HA / multi-AZ | Prefer managed/pgvector/etc. |
Benchmark your query filter selectivity — sparse ACL filters change ANN behavior.
Backup / restore drill
- Snapshot persistence directory or server volume.
- Restore to a scratch instance.
- Run golden queries; compare hit ids.
- Document RPO/RTO even for “just a lab DB.”
Migration playbook off Chroma
- Freeze writes or dual-write.
- Export ids, documents, metadatas, embeddings (or re-embed).
- Import to target store with same ids.
- Shadow query diff on golden set.
- Flip read traffic; keep Chroma read-only briefly.
Stable ids make this boring — which is the goal.
Interview whiteboard
Five fields: id, embedding, document, metadata, collection. Draw filter before ANN. State graduate-when criteria aloud.
Failure story bank
- Re-embed with new model into same collection → garbage distances.
- Forgot
where={tenant}in one code path. - Local ephemeral container lost volume → empty prod.
- top_k=50 packed into 8k context → truncated citations.
End-to-end lab checklist (do this once)
- Persistent volume configured
- Upsert idempotency proven
- Filter unit tests
- Golden recall@5 recorded
- Graduate-when paragraph in README
- Backup restore once
Production readiness checklist (Chroma)
- Persistence volume durable across restarts
- Collection naming encodes embed model + dim
- Upsert ids stable; duplicate test in CI
- Tenant/ACL filters on every query path
- Backup/restore drill completed once
- Golden recall@5 baselined
- Empty-hit metric + alert
- Graduate-when criteria written
- Embed service single-sourced for read/write
What “good” looks like in a design doc
Collection schema, id scheme, filter fields, embed model pins, persistence/backup, expected vector count growth, and the next store you’ll migrate to (with why). “Chroma for now” needs an exit ramp.
Common interview traps
- Treating Chroma as interchangeable with keyword search
- No metadata → no multi-tenant story
- Re-embedding into the same collection blindly
- Claiming ANN is exact nearest neighbor
Distance, recall, and honesty
ANN is approximate. Measure recall@k against brute force on a sample periodically. If filters are ultra-selective, verify the engine isn’t returning nonsense under empty candidate sets.
Glossary addendum
| Term | Meaning |
|---|---|
| Collection | Named vector space + metadata |
| Upsert | Insert-or-replace by id |
| where filter | Metadata predicate pre/post ANN |
| top_k | Number of neighbors returned |
| Graduate | Move to heavier store when criteria hit |
Micro-project stretch
Export your 50-chunk lab to a JSONL of {id,document,metadata,embedding}; re-import into a second Chroma collection; diff query hit ids — rehearsal for store migration.
How to evaluate Chroma retrieval
- Freeze a golden set of (query → relevant chunk ids).
- Measure recall@k and MRR.
- Slice by doc_type and tenant.
- Re-run after chunker or embed changes.
Do not judge Chroma by LLM answer vibes alone — split retrieval metrics. See Vector databases for ANN context.
Metadata cardinality pitfalls
High-cardinality filters (per-user ids on millions of rows) can hurt. Prefer tenant-level collections or composite indexes as you scale. Document the filter fields you promise to product.
Embedding cache for ingest
When re-running ingest, cache embeddings by content_hash in Redis/disk to save $. Invalidation: chunker version bump clears cache keys. This matters once corpora leave toy size.
Local RAG with Ollama
Pair Chroma with Ollama for offline demos: retrieve → pack → local generate. Same ids and filters you will use in prod; only the LLM base_url changes.
Putting what / why / how together
| Lens | Answer |
|---|---|
| What | Friendly vector DB for RAG learning/shipping early |
| Why | Persist embeddings + filtered ANN without heavy ops |
| How | Collections, upsert ids, filters, evals, graduate plan |
Ops metrics worth graphing
| Metric | Why |
|---|---|
| Query latency p95 | UX budget |
| Upsert rate / lag | Freshness SLO |
| Collection count / vector count | Capacity |
| Empty-hit rate | Hallucination precursor |
| Error rate | Server health |
Alert on empty-hit spikes after deploys — usually embed mismatch or filter bugs.
Tenant isolation test (automate)
Create two tenants’ chunks; query as tenant A with filter; assert zero B ids across 100 random queries. Run in CI against ephemeral Chroma.
Hands-on stretch: hybrid sidecar
Keep Chroma for dense; add Postgres tsvector or a tiny OpenSearch for BM25 on the same chunk ids; fuse with RRF in 40 lines of Python. This teaches graduation without abandoning Chroma prematurely.
FAQ addendum
Can Chroma replace Redis caching?
No — different job. See Redis for AI caching.
Should I store raw PDFs in Chroma?
No. Store chunk text + metadata; PDFs in object storage.
Sizing worksheet (fill before “prod”)
| Input | Your number |
|---|---|
| Docs / chunks expected in 6 months | |
| Embedding dims | |
| Peak QPS | |
| Filter selectivity (typical % of corpus) | |
| Persistence disk headroom | |
| Graduate threshold (vectors or HA need) |
If you cannot fill this, you are guessing — fine for a lab, not for a launch review.
Citation packing reminder
Chroma returns chunks; your packer must: respect token budgets, prefer neighbors via chunk_index, and never drop source metadata. Bad packing looks like a vector-DB failure in demos.
Putting the lab on a resume (honest claims)
Good: “Built filtered RAG over N docs with Chroma; measured recall@5; documented graduate-when criteria.” Bad: “Implemented production vector database at global scale” for a laptop collection.
Related
Data track: Vector databases, Choosing vector stores, Chunking and metadata. Key tech: LlamaIndex. Core: RAG building blocks, Embeddings. Guided RAG.