Combine lexical precision with semantic recall
How to study this lesson
Read one section, trace the worked example, and try the knowledge check. Bookmark saves a shortcut; notes appear in My notebook. Mark lesson complete is your own assessment and does not mark its lab passed.
By the end, you can
- Explain term-frequency saturation and length normalization in sparse retrieval.
- Distinguish dense similarity from factual support.
- Fuse heterogeneous rankings without pretending their raw scores are comparable.
Sparse retrieval rewards matching evidence
Sparse retrieval represents documents through terms and associated weights. BM25 combines term rarity, a saturating contribution from repeated terms, and document-length normalization. This makes a rare exact identifier useful without letting endless repetition increase its contribution linearly. Tokenization, case handling, stemming, and field structure still influence which matches exist. The ranking formula cannot repair an identifier destroyed during ingestion.
For a query about error E742, exact lexical matching can be more useful than broad semantic similarity. A dense retriever might return documents about similar errors while missing the exact code. Keep an explicit evaluation slice for identifiers, product names, versions, and quoted phrases. These cases often reveal why replacing a good lexical baseline with a more elaborate model can reduce practical reliability.
Dense retrieval learns another notion of proximity
Dense Passage Retrieval encodes questions and passages into vectors whose similarity supports retrieval. Bi-encoder designs allow document representations to be computed ahead of time, which makes large candidate searches practical. A query about requests waiting too long may retrieve a passage using timeout configuration even without exact word overlap. This is useful semantic recall, but the similarity value is not the probability that the passage answers the question correctly.
Dense representations can blur distinctions involving negation, dates, version numbers, or neighboring entities. Embedding choice, training domain, vector normalization, and similarity metric affect results. Record the embedding model and index version together. Mixing query vectors from one model with stored vectors from another generally breaks the intended geometry even if both happen to have the same dimensionality.
A small BM25 trace
The code indexes three short documents. One contains timeout configuration, another billing payment, and the third configuration guide. The query asks for timeout configuration. The first document matches both terms, the third matches one, and the billing document matches neither. A positive-IDF BM25 variant produces that expected order while accounting for term counts and document lengths.
With parameters k1 equal to 1.2 and b equal to 0.75, increasing a term's frequency from one to two helps less than doubling a linear count would. Longer documents receive a length adjustment relative to average length. These are tuning parameters, not universal constants. The implementation is intentionally small: it omits fields, token positions, language analysis, and an inverted index, making it suitable for inspection rather than production-scale search.
Rank fusion avoids raw-score confusion
A BM25 score of 7 and a cosine score of 0.8 are not naturally comparable. Reciprocal rank fusion combines positions instead: each list contributes 1 / (k + rank) for a document it contains. Adding contributions rewards candidates appearing near the top of multiple lists. The smoothing constant k limits how strongly a single first-place rank dominates. The original RRF paper provides the method; the lab implements a transparent variant with deterministic ties and access filtering.
If document A ranks first in one list and third in another, while B ranks second in both, their contributions can be calculated directly. Rank fusion loses information about score margins, so it is not automatically better than a calibrated weighted-score model. It is a robust baseline when heterogeneous score scales are difficult to align.
Candidate diversity needs controlled boundaries
Run sparse and dense retrieval under the same access and time filters, merge candidates by stable chunk identity, and inspect where each method contributes unique relevant evidence. Deduplicate repeated chunks before final context selection. If one retriever returns many overlapping versions of the same paragraph, agreement may reflect indexing duplication rather than independent evidence. A fusion score should not be described as source consensus.
Evaluate sparse-only, dense-only, and hybrid retrieval on the same questions and corpus snapshot. Report per-query wins and failures as well as an average. A hybrid system adds indexing and query cost, so it should solve a concrete recall or precision gap. When it fails, inspect candidate presence first. No reranker can promote an answer-bearing passage that neither first-stage retriever included in its candidate pool.
Work through the code
This executable BM25 variant uses positive IDF, whitespace-like regex tokenization, and a tiny in-memory corpus. It demonstrates sparse scoring exactly; it is not dense retrieval or a scalable search engine. Change document lengths to inspect normalization.
import math
import re
from collections import Counter
texts = {"d1": "timeout configuration timeout", "d2": "billing payment", "d3": "configuration guide"}
documents = {key: re.findall(r"\w+", text.lower()) for key, text in texts.items()}
query = {"timeout", "configuration"}
average = sum(map(len, documents.values())) / len(documents)
k1, b = 1.2, 0.75
def score(tokens):
counts = Counter(tokens)
result = 0.0
for term in query:
frequency = counts[term]
containing = sum(term in words for words in documents.values())
idf = math.log(1 + (len(documents) - containing + 0.5) / (containing + 0.5))
normalizer = frequency + k1 * (1 - b + b * len(tokens) / average)
if frequency:
result += idf * frequency * (k1 + 1) / normalizer
return result
ranked = sorted(documents, key=lambda key: (-score(documents[key]), key))
print(ranked)
print("positive matches:", [key for key in ranked if score(documents[key]) > 0])
['d1', 'd3', 'd2'] positive matches: ['d1', 'd3']
Run Python snippets locally with the prerequisites named above. The module coding lab runs directly in your browser.
Pause and reason
A dense result has cosine 0.82 and a lexical result has BM25 6.4. Why is adding these raw scores questionable, and what baseline avoids that issue?
Check your understanding
A passage is absent from every first-stage candidate list. What can a reranker do?
Your notes
Explain the mechanism in your own words. Add a failure you want to test.
Go deeper with primary sources
- Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks
- Dense Passage Retrieval for Open-Domain Question Answering
- The Probabilistic Relevance Framework: BM25 and Beyond
- Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning Methods
- Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks