Source code for axiom.retrieval.fusion
"""Rank fusion for hybrid search.
Algorithm adapted from Hindsight (MIT, ``engine/search/fusion.py``), kept
dependency-free and deterministic. We fuse *ranked lists of document ids* (one
per retrieval arm — semantic, lexical, …) into a single ranking using
Reciprocal Rank Fusion (RRF).
RRF is rank-based, not score-based, on purpose: each arm produces scores on its
own incomparable scale (cosine distance vs BM25), so combining raw scores would
let one arm's scale dominate. RRF only looks at *where* a document ranks within
each arm, which makes the arms commensurable.
"""
from __future__ import annotations
_DEFAULT_RRF_K: int = 60
[docs]
def cap_per_source(ranked_ids: list[str], cap: int) -> list[str]:
"""Truncate a single arm's ranked ids to its top ``cap``.
Applied per arm *before* fusion so one over-expanding backend cannot crowd
out the others. ``cap <= 0`` disables the cap. The caller is responsible for
ordering ``ranked_ids`` best-first; this only slices.
"""
if cap <= 0 or len(ranked_ids) <= cap:
return ranked_ids
return ranked_ids[:cap]
[docs]
def reciprocal_rank_fusion(
ranked_lists: list[list[str]], k: int = _DEFAULT_RRF_K
) -> list[tuple[str, float]]:
"""Merge several ranked id lists into one via Reciprocal Rank Fusion.
RRF formula: ``score(d) = sum over arms of 1 / (k + rank(d))`` where ``rank``
is 1-based within each arm. A document absent from an arm contributes nothing
for that arm.
Args:
ranked_lists: One list of document ids per arm, each ordered best-first.
k: RRF damping constant (default 60). Larger ``k`` flattens the
contribution of top ranks, smaller ``k`` sharpens it.
Returns:
``(doc_id, rrf_score)`` pairs sorted by score descending. Ties are broken
deterministically by first appearance order across the input lists, so
the result is stable for identical inputs.
"""
scores: dict[str, float] = {}
first_seen: dict[str, int] = {}
order = 0
for ranked in ranked_lists:
for rank, doc_id in enumerate(ranked, start=1):
scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + rank)
if doc_id not in first_seen:
first_seen[doc_id] = order
order += 1
return sorted(
scores.items(),
key=lambda item: (-item[1], first_seen[item[0]]),
)