Phase 3 enrichment ran a sequential Neo4j query per fused result and the final trim then discarded most of the output. Enrichment now covers only results that can still reach the response - the Phase 4 rerank slice (RERANK_SLICE_SIZE = 20, results beyond it are dropped when reranking) or final_result_count, whichever applies - and resolves every page in a single UNWIND $page_ids Cypher query via the new tenant-scoped GraphService.get_related_documents_batch (per-page ordering by shared_entities and per-page limit preserved via ORDER BY + collect()). Unenriched tail results still carry related_dossiers: [] so the response shape is unchanged. Query validated with EXPLAIN against the live Neo4j. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbFZyDvYksazX6nYQYZ67L
119 lines
3.7 KiB
Python
119 lines
3.7 KiB
Python
"""
|
|
Offline tests for Phase 3 enrichment running only on the top-k slice with
|
|
ONE batched related-documents lookup (perf: the old code ran a sequential
|
|
Neo4j query per fused result and the final trim discarded most of it).
|
|
"""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
from src.services.hybrid_rag_service import HybridRAGService
|
|
|
|
TENANT = "llm_tester"
|
|
|
|
|
|
@pytest.fixture
|
|
def graph():
|
|
g = MagicMock()
|
|
g.get_related_documents_batch = AsyncMock(return_value={})
|
|
return g
|
|
|
|
|
|
@pytest.fixture
|
|
def service(graph):
|
|
settings = MagicMock()
|
|
settings.ollama_llm_model = "test-model"
|
|
return HybridRAGService(
|
|
vector_service=MagicMock(),
|
|
graph_service=graph,
|
|
searxng_client=MagicMock(),
|
|
ollama_client=MagicMock(),
|
|
content_extractor=MagicMock(),
|
|
settings=settings,
|
|
)
|
|
|
|
|
|
def _fused(n):
|
|
return [
|
|
{
|
|
"result": {"page_id": i + 1, "title": f"page {i + 1}"},
|
|
"rrf_score": 1.0 / (i + 1),
|
|
"sources": ["vector"],
|
|
}
|
|
for i in range(n)
|
|
]
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestTopKEnrichment:
|
|
async def test_only_top_k_enriched_with_single_batched_lookup(
|
|
self, service, graph
|
|
):
|
|
results = _fused(30)
|
|
|
|
enriched = await service._enrich_with_related_dossiers(
|
|
results, user=TENANT, top_k=5
|
|
)
|
|
|
|
# ONE batched lookup, only for the top-k page ids
|
|
graph.get_related_documents_batch.assert_awaited_once()
|
|
kwargs = graph.get_related_documents_batch.await_args.kwargs
|
|
assert kwargs["page_ids"] == [1, 2, 3, 4, 5]
|
|
assert kwargs["user"] == TENANT
|
|
# The single-page method must not be used anymore
|
|
graph.get_related_documents.assert_not_called()
|
|
|
|
# Every result still carries the key (tail is empty)
|
|
assert all("related_dossiers" in r for r in enriched)
|
|
assert all(r["related_dossiers"] == [] for r in enriched[5:])
|
|
|
|
async def test_related_docs_mapped_onto_results(self, service, graph):
|
|
graph.get_related_documents_batch = AsyncMock(return_value={
|
|
1: [{
|
|
"page_id": 9, "title": "rel", "path": "p/rel",
|
|
"tags": ["docker", "infra", "misc", "extra-tag-ignored"],
|
|
"shared_entities": 3,
|
|
}],
|
|
})
|
|
results = _fused(3)
|
|
|
|
enriched = await service._enrich_with_related_dossiers(
|
|
results, user=TENANT, top_k=2
|
|
)
|
|
|
|
dossiers = enriched[0]["related_dossiers"]
|
|
assert len(dossiers) == 3 # max 3 tags per related doc
|
|
assert dossiers[0] == {
|
|
"page_id": 9, "title": "rel", "path": "p/rel",
|
|
"tag": "docker", "shared_entities": 3,
|
|
}
|
|
assert enriched[1]["related_dossiers"] == []
|
|
assert enriched[2]["related_dossiers"] == []
|
|
|
|
async def test_results_without_page_id_are_skipped(self, service, graph):
|
|
results = [
|
|
{"result": {"url": "http://x", "title": "web"}, "sources": ["web"]},
|
|
{"result": {"page_id": 7, "title": "wiki"}, "sources": ["vector"]},
|
|
]
|
|
|
|
enriched = await service._enrich_with_related_dossiers(
|
|
results, user=TENANT, top_k=10
|
|
)
|
|
|
|
kwargs = graph.get_related_documents_batch.await_args.kwargs
|
|
assert kwargs["page_ids"] == [7]
|
|
assert enriched[0]["related_dossiers"] == []
|
|
|
|
async def test_batch_lookup_failure_degrades_gracefully(self, service, graph):
|
|
graph.get_related_documents_batch = AsyncMock(
|
|
side_effect=RuntimeError("neo4j down")
|
|
)
|
|
results = _fused(3)
|
|
|
|
enriched = await service._enrich_with_related_dossiers(
|
|
results, user=TENANT, top_k=3
|
|
)
|
|
|
|
assert all(r["related_dossiers"] == [] for r in enriched)
|