Retrievers — Dense, Sparse, Hybrid¶
1. Why does this topic exist?¶
We have a vector store. We could just call vector_store.similarity_search(query). Why do we need a Retriever abstraction?
Three real-world reasons:
-
Vector stores are one of many knowledge sources. Real apps retrieve from BM25 indexes, SQL databases, web search APIs, knowledge graphs. We need a uniform interface.
-
Vector search has a blind spot. Embedding-based retrieval misses exact terms (product codes, named entities, error codes). Hybrid retrieval (dense + sparse) fixes this.
-
Retrievers compose. Need multi-query? Wrap a base retriever. Need reranking? Wrap again. The retriever abstraction makes pipelines hot-swappable.
Industry pain example: A SaaS company built dense-only RAG. Users searched "Error 0x80070005" — vector retrieval returned random pages. Adding BM25 (which exactly matches 0x80070005) into a hybrid retriever fixed it overnight. Recall@5 on error-code queries: 0.3 → 0.95.
2. What is it?¶
Simple explanation¶
A Retriever is a function: given a query string, return a list of relevant documents. The implementation can be vector search, keyword search, web search, SQL, or any combination.
Technical explanation¶
A Retriever is any class implementing the BaseRetriever interface with .invoke(query: str) → List[Document]. It's a Runnable — composable into LCEL pipelines.
from langchain_core.retrievers import BaseRetriever
class MyRetriever(BaseRetriever):
def _get_relevant_documents(self, query, *, run_manager):
return [Document(page_content="...")]
Industry definition¶
The "retriever" abstraction comes from information retrieval (decades old). In RAG-era frameworks (LangChain, LlamaIndex), it's the standard interface between knowledge sources and LLM pipelines.
Mental model¶
A retriever is a library card catalog: you give it a topic, it tells you which books (chunks) to grab. Different catalogs use different organization (alphabetical/keyword vs subject/semantic) — but the patron sees the same interface.
Analogy¶
Like a search bar: it doesn't matter if Google uses link analysis, semantic embeddings, or freshness signals — you type a query, you get results. The retriever abstracts away how the matching happens.
3. How does it work?¶
The three retrieval paradigms¶
flowchart TD
A[Retriever] --> B[Dense - Semantic Search]
A --> C[Sparse - Keyword Search]
A --> D[Hybrid - Both, fused]
B --> B1[Vector embeddings, cosine similarity]
C --> C1[BM25, TF-IDF, exact term match]
D --> D1[Reciprocal Rank Fusion of both]
Paradigm 1: Dense retrieval (vector embeddings)¶
This is what we covered in Chapters 4-5. Embed query → cosine similarity → top-k.
retriever = vector_store.as_retriever(search_kwargs={"k": 5})
docs = retriever.invoke("What is RAG?")
Pros: semantic match, paraphrase-friendly, multilingual. Cons: misses exact terms (codes, IDs, named entities).
Paradigm 2: Sparse retrieval (BM25 / TF-IDF)¶
Sparse retrieval scores documents by term overlap with the query. The vectors are sparse (one dimension per vocabulary word, mostly zeros).
TF-IDF (the classic, 1972)¶
Formula:
Where: - \(\text{tf}(t, d)\) — count of term \(t\) in document \(d\) - \(N\) — total number of documents - The denominator: number of documents containing \(t\)
Intuition: A term that appears often in one doc (high TF) but rarely in others (high IDF) is important for that doc.
BM25 — the improved standard (1994)¶
Formula:
Where: - \(k_1 \approx 1.2-2.0\) — term saturation parameter - \(b \approx 0.75\) — document length normalization - avgdl — average document length
BM25 corrects TF-IDF's main flaws: 1. Term saturation — 100 occurrences of "the" don't make it 100× more important. 2. Length normalization — long docs don't unfairly win because they contain more words.
Implementation:
from langchain_community.retrievers import BM25Retriever
bm25 = BM25Retriever.from_documents(chunks)
bm25.k = 5
results = bm25.invoke("Error 0x80070005 troubleshooting")
flowchart LR
Q[Query: error 0x80070005] --> T[Tokenize: error, 0x80070005, troubleshooting]
T --> BM[BM25 score per doc]
BM --> R[Top-k by score]
Pros (sparse): - Exact terms match perfectly — product codes, error codes, named entities. - Fast (inverted index). - Interpretable — you can see WHY a doc matched. - No embedding cost.
Cons: - Doesn't capture synonyms ("car" vs "automobile"). - Doesn't capture paraphrases ("how to bake" vs "baking instructions"). - Vocabulary mismatch problem.
Paradigm 3: Hybrid retrieval (dense + sparse)¶
The killer combo. Run both. Merge results.
flowchart LR
Q[Query] --> D[Dense retriever embeddings]
Q --> S[Sparse retriever BM25]
D --> L1[Ranked list A]
S --> L2[Ranked list B]
L1 --> RRF[Reciprocal Rank Fusion]
L2 --> RRF
RRF --> R[Final ranked top-k]
Reciprocal Rank Fusion (RRF)¶
Formula:
- \(\text{rank}_i(d)\) — position of \(d\) in retriever \(i\)'s ranking (1-indexed)
- \(k\) — smoothing constant (typically 60)
- \(w_i\) — weight per retriever (often 0.5/0.5 or 0.7/0.3)
Implementation:
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
bm25 = BM25Retriever.from_documents(chunks)
bm25.k = 5
dense = vector_store.as_retriever(search_kwargs={"k": 5})
hybrid = EnsembleRetriever(
retrievers=[bm25, dense],
weights=[0.4, 0.6], # bias toward semantic
)
docs = hybrid.invoke("Find SKU-A4-12 documentation")
Why hybrid wins:
| Query type | Dense wins | Sparse wins | Hybrid wins |
|---|---|---|---|
| "How does X work?" | ✅ | ✅ | |
| "Error code 0x80070005" | ✅ | ✅ | |
| "Refund policy" | ✅ | ✅ | ✅ (slight edge) |
| "Product SKU-A4-12 specs" | ✅ | ✅ | |
| "Synonyms of efficient" | ✅ | ✅ |
Hybrid covers all bases. Production RAG uses it by default.
Other retriever variations¶
Threshold-based retrieval¶
retriever = vector_store.as_retriever(
search_type="similarity_score_threshold",
search_kwargs={"score_threshold": 0.7, "k": 10},
)
Returns at most k results, only those above 0.7. Use when "no answer" is better than "bad answer."
MMR (Maximum Marginal Relevance)¶
retriever = vector_store.as_retriever(
search_type="mmr",
search_kwargs={"k": 4, "fetch_k": 20, "lambda_mult": 0.6},
)
Picks diverse top-k from a larger pool. Avoids returning 4 near-duplicates.
Metadata-filtered retrieval¶
retriever = vector_store.as_retriever(
search_kwargs={
"k": 5,
"filter": {"customer_id": "acme", "doc_type": "policy"},
},
)
Multi-tenant safe; auditable.
4. Visual Learning¶
Architecture — retrievers as plug-ins¶
flowchart LR
Q[Query] --> R[Retriever interface]
R --> A[Dense vector]
R --> B[Sparse BM25]
R --> C[Web search API]
R --> D[SQL]
R --> E[Hybrid ensemble]
A & B & C & D & E --> DOCS[List of Documents]
The retriever interface lets any knowledge source plug in.
Workflow — hybrid retrieval¶
flowchart LR
A[Query] --> B[Run both dense + sparse]
B --> C1[Dense top-10]
B --> C2[Sparse top-10]
C1 & C2 --> M[Merge with RRF]
M --> R[Top-k final]
Sequence — query path¶
sequenceDiagram
participant App
participant Hybrid as Ensemble Retriever
participant Dense
participant Sparse
App->>Hybrid: invoke query
par Run in parallel
Hybrid->>Dense: vector search
Dense-->>Hybrid: ranked list A
and
Hybrid->>Sparse: BM25 search
Sparse-->>Hybrid: ranked list B
end
Hybrid->>Hybrid: RRF merge
Hybrid-->>App: final top-k
Real-world example — customer support hybrid¶
flowchart LR
Q[user query] --> D[Dense: semantic match on intent]
Q --> S[Sparse: exact match on error codes/product IDs]
D --> R1[Conceptually related articles]
S --> R2[Articles with specific terms]
R1 & R2 --> F[Fuse RRF]
F --> A[Best of both signals]
5. Pros¶
| Benefit | Detail |
|---|---|
| Uniform interface | Vector, BM25, web, SQL all look the same to the app |
| Composable | Wrap retrievers in retrievers (multi-query, ensemble) |
| LCEL-friendly | Pipes into prompt | model | parser |
| Pluggable | Swap implementation without changing pipeline |
| Hybrid power | Dense + sparse covers complementary failure modes |
6. Cons¶
| Limitation | Detail |
|---|---|
| More moving parts | Hybrid has two retrieval calls per query |
| Tuning hybrid weights | Empirical, depends on corpus |
| BM25 needs separate index | Memory or disk overhead alongside the vector store |
| Score normalization | Dense scores ∈ [-1,1]; BM25 unbounded — need RRF (rank-based, not score-based) |
7. Trade-offs¶
| Choice | Trade-off |
|---|---|
| Dense only | Simpler, misses exact terms |
| Sparse only | Misses semantics |
| Hybrid | Best recall, 2× cost & latency |
| Threshold-based | Safer, may return empty |
| MMR | Diverse, may miss the actual best |
Default: hybrid retrieval (dense + BM25) for any non-trivial production RAG.
8. Real-world Industry Usage¶
OpenAI¶
- File search in Assistants API uses hybrid retrieval internally.
- ChatGPT memory retrieval is dense-only (smaller corpus, simpler match).
Anthropic¶
- Contextual Retrieval research recommends dense + BM25 + reranker as the standard stack.
- Anthropic's reference architecture uses hybrid by default.
Google¶
- Vertex AI Search offers both dense and sparse, configurable.
- Google Search itself is hybrid (BM25 + neural ranking + many signals).
Enterprise¶
- Bloomberg Terminal uses hybrid: BM25 for tickers/codes, dense for narrative.
- GitHub Code Search uses hybrid: BM25 for symbol names, dense for natural language.
- Notion AI uses pure dense (their content is mostly prose).
Production patterns¶
| Pattern | Where |
|---|---|
| Pure dense | Notion AI, NotebookLM (narrative-heavy) |
| Hybrid (default) | Most enterprise RAG |
| Dense + reranker | High-precision use cases |
| Hybrid + reranker | Best-in-class (Anthropic Contextual Retrieval) |
9. Interview Questions¶
Beginner¶
- What's a retriever? — Function: query → list of relevant documents.
- Dense vs sparse? — Dense = embeddings (semantic). Sparse = BM25/TF-IDF (keyword).
- What's BM25 for? — Keyword matching with frequency normalization.
Intermediate¶
- Why hybrid retrieval beats pure dense? — Dense misses exact terms (codes, names). Hybrid covers both signals.
- What's RRF? — Reciprocal Rank Fusion: combines ranked lists by
1/(k + rank)summed across retrievers. - TF-IDF vs BM25 — what changed? — BM25 added term saturation (k1) and document length normalization (b).
- What's MMR for? — Diverse top-k. Avoids returning many near-duplicates.
Advanced¶
- Why does score-based fusion fail and rank-based (RRF) succeed? — Dense cosine ∈ [0,1]; BM25 unbounded. Direct score sum is biased toward whichever has higher scale. RRF uses ranks (1, 2, 3) which are comparable across retrievers.
- How tune hybrid weights? — Build eval set, grid-search weights (0.0-1.0 for one retriever, complement for the other), pick best recall@k. Often 0.6/0.4 dense-favoring works.
- When pure BM25 still wins? — Legal contracts, code search by exact symbol, error-code lookup — anywhere terminology matters more than meaning.
System design¶
- Design retrieval for a code-search product. — Dual indexes: BM25 (exact symbols, identifier matches) + dense (natural language queries → code). Hybrid + reranker. Tree-sitter splitting for code chunks.
- Plan A/B test of dense vs hybrid retrieval. — Build both retrievers; run 1000 queries through each; compute recall@5, precision@5, MRR; compare; statistical significance test.
10. Common Mistakes¶
Beginners¶
- ❌ Calling
.get_relevant_documents()(deprecated) instead of.invoke(). - ❌ Hardcoding
k=4without measuring. - ❌ Skipping hybrid retrieval → missing exact-term queries.
- ❌ Confusing
MMRwith reranking — they're different.
Production teams¶
- ❌ No retrieval logging — can't debug bad answers.
- ❌ BM25 in-memory at 100M chunks → OOM. Use a proper BM25 store (Elasticsearch, OpenSearch, Vespa).
- ❌ Not tuning hybrid weights → suboptimal recall.
- ❌ Using dense for queries with exact identifiers (error codes, SKUs).
How to avoid¶
- Log
(query, retrieved_chunks, scores)to LangSmith. - For corpora >10M: serve BM25 from Elasticsearch alongside the vector store.
- Build a RAGAS eval set; tune weights via grid search.
- Detect identifier-heavy queries; route to BM25-favored retrieval.
11. Best Practices¶
Industry standards¶
- Hybrid retrieval (dense + BM25) is the default for production.
- Use
.invoke()(modern API); not.get_relevant_documents(). - Log every retrieval — query, retriever name, returned chunks, scores.
- Always filter by metadata in multi-tenant.
Production¶
- Tune
kper use case — 3-5 for chat, 8-15 for summarization. - Threshold filtering for safety-critical paths.
- Retrieve broadly (k=20), rerank to k=5 (Chapter 7) for high quality.
- Cache common queries at the retriever layer.
Optimization¶
- Pre-build the BM25 index offline; reload at startup.
- Use
asyncretrievers in async pipelines. - Batch retrieval if processing many queries.
12. Evolution Story¶
flowchart LR
A[TF-IDF<br/>keyword frequency] --> B[BM25<br/>term saturation + length norm]
B --> C[Dense embedding retrievers<br/>semantic match]
C --> D[Hybrid dense + sparse<br/>RRF fusion]
D --> E[Multi-query retrievers<br/>paraphrase expansion]
E --> F[Self-query retrievers<br/>NL filters]
F --> G[Reranking<br/>cross-encoder precision]
Where we are: Retrievers are a composable layer. Choose dense, sparse, or hybrid based on workload.
Where we're going (next chapter): Production RAG needs more than just dense+sparse. We'll cover Advanced Retrievers — Multi-Query (paraphrase coverage), Self-Query (NL → metadata filter), Parent-Document (small match + big context), Contextual Compression (filter irrelevant sentences), and the king of all: Reranking with cross-encoders.
Practice¶
What does this print?
Expected: True
Use hybrid (not pure dense) for queries with error codes / SKUs
Expected: True
Quiz — Quick check¶
What you remember
Q1. Why hybrid retrieval?
- Combines semantic (dense) with exact-keyword (sparse) — each catches what the other misses
- Pure dense is deprecated
- BM25 is faster
- Required by LangChain
Q2. What's the smoothing constant k in RRF set to?
- 1
- 10
- 60 (from the original paper)
- 1000
Q3. When does BM25 outperform dense retrieval?
- Queries with exact terms (error codes, identifiers, named entities)
- Multilingual queries
- Paraphrased queries
- Long documents
Common doubts¶
Hybrid is more expensive — when worth it?
For any production RAG. The +10-30% recall easily justifies 2× retrieval cost (retrieval is the cheapest part of the pipeline — LLM dominates).
Can I do score-based fusion instead of RRF?
Dangerous. Dense and sparse scores live on different scales (cosine ∈ [0,1] vs BM25 unbounded). Direct sum is biased. RRF uses ranks → fair across retrievers.
Should I always use MMR?
Only when your corpus has redundancy (multiple chunks restating same fact). For diverse, non-repetitive corpora, pure similarity is fine.