BM25 and Sparse Lexical Retrieval
No embeddings, no neural network — just term counts, weighted by how rare each term is and how long each document is. Still the backbone of most production search.
Overview
What this is
Before dense vector search, and still running alongside it in most real systems, is sparse lexical search: score a document by which query terms it contains, weighted by how informative each term is. BM25 is the version of this idea that actually works well, and it needs no training, no GPU, and no embedding model.
Parameters
query: "python list methods"
Ranked Documents
—Top Document, Term By Term
Corpus Stats
BM25: A Practical Guide
Keyword search, done properly.
The formula
score(D,Q) = Σt∈Q IDF(t) · f(t,D)(k1+1) / (f(t,D) + k1(1 − b + b·|D|/avgdl))
Three ideas, one formula. IDF(t) — a term that appears in fewer documents is more informative, so it is weighted higher. f(t,D) — more occurrences of a query term help, but with diminishing returns, controlled by k1. |D|/avgdl — a document's raw term count is normalised against its own length, controlled by b, so a long document does not win purely by containing more words.
Scoring by term overlap, carefully
BM25 is the keyword retrieval function that most search engines are built on. It scores a document against a query by adding up a contribution per matching term:
score(d, q) = Σt ∈ q IDF(t) × (f(t,d) × (k₁+1)) / (f(t,d) + k₁ × (1 − b + b × |d|/avgdl))
That looks worse than it is. Three ideas are encoded in it, and each fixes a specific defect of naive term counting.
Rare terms count more (IDF). A match on "encephalopathy" is far more informative than a match on "the". The inverse document frequency term weights each match by how rare the term is across the collection.
Repetition saturates. A document mentioning a term twenty times is not twenty times more relevant than one mentioning it once. The k₁ parameter controls how quickly the contribution flattens — typically around 1.2, which means the third and fourth occurrences add much less than the first.
Long documents are penalised. A 10,000-word document will contain most query terms by chance. Dividing by document length relative to the average corrects for it, with b (typically 0.75) controlling how strongly.
Those three corrections are the entire difference between BM25 and TF-IDF, and they are why BM25 has been the standard for thirty years.
Where it beats embeddings
Sparse retrieval matches tokens, which is exactly what embeddings are bad at:
| Query | BM25 | Dense |
|---|---|---|
| "error code E4471" | Exact match | Blurs into other error codes |
| "section 12.3(b)" | Exact match | Approximate |
| "Nakamura et al 2019" | Exact match | Similar-looking citations |
| "holiday allowance" | Misses "annual leave" | Finds it |
| "cheapest shipping" | Misses "lowest-cost delivery" | Finds it |
The pattern: identifiers, names, codes and rare technical terms belong to BM25; paraphrase belongs to embeddings. Their failures do not overlap, which is the argument for using both.
Three further advantages that matter operationally. BM25 needs no model, no GPU and no embedding step, so indexing is fast and cheap. It handles a brand-new term the moment it is indexed, where an embedding model has never seen it. And it is transparent — you can explain exactly why a document scored highly, which matters for debugging and for audit.
Building an index
Sparse retrieval is an inverted index: for each term, the list of documents containing it and their term frequencies. A query touches only the lists for its own terms, which is why it is fast even over very large collections.
Preprocessing matters more here than for dense retrieval, because matching is literal:
- Lowercase, so "Password" matches "password".
- Tokenise consistently between indexing and querying — a mismatch means nothing matches.
- Remove stop words, or accept that IDF already suppresses them.
- Stem or lemmatise so "running" matches "run". Apply the same treatment to the query.
- Keep identifiers intact. A tokeniser that splits "E4471" into "E" and "4471" has destroyed the thing BM25 was best at.
That last point is the one that goes wrong most often in practice, and it is worth testing explicitly with a few known identifiers.
The two knobs TF-IDF was missing
BM25 is TF-IDF with two corrections -- term frequency saturates, and document length is discounted by a tunable amount. This implements it beside TF-IDF on the same documents so you can see exactly which ranking each correction changes.
Things to try
- Read the default ranking. D2 leads — it repeats "list" four times and mentions every query term at least twice, and that repetition still wins even after the default length normalization is applied.
- Drag b to 0. Length normalization is off entirely. D2's lead grows further still, since its extra length now costs it nothing at all.
- Push b to 1. Full length normalization. D2's lead shrinks — but does not disappear. A four-times repeated rare term is still worth more than one clean mention, even once length is fully accounted for.
- Now drag k1 to 0. This is the setting that actually flips it: D1 overtakes D2. With k1 at 0, repeating a term buys nothing at all — only whether a term is present matters — so D2's four mentions of "list" count exactly the same as D1's one, and D1 wins on being the tighter, fully on-topic match.
- The lesson in that flip: b alone tempers term-stuffing; k1 is what actually caps it. The two parameters are doing different jobs, and neither one alone fully neutralises a document that just repeats the query terms.
Why it still matters next to embeddings
BM25 gets exact terms right where embeddings can blur them — product codes, error messages, names, acronyms. It is also completely interpretable: every score decomposes into per-term contributions, which is why the breakdown panel above can show exactly where a score came from. This is the sparse half of the hybrid search that most production RAG systems actually run.
What to remember
BM25 scores a document by summing, over each query term it contains, that term's rarity across the corpus times a saturating function of how often it appears, normalised by document length. b controls how much long documents are penalised for being long; k1 controls how much repeated terms are rewarded before diminishing returns kick in. No embeddings, no training — and still the metric most search engines reach for first.
In code
from rank_bm25 import BM25Okapi
corpus = [doc.lower().split() for doc in documents] # consistent tokenising
bm25 = BM25Okapi(corpus, k1=1.5, b=0.75)
scores = bm25.get_scores("annual leave policy".lower().split())
top = sorted(range(len(scores)), key=lambda i: -scores[i])[:10]For anything beyond a prototype, use a real search engine — Elasticsearch, OpenSearch, Lucene, Tantivy or a vector database with built-in sparse support. They provide the inverted index, incremental updates, filtering, and analysers that handle identifiers and multiple languages properly.
Parameter guidance: k₁ between 1.2 and 2.0, b around 0.75. Neither is sensitive, and the defaults are almost always fine. Effort spent on tokenisation and analysers pays far better than tuning these.
Learned sparse retrieval
A newer family keeps the inverted-index machinery and learns the weights.
SPLADE uses a transformer to predict, for each document, a sparse weight over the whole vocabulary — including terms that do not literally appear. A document about "annual leave" can be given weight on "holiday", so the sparse index gains some semantic matching.
The result sits between the two worlds: it retains the exact-match strength and index efficiency of sparse retrieval while handling some paraphrase. The cost is a model pass per document at indexing time, and larger posting lists.
Whether it beats BM25-plus-dense-hybrid depends on the corpus, and it is worth evaluating rather than assuming.
Questions people ask
Is BM25 outdated? No. It remains a strong baseline that dense retrieval alone often fails to beat, particularly on technical corpora.
Do I need both BM25 and embeddings? For most real corpora, yes — hybrid retrieval is the standard recommendation and usually the largest single quality gain.
What is the difference from TF-IDF? Term-frequency saturation and document-length normalisation. Both matter, and both make BM25 better.
Should I remove stop words? IDF already suppresses them, so it is optional. Removing them shrinks the index.
Does it work for other languages? Yes, with appropriate tokenisation and stemming. Languages without whitespace word boundaries need a proper analyser.
How do I combine it with dense scores? Reciprocal rank fusion, which uses ranks only and needs no score normalisation.
Recap in one screen
- BM25 scores term overlap with three corrections: rare terms count more, repetition saturates, long documents are penalised.
- It excels at identifiers, codes, names and rare terms — exactly where embeddings blur.
- It needs no model or GPU, indexes instantly, and is fully explainable.
- Tokenisation must match between index and query, and must not split identifiers.
- Combine with dense retrieval via reciprocal rank fusion; learned sparse models such as SPLADE blur the boundary.