diff --git a/python/python/lancedb/rerankers/rrf.py b/python/python/lancedb/rerankers/rrf.py index 6d5b9a2ba..9c835a3ea 100644 --- a/python/python/lancedb/rerankers/rrf.py +++ b/python/python/lancedb/rerankers/rrf.py @@ -107,8 +107,24 @@ class RRFReranker(Reranker): `search().with_row_id(True)`" ) - combined = pa.concat_tables(vector_results, **self._concat_tables_args) - empty_table = pa.Table.from_arrays([], names=[]) - reranked = self.rerank_hybrid(query, combined, empty_table) + # RRF ranks each result list on its own, so a document's rank is its + # position within the list it came from, not within the concatenation. + rrf_score_map = defaultdict(float) + for result in vector_results: + for i, result_id in enumerate(result["_rowid"].to_pylist(), 1): + rrf_score_map[result_id] += 1 / (i + self.K) - return reranked + combined = pa.concat_tables(vector_results, **self._concat_tables_args) + combined = self._deduplicate(combined) + relevance_scores = [ + rrf_score_map[row_id] for row_id in combined["_rowid"].to_pylist() + ] + combined = combined.append_column( + "_relevance_score", pa.array(relevance_scores, type=pa.float32()) + ) + combined = combined.sort_by([("_relevance_score", "descending")]) + + if self.score == "relevance": + combined = self._keep_relevance_score(combined) + + return combined diff --git a/python/python/tests/test_rerankers.py b/python/python/tests/test_rerankers.py index b4b9bc9e2..850b477a0 100644 --- a/python/python/tests/test_rerankers.py +++ b/python/python/tests/test_rerankers.py @@ -395,6 +395,38 @@ def test_mrr_multivector_rewards_consensus(): assert result["_rowid"].to_pylist()[0] == 2 +def test_rrf_multivector_ranks_each_result_list(): + # RRF scores a document by its rank *within each* result list. The top hit + # of the second list must score the same as the top hit of the first list, + # not as if it were ranked after every row of the first list. + reranker = RRFReranker(K=60) + + def ranking(row_ids): + return pa.table({"_rowid": pa.array(row_ids, type=pa.int64())}) + + rs1 = ranking([1, 2, 3]) + rs2 = ranking([4, 5, 6]) + + result = reranker.rerank_multivector([rs1, rs2]) + scores = dict( + zip(result["_rowid"].to_pylist(), result["_relevance_score"].to_pylist()) + ) + + assert scores[1] == pytest.approx(1 / 61) + assert scores[4] == pytest.approx(1 / 61) + assert scores[5] == pytest.approx(1 / 62) + # the second-ranked hit of the second list beats the third hit of the first + assert scores[5] > scores[3] + + # A document found by both lists sums its reciprocal ranks from each list. + result = reranker.rerank_multivector([ranking([1, 2]), ranking([2, 3])]) + scores = dict( + zip(result["_rowid"].to_pylist(), result["_relevance_score"].to_pylist()) + ) + assert scores[2] == pytest.approx(1 / 62 + 1 / 61) + assert result["_rowid"].to_pylist() == [2, 1, 3] + + def test_rrf_reranker_distance(): data = pa.table( {