Skip to content
RAG Explained Better

BM25 Explained: How the BM25 Ranking Algorithm Scores Documents for RAG

BM25 ranks documents by term frequency, term rarity and length. The formula, a worked example, k1 and b defaults, where it fails, and hybrid search.

BM25 is a lexical ranking function that scores how well a document matches a keyword query from three signals: how often each query term appears in the document, how rare that term is across the whole collection, and how long the document is compared with the average. The name stands for “Best Matching 25”, and the function was described by Stephen Robertson and colleagues from the Okapi retrieval system at City University London in 1994. BM25 still matters in retrieval-augmented generation because it finds the exact tokens that embeddings blur, such as error codes, product identifiers and function names, and the BEIR benchmark (Thakur et al., 2021) found it to be a robust baseline across 18 retrieval datasets. It works over an inverted index: for every query term it adds a score that grows with term frequency but saturates, rewards rare terms, and discounts long documents, using two parameters, k1 and b, whose defaults are 1.2 and 0.75 in Weaviate, Elasticsearch, Vespa and Apache Lucene. In a production RAG stack, BM25 is the keyword half of retrieval, the step the whole architecture is named after, and it runs beside vector search as part of hybrid retrieval.

What is BM25, and what does the name BM25 mean?

BM25 is a ranking function rather than a trained model. It takes a query, splits it into tokens, looks those tokens up in an inverted index, and returns a numeric relevance score for every document that contains at least one of them. Nothing is learned from data: the score is computed from counts that the index already stores. Weaviate’s keyword search documentation describes it as an exact matching-based search using tokens, which rests on term frequency within a document and inverse document frequency across the dataset. The document is treated as a bag of words, so BM25 knows which terms a document contains and how often, but not what those terms mean or in which order they appear.

The name records its history. “Okapi” was the experimental retrieval system built at City University London, and “BM” stands for Best Matching. Robertson, Walker, Jones, Hancock-Beaulieu and Gatford reported the function in “Okapi at TREC-3”, published in the proceedings of the Third Text REtrieval Conference in 1994. The number 25 marks one iteration in a numbered series of Best Match weighting functions; Weaviate’s v1.17 release notes put it plainly when they explain that “it took quite a few iterations” to reach the version that stuck.

A later variant, BM25F, extends the function to documents with several fields. Robertson, Zaragoza and Taylor described it in “Simple BM25 extension to multiple weighted fields” at the ACM CIKM conference in 2004, so that a match in a title can count for more than a match in a body. Weaviate has scored keyword search with BM25F since v1.17, and its keyword search documentation uses the two names interchangeably; the “F” stands for field, which is what makes per-property weights possible. A scoring function designed for 1990s document retrieval raises a fair question for anyone building with embeddings today: why keep it in a RAG pipeline when a neural model can already read meaning?

Why do RAG systems still use BM25 when embeddings exist?

RAG systems keep BM25 because exact tokens carry information that vector similarity does not guarantee to preserve. Dense retrieval depends on what a vector encodes: an embedding model maps a passage to a point where nearby points mean similar things, and the retriever ranks by distance between points. Identifiers such as HTTP 502, SKU-4471 or a function called parse_invoice_v2 are not ideas with neighbours; the literal string is the whole signal. Weaviate’s search concepts documentation lists the cases where keyword search is the right tool, and they read like a RAG backlog: technical documentation and API references where exact terminology is crucial, product names and SKUs, and code snippets or error messages in a programming context.

The same pattern shows up in specialised domains. Weaviate’s article on advanced RAG techniques points out that in medicine many important terms, such as medication names, anatomical terms and diagnoses, behave as specific keywords rather than concepts, so pure vector search can miss them while keyword search keeps them in play. Weaviate Academy’s comparison of search types makes the trade-off explicit: keyword search is predictable and transparent, so it is easy to explain why a result matched, while vector search favours recall of related items and keyword search favours precision on exact terms. For RAG retrieval that has to cite the right clause or the right error code, that precision is the point.

