Back to Practice

store_nodes() for BM25 hybrid

Worked

Worked Example: Hybrid Retrieval with BM25 Fusion using store_nodes()

In this worked example, you will learn how to combine a dense retriever (semantic search) with a BM25 sparse retriever (lexical/term-matching) using Reciprocal Rank Fusion (RRF). The key challenge: when using Qdrant as the vector store, the LlamaIndex docstore is deliberately empty – nodes are stored only in Qdrant point payloads. Therefore, to build the BM25 index (which needs the full corpus in-process), you must extract the nodes from the vector store itself. The function store_nodes(index) handles this: it first tries index.docstore.docs (populated only for in-memory indices), and if that is empty, it falls back to index.vector_store.get_nodes().

Your task (study this worked solution): Complete the function build_fusion_retriever so that it:

  1. Retrieves the corpus nodes by calling store_nodes(index).
  2. Creates a BM25Retriever from those nodes, with similarity_top_k clamped to min(top_k, len(nodes)) (BM25 raises if top_k exceeds corpus size).
  3. Fuses the dense_retriever and the BM25 retriever using QueryFusionRetriever in 'reciprocal_rerank' mode. Set num_queries=1 and pass llm=None (so no LLM call is made). Keep use_async=False and verbose=False.
  4. Return the fused retriever, or if any exception occurs (e.g., missing stemmer), return the original dense_retriever unchanged (graceful degradation).

This exact pattern is used in the real codebase (see build_fusion_retriever and _build_retriever). The solution below is the fully realized version for you to study.

Why this matters: The dense retriever excels at semantic similarity but can miss exact identifiers (e.g., NativeD1Saver, route_for, error codes). BM25 catches those lexical hits. RRF merges the ranked lists without normalizing scores, producing a stronger candidate set for downstream reranking. The use of store_nodes() guarantees the BM25 index is built from the actual stored documents, even on the Qdrant path.

Your code
Sources
  • roadmap-kg/kg/rerank.py:188-225
  • roadmap-kg/kg/rerank.py:151-186
  • roadmap-kg/kg/memory_common.py:975-990