Dense embeddings (like MiniLM's 384-dimensional vectors) capture semantic similarity. But for exact term matching — searching for "Nash equilibrium" or "Shapley value" — sparse retrieval is superior. ReLU.chat implements BM25 entirely in JavaScript.
Why BM25?
BM25 (Best Matching 25) is the workhorse of information retrieval. It scores documents based on term frequency (TF) and inverse document frequency (IDF). Unlike cosine similarity, it excels at:
- Exact keyword matching
- Rare term boosting ("Shapley" scores higher than "the")
- Field-aware scoring (name matches matter more than body matches)
Field-Weighted Indexing
During indexing, we boost important fields by repeating them:
const text = ${name} ${name} ${name} ${aliases} ${aliases} ${summary} ${summary} ${def} ${int} ${ex};
Entry names appear 3× and aliases 2×. This means a query matching an entry's name gets a naturally higher BM25 score via term frequency — no explicit boosting logic needed.
Bigram Phrase Matching
Standard BM25 treats "Nash equilibrium" as two separate tokens. We add bigram phrase matching:
- Generate bigrams:
["nash", "equilibrium"]→"nash_equilibrium" - Add bigrams to the document index
- Score bigram matches with an IDF-weighted bonus
Ensemble Ranking
BM25 scores are fused with dense cosine similarity at 70/30 weights:
const blended = 0.7 denseScore + 0.3 bm25Score;
The 70/30 split was determined empirically — dense similarity captures semantic meaning, BM25 captures lexical precision. Neither alone is sufficient.
Performance
BM25 scoring is O(n) where n is the number of documents. For a 100-entry knowledge base, this takes ~0.1ms. The inverted index is built once at initialization and reused for every query.
Key Takeaway
BM25 is simple to implement, fast to run, and complementary to dense retrieval. Implementing it in the browser gives you hybrid search without any server dependency.