The strongest outside evidence comes from out-of-domain evaluation. Thakur, Reimers and colleagues built BEIR, published at the NeurIPS 2021 Datasets and Benchmarks track, from 18 public datasets and tested 10 retrieval systems covering lexical, sparse, dense, late-interaction and re-ranking architectures. Their abstract reports that BM25 is a robust baseline, that re-ranking and late-interaction models achieve the best zero-shot results on average at high computational cost, and that dense retrievers are more efficient but often underperform in generalization. BM25 also needs no model call at indexing time, and in Weaviate every text property gets a searchable inverted index by default because indexSearchable defaults to true. Keeping BM25 in the stack pays off only when its scores can be read and reasoned about, and reading them starts with the formula.

How does the BM25 algorithm calculate a relevance score?

BM25 calculates a document’s score by adding one contribution per query term. Each contribution multiplies the term’s inverse document frequency by a term-frequency factor that saturates and that is damped when the document is longer than average. The standard formula, as Weaviate’s “Hybrid Search Explained” article presents it, reads as follows with the IDF variant that Apache Lucene documents for its BM25Similarity class, the similarity Elasticsearch’s reference lists as its default:

BM25 scoring function, with Lucene’s IDF
score(D, Q) = sum over each query term q of:

    IDF(q) * ( f(q,D) * (k1 + 1) ) / ( f(q,D) + k1 * (1 - b + b * |D| / avgdl) )

  IDF(q)  = ln( 1 + (N - n(q) + 0.5) / (n(q) + 0.5) )

  f(q,D)  occurrences of q in document D     k1  term-frequency saturation (default 1.2)
  |D|     length of D in tokens              b   length normalisation     (default 0.75)
  avgdl   average document length            N   documents in the collection
  n(q)    documents that contain q

The term-frequency factor is where saturation happens. With k1 at 1.2 and a document of average length, one occurrence of a term contributes 1.0 times its IDF, two occurrences contribute 1.375 times, five contribute 1.774 times, and no number of repetitions can push the factor past k1 + 1, which is 2.2. Each extra occurrence is worth less than the one before, so a chunk that repeats a keyword ten times gains only 1.964 times the IDF instead of ten times. Weaviate Academy describes k1 the same way: higher values let repeated terms keep increasing relevance, and lower values cause diminishing returns sooner.

Abhishek Thakur’s YouTube video “A no nonsense intro to BM25” introduces the ranking function behind these three signals.

Inverse document frequency rewards rare terms. In a collection of 1,000,000 documents, Lucene’s formula gives a term found in 10 documents an IDF of 11.464 and a term found in 500,000 documents an IDF of 0.693, so one match on the rare term is worth more than sixteen matches on the common one. The 1 added inside the logarithm is what keeps the weight positive: the classic Robertson and Spärck Jones form without it gives exactly 0 for a term in half the collection and -2.197 for a term in 900,000 documents, which means a BM25 score can go negative in implementations that use the classic form, while Lucene-style scores cannot.

Length normalisation is controlled by b. With b at 0.75, a document twice the average length multiplies k1 by 1.75 inside the denominator, and a document half the average length multiplies it by 0.625, so the same single match scores higher in the short document. Weaviate’s search concepts documentation shows the effect on a tiny collection: a keyword search for “black” ranks “black bear” above “small domestic black cat” because the term makes up a larger proportion of the text. Formulas describe behaviour in the abstract, and the quickest way to see all three signals interact is to score a small collection by hand.

How does BM25 rank five documents for the query “error code 502”?

BM25 ranks a five-document collection by summing three term contributions for each one. The collection is five short support-log lines, tokenized on whitespace with no stopword removal, scored with k1 = 1.2 and b = 0.75. That gives N = 5 and an average length of 7.6 tokens. “error” appears in three documents, so its IDF is 0.539, while “code” and “502” each appear in two documents and get an IDF of 0.875. The table lists every document with its total score and the contribution of each query term, computed term by term:

BM25 scores for “error code 502” over five documents (k1 = 1.2, b = 0.75, avgdl = 7.6)
Rank Doc Tokens Score Per-term contribution Document text
1 D1 8 2.242 error 0.528 · code 0.857 · 502 0.857 the gateway returned error code 502 during deploy
2 D4 7 0.905 error 0 · code 0.905 · 502 0 code review checklist for the deploy pipeline
3 D5 8 0.857 error 0 · code 0 · 502 0.857 the 502 response means a bad gateway upstream
4 D3 7 0.557 error 0.557 · code 0 · 502 0 error handling improves reliability of the service
5 D2 8 0.528 error 0.528 · code 0 · 502 0 restart the gateway service to clear the error

