File size: 2,663 Bytes
547ce6e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | """Reciprocal Rank Fusion — the maths the whole retrieval story rests on."""
from __future__ import annotations
import pytest
from app.rag.retrieve import reciprocal_rank_fusion
class TestReciprocalRankFusion:
def test_single_list_is_monotonically_decreasing(self):
fused = reciprocal_rank_fusion({"dense": ["a", "b", "c"]}, k=60)
assert fused["a"] > fused["b"] > fused["c"]
def test_exact_values_match_the_formula(self):
"""RRF(d) = sum over retrievers of 1 / (k + rank), rank 1-based."""
fused = reciprocal_rank_fusion({"dense": ["a", "b"]}, k=60)
assert fused["a"] == pytest.approx(1 / 61)
assert fused["b"] == pytest.approx(1 / 62)
def test_agreement_beats_a_single_strong_vote(self):
"""A document both retrievers rank first must outrank one retriever's favourite.
This is the property that makes fusion worth doing: it rewards agreement rather
than any single retriever's confidence.
"""
fused = reciprocal_rank_fusion(
{"dense": ["both", "dense_only"], "sparse": ["both", "sparse_only"]}, k=60
)
assert fused["both"] == pytest.approx(2 / 61)
assert fused["both"] > fused["dense_only"]
assert fused["both"] > fused["sparse_only"]
def test_scores_are_never_compared_across_retrievers(self):
"""Only ranks matter — a retriever's score scale cannot influence the result."""
a = reciprocal_rank_fusion({"dense": ["x", "y"], "sparse": ["y", "x"]}, k=60)
b = reciprocal_rank_fusion({"sparse": ["y", "x"], "dense": ["x", "y"]}, k=60)
assert a == b
def test_k_damps_the_head_of_each_list(self):
"""Larger k flattens the gap between rank 1 and rank 2."""
small = reciprocal_rank_fusion({"d": ["a", "b"]}, k=1)
large = reciprocal_rank_fusion({"d": ["a", "b"]}, k=1000)
assert (small["a"] - small["b"]) > (large["a"] - large["b"])
def test_empty_input_yields_empty_output(self):
assert reciprocal_rank_fusion({}, k=60) == {}
assert reciprocal_rank_fusion({"dense": []}, k=60) == {}
@pytest.mark.parametrize("k", [0, -1, -60])
def test_rejects_k_below_one(self, k: int):
"""k=-1 divides by zero at rank 1 and small k inverts the ordering."""
with pytest.raises(ValueError, match="RRF k must be"):
reciprocal_rank_fusion({"dense": ["a"]}, k=k)
def test_missing_from_one_list_still_scores(self):
fused = reciprocal_rank_fusion({"dense": ["a"], "sparse": ["b"]}, k=60)
assert fused["a"] == pytest.approx(1 / 61)
assert fused["b"] == pytest.approx(1 / 61)
|