bayesian-bm25: Bayesian BM25: Turning Search Scores Into Probabilities You Can Trust

How Cognica’s library converts raw BM25 into calibrated relevance signals, then uses log-odds fusion and Bayesian priors to make hybrid search less ad hoc.

10 min read • View on GitHub • More from cognica-io

A wide editorial engraving of a search engineer at a workbench where raw BM25 score strips pass through a mechanical press labeled Bayes and emerge as calibrated probability dials. Two output paths branch from the dials, one toward lexical search and one toward vector search, showing how a ranking score becomes a shared signal for hybrid retrieval.
Bayesian BM25 turns a score that only ranks into a probability that can be thresholded, fused, and compared across retrieval systems.

Standard BM25 produces unbounded scores that lack consistent meaning across queries, making threshold-based filtering and multi-signal fusion unreliable. Bayesian BM25 addresses this by applying a sigmoid likelihood model with a composite prior (term frequency + document length normalization) and computing Bayesian posteriors that output well-calibrated probabilities in [0, 1].

Jaepil Jeong, Author, Cognica, Inc. · Repository: cognica-io/bayesian-bm25
Key Takeaways

BM25 Is Useful, but Its Scores Lie

BM25 is excellent at ranking. It is bad at telling you what a score means. The same number can imply very different things across queries, corpora, and retrieval settings, which makes thresholding and signal fusion feel improvised.

That is the opening move in Bayesian BM25. It does not argue that BM25 is broken. It argues that BM25 should not be treated like a probability when it is really only a rank signal.

Hybrid search practitioners struggle, not because of embeddings, but because BM25 fails to fit into clean expectations of a probability.

Doug Turnbull, Search expert, Author · Can BM25 be a probability?

The Move: From Raw Score to Calibrated Probability

The repo’s central idea is simple to state and subtle to execute: start with a raw BM25 score, model it as a likelihood, fold in a composite prior, then apply base-rate correction to get a posterior probability. That is a very different claim from normalization. Normalization rescales. Bayesian inference updates belief.

The library treats BM25 as an input to Bayesian updating, not as a score that merely needs rescaling.

That shift matters because a posterior can do things a ranking score cannot. You can threshold it. You can compare it across queries more safely. You can fuse it with another calibrated signal without inventing a new weighting scheme each time.

We introduce a corpus-level base rate prior that decomposes the posterior into three additive log-odds terms, reducing expected calibration error by 68-77% without requiring relevance labels.

Jaepil Jeong, Author, Cognica, Inc. · Bayesian BM25 paper

What the Library Actually Computes

The heart of the implementation lives in probability.py. The main abstraction, BayesianProbabilityTransform, wires together the score model, a composite prior, and a base-rate prior so the final output is numerically stable and interpretable.

The details matter. The code clamps probabilities before taking logits, and it uses split-form exponentials to avoid overflow. That is not cosmetic. Search systems live or die on edge cases, and calibration is only useful if it survives real distributions.

# Conceptual flow, simplified
score = bm25_score(query, document)
likelihood = sigmoid(score)
prior = tf_prior(term_frequency) * norm_prior(document_length)
base_rate = corpus_prior(score_distribution)
posterior = update_bayes(likelihood, prior, base_rate)
return clamp_probability(posterior)

The implementation also reflects a strong bias toward production safety. If a transform is mathematically elegant but brittle at the tails, it is not ready for search traffic.

Why Fusion Gets Interesting Once Everything Is a Probability

Once BM25 is calibrated, fusion stops being a weighted guessing game. In fusion.py, the project uses log-odds conjunction and related gating logic to combine signals in a way that preserves uncertainty instead of flattening it.

A tight technical engraving of two calibrated signal streams, one from lexical search and one from vector similarity, entering a log-odds bridge. The streams interlock like gears, and a small mechanism labeled by its function adds confidence when both sides agree, showing principled fusion instead of a simple average.
When both signals are probabilities, fusion can reward agreement without collapsing into a crude weighted sum.

That is the real conceptual upgrade here. A heuristic like 0.7 * BM25 + 0.3 * vector is easy to write and hard to defend. A probabilistic conjunction at least tells you what kind of uncertainty you are combining.

MethodCan threshold it?Calibrated?Interpretable across queries?Fuses lexical and vector cleanly?Complexity trade-off
Standard BM25 raw scoringNot reliablyNoWeakNoLow, but ad hoc
Manual normalization and weighted sumsSometimesUsually notWeakSometimesLow, but hand-tuned
Reciprocal Rank FusionNoNoModerate for rankingGood for rank lists, not probabilitiesLow and robust
Bayesian BM25YesYes, by designStrongYesModerate, with more math upfront
Cross-encoder rerankingYes, after rerankOften yes, if trained wellStrong on reranked setNot a fusion methodHighest compute cost

The Hybrid Search Bridge

The library does not stop at lexical search. vector_probability.py extends the same probabilistic framing to dense retrieval, using likelihood-ratio ideas and index-aware priors so vector similarity can be compared on the same conceptual scale as BM25.

That is why the project feels bigger than a BM25 wrapper. It is trying to give hybrid search a common language. Once that exists, lexical and dense systems stop competing as incompatible scoring worlds.

Why This Is Better Than Manual Weight Tuning

The practical appeal is not subtle. If your retrieval signals are probabilities, you can reason about them the way you reason about any other model output. You can set thresholds, inspect confidence, and combine stages without pretending raw scores are interchangeable.

That also makes Bayesian BM25 a useful middle layer in the stack. It is earlier and cheaper than reranking, but more principled than rank-only fusion. It sits in the awkward but valuable space between heuristics and heavy models.

Origin, Validation, and Where It Fits

The project comes from Cognica and Jaepil Jeong, and the code reads like a reference implementation of a paper trail rather than a one-off library. The repository structure backs that up: typed Python, tests, benchmarks, docs, and examples all point in the same direction.

A hedcut-style editorial portrait of Jaepil Jeong, the author of Bayesian BM25. The portrait uses his verified GitHub avatar as the likeness source and pairs the face with a sparse technical background to emphasize authorship and credibility rather than decoration.

There is also real-world validation. The project shows up as a baseline model in MTEB and in txtai for score normalization. That is the difference between a clever theory repo and a tool people actually wire into systems.