feat(graphrag): seed the walk from what entities are, and rank with passages and vector hits

Graph retrieval tied plain vector search at best and never beat it. Measured
across five corpora, the bottleneck was seeding, not the graph: the walk
started from nodes whose embeddings were computed from bare entity names, and
a whole question shares almost nothing with a name like "Quill".

Extraction now embeds each node from "name (type): description" and each
relationship as the fact it asserts ("Alder streams_to Quill: ..."), stored on
a new nullable graph_edges.fact_embedding column that ensure_vector_schema adds
in place. Entity names are canonicalised (case, punctuation, word breaks and a
cautious plural) so "VECTOR_STORE" and "vector stores" land on one node. Extraction calls run
concurrently (GRAPHRAG_EXTRACTION_WORKERS, default 8) while embedding and graph
writes stay serial on the task thread, so ordering and idempotency are
unchanged; that measured 8.4x faster with identical output.

Retrieval gains per-source options, stored under retrieval.graph and read live
at query time:

- seed_strategy: start from matching entities (default) or matching
  relationships, which can reach an entity the question never names;
- passage_nodes (on): walk the source's passages alongside entities, with
  PageRank damping 0.5 instead of 0.85;
- blend_vector (on): fuse the graph ranking with the source's vector ranking
  by reciprocal rank.

The defaults are the measured-best configuration. Through GraphRAGRetriever,
the new seeding moved recall@4 from 0.41 to 0.68 on a multi-hop corpus and
from 0.50 to 1.00 on the docs corpus, and regressed none of the corpora
measured. Existing graphs keep name-only embeddings until rebuilt.
This commit is contained in:
Alex committed 2026-09-19 14:07:41 +01:00
1 parent b7e7872bf7
commit a83e1dc0af
13 files changed
+1577 -79

No files matched your search

+23
View File
@@ -45,6 +45,29 @@ def _patch_embed(monkeypatch):
)
@pytest.fixture(autouse=True)
def _entity_only_ranking(monkeypatch):
"""Pin the ranking path these tests were written for.
Everything here exercises entity-only PPR ranking without vector blending,
driven through ``MagicMock`` stores. The shipped default now walks the
passages and blends with vector search — covered end to end in
``tests/graphrag/test_retriever_default_path.py`` with a store that returns
real values. Pinning keeps each test here asserting what it was written to
assert, rather than whatever a mock happens to return on a path it never set
up.
"""
from docsgpt.storage.db.source_config import GraphRetrievalConfig
monkeypatch.setattr(
GraphRAGRetriever,
"_graph_options",
lambda self, source_id: GraphRetrievalConfig(
passage_nodes=False, blend_vector=False
),
)
# ── Fallback to ClassicRAG ────────────────────────────────────────────────────