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+
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 github.com/richardwooding/bm25
import "github.com/richardwooding/bm25"
Pairs with ollamaembed (semantic) for hybrid search; extracted from file-search-on. Full API on the Go Reference.