| """ |
| Component 4 test β multi_query.py |
| |
| Checks: |
| 1. generate_query_variants returns original + N distinct phrasings |
| 2. Variants are non-empty strings, not duplicates of each other |
| 3. Graceful fallback: if LLM fails, returns [original_query] only |
| 4. multi_query_retrieve returns no duplicate node IDs |
| 5. Result count is <= top_k |
| 6. Multi-query recall >= single-query recall on the same retriever |
| """ |
|
|
| import sys, os |
| sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..")) |
|
|
| from unittest.mock import patch, MagicMock |
| from src.pipelines.multi_query import generate_query_variants, multi_query_retrieve |
| from src.pipelines.retrieval_pipeline import HybridRetriever |
| from entrypoint.ingest import load_existing_index |
| import config |
|
|
|
|
| |
|
|
| def test_variants_include_original(): |
| variants = generate_query_variants("What is hybrid search?", n=3) |
| assert variants[0] == "What is hybrid search?", "First element must be the original query" |
| print(f" [PASS] Original query is first: '{variants[0]}'") |
|
|
|
|
| def test_variants_count(): |
| variants = generate_query_variants("How does RRF work?", n=3) |
| assert 1 <= len(variants) <= 4, f"Expected 1β4 variants, got {len(variants)}" |
| print(f" [PASS] Generated {len(variants)} variants (original + {len(variants)-1} alternatives)") |
| for i, v in enumerate(variants): |
| print(f" [{i}] {v}") |
|
|
|
|
| def test_variants_no_duplicates(): |
| variants = generate_query_variants("What is confidence scoring?", n=3) |
| lower = [v.lower() for v in variants] |
| assert len(lower) == len(set(lower)), f"Duplicate variants found: {variants}" |
| print(f" [PASS] No duplicate variants") |
|
|
|
|
| def test_variants_non_empty(): |
| variants = generate_query_variants("Explain small-to-big chunking", n=3) |
| for v in variants: |
| assert len(v.strip()) > 5, f"Variant too short: '{v}'" |
| print(f" [PASS] All {len(variants)} variants are non-empty strings") |
|
|
|
|
| def test_fallback_on_llm_failure(): |
| with patch("multi_query.requests.post", side_effect=Exception("network error")): |
| variants = generate_query_variants("test query", n=3) |
| assert variants == ["test query"], f"Should fall back to original only, got: {variants}" |
| print(" [PASS] Graceful fallback to [original] on LLM failure") |
|
|
|
|
| |
|
|
| def build_retriever(): |
| index, bm25, child_nodes, parent_node_map = load_existing_index() |
| vector_retriever = index.as_retriever(similarity_top_k=config.TOP_K_RETRIEVAL) |
| return HybridRetriever(child_nodes, vector_retriever, bm25=bm25), child_nodes |
|
|
|
|
| def test_no_duplicate_nodes(retriever): |
| results = multi_query_retrieve("what is cross-encoder reranking", retriever, top_k=15) |
| ids = [n.node_id for n in results] |
| assert len(ids) == len(set(ids)), "Duplicate nodes in multi-query results" |
| print(f" [PASS] No duplicates in {len(results)} merged results") |
|
|
|
|
| def test_respects_top_k(retriever): |
| top_k = 8 |
| results = multi_query_retrieve("hybrid search BM25", retriever, top_k=top_k) |
| assert len(results) <= top_k, f"Got {len(results)} results, expected <= {top_k}" |
| print(f" [PASS] Result count {len(results)} <= top_k {top_k}") |
|
|
|
|
| def test_multi_query_recall_vs_single(retriever): |
| query = "how does the pipeline expand context for generation" |
|
|
| single = retriever.retrieve(query, top_k=config.TOP_K_RETRIEVAL) |
| single_ids = {n.node_id for n in single} |
|
|
| multi = multi_query_retrieve(query, retriever, top_k=config.TOP_K_RETRIEVAL * 3) |
| multi_ids = {n.node_id for n in multi} |
|
|
| assert len(multi_ids) >= len(single_ids), ( |
| f"Multi-query ({len(multi_ids)}) should recall >= single ({len(single_ids)})" |
| ) |
| extra = len(multi_ids - single_ids) |
| print(f" [PASS] Multi-query recall: {len(multi_ids)} nodes vs single {len(single_ids)} " |
| f"(+{extra} extra unique nodes found)") |
|
|
|
|
| |
|
|
| if __name__ == "__main__": |
| print("\n=== Component 4: multi_query.py ===\n") |
|
|
| print("-- Unit tests: variant generation --") |
| test_variants_include_original() |
| test_variants_count() |
| test_variants_no_duplicates() |
| test_variants_non_empty() |
| test_fallback_on_llm_failure() |
|
|
| print("\n-- Integration tests: parallel retrieval --") |
| retriever, child_nodes = build_retriever() |
| test_no_duplicate_nodes(retriever) |
| test_respects_top_k(retriever) |
| test_multi_query_recall_vs_single(retriever) |
|
|
| print("\n=== All tests passed β ready to build Component 5 (hyde.py) ===\n") |
|
|