Where we left off
Both halves of this course's hybrid pipeline now have a real-library replacement, chromadb for dense (same as naive_rag's Advanced tier) and rank_bm25 for sparse (Lesson 20). This lesson wires both in at once, fused with the exact same RRF function from Lesson 8, unchanged.
The code, piece by piece
chroma_client = chromadb.Client()collection = chroma_client.create_collection(name="notes")collection.add(ids=names, documents=texts, embeddings=vectors)Same pattern as naive_rag Lesson 20: an ephemeral, in-memory collection standing in for this course's own list-based dense store.
dense_results = collection.query(query_embeddings=[query_vector], n_results=len(names))dense_ranking = dense_results["ids"][0]n_results=len(names) asks chromadb for every document, ranked, not just a top-k. RRF needs a full ranking from each side to fuse correctly, the same reason Lessons 6-18's hand-rolled dense and sparse functions always returned complete rankings, not pre-truncated ones.
fused = reciprocal_rank_fusion([dense_ranking, sparse_ranking])Lesson 8's function, character-for-character unchanged. RRF never cared whether a ranking came from a hand-rolled list or a real library, it only ever needed a list of names in order, which is exactly what both chromadb and rank_bm25 hand back.
Checkpoint
- Both retrievers are now real libraries,
chromadb(dense) andrank_bm25(sparse), the fusion logic connecting them (Lesson 8's RRF) needed zero changes. - Requesting a full ranking (
n_results=len(names)), not a pre-truncated top-k, from each retriever is what makes fusion possible at all. - This is the shape a production hybrid retriever actually takes: two independent, well-tested libraries, one small fusion function gluing their outputs together.
If anything here still feels unclear, ask before moving to Lesson 22.