Back to roadmap
EnglishAbout 8 min read

Exercise 2: Vector DB (ChromaDB) + semantic vs keyword

examples/stage-6/02-vector-db/README.en.md

Exercise 2: Vector DB (ChromaDB) + semantic vs keyword

Pairs with Stage 6 — Memory & RAG Exercise 2.

🎓 How to use this: starter.py is the complete solution, not a TODO skeleton. The active approach works better — mv starter.py starter_reference.py, read the signatures but not the bodies, write your own starter.py from scratch, then run python test.py to check it; if you are stuck for 20 minutes, go back and compare against the reference. Full methodology in docs/HOW_TO_USE.md.

📚 Want the chapter-length version? The starter in this folder is an illustrative build focused on the core pattern plus two SDK paths — it is not in-depth teaching material. Recommended for depth:

#Task

Index 8 docs into Chroma; compare semantic (vector) vs keyword (substring) retrieval on the same query.

#How to run

pip install -r requirements.txt
python starter.py   # auto-downloads embedding model on first run

Budget: $0. In-memory mode; released after process exits.

python test.py             # 5 tests for index/query/ranking
python test_anthropic.py   # Path B concept demo (same as starter)

#When to use a vector DB

ScenarioList + cosineChromaDB
< 100 docs✅ enoughoverkill
100-10K docsSlow (re-embed each query)✅ persistent + indexed
10K+ docsNo✅ (consider Qdrant / Weaviate at huge scale)
PersistenceRe-embed✅ SQLite backend
Filter / metadataDIY✅ where clause
Hybrid searchDIY✅ built-in BM25 + vector

Rule of thumb: experimentation = EphemeralClient; production = PersistentClient(path=...).

#Semantic vs keyword

Query: "where to drink good coffee in Asian cities"

📝 Keyword (substring) → misses doc 3
    Query doesn't have the exact word "coffee"

🔍 Semantic (vector) → hits doc 3
    "Coffee shops in Taipei often serve pour-over..."
    Semantic alignment, not literal match
DimensionKeywordSemantic
Synonyms ("car" vs "auto")MissCatch
RephrasingsMissCatch
TyposMissCatch (small)
Exact proper nounsStrongOccasionally confused
Negation (NOT)EasyHard (embeddings don't grok negation)
SpeedFastMedium (need to embed the query)
ProductionBM25 + vector hybridSame

Production takeaway: use both — hybrid search is best practice. Chroma 0.4+ has BM25 + vector built in.

#Chroma API

client = chromadb.EphemeralClient()    # in-memory; PersistentClient(path=...) for disk
embed_fn = embedding_functions.SentenceTransformerEmbeddingFunction(model_name="...")
collection = client.get_or_create_collection(name="demo", embedding_function=embed_fn)

collection.add(ids=[...], documents=[...], metadatas=[{"category": "..."}, ...])
collection.query(query_texts=[query], n_results=3, where={"category": "tech"})
collection.upsert(...)
collection.delete(ids=[...])

#Common pitfalls

  • Duplicate ids in .add(): raises. Use .upsert() or check .get()["ids"] first
  • Rebuilding the collection each query: don't! PersistentClient indexes once
  • n_results too high: no reranker — large k pulls in noise. 3-10 typically
  • Filter confusion: where={"category": "tech"} is metadata; where_document={"$contains": "..."} is content
  • Inconsistent embedding function: indexing with model A and querying with model B breaks retrieval. Chroma binds embedding_function to the collection to prevent this

#Production-ready alternatives

# Persistent
collection = build_collection(path="./chroma_db")

# Cloud embeddings (higher quality)
embed_fn = embedding_functions.OpenAIEmbeddingFunction(api_key=..., model_name="text-embedding-3-small")

#Extensions

  • Metadata filter: collection.query(query_texts=[q], where={"category": "food"})
  • Hybrid search: BM25 + vector via Chroma 0.4+ or external rank_bm25
  • Swap to Qdrant / Weaviate at production scale
  • Plug into Exercise 4: full RAG pipeline reuses this collection