| """ |
| Tests for DocMind Hybrid Retriever — RRF fusion logic. |
| """ |
|
|
| import sys |
| import os |
|
|
| |
| sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) |
|
|
| from pipeline.retriever import rrf_fusion |
|
|
|
|
| class TestRRFFusion: |
| """Test Reciprocal Rank Fusion implementation.""" |
|
|
| def test_basic_fusion(self): |
| """Two lists with overlapping items should sum scores.""" |
| bm25 = [("c1", 5.0), ("c2", 4.0), ("c3", 3.0)] |
| dense = [("c2", 0.9), ("c3", 0.8), ("c4", 0.7)] |
|
|
| result = rrf_fusion(bm25, dense, k=60, top_k=3) |
|
|
| |
| ids = [cid for cid, _ in result] |
| assert ids[0] == "c2", f"Expected c2 first, got {ids[0]}" |
| assert len(result) == 3 |
|
|
| def test_no_overlap(self): |
| """Disjoint lists should include items from both.""" |
| bm25 = [("a1", 5.0), ("a2", 4.0)] |
| dense = [("b1", 0.9), ("b2", 0.8)] |
|
|
| result = rrf_fusion(bm25, dense, k=60, top_k=4) |
|
|
| ids = {cid for cid, _ in result} |
| assert ids == {"a1", "a2", "b1", "b2"} |
|
|
| def test_complete_overlap(self): |
| """Identical lists should give higher scores than single-list items.""" |
| items = [("c1", 5.0), ("c2", 4.0)] |
| result = rrf_fusion(items, items, k=60, top_k=2) |
|
|
| |
| for _, score in result: |
| |
| assert score > 1.0 / 62 |
|
|
| def test_empty_inputs(self): |
| """Empty input lists should return empty results.""" |
| result = rrf_fusion([], [], k=60, top_k=8) |
| assert result == [] |
|
|
| def test_one_empty_list(self): |
| """One empty list should still return items from the other.""" |
| bm25 = [("c1", 5.0), ("c2", 4.0)] |
| result = rrf_fusion(bm25, [], k=60, top_k=2) |
|
|
| assert len(result) == 2 |
| ids = [cid for cid, _ in result] |
| assert "c1" in ids |
|
|
| def test_top_k_limit(self): |
| """Should respect the top_k parameter.""" |
| bm25 = [(f"c{i}", float(10 - i)) for i in range(10)] |
| dense = [(f"c{i}", float(10 - i) / 10) for i in range(10)] |
|
|
| result = rrf_fusion(bm25, dense, k=60, top_k=3) |
| assert len(result) == 3 |
|
|
| def test_score_ordering(self): |
| """Results should be sorted by descending RRF score.""" |
| bm25 = [("c1", 5.0), ("c2", 4.0), ("c3", 3.0)] |
| dense = [("c1", 0.9), ("c2", 0.8), ("c3", 0.7)] |
|
|
| result = rrf_fusion(bm25, dense, k=60, top_k=3) |
|
|
| scores = [score for _, score in result] |
| assert scores == sorted(scores, reverse=True), "Scores should be descending" |
|
|
| def test_rrf_score_formula(self): |
| """Verify the exact RRF score computation.""" |
| bm25 = [("c1", 5.0)] |
| dense = [("c1", 0.9)] |
|
|
| result = rrf_fusion(bm25, dense, k=60, top_k=1) |
|
|
| expected_score = 1.0 / (60 + 1) + 1.0 / (60 + 1) |
| assert abs(result[0][1] - expected_score) < 1e-10, \ |
| f"Expected {expected_score}, got {result[0][1]}" |
|
|
|
|
| |
|
|
| if __name__ == "__main__": |
| test = TestRRFFusion() |
| test_methods = [m for m in dir(test) if m.startswith("test_")] |
|
|
| passed = 0 |
| failed = 0 |
| for method_name in test_methods: |
| try: |
| getattr(test, method_name)() |
| print(f" [PASS] {method_name}") |
| passed += 1 |
| except AssertionError as e: |
| print(f" [FAIL] {method_name}: {e}") |
| failed += 1 |
| except Exception as e: |
| print(f" [ERROR] {method_name}: {type(e).__name__}: {e}") |
| failed += 1 |
|
|
| print(f"\n{'='*40}") |
| print(f"Results: {passed} passed, {failed} failed") |
|
|