Documentation
¶
Index ¶
Constants ¶
const DefaultRRFK = 60
DefaultRRFK is the dampening constant the configuration wires when the RRF scorer is selected. 60 is the empirical standard from Cormack, Clarke & Büttcher (SIGIR 2009), where it beat every individual ranker and Condorcet fusion across TREC collections. It is a constant, not configuration: RRF exists for comparison runs against the shipped excess methodology, and a comparison baseline you can tune is not a baseline. NewRRFScorer still takes k, because k is a genuine parameter of the algorithm family — the contract tests exercise it — but the wiring always passes this value.
Variables ¶
This section is empty.
Functions ¶
func ClampCount ¶
clampCount bounds a funding-seed count to Contribution.Count's range, for the same reason as clampRank: a wrapped count would misreport a heavily funded anchor as barely funded.
func ClampDegree ¶
clampDegree bounds an anchor degree to Contribution.Degree's range.
Types ¶
type Candidates ¶
type Candidates[K comparable, P float32 | float64] map[K][]Contribution[K, P]
Candidates pools every contribution made during one search, keyed by candidate node. Append order is deterministic — the sources run in a fixed order and seed traversals run in ascending key order — so a Scorer may fold a list's floats front to back without run-to-run drift in the low bits.
type Contribution ¶
type Contribution[K comparable, P float32 | float64] struct { Src Source Score P Rank uint16 Via K Degree uint32 Count uint16 }
Contribution records one observation of a candidate by one retrieval source. Collection sites record what they saw — who, through which anchor, how much mass sat on it — and apply no policy of their own: the hinge, the null model and the attenuation all belong to the Scorer, so changing how ranking works never touches the collection sites again.
Score is oriented so that bigger is always better: the vector site converts the distance its index reports (smaller is nearer) to 1/(1+distance) on the way in; a graph observation carries the funding anchor's full observed mass. Rank is the candidate's position in the producing source's own result list (0 is best) — a graph observation has no list and leaves it zero. Via, Degree and Count exist for graph observations: the funding anchor, its degree at collection time, and how many seed members funded it; a seed contribution's Count is 1.
type ExcessScorer ¶
type ExcessScorer[K comparable, P float32 | float64] struct { // contains filtered or unexported fields }
ExcessScorer folds a candidate's observations under the excess-transmission methodology: relevance is the candidate's own seed mass plus the above-background surplus its anchors transmitted, attenuated α² for the two-edge path. Each graph observation carries its anchor's full observed mass; the fold subtracts the candidate's own mass (self-exclusion — a fact never funds its own boost) and the anchor's size-proportional share of the background, and keeps only what remains above zero. An anchor at or below its fair share therefore contributes nothing — hubs are heard exactly when they are surprising, and silent when they are merely large.
Scores stay in raw seed units end to end: relevance is homogeneous of degree 1 in the mass scale, so normalizing anywhere is a provable ordering no-op that only breaks the commensurability of the channels.
func NewExcessScorer ¶
func NewExcessScorer[K comparable, P float32 | float64]() *ExcessScorer[K, P]
NewExcessScorer returns the excess-transmission fold, unbound (background zero) until WithBackground binds a query's rate.
func (*ExcessScorer[K, P]) Score ¶
func (s *ExcessScorer[K, P]) Score(contributions []Contribution[K, P]) P
Score folds contributions at the bound background rate. Seed mass first — the text and vector observations sum directly — then the hinge over each graph observation, in list order, so identical inputs fold to byte-identical scores.
func (*ExcessScorer[K, P]) WithBackground ¶
func (s *ExcessScorer[K, P]) WithBackground(background P) Scorer[K, P]
WithBackground returns a scorer bound to one query's background rate.
type RRFScorer ¶
type RRFScorer[K comparable, P float32 | float64] struct { // contains filtered or unexported fields }
RRFScorer fuses a candidate's contributions by reciprocal rank, Σ 1/(k+Rank). Rank is the only input on purpose: the sources score on incomparable scales, and RRF sidesteps calibrating them by trusting only the position each source assigned. Contribution.Score goes deliberately unused — magnitudes carry no rank information — and so does the query's background rate: rank fusion has no null model, which is precisely the property that let mega-hubs manufacture consensus from size alone (RRF_FINDINGS Rounds 1–8). It remains available as an alternative fold for comparison runs; the shipped default is the ExcessScorer.
The consensus property this buys: a candidate two sources place mid-list outranks one a single source places first (2/(k+3) > 1/(k+1) for k = 60), so agreement between text and vector beats either alone.
func NewRRFScorer ¶
func NewRRFScorer[K comparable, P float32 | float64](k int) *RRFScorer[K, P]
NewRRFScorer returns an RRFScorer fusing contributions as Σ 1/(k+Rank).
func (*RRFScorer[K, P]) Score ¶
func (s *RRFScorer[K, P]) Score(contributions []Contribution[K, P]) P
Score sums 1/(k+Rank) over the contributions, in list order.
func (*RRFScorer[K, P]) WithBackground ¶
WithBackground returns the scorer itself: rank fusion carries no null model (see the type comment), so there is nothing to bind.
type Scorer ¶
type Scorer[K comparable, P float32 | float64] interface { // Score folds contributions at the scorer's bound background rate. Score(contributions []Contribution[K, P]) P // WithBackground returns a scorer bound to background; a fold with no // null model returns itself. WithBackground(background P) Scorer[K, P] }
Scorer folds one candidate's contributions into its relevance score; higher wins. Search applies it twice — to each seed's own contributions before any traversal (fixing the seed masses the traversal aggregates) and to every candidate's full list at the end — and one instance is shared by every concurrent search on the graph, so implementations must be pure: no mutation of the slice, no mutable state, same input same output.
Query-scoped inputs arrive by binding, never by mutation. WithBackground returns a scorer bound to one query's background rate — the average mass density per unit of anchor degree the traversal observed, the one query-global number a null model needs. An unbound scorer folds at background zero, which is exactly what seed fusion wants: before the traversal has observed anything there is no null to compare against. Mutating the shared instance instead would race one query's background into another's folds, since reads run concurrently under RLock.
type Source ¶
type Source uint8
Source identifies the retrieval stage that produced a Contribution. Collection sites record observations on each source's own scale; the Scorer is what knows how to combine them, so contributions can only be read by knowing where each one came from.
const ( // SrcText is the full-text index; Score is the BM25 × coverage mass. SrcText Source = iota // SrcVector is the vector index; Score is the similarity 1/(1+distance). SrcVector // SrcGraph is the anchor traversal; Score is the funding anchor's full // observed mass M_A — the raw observation, before the scorer subtracts // the fair share and applies the hinge. SrcGraph )