← All projects

Project — Interactive Learning

BM25 & Keyword Retrieval

Semantic search understands what you mean. But sometimes you don't want the meaning — you want the exact word: a part number, an error code, a surname. This page builds BM25, the search algorithm that still runs the web, one honest fix at a time — with a live experiment for every idea.

BM25 Keyword Search TF-IDF Lexical Retrieval Ranking
Contents
The blind spot
Ask a semantic search for "the cozy red animal" and it finds your story about a fox.
Ask it for "SKU‑4471X" — and it hands you 4470, 4472, anything shaped like a code.
Embeddings are brilliant at meaning and clumsy at identity. They blur near-neighbours together on purpose — that's the whole trick. But some searches don't want a neighbour; they want the one exact token and nothing else. For those, we need a method that reads the literal words. That method is keyword retrieval, and its modern form is BM25.

If you read the embeddings page, you saw the great modern idea: turn words into points in space so that close means similar in meaning. It's the engine behind semantic search and RAG. So why would anyone go back to matching literal words?

Because meaning-based search has a failure mode that is easy to miss: it never says "I don't know." Search for the error string "ERR_CONN_TIMED_OUT", a citation like "§ 230(c)(1)", a rare surname, a product SKU, a function name in a codebase — and an embedding model will confidently return things that are vaguely similar in vibe while sailing right past the exact match you needed. The words carry information that lives in the spelling itself, not in the cloud of meaning around them.

The one-sentence version: semantic search is recall for ideas; keyword search is precision for exact tokens. Rare, literal, out-of-vocabulary strings are exactly where embeddings are weakest and where keyword matching is strongest — which is the whole reason both still exist in 2026.

So this page rebuilds keyword search from its oldest form and watches it grow — through three specific, fixable problems — into BM25 (the name is an accident of history: it was the 25th "Best Matching" formula in a 1970s–90s research line at City University London). BM25 is thirty years old, embarrassingly simple, and still the default ranking function in Elasticsearch, OpenSearch, Lucene, and the search bar of half the sites you used today. Let's earn every piece of it.

The oldest idea

Start by just counting words

Here is the most naive search imaginable, and it's where everything begins. To find documents about "python," count how many times each document says "python." More mentions, higher rank. This is called bag‑of‑words, and the count is the term frequency (TF).

It is a genuinely good instinct — a document that keeps saying your word probably is about your word. But used raw, it has two glaring bugs that anyone can feel:

Bug 1 — repetition runs away. Is a page that says "python" 40 times really 40× more relevant than one that says it once? A spammer could win every search just by pasting a word a thousand times. There should be a point of diminishing returns.

Bug 2 — common words drown out rare ones. Search "the python tutorial." The word "the" appears in every document ever written — counting it adds noise, not signal. The rare, specific word ("python") is the one that actually tells you where to look. Raw counting treats them as equals.

And a quieter third problem — long documents cheat. A 10,000‑word page will rack up more mentions of almost any word than a tight 50‑word snippet, just by being long. Length shouldn't buy relevance.

Everything BM25 is can be read as three targeted repairs to raw counting — one per bug. The next section fixes them one at a time, each with a slider you can drag, so you feel why the knob exists before you ever see the whole formula. That's the whole method: never introduce a symbol you haven't already felt the need for.
Your turn — build it, one fix at a time

Three repairs, three knobs

Everything below runs the real BM25 math live in your browser — no faked numbers. Each widget isolates a single term of the formula so you can watch exactly what it does. Drag things. Break things. The formula will assemble itself by the end.

Fix A — Saturation: repetition with a ceiling
The cure for Bug 1. Instead of letting the count climb forever, BM25 runs it through a curve that rises fast, then flattens. The first mention of a word is a big deal; the twentieth barely moves the needle. Drag term frequency and watch the amber (BM25) curve peel away from the grey (raw) line. Then drag k₁ — the knob that sets how quickly the reward flattens.
Raw count (no ceiling) BM25 saturated
Term frequency5
How many times the word appears in the document.
k₁ · saturation1.5
Low = one mention is enough. High = repetition keeps earning credit.
Fix B — Length normalization: a word in a haystack counts less
The cure for the "long documents cheat" problem. All three documents below mention "python" exactly once — but they're wildly different lengths. A word in a tight abstract is a stronger signal than the same word lost in a textbook chapter. The b knob controls how hard BM25 leans on this: 0 ignores length entirely, 1 fully penalizes long documents.
b · length penalty0.75
0 = length is invisible · 0.75 = the sensible default · 1 = punish length fully.
Fix C — IDF: rare words carry the signal
The cure for Bug 2. Inverse Document Frequency asks a simple question of every word: how many documents in the whole collection contain it? A word in almost every document ("the") tells you nothing about which one you want, so it earns almost zero weight. A word in just one or two documents is a laser pointer. Click a term to light up the documents that contain it.
IDF(term) = ln( ( N df + 0.5 ) / ( df + 0.5 ) )
N = total documents (10 here) · df = documents containing the term. Fewer documents → higher IDF → more power to rank.
Everything, together

Assemble the whole formula

Now snap the three fixes together and you have BM25 — nothing more is hidden. For each word in the query, BM25 computes three things you already met — IDF (how rare, how much it matters), saturated TF (mentions, with a ceiling), length factor (adjusted for document size) — multiplies them, and sums across the query's words. That sum is the score. That's the entire algorithm.

score(query, doc) = term ∈ query   IDF(term) × TF·(k₁+1) / ( TF + k₁·( 1−b + b·(docLen / avgLen) ) )
Three familiar pieces: IDF = how much the word matters · the middle fraction = saturated TF · the b·(docLen/avgLen) term = the length factor.

Below is a tiny corpus with a fixed query — python transformers. Drag k₁ and b, then click any document to see its score fully worked out, term by term. The ranking re-sorts live. Try the challenge underneath.

Widget — the live BM25 ranker
Query: python transformers  ·  scored against a 10‑document collection (three shown, avg length ≈ 38 words).
k₁ · saturation1.5
Low = one mention is enough · high = repetition keeps helping.
b · length penalty0.75
0 = ignore length · 1 = fully penalize long docs.
Challenge: Doc B stuffs "python" nine times to game the search; Doc A says it once, honestly. Slide k₁ down toward 0.5 and watch A take #1 — saturation refuses to reward the spam. Now slide k₁ up past ~1.9 and B wins — you've told BM25 that repetition really does matter. That single knob is the whole defense against keyword stuffing. (Bonus: notice Doc C, the long one, never reaches #1 — it says each word once but pays the length penalty. Drag b to 0 and see how much it closes the gap.)
An honest ledger

Where BM25 shines — and where it goes blind

Every retrieval method is a set of trade-offs wearing a formula. BM25's strengths and its blind spots are two sides of the exact same coin: it reads the literal words and nothing else. That makes it razor-sharp on identity and stone-deaf to meaning.

Where it shines
  • Exact tokens. Part numbers, error codes, SKUs, function names, citations — the strings whose value is the spelling itself.
  • Rare & out-of-vocabulary words. A surname or acronym an embedding model never saw in training still matches perfectly, letter for letter.
  • Fully interpretable. You can read why a document ranked #1, term by term — you just did it above. No black box.
  • Fast & cheap. No GPU, no model, no vectors to store. An inverted index over billions of documents answers in milliseconds.
  • Zero training. It works on your data the moment you index it — no fine-tuning, no embeddings to compute.
Where it goes blind
  • Synonyms. "car" and "automobile" share no letters, so to BM25 they are strangers. It matches spellings, not concepts.
  • Paraphrase. "How do I fix a flat tyre?" won't find a doc titled "repairing a punctured wheel." No overlapping words, no match.
  • Meaning & intent. It has no idea "bank" can be a river or a vault. Every sense of a word is the same string to it.
  • Vocabulary mismatch. When the user and the document use different words for the same thing, BM25 simply misses — silently.

Look at those two columns side by side and something jumps out: they are almost perfect mirror images. Everything BM25 is bad at, semantic search is good at — and the one thing embeddings fumble (exact, rare tokens) is precisely what BM25 nails. That is not a coincidence. It's the whole reason for the last section.

The one thing to remember

An addition, never a replacement

It would be easy to leave this page thinking BM25 is the "old way" and embeddings are the "new way" — that one replaces the other. That is the single most common misread of modern search, and it's backwards. In serious systems, BM25 doesn't compete with semantic search; it rides alongside it, covering the exact cases where meaning-based search is weakest.

The mental model
Keep vector search as your default. Add BM25 as the specialist it can't replace.
Vector / Semantic
Your primary lens. Understands intent, synonyms, paraphrase, meaning. The right default for natural-language questions.
BM25 / Keyword
The specialist beside it. Catches the exact tokens, rare strings, and codes that embeddings blur — the searches that must be literal.

Think of it as a safety net under semantic search, not a rival to it. When a user pastes an error code or a product ID, BM25 is the layer that guarantees the exact match surfaces instead of being smoothed into a cloud of "close enough." You are not choosing between them — you are keeping vector search and letting BM25 catch what it drops.

The word for fusing the two scores into one ranked list is hybrid search — and it earns its own page, because doing it well (score normalization, reciprocal-rank fusion, re-ranking) is a real topic in itself. This page stops one step short on purpose: understand BM25 as a complement first, and hybrid will make far more sense when you get there.

Hybrid Search — fusing the two → (coming soon)
Last word
The oldest idea in search — count the words
never actually left.

Thirty years on, after every leap in neural search, the fastest way to guarantee you find the exact thing you typed is still to count words, weight the rare ones, and forgive the long documents. BM25 isn't a relic that embeddings made obsolete — it's the sharp, literal partner that makes meaning-based search trustworthy. Keep them both. The best systems always have.

What to explore next
Deep dive
Vectorization & Embeddings — the other half
Project
RAG Pipeline — retrieval in context
Concept · soon
Hybrid Search — fusing BM25 & vectors
Concept
TF Saturation & the k₁ parameter
Concept
Inverse Document Frequency (IDF)
Concept
Lexical vs. Semantic Retrieval