Skip to content
bm25

go · zero deps · bm25 + rrf

Keyword ranking & hybrid search.

Okapi BM25 keyword-relevance ranking plus reciprocal-rank fusion (RRF) for Go — small, dependency-free, and built for the keyword half of hybrid (keyword + semantic) search.

MIT licensed · no third-party dependencies · Go 1.21+

bm25.go
c := bm25.New()
c.Add("moby",   "Call me Ishmael. The whale, the ship, the sea.")
c.Add("austen", "It is a truth universally acknowledged…")

for _, r := range c.Search("whale ship", 5) {
    fmt.Printf("%.3f  %s\n", r.Score, r.ID) // 1.39  moby
}

what you get

The keyword half of hybrid search.

A one-call corpus for the common case, low-level primitives when you tokenize yourself, and RRF to fuse it all with a vector ranker.

Okapi BM25

Standard keyword-relevance ranking; IDF and average document length are computed over the whole corpus.

Zero dependencies

math / strings / unicode only — nothing else in your module graph.

Two layers

A one-call Corpus for the common case, plus low-level Scorer primitives when you tokenize yourself.

RRF built in

Fuse a BM25 ranking with a vector-similarity ranking via reciprocal-rank fusion — the standard hybrid-search recipe, with no weights to tune.

Tunable

NewWithParams(k1, b); ties broken by ID; k <= 0 returns all.

Bring your own tokenizer

StatsForTokens / NewScorer let you stem, stop-word, or compute IDF over a per-query candidate set. Go 1.21+.

usage

Rank keywords, then fuse.

Start with the one-call Corpus; when you also have a vector ranker, blend the two orderings with reciprocal-rank fusion.

// Corpus — the one-call common case
c := bm25.New()
c.Add("moby",   "Call me Ishmael. The whale, the ship.")
c.Add("austen", "It is a truth universally acknowledged…")

for _, r := range c.Search("whale ship", 5) {
    fmt.Printf("%.3f  %s\n", r.Score, r.ID)
}
// Hybrid — fuse keyword + semantic rankings
keyword  := c.Search("http caching proxies", 0)
semantic := myVectorIndex.Search("http caching", 0)

fused := bm25.FuseRanked(bm25.DefaultRRFK,
    ids(keyword),   // []string of IDs, best-first
    ids(semantic),
)
  • New / NewWithParams
  • Corpus.Add
  • Corpus.Search
  • FuseRanked
  • ReciprocalRankFusion
  • StatsForTokens
  • NewScorer

install

Add it to your module.

One import, no transitive dependencies. Requires Go 1.21+.

go get go get github.com/richardwooding/bm25
import import "github.com/richardwooding/bm25"

Pairs with ollamaembed (semantic) for hybrid search; extracted from file-search-on. Full API on the Go Reference.