Dense retrieval finds relevant documents by comparing the meaning of a query against the meaning of every document, using learned vector embeddings and nearest-neighbor search. Sparse retrieval finds relevant documents by comparing the words a query and a document have in common, using weighted term-matching algorithms like BM25 over an inverted index. Both are old ideas wearing different-generation clothes: sparse retrieval descends from classical information retrieval going back to the 1970s, dense retrieval from neural encoders that only became practical for large-scale search around 2020. The choice between them, and increasingly how to combine them rather than choose, is one of the first architectural decisions behind every modern search engine, RAG pipeline, and enterprise knowledge base.
Sparse Retrieval: Matching Words
Sparse retrieval represents a query or document as a vector where almost every dimension is zero: one dimension per term in the vocabulary, with a nonzero weight only where that term actually appears. This sparsity is what gives the family its name, and it maps directly onto the inverted index, a data structure that stores, for every term, the list of documents containing it. Looking up matches for a query becomes a fast lookup and merge over a handful of posting lists instead of a scan over the full corpus. This is also why sparse retrieval scales so cheaply: adding a document means appending to a few posting lists, not retraining anything.
Sparse retrieval’s clearest win shows up whenever a query hinges on one exact, rarely-seen token, such as an error code a dense encoder has barely trained on:
# Scenario: a support search bar where users paste exact error codes
from rank_bm25 import BM25Okapi
docs = [
"Error E1042: disk quota exceeded on volume /data.",
"Error E2091: authentication token expired, please re-login.",
"General troubleshooting guide for slow uploads.",
]
bm25 = BM25Okapi([d.lower().split() for d in docs])
query = "E1042"
scores = bm25.get_scores(query.lower().split())
print(docs[scores.argmax()])
# "Error E1042: disk quota exceeded on volume /data."
# A dense encoder, which rarely sees this exact alphanumeric code during
# training, might not rank it above the generic troubleshooting guide.
A Short History: From Term Counting to BM25
Sparse retrieval’s lineage runs through decades of information retrieval research, long before “retrieval” was something an LLM needed. Early systems in the 1960s and 1970s scored documents by raw term frequency: the more times a query word appeared, the higher the document ranked. That approach broke down quickly, since common words like “the” or “system” appear everywhere and carry little signal. TF-IDF (term frequency, inverse document frequency) fixed this by downweighting terms that appear in most documents and upweighting terms that are rare and therefore more discriminating.
BM25 (Best Matching 25, so named because it was the 25th ranking function variant tried in the series of experiments that produced it) refined TF-IDF with two ideas that turned out to matter enormously in practice: term-frequency saturation (a term appearing 20 times shouldn’t score 20 times higher than a term appearing once) and document-length normalization (a long document shouldn’t win purely by containing more words). Formalized by Stephen Robertson and Karen Spärck Jones’s research group at City University London through the 1980s and 1990s (BM25 itself was published in 1994, and first used in the Okapi retrieval system, which is why it’s sometimes called Okapi BM25) and consolidated in Robertson and Zaragoza’s 2009 survey, BM25 has remained the default scoring function in Lucene, Elasticsearch, OpenSearch, and every major sparse retrieval library for over three decades, and it is still the baseline every new dense or learned-sparse method reports its improvement against.
BM25’s tunable constants (k₁ for term-frequency saturation, b for length normalization) and the formula itself are covered in full, with an interactive widget, in the dedicated BM25 entry. This gives sparse retrieval real strengths: it is exact, interpretable (you can point to precisely which shared terms drove a match), cheap to run at any scale, and unbeatable at matching rare, specific tokens such as product SKUs, error codes, legal citations, or a person’s name, exactly the tokens a dense model tends to blur together. Its weakness is the mirror image of that strength: it only sees vocabulary, not concepts. A query about a “heart attack” will not match a document that only says “myocardial infarction,” even though they mean the same thing. This is the classic vocabulary mismatch problem.
Dense Retrieval: Matching Meaning
Dense retrieval replaces the sparse, mostly-zero term vector with a dense, low-dimensional embedding: every document is passed once through a neural encoder and reduced to a few hundred or thousand real-valued numbers that capture its meaning, not its exact wording. Queries are encoded with the same (or a paired) model at search time, and retrieval becomes nearest-neighbor search in that embedding space, typically accelerated with an index like HNSW.
Because the query and document are each encoded independently, with no interaction between them until the similarity score is computed, dense retrievers are architecturally bi-encoders; see Cross-Encoder vs. Bi-Encoder for how that compares to the joint-encoding models used downstream for reranking. Dense retrieval only became competitive with decades-tuned sparse baselines once Dense Passage Retrieval (DPR) showed, in 2020, that a BERT-based bi-encoder trained with contrastive learning on question-passage pairs could outperform a strong Lucene-BM25 system by 9 to 19 points of top-20 retrieval accuracy on open-domain QA. That result is generally treated as the moment dense retrieval became the default choice for semantic search rather than a research curiosity.
Dense retrieval’s strength is exactly sparse retrieval’s weakness: it generalizes across paraphrase, synonymy, and cross-lingual phrasing, because the encoder has learned what concepts mean, not just which characters appear. Its weakness is the mirror image too: embeddings compress meaning into a fixed-size vector, and fine-grained lexical detail, an exact part number, a specific date, a rare acronym the encoder never saw much of during training, can get lost in that compression. This is sometimes called the exact-match problem, and it’s why a purely dense system can confidently retrieve a plausible-sounding but factually wrong passage when the query hinges on one precise token.
Dense retrieval’s clearest win is the mirror image of the BM25 scenario above: a query that shares almost no vocabulary with the document that actually answers it.
# Scenario: the user's wording never overlaps with the document's wording
from sentence_transformers import SentenceTransformer
encoder = SentenceTransformer("BAAI/bge-base-en-v1.5")
docs = [
"Employees may expense meals during business travel up to $75/day.",
"The office kitchen is stocked with coffee and snacks.",
]
doc_vectors = encoder.encode(docs, normalize_embeddings=True)
query = "What's the per diem for food when I'm on a work trip?"
query_vector = encoder.encode(query, normalize_embeddings=True)
scores = doc_vectors @ query_vector
print(docs[scores.argmax()])
# Correctly surfaces the expense policy even though "per diem," "food,"
# and "work trip" never appear in the source text; BM25 alone would see
# zero shared vocabulary between the query and the correct document.
Architecture Side by Side
graph TB
classDef default fill:#ffffff,stroke:#4338CA,stroke-width:2px,color:#0F172A,rx:8px,ry:8px;
classDef data fill:#EEF0F7,stroke:#0D9488,stroke-width:2px,color:#0F172A,rx:8px,ry:8px;
classDef process fill:#F7F8FC,stroke:#6366F1,stroke-width:2px,color:#0F172A,rx:8px,ry:8px;
classDef output fill:#4338CA,stroke:#4338CA,stroke-width:2px,color:#ffffff,rx:8px,ry:8px;
subgraph SP["Sparse Retrieval"]
D1([Documents]):::data --> TOK[Tokenize + Weight<br/>TF-IDF / BM25]:::process --> INV[(Inverted Index)]:::data
Q1([Query]):::data --> QTOK[Tokenize]:::process --> INV
INV --> SR[Term-Overlap Ranking]:::output
end
subgraph DN["Dense Retrieval"]
D2([Documents]):::data --> ENC1[Embedding Model]:::process --> VEC[(Vector Index HNSW)]:::data
Q2([Query]):::data --> ENC2[Embedding Model]:::process --> VEC
VEC --> DR[Similarity Ranking]:::output
end
Both pipelines share the same shape: an offline indexing path that processes documents once, and an online query path that only has to process the incoming query, which is what makes either approach fast enough for production search at scale.
Head-to-Head Comparison
| Sparse Retrieval (BM25) | Dense Retrieval (embeddings) | |
|---|---|---|
| Representation | Weighted term vector, mostly zeros | Dense vector, every dimension populated |
| Matches on | Shared vocabulary | Shared meaning |
| Training required | None; a closed-form statistical formula | Yes; a neural encoder trained on relevance pairs |
| Strong on | Exact identifiers, rare tokens, keyword queries | Paraphrase, synonymy, cross-lingual queries |
| Weak on | Synonyms, paraphrase, conceptual queries | Exact IDs, numbers, rare acronyms |
| Interpretability | High; matched terms are visible | Low; similarity score has no human-readable cause |
| Index structure | Inverted index | ANN graph or cluster index (e.g. HNSW) |
| Out-of-domain generalization | Stable; statistics don’t depend on training data | Can degrade on domains far from training data |
Learned Sparse Retrieval: A Middle Ground
SPLADE (Sparse Lexical and Expansion model) sits between the two families. Like classical sparse retrieval, it produces a sparse, vocabulary-sized vector that plugs directly into an ordinary inverted index, keeping the speed and interpretability of BM25-style search. But unlike BM25, the weights aren’t hand-derived statistics: a transformer learns which terms to weight highly, and which related terms to add even if they never appeared in the original text (query and document expansion), closing much of the vocabulary-mismatch gap that plain sparse retrieval suffers from. SPLADE and its successors (SPLADE v2, and 2025-era variants) are the clearest evidence that “sparse” and “semantic” are not opposites: sparsity is a representation choice, and semantics can be learned on top of it.
Hybrid Retrieval: Combining Both
Because dense and sparse retrieval fail in complementary ways, production systems increasingly run both and fuse the results rather than committing to one. The hard part isn’t running two searches, it’s combining two scores that live on entirely different, incomparable scales: a BM25 score is an unbounded statistic that depends on corpus size and term rarity, while a cosine similarity from an embedding model is bounded between -1 and 1 and means something completely different. Averaging them directly, without accounting for that mismatch, tends to let whichever method happens to produce larger raw numbers dominate the result regardless of which one is actually more relevant.
Two fusion strategies address this in different ways:
- Score-based fusion (e.g. Weaviate’s
alphaparameter): first normalize each method’s scores onto a shared 0-to-1 scale (typically min-max normalization within the current result set), then linearly combine them with a tunable weight controlling how much each contributes. - Rank-based fusion, most commonly Reciprocal Rank Fusion (RRF): sidestep the normalization problem entirely by ignoring the raw scores and combining results using only each item’s rank position in each list, as shown in the code example above. This is why RRF has become the more common default in vector databases: it needs no calibration step and is immune to one method’s scores being on a wildly different scale than the other’s.
Some embedding models now collapse this into a single pass: BGE-M3 produces dense, sparse, and multi-vector (ColBERT-style) representations from one forward pass, so a hybrid pipeline doesn’t require hosting two separate models.
Tuning the Blend: An Interactive Alpha Slider
The widget below makes the score-based fusion tradeoff concrete. Four candidate passages are scored against the query “return policy for defective item SKU-4471” by both a sparse method (which rewards the exact SKU and “return policy” match) and a dense method (which rewards the paraphrased “broken product” passage even though it never says “SKU-4471” or “policy”). Drag alpha from 0 (pure BM25) to 1 (pure dense) and watch the ranking itself reorder, not just the numbers change:
At low alpha, the exact SKU match wins on the strength of its keyword overlap. Push alpha past roughly 0.6 and the paraphrased “broken product” passage overtakes it, since nothing in that passage shares vocabulary with the query, but a dense encoder recognizes the intent is identical. Neither ranking is “wrong;” they’re optimizing for different notions of relevance, which is exactly why teams tune alpha (or switch to RRF) against their own labeled query set rather than trusting a default blindly.
Case Study: Hybrid Search in E-commerce Product Search
E-commerce search is one of the clearest real-world cases for hybrid retrieval, because the two failure modes it needs to avoid are almost a textbook description of sparse and dense weaknesses. A shopper searching for an exact model number, “WH-1000XM5,” needs sparse retrieval’s exact-match precision; an embedding model might retrieve a similar-sounding but wrong product because it never learned that specific alphanumeric string carries almost all the meaning in that query. A shopper searching “comfortable noise-cancelling headphones for long flights” needs dense retrieval’s conceptual understanding; a pure BM25 system will underweight or miss products whose listings never use the words “comfortable” or “flights” even though they’re the best match.
Production catalogs typically resolve this by running BM25 over structured fields (SKU, brand, model number, category) and dense retrieval over unstructured fields (descriptions, reviews, marketing copy), then fusing the two candidate sets, often with a higher alpha weight toward sparse for queries that look like model numbers and a higher weight toward dense for queries that read like natural language. This is one of the main reasons Qdrant, Weaviate, and Elasticsearch all ship hybrid search as a first-class query type rather than something a team assembles from two separate services.
# Scenario: route the sparse/dense blend based on what the query looks like
import re
def search(query, sparse_index, dense_index, top_k=10):
looks_like_sku = bool(re.fullmatch(r"[A-Z]{2,}-?\d{3,}[A-Z0-9]*", query.strip()))
alpha = 0.15 if looks_like_sku else 0.75 # lean sparse for ID-shaped queries
sparse_hits = sparse_index.search(query, top_k=50)
dense_hits = dense_index.search(query, top_k=50)
return fuse(sparse_hits, dense_hits, alpha=alpha)[:top_k] # same alpha blend as the widget above
What’s New (2025-2026)
- Hybrid search as the default, not the advanced option. Qdrant’s Universal Query API (introduced in Qdrant 1.10 and matured through 2025) lets a single query combine dense vectors, sparse vectors, and even ColBERT-style multi-vectors with a chosen fusion method in one call, reflecting a broader shift: hybrid retrieval is now the out-of-the-box recommendation from most major vector databases (Qdrant, Weaviate, Elasticsearch, Pinecone), not a manual pipeline teams had to assemble themselves.
- Unified dense/sparse/multi-vector models. BGE-M3-style models, which emit all three representations from one encoder, have become the practical default for teams that don’t want to operate a separate BM25 stack and a separate embedding model.
- Learned sparse retrieval in production. SPLADE-family models moved from research benchmarks to production search in 2025-2026, particularly in e-commerce and enterprise search where exact product identifiers and codes matter alongside semantic intent.
- Adaptive, query-dependent fusion weights. Rather than a single fixed alpha for an entire index, 2025-2026 hybrid search implementations increasingly classify the incoming query first (does it look like a keyword lookup or a natural-language question?) and adjust the sparse/dense blend per query, automating the manual alpha-tuning that early hybrid systems required.
- Reranking as the standard third stage. Whether retrieval is dense, sparse, or hybrid, teams increasingly treat the retrieved candidate set as an input to a separate reranking stage rather than a final answer, since neither dense nor sparse (nor their fusion) fully replaces the precision of a joint query-document model.
Practical Guidance
| Scenario | Recommendation |
|---|---|
| Legal, medical, or product-catalog search with exact codes/IDs | Sparse (BM25) or hybrid; pure dense risks missing exact matches |
| Conversational or paraphrased natural-language queries | Dense retrieval; embeddings generalize across phrasing |
| General-purpose enterprise search or RAG | Hybrid (dense + sparse), fused with RRF or a native hybrid API |
| Multilingual or cross-lingual search | Dense retrieval with a multilingual encoder (or BGE-M3 hybrid) |
| Minimal infrastructure, no training data available | Sparse (BM25); works out of the box with no model to host |
| High query volume, tight latency budget | Sparse alone, or dense with a well-tuned ANN index; hybrid adds a fusion step’s worth of latency |
Neither family is strictly better: sparse retrieval is a precise, cheap, statistically grounded baseline that can’t see past vocabulary, and dense retrieval understands meaning at the cost of losing exact lexical precision. The dominant pattern in 2025-2026 production systems isn’t choosing one, it’s routing both into a shared candidate set and letting fusion, and often a reranker downstream, sort out which evidence actually answers the query.
How to Use: Hybrid retrieval combining BM25 sparse search and dense embeddings
from rank_bm25 import BM25Okapi
from sentence_transformers import SentenceTransformer
import numpy as np
corpus = [
"Qdrant's Universal Query API fuses dense and sparse vectors in one call.",
"BM25 ranks documents by term frequency, inverse document frequency, and length.",
"SPLADE learns sparse term weights and expansions directly from a transformer.",
"Restarting the Kubernetes pod cleared the OOMKilled error on node-4.",
]
# Sparse side: BM25 over a tokenized corpus (exact term matching)
tokenized_corpus = [doc.lower().split() for doc in corpus]
bm25 = BM25Okapi(tokenized_corpus)
# Dense side: sentence embeddings (semantic matching)
encoder = SentenceTransformer("BAAI/bge-base-en-v1.5")
doc_vectors = encoder.encode(corpus, normalize_embeddings=True)
query = "How does BM25 score a document?"
sparse_scores = bm25.get_scores(query.lower().split())
sparse_rank = np.argsort(sparse_scores)[::-1]
query_vector = encoder.encode(query, normalize_embeddings=True)
dense_scores = doc_vectors @ query_vector
dense_rank = np.argsort(dense_scores)[::-1]
# Fuse both rankings with Reciprocal Rank Fusion
# see /glossary/reciprocal-rank-fusion-rrf
def rrf_fuse(*rankings, k=60):
fused = {}
for ranking in rankings:
for pos, idx in enumerate(ranking):
fused[idx] = fused.get(idx, 0) + 1 / (k + pos + 1)
return sorted(fused, key=fused.get, reverse=True)
for idx in rrf_fuse(sparse_rank, dense_rank):
print(corpus[idx])
Ready to build?
Leverage AI technologies to build your product stack
Superteams can help you build, deploy and launch AI application stacks using open source technologies — from architecture through to production.
Talk to Superteams