Spaces:
Running on Zero
Running on Zero
| """Tests for fusion algorithms.""" | |
| from unittest.mock import MagicMock | |
| import pytest | |
| from langchain_core.documents import Document | |
| from src.retrieval.fusion import FusionRetriever, reciprocal_rank_fusion, weighted_fusion | |
| def sample_results(): | |
| """Create sample ranked results for testing.""" | |
| # Simulate results from two different retrievers | |
| vector_results = [ | |
| Document(page_content="Document A about machine learning", metadata={"source": "a"}), | |
| Document(page_content="Document B about deep learning", metadata={"source": "b"}), | |
| Document(page_content="Document C about neural networks", metadata={"source": "c"}), | |
| ] | |
| bm25_results = [ | |
| Document(page_content="Document B about deep learning", metadata={"source": "b"}), | |
| Document(page_content="Document D about Python", metadata={"source": "d"}), | |
| Document(page_content="Document A about machine learning", metadata={"source": "a"}), | |
| ] | |
| return vector_results, bm25_results | |
| def mock_retrievers(): | |
| """Create mock retrievers for testing.""" | |
| from langchain_core.retrievers import BaseRetriever | |
| # Use spec to satisfy Pydantic validation | |
| retriever1 = MagicMock(spec=BaseRetriever) | |
| retriever1.invoke.return_value = [ | |
| Document(page_content="Doc A from retriever 1"), | |
| Document(page_content="Doc B from retriever 1"), | |
| ] | |
| retriever2 = MagicMock(spec=BaseRetriever) | |
| retriever2.invoke.return_value = [ | |
| Document(page_content="Doc B from retriever 1"), # Same as above | |
| Document(page_content="Doc C from retriever 2"), | |
| ] | |
| return [retriever1, retriever2] | |
| # --- Reciprocal Rank Fusion Tests --- | |
| def test_rrf_empty_results(): | |
| """Test RRF with empty results.""" | |
| result = reciprocal_rank_fusion([]) | |
| assert result == [] | |
| def test_rrf_single_result_list(sample_results): | |
| """Test RRF with single result list.""" | |
| vector_results, _ = sample_results | |
| result = reciprocal_rank_fusion([vector_results]) | |
| # Order should be preserved | |
| assert len(result) == 3 | |
| assert result[0].page_content == vector_results[0].page_content | |
| def test_rrf_combines_results(sample_results): | |
| """Test that RRF combines results from multiple sources.""" | |
| vector_results, bm25_results = sample_results | |
| result = reciprocal_rank_fusion([vector_results, bm25_results]) | |
| # Should have all unique documents | |
| contents = {doc.page_content for doc in result} | |
| assert len(contents) == 4 # A, B, C, D | |
| def test_rrf_ranks_overlapping_higher(sample_results): | |
| """Test that documents in both lists rank higher.""" | |
| vector_results, bm25_results = sample_results | |
| result = reciprocal_rank_fusion([vector_results, bm25_results]) | |
| # Documents A and B appear in both lists, should rank high | |
| top_2_contents = {doc.page_content for doc in result[:2]} | |
| assert ( | |
| "Document A about machine learning" in top_2_contents | |
| or "Document B about deep learning" in top_2_contents | |
| ) | |
| def test_rrf_with_equal_weights(sample_results): | |
| """Test RRF with equal weights.""" | |
| vector_results, bm25_results = sample_results | |
| result = reciprocal_rank_fusion( | |
| [vector_results, bm25_results], | |
| weights=[0.5, 0.5], | |
| ) | |
| assert len(result) == 4 | |
| def test_rrf_prefers_higher_weight(sample_results): | |
| """Test that RRF prefers results from higher-weighted source.""" | |
| vector_results, bm25_results = sample_results | |
| # Heavy weight on vector | |
| result_vector_heavy = reciprocal_rank_fusion( | |
| [vector_results, bm25_results], | |
| weights=[0.9, 0.1], | |
| ) | |
| # Heavy weight on BM25 | |
| result_bm25_heavy = reciprocal_rank_fusion( | |
| [vector_results, bm25_results], | |
| weights=[0.1, 0.9], | |
| ) | |
| # Both should produce valid results | |
| assert len(result_vector_heavy) == 4 | |
| assert len(result_bm25_heavy) == 4 | |
| def test_rrf_custom_k_parameter(sample_results): | |
| """Test RRF with different k values.""" | |
| vector_results, bm25_results = sample_results | |
| result_low_k = reciprocal_rank_fusion([vector_results, bm25_results], k=1) | |
| result_high_k = reciprocal_rank_fusion([vector_results, bm25_results], k=1000) | |
| # Both should produce same set of documents, potentially different order | |
| assert len(result_low_k) == 4 | |
| assert len(result_high_k) == 4 | |
| def test_rrf_weights_must_match_results(sample_results): | |
| """Test that mismatched weights raise error.""" | |
| vector_results, bm25_results = sample_results | |
| with pytest.raises(ValueError, match="weights must match"): | |
| reciprocal_rank_fusion([vector_results, bm25_results], weights=[0.5]) | |
| # --- Weighted Fusion Tests --- | |
| def test_weighted_fusion_empty_results(): | |
| """Test weighted fusion with empty results.""" | |
| result = weighted_fusion([]) | |
| assert result == [] | |
| def test_weighted_fusion_with_scores(): | |
| """Test weighted fusion with scored results.""" | |
| results1 = [ | |
| (Document(page_content="Doc A"), 0.9), | |
| (Document(page_content="Doc B"), 0.7), | |
| ] | |
| results2 = [ | |
| (Document(page_content="Doc B"), 0.8), | |
| (Document(page_content="Doc C"), 0.6), | |
| ] | |
| result = weighted_fusion([results1, results2], weights=[0.5, 0.5]) | |
| # Doc B should rank highest (appears in both) | |
| assert len(result) == 3 | |
| def test_weighted_fusion_no_weights(): | |
| """Test weighted fusion without explicit weights.""" | |
| results1 = [ | |
| (Document(page_content="Doc A"), 0.9), | |
| (Document(page_content="Doc B"), 0.7), | |
| ] | |
| results2 = [ | |
| (Document(page_content="Doc C"), 0.8), | |
| ] | |
| result = weighted_fusion([results1, results2]) | |
| assert len(result) == 3 | |
| # --- FusionRetriever Tests --- | |
| def test_fusion_retriever_init(mock_retrievers): | |
| """Test FusionRetriever initialization.""" | |
| retriever = FusionRetriever( | |
| retrievers=mock_retrievers, | |
| weights=[0.7, 0.3], | |
| fusion_algorithm="rrf", | |
| rrf_k=60, | |
| final_k=4, | |
| ) | |
| assert retriever.fusion_algorithm == "rrf" | |
| assert retriever.rrf_k == 60 | |
| assert retriever.final_k == 4 | |
| def test_fusion_retriever_invoke(mock_retrievers): | |
| """Test FusionRetriever invoke method.""" | |
| retriever = FusionRetriever( | |
| retrievers=mock_retrievers, | |
| weights=[0.5, 0.5], | |
| fusion_algorithm="rrf", | |
| final_k=4, | |
| ) | |
| results = retriever.invoke("test query") | |
| assert len(results) <= 4 | |
| assert all(isinstance(doc, Document) for doc in results) | |
| def test_fusion_retriever_handles_failures(mock_retrievers): | |
| """Test FusionRetriever handles retriever failures gracefully.""" | |
| mock_retrievers[0].invoke.side_effect = Exception("Retriever 1 failed") | |
| retriever = FusionRetriever( | |
| retrievers=mock_retrievers, | |
| weights=[0.5, 0.5], | |
| fusion_algorithm="rrf", | |
| final_k=4, | |
| ) | |
| # Should not raise, just return results from working retriever | |
| results = retriever.invoke("test query") | |
| assert len(results) > 0 | |
| def test_fusion_retriever_non_rrf_fallback(mock_retrievers): | |
| """Test FusionRetriever falls back to RRF for unsupported algorithms.""" | |
| retriever = FusionRetriever( | |
| retrievers=mock_retrievers, | |
| weights=[0.5, 0.5], | |
| fusion_algorithm="unsupported", # This should trigger fallback | |
| final_k=4, | |
| ) | |
| results = retriever.invoke("test query") | |
| # Should still work, falling back to RRF | |
| assert len(results) <= 4 | |
| def test_fusion_retriever_get_info(mock_retrievers): | |
| """Test FusionRetriever get_retriever_info method.""" | |
| retriever = FusionRetriever( | |
| retrievers=mock_retrievers, | |
| weights=[0.7, 0.3], | |
| fusion_algorithm="rrf", | |
| rrf_k=60, | |
| final_k=4, | |
| ) | |
| info = retriever.get_retriever_info() | |
| assert info["num_retrievers"] == 2 | |
| assert info["weights"] == [0.7, 0.3] | |
| assert info["fusion_algorithm"] == "rrf" | |
| assert info["rrf_k"] == 60 | |
| assert info["final_k"] == 4 | |
| def test_fusion_retriever_no_weights(mock_retrievers): | |
| """Test FusionRetriever without explicit weights.""" | |
| retriever = FusionRetriever( | |
| retrievers=mock_retrievers, | |
| fusion_algorithm="rrf", | |
| final_k=4, | |
| ) | |
| results = retriever.invoke("test query") | |
| assert len(results) <= 4 | |