D1 wins with 2.242 because it is the only document containing all three terms. The more instructive result is rank two. D4, a code review checklist with nothing to say about a 502 error, scores 0.905 and beats D5, the one document that explains what a 502 response is, at 0.857. Both documents match exactly one term with the same IDF of 0.875, and D4 is one token shorter, so length normalisation lifts its single match slightly higher. BM25 behaved correctly by its own definition, and the ranking is still wrong for a reader who wanted to understand the error.

Changing the words changes the outcome far more than changing the parameters. Querying the same collection for “bad gateway” puts D5 first with 1.885, because “bad” appears in only one document and earns an IDF of 1.386. Raising k1 from 1.2 to 1.5 for the original query, by contrast, moves D1 from 2.242 to 2.237 and leaves the order of all five documents untouched. BM25 rewards the vocabulary of the query, not the intent behind it, and that behaviour looks enough like TF-IDF that the comparison between the two is the next thing worth settling.

How is BM25 different from TF-IDF?

BM25 differs from TF-IDF mainly in how it treats repetition. Plain TF-IDF multiplies a term’s raw frequency by its IDF, so six occurrences weigh twice as much as three. Weaviate’s v1.17 release notes describe the intuition BM25 encodes instead: a term appearing six times does not make a document twice as relevant as one where it appears three times. With k1 at 1.2 and average document length, BM25’s factor is 1.571 for three occurrences and 1.833 for six, so doubling the repetitions raises the term’s contribution by 16.7 percent rather than 100 percent.

The second difference is document length. Weaviate’s “Hybrid Search Explained” article describes BM25 as building on TF-IDF by taking the Binary Independence Model into the IDF calculation and adding a normalisation penalty that weighs a document’s length against the average length of all documents in the database. That penalty is the b parameter at work, from no normalisation at b = 0 to full normalisation at b = 1, and it is applied inside each term’s contribution rather than to the document as a whole. Both functions share the idea of inverse document frequency, which is why BM25 is so often introduced as a refined TF-IDF.

Neither function uses embeddings or cosine similarity between query and document meaning. A BM25 score is a sum of per-term weights, not an angle between vectors. A document can still be written as a sparse vector with one dimension per vocabulary term, which is why Weaviate’s documentation also calls keyword search “sparse vector” search. The same Weaviate article names BM25 and SPLADE as algorithms that produce sparse embeddings, and notes that Weaviate’s hybrid search uses BM25 and BM25F for its sparse half. Saturation and length normalisation are governed by k1 and b, so the practical question that follows is when those two numbers should move away from their defaults.

When should the BM25 parameters k1 and b be tuned?

The BM25 parameters k1 and b should be tuned only when measured retrieval quality on real queries says the defaults are wrong for the corpus. The defaults are the same across the major engines: Weaviate’s inverted index reference, Apache Lucene’s BM25Similarity documentation, Elasticsearch’s similarity reference and Vespa’s BM25 rank feature documentation all set k1 to 1.2 and b to 0.75. Weaviate’s configuration reference states that these defaults work well for most cases, and its keyword search best practices advise changing them only with a good reason.

The direction of a change follows from what each parameter does. Weaviate Academy’s lesson on keyword search optimization gives k1 a typical range of 0 to 2 and b a range of 0 to 1, and recommends increasing k1 when repetition genuinely signals relevance, as in product catalogs, and decreasing b when length should not matter, as when short posts and long articles share a collection. Setting b to 0 switches length normalisation off entirely. In Weaviate, both values are set on the collection’s inverted index configuration and can be overridden per property, which helps when one property, such as a full document body, is far longer than the others.

A tuning change is worth keeping only if it moves a metric the generator depends on. The reliable method is a held-out set of real queries with known relevant chunks, one parameter changed at a time, and recall measured at the depth the RAG prompt actually reads, for example the top 5 or top 10 chunks. The five-document example shows why intuition misleads here: a 25 percent change in k1 moved the top score by 0.005 and changed no ranks. The weighting between keyword and vector results when combining BM25 and vector retrieval is a separate lever with its own evaluation. Parameter tuning adjusts scores within what BM25 can see, and the more consequential limits are the cases it cannot see at all.

