bm25

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 18, 2026 License: AGPL-3.0 Imports: 6 Imported by: 0

Documentation

Overview

Package bm25 provides candidate-set BM25 rerank for CKV's query path.

The Okapi implementation + code-aware tokenizer are adapted from the sibling repo github.com/0xmhha/code-knowledge-graph at pkg/bm25 (2026-05-26 snapshot). Per that package's header note, the algorithm is hand-written from the BM25 description (Robertson, Walker 1994) and is not derivative of any third-party library; we copy rather than depend across repos so the CKV build does not require a CKG checkout.

CKV-specific surface (this package, not in CKG):

  • rerank.go candidate-set Rerank + RRF fusion (k=60 default)

CKV remains dense-only at the schema layer; this is a candidate-rerank overlay measured for impact alongside the vector-only baseline.

Index

Constants

View Source
const (
	DefaultK1 = 1.5
	DefaultB  = 0.75
)

Default Okapi BM25 hyperparameters. K1 controls term-frequency saturation (higher = more weight to repeated matches); B controls length normalization (0 = ignore length, 1 = full normalization). 1.5 / 0.75 is the most widely cited starting point in the literature and matches both bleve and rank_bm25 defaults.

View Source
const DefaultRRFK = 60

DefaultRRFK is the canonical RRF k constant (Cormack, Clarke, Buettcher 2009 — "Reciprocal Rank Fusion outperforms Condorcet and individual rank learning methods"). 60 is the value the original paper proposed and the value Elasticsearch / OpenSearch default to.

Variables

This section is empty.

Functions

func BuildCorpusText

func BuildCorpusText(h types.Hit) string

BuildCorpusText is the canonical BM25 corpus shape: symbol_name + the first non-empty line of the chunk text (typically the signature). Centralized so engine.Search and tests don't drift on the spec.

When the chunk has no symbol name (rare for code chunks, common for doc / header chunks), the first text line stands in for symbol.

func Rerank

func Rerank(candidates []Candidate, intent string) ([]Result, Stats)

Rerank applies candidate-set BM25 to the input hits and combines with the existing vector ordering via Reciprocal Rank Fusion. Inputs are assumed already sorted by vector score descending (the contract the store layer satisfies); each input's vector rank is taken as its position + 1.

The IDF in this BM25 is computed over the candidate set only, not the global chunk corpus — this is a candidate-set bias by design. The signal is still useful as a *rerank within candidates* because IDF rewards terms that vary across the local set, which is what differentiates one candidate from another.

On empty input, empty intent, or zero query tokens the function returns the input unchanged (each result carries its original index as HybridRank, BM25Score = 0). Callers can detect the no-op via Stats.BM25Disabled.

func Tokenize

func Tokenize(s string) []string

Tokenize converts an arbitrary string into BM25 tokens with code-aware splitting. The output preserves the joined identifier (lowercased) AND its sub-words so a query for either form scores. Concretely:

  • "parseFile" → ["parsefile", "parse", "file"]
  • "HTTPServer" → ["httpserver", "http", "server"]
  • "read_file" → ["read_file", "read", "file"]
  • "pkg.Type.Method" → ["pkg", "type", "method"]
  • "json-rpc/v1" → ["json", "rpc", "v1"]

All tokens are lowercased; tokens of length < 2 are dropped (they add noise without helping rank). Caller-supplied tokens are not deduped — repetition is meaningful to BM25's TF term.

Types

type Candidate

type Candidate struct {
	Hit    types.Hit
	Corpus string
}

Candidate pairs a vector-retrieved Hit with the BM25 corpus text for that candidate. CKV's caller (engine.Search) builds Corpus from chunk.SymbolName + signature first line. Whole chunk.Text is not used because tokenizing 30 long bodies dominates the rerank latency and the noise overwhelms the symbol-level signal.

type Document

type Document struct {
	ID     string
	Tokens []string
}

Document is one indexable record. Tokens are pre-tokenized — call Tokenize for the package's standard code-aware splitter, or supply custom tokens for a domain-specific corpus.

type Okapi

type Okapi struct {
	K1 float64
	B  float64
	// contains filtered or unexported fields
}

Okapi is the standard BM25Okapi scorer. Concurrent reads (Score, TopK) after a completed Index are safe; do NOT call Index concurrently with a reader. Build once, query many.

In CKV the corpus is built per-Search call from the vector-retrieved candidate set (~30 documents), so "concurrent reads" simply means the Engine.Search owner runs Score in a single goroutine; the parameters remain useful documentation for any future consumer.

func NewOkapi

func NewOkapi() *Okapi

NewOkapi returns a scorer with default hyperparameters. Override K1/B directly on the returned struct before calling Index.

func (*Okapi) Index

func (o *Okapi) Index(docs []Document)

Index registers the corpus. Empty input is allowed — the scorer will return 0 for all queries until a non-empty Index call replaces state.

func (*Okapi) Score

func (o *Okapi) Score(query []string, docID string) float64

Score returns the BM25 score for one document under the given query. Both K1 and B come from the Okapi struct so callers can tune at runtime without recreating the scorer.

func (*Okapi) TopK

func (o *Okapi) TopK(query []string, k int) []ScoredDoc

TopK returns the top-k documents by score, descending. Documents with score 0 (no matching terms) are excluded. k <= 0 means "all matches".

CKV's Rerank uses Score directly rather than TopK because it needs every candidate's rank to feed RRF — including zero-scoring ones — not just the top-k. TopK is kept for parity with the CKG interface and for ad-hoc debugging.

type Result

type Result struct {
	Hit         types.Hit
	BM25Score   float64
	HybridRank  int // 1-based, after RRF
	OriginalIdx int // 0-based position in the input (= vector rank - 1)
}

Result is one row in the reordered output. Hit.Score has BM25Score and HybridRank populated; the wrapping fields duplicate them for callers that prefer the rerank-package shape (e.g. footprint summarization).

type ScoredDoc

type ScoredDoc struct {
	ID    string
	Score float64
}

ScoredDoc pairs a document ID with its BM25 score for the most recent query. Score is always >= 0; documents with no matching terms are not returned by TopK.

type Scorer

type Scorer interface {
	// Index registers the full corpus. Repeated calls overwrite earlier
	// state — Scorer is not append-only.
	Index(docs []Document)
	// Score returns the BM25 score for one document under one query.
	// Returns 0 when docID is unknown or no query term matches.
	Score(query []string, docID string) float64
	// TopK returns the top-k documents by score, descending. k <= 0
	// returns every matching document.
	TopK(query []string, k int) []ScoredDoc
}

Scorer is the contract every BM25 implementation in this package satisfies. The two-phase pattern (Index then Score / TopK) lets callers build the corpus once per query and reuse for many lookups.

type Stats

type Stats struct {
	CandidatesIn   int     // = len(input)
	CandidatesOut  int     // = len(output); should equal input
	RankChanges    int     // count of candidates whose HybridRank != OriginalIdx+1
	Top1ScoreDelta float64 // Normalized score: final top-1 minus original top-1; positive when BM25 pulled a still-strong vector match to top, negative when it demoted vector
	Top1ChunkID    string  // first 12 hex chars of new top-1 chunk_id, for fingerprint drift detection
	BM25Disabled   bool    // true when Rerank was a no-op (empty input / empty intent / no query tokens)
}

Stats summarizes the rerank effect for footprint logging. Designed for single-fingerprint log lines: every field is a scalar an operator can grep / aggregate without joining multiple log entries.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL