mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-14 20:16:48 +00:00
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:
1 parent
b7e7872bf7
commit
a83e1dc0af
13 files changed
+1577
-79
No files matched your search
@@ -0,0 +1,94 @@
|
||||
"""Canonical entity naming for the per-source knowledge graph.
|
||||
|
||||
Nodes are merged on ``normalized_name``, which has been ``name.lower()``. That
|
||||
splits entities a reader would call the same thing: measured on the DocsGPT docs
|
||||
corpus, ``agent``/``agents``, ``VECTOR_STORE``/``Vector store``/``vector stores``,
|
||||
``Celery worker``/``Celery workers`` and ``.env file``/``env_file`` all landed as
|
||||
separate nodes — 58 such collisions across 1,704 entities, with 75% of entities
|
||||
appearing in exactly one chunk as a result.
|
||||
|
||||
:func:`canonical_name` folds the differences that are purely orthographic:
|
||||
case, surrounding punctuation, underscore/hyphen word breaks, and a *cautious*
|
||||
plural. Cautious matters: this corpus contains ``postgres``, ``kubernetes``,
|
||||
``https`` and ``aws``, none of which are plurals, so a naive "strip trailing s"
|
||||
would corrupt them into new entities rather than merge anything.
|
||||
|
||||
Always on: every graph is built with canonical names.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
_PUNCT = re.compile(r"[^\w\s]+", re.UNICODE)
|
||||
_UNDERSCORE = re.compile(r"[_\-]+")
|
||||
_SPACE = re.compile(r"\s+")
|
||||
|
||||
#: Words that end in "s" without being plural. Singularising these would invent
|
||||
#: entities ("postgre", "kubernete") instead of merging existing ones.
|
||||
_NOT_PLURAL = frozenset(
|
||||
{
|
||||
"postgres", "kubernetes", "https", "aws", "dns", "tls", "cors", "css",
|
||||
"js", "sas", "gas", "ss", "class", "access", "process", "status",
|
||||
"analysis", "basis", "axis", "https", "rss", "less", "express",
|
||||
"redis", "nats", "kibana", "elasticsearch", "os", "ios", "macos",
|
||||
"always", "sometimes", "series", "docs", "ops", "devops", "sse",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _singular(word: str) -> str:
|
||||
"""Best-effort singular of one word, biased hard towards leaving it alone.
|
||||
|
||||
Only the endings that are unambiguous in this domain are touched:
|
||||
``-ies`` -> ``-y`` (``policies``), ``-ses``/``-xes``/``-zes``/``-ches``/
|
||||
``-shes`` -> drop ``es`` (``indexes``, ``batches``), and a bare trailing
|
||||
``s`` on a word long enough to be safe. Everything in :data:`_NOT_PLURAL`,
|
||||
and anything ending in ``ss``/``us``/``is``, is returned unchanged.
|
||||
"""
|
||||
if len(word) < 4 or word in _NOT_PLURAL:
|
||||
return word
|
||||
if word.endswith(("ss", "us", "is")):
|
||||
return word
|
||||
if word.endswith("ies") and len(word) > 4:
|
||||
return word[:-3] + "y"
|
||||
if word.endswith(("ses", "xes", "zes", "ches", "shes")):
|
||||
return word[:-2]
|
||||
if word.endswith("s"):
|
||||
return word[:-1]
|
||||
return word
|
||||
|
||||
|
||||
def canonical_name(name: str) -> str:
|
||||
"""Merge key for an entity name.
|
||||
|
||||
Args:
|
||||
name: The entity name as the model wrote it.
|
||||
|
||||
Returns:
|
||||
A lowercase, punctuation-free, singularised key. Returns ``""`` for an
|
||||
empty or punctuation-only name, which callers treat as "no entity".
|
||||
|
||||
Examples:
|
||||
``VECTOR_STORE`` and ``Vector stores`` -> ``vector store``;
|
||||
``.env file`` and ``env_file`` -> ``env file``;
|
||||
``postgres`` stays ``postgres``.
|
||||
"""
|
||||
if not name:
|
||||
return ""
|
||||
text = _UNDERSCORE.sub(" ", str(name))
|
||||
text = _PUNCT.sub(" ", text)
|
||||
text = _SPACE.sub(" ", text).strip().lower()
|
||||
if not text:
|
||||
return ""
|
||||
return " ".join(_singular(word) for word in text.split())
|
||||
|
||||
|
||||
def normalize_entity_name(name: str) -> str:
|
||||
"""The key an entity is merged on: its :func:`canonical_name`.
|
||||
|
||||
Every graph the corpora were measured on was built this way, so it is the
|
||||
only mode rather than a flag. A graph built before this used plain
|
||||
``lower()`` keys; re-extracting it merges onto these instead.
|
||||
"""
|
||||
return canonical_name(name)
|
||||
Reference in new issue
Block a user