Where does BM25 fail in RAG retrieval?

BM25 fails first on vocabulary mismatch. A query for “car” gives no credit to a chunk that only says “automobile”, and a query for “bad gateway” scores zero against any document that only mentions a 502 response. Related word forms are not merged in every engine: Weaviate’s search operator reference states that stemming is not supported yet (as of September 2026), so “deploy” and “deployment” are different tokens there. Typos fail the same way under the default word tokenization, and Weaviate Academy notes that a query for “adventur” returns nothing in keyword search while vector search still finds the intended results.

Some of that brittleness is a tokenization choice rather than a property of BM25. Weaviate’s keyword search documentation describes trigram tokenization, which splits text into overlapping three-character sequences so that “Morgn” and “Morgan” still match through shared trigrams such as “org” and “gan”; the same page warns that filtering behaviour changes significantly on those properties, so trigram belongs only on fields that need fuzzy matching. Tokenization also decides what a word is: the default word tokenizer turns “Test_domain_weaviate” into “test”, “domain” and “weaviate”, while lowercase keeps symbols such as underscores and field treats the whole value as one token for IDs and URLs. BM25 ignores word order in every case, so “dog bites man” and “man bites dog” score identically.

The third failure is that BM25 scores have no fixed scale. Weaviate’s reference for search metadata states it directly: the BM25F score is relative to the dataset and the query. A score of 2.2 in one collection or for one query says nothing about relevance elsewhere, so a fixed “relevant above X” threshold does not transfer. Matching is also generous by default: Weaviate returns any object that matches at least one query token unless a search operator says otherwise, so long natural-language questions pull in long tails of weak matches. Several of these limits are configuration choices rather than fixed properties, which is why the implementation inside the database matters.

How do you run BM25 keyword search in Weaviate?

Weaviate runs BM25 keyword search through the bm25 query on a collection, scoring with BM25F across the text properties it searches. According to Weaviate’s keyword search documentation, the query is tokenized with each property’s tokenization method (word by default), English stopwords are removed through the en preset, and the remaining tokens are scored with the collection’s k1 and b values. Stopwords stay in the index and are filtered only at query time, so changing the stopword list does not require reindexing, and individual properties can be boosted with a caret, so title^3 counts a title match three times as much as a body match. Keyword search is one part of what Weaviate for RAG brings to a retrieval stack, alongside vector and hybrid search.

A minimal Python program creates a collection for support documents, keeps error codes whole with lowercase tokenization, and sets k1 and b explicitly so the scoring is visible in code. It then runs a keyword query that boosts the title, requires every query token to appear, stops at the first large drop in score, and returns each object’s BM25 score with its explanation. It uses the Weaviate Python client v4, and the operator argument needs Weaviate v1.31 or later:

Python, weaviate-client v4
import weaviate
from weaviate.classes.config import Configure, DataType, Property, Tokenization
from weaviate.classes.query import BM25Operator, MetadataQuery

client = weaviate.connect_to_local()

client.collections.create(
    "SupportDoc",
    properties=[
        Property(name="title", data_type=DataType.TEXT),
        Property(name="body", data_type=DataType.TEXT),
        Property(name="error_code", data_type=DataType.TEXT,
                 tokenization=Tokenization.LOWERCASE),
    ],
    inverted_index_config=Configure.inverted_index(bm25_k1=1.2, bm25_b=0.75),
)

docs = client.collections.use("SupportDoc")
response = docs.query.bm25(
    query="gateway timeout 504",
    query_properties=["title^3", "body", "error_code"],
    operator=BM25Operator.and_(),
    auto_limit=1,
    limit=20,
    return_metadata=MetadataQuery(score=True, explain_score=True),
)
for obj in response.objects:
    print(round(obj.metadata.score, 3), obj.properties["title"])
    print(obj.metadata.explain_score)

client.close()

