mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-15 08:16:38 +00:00
fix(retriever): search a graph source classically when its graph answers nothing
Only a raise routed a source to the ClassicRAG fallback, and every graph read logs its own failure and returns empty. So a query that broke, a half-built graph and a walk that genuinely found nothing were indistinguishable, and each made the source contribute nothing at all to the answer -- no fallback, no vector blend, which is skipped by the same early return. A graph source that produces no documents now joins the classic batch, exactly as a source with no graph already does. The passage stage also rescanned every node's chunk list once per candidate. It inverts the links once instead: 7.3 ms to 0.16 ms on a 400-node subgraph at the candidate cap, with identical output.
This commit is contained in:
1 parent
8c5a5190d9
commit
bc0ef9f3b0
3 files changed
+76
-16
No files matched your search
@@ -130,3 +130,22 @@ class TestChunkSimilaritiesGuard:
|
||||
store = object.__new__(GraphStore)
|
||||
|
||||
assert store.chunk_similarities("src", chunk_ids, embedding) == {}
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestNodesByChunk:
|
||||
"""The passage stage inverts node->chunks once instead of rescanning."""
|
||||
|
||||
def test_inverts_and_keeps_node_order(self):
|
||||
from docsgpt.retriever.graph_rag import _nodes_by_chunk
|
||||
|
||||
assert _nodes_by_chunk({"n1": ["c1", "c2"], "n2": ["c2"], "n3": []}) == {
|
||||
"c1": ["n1"],
|
||||
"c2": ["n1", "n2"],
|
||||
}
|
||||
|
||||
def test_no_links_invert_to_nothing(self):
|
||||
from docsgpt.retriever.graph_rag import _nodes_by_chunk
|
||||
|
||||
assert _nodes_by_chunk({}) == {}
|
||||
assert _nodes_by_chunk({"n1": None}) == {}
|
||||
@@ -180,7 +180,9 @@ class TestGraphRAGPoolDiscipline:
|
||||
mock_store_cls.return_value = store
|
||||
|
||||
rag = _make_retriever()
|
||||
with patch.object(rag, "_graph_docs_for_source", return_value=[]):
|
||||
# A real result: an empty one now falls back like a failure does.
|
||||
graph_docs = [{"title": "g", "text": "graph text", "source": "src1", "filename": "g"}]
|
||||
with patch.object(rag, "_graph_docs_for_source", return_value=graph_docs):
|
||||
with patch.object(rag, "_classic_for_sources") as classic:
|
||||
rag._get_data()
|
||||
|
||||
@@ -355,15 +357,24 @@ class TestGraphRAGHappyPath:
|
||||
@patch("docsgpt.retriever.graph_rag.num_tokens_from_string", return_value=10)
|
||||
@patch("docsgpt.retriever.graph_rag.GraphStore")
|
||||
@patch("docsgpt.retriever.graph_rag.graphrag_available", return_value=True)
|
||||
def test_no_seeds_returns_empty(
|
||||
def test_a_graph_that_answers_nothing_falls_back_to_classic(
|
||||
self, _avail, mock_store_cls, _tok, _patch_llm_creator, _patch_embed
|
||||
):
|
||||
"""Empty is not an answer. Every graph read swallows its own errors and
|
||||
returns nothing, so "no rows" covers a broken query as much as a walk
|
||||
that found nothing — and the source would contribute nothing at all,
|
||||
with no fallback, because only a raise routes one to ClassicRAG."""
|
||||
store = _store_with_graph([], [], {}, {}, [])
|
||||
store.count_nodes_many.side_effect = lambda ids: {s: 5 for s in ids}
|
||||
mock_store_cls.return_value = store
|
||||
|
||||
rag = _make_retriever()
|
||||
assert rag._get_data() == []
|
||||
seen = _recording_classic(rag, [_CLASSIC_DOC])
|
||||
|
||||
docs = rag._get_data()
|
||||
|
||||
assert seen == [["src1"]]
|
||||
assert [doc["text"] for doc in docs] == ["classic"]
|
||||
|
||||
|
||||
# ── IDF down-weighting ────────────────────────────────────────────────────────
|
||||
|
||||
Reference in new issue
Block a user