Search operators control how many tokens must match. The default is or with a minimum of one matching token; BM25Operator.or_(minimum_match=2) raises that floor; and requires every token within a single property; and and_cross lets the tokens spread across the searched properties, available from Weaviate v1.38.8 and, according to the Python client’s documentation, also from 1.37.15. Weaviate’s operator documentation describes these as working like a filter applied to the BM25 results. The explain_score metadata breaks each BM25F score into its components, which is the fastest way to see why a chunk like D4 in the worked example outranked a better one.

Autocut decides how many chunks of a keyword result actually reach the prompt. Weaviate’s autocut documentation describes it as cutting the result list after a chosen number of jumps in score, and it works with bm25, hybrid and vector queries; when combined with limit, it only considers the objects inside that limit. Weaviate’s v1.20 release notes explain the RAG motivation by citing Shi et al., “Large Language Models Can Be Easily Distracted by Irrelevant Context” (2023), which found that model performance drops sharply when irrelevant passages are included in the prompt.

Speed at scale comes from how the index is traversed. Weaviate added BlockMax WAND, which groups postings into blocks with a maximum possible score so that blocks unable to reach the top results are skipped, and made it generally available and the default for new instances in v1.30. Weaviate’s BlockMax WAND article reports, from its own measurements on v1.29.0, that median query time on MS MARCO (8.6 million documents) fell from 136 ms to 27 ms, that the inverted index for the same dataset shrank from 10,531 MB to 941 MB, and that on internal tests with 100 million documents throughput rose from 1 to 50 queries per second. Its inverted index documentation adds that scores can differ slightly from the older WAND path. Keyword search alone still misses synonyms, word order and meaning, which is why RAG pipelines on Weaviate usually run it together with vector search.

How does BM25 combine with vector search in hybrid retrieval?

Hybrid search combines BM25 with vector search by running both searches in parallel and fusing their result lists into one ranking. In Weaviate, the alpha parameter sets the balance: 0 is pure keyword search, 1 is pure vector search, and 0.5 weighs both equally. Weaviate’s hybrid search documentation gives 0.75 as the server default and notes that it applies only when a request arrives with no alpha value, because some client libraries send their own, so any pipeline where the weighting matters should set alpha explicitly. The keyword leg accepts the same k1, b, tokenization, boosting and operator settings as a standalone BM25 query.

Fusion is also the answer to BM25’s missing scale. Weaviate’s default fusion method since v1.24, relativeScoreFusion, normalises each result set so that the highest score becomes 1 and the lowest becomes 0, weights the two sets by alpha, and adds them. The older rankedFusion, the default up to v1.23, scores each object by position as 1/(rank + 60), so the size of a score gap disappears. Weaviate’s fusion algorithms article reports that, in its own recall benchmark on the FIQA dataset, relative score fusion improved recall by about 6 percent over ranked fusion, and autocut works with hybrid search only when relative score fusion is selected.

BM25 is a first-stage retriever, not a reranker. It narrows millions of chunks to a candidate list cheaply, hybrid search improves that list by adding the matches that only meaning can find, and Reranking in RAG reorders the short list with a more expensive model. Weaviate’s reranker modules work with vector, BM25 and hybrid queries, and Weaviate integrates reranker models from providers including Cohere, JinaAI, NVIDIA and Voyage AI. Weaviate Academy notes that the reranker reorders exactly the objects returned by limit, with no wider candidate pool, so a pipeline that wants the reranker to choose from more candidates raises limit and keeps the top results itself. The BEIR results point the same way: re-ranking and late-interaction models scored best zero-shot, at a higher computational cost that fits a short candidate list rather than a whole corpus.

BM25 has lasted since 1994 because it answers a narrow question exactly: which documents contain these tokens, how rare those tokens are, and how concentrated they are in each document. That precision is what RAG needs for error codes, identifiers and names, and it is also why BM25 misses synonyms, ignores word order and produces scores that mean nothing outside their own collection. The working setup follows directly from both sides: keep the 1.2 and 0.75 defaults until real queries show otherwise, pick tokenization per property, require stricter token matching and use autocut where long queries drift, and run BM25 inside hybrid search with an explicit alpha, relative score fusion and a reranker on the short list, so exact matches and semantic matches reach the generator together.