Model retrieval and knowledge graphs describes the practice of grounding an LLM’s output in a structured graph of entities and relationships instead of, or in addition to, unstructured text passages. Plain retrieval-augmented generation retrieves the k chunks most similar to a query and hopes the answer lives inside one of them. That works well for single-fact lookups and breaks down for questions that require connecting facts scattered across many documents, or questions that ask about the corpus as a whole rather than any one passage in it. A knowledge graph makes those connections explicit and queryable: entities become nodes, relationships become edges, and answering a multi-hop question becomes a graph traversal instead of a hope that one chunk happens to contain the whole chain. GraphRAG is the reference architecture for this pattern, and this entry covers the retrieval mechanics that sit underneath it: how a graph gets built from text, how local and global search differ, and when the added cost of graph construction is worth paying.
The Question a Vector Index Cannot Answer
Dense retrieval finds text that is semantically similar to a query. It has no notion of a relationship that spans two documents unless both documents happen to be retrieved together and the model stitches them at generation time, unreliably. Three question shapes expose this limit:
- Multi-hop questions. “Which vendors does the team that owns billing reconciliation depend on?” requires walking: team → owns → billing reconciliation, then billing reconciliation → depends-on → vendors. No single chunk contains that chain unless someone happened to write it down as one sentence.
- Corpus-level questions. “What are the main themes across our incident postmortems this year?” is not about any one postmortem; it requires a summary of the collection, which a top-k retrieval of individual documents cannot produce because no individual document is the answer.
- Disambiguation-sensitive questions. “What did Ada say about the migration?” needs to resolve which “Ada” (there may be several) before retrieving anything, which is an entity-resolution problem a vector index does not solve.
# Scenario: a single query that a flat vector index answers wrong not because
# retrieval failed, but because the answer requires connecting two facts that
# live in two different documents, neither of which mentions the other.
question = "Which of our vendors are affected if the payments team's on-call rotation changes?"
# Vector search retrieves documents ABOUT payments and documents ABOUT vendors,
# but "affected if X changes" is a relationship, not a phrase either document contains.
naive_chunks = vector_index.search(question, top_k=8) # likely misses the connection
# Graph traversal makes the relationship explicit and walks it directly.
subgraph = graph_index.traverse(
start=["payments team"], relation="depends_on", max_hops=2
)
Building the Graph: Extraction, Communities, Summaries
GraphRAG’s construction pipeline runs offline, once per corpus (or per update), and produces the structure that queries later traverse. Three stages matter:
flowchart LR
subgraph Offline["Offline: build once per corpus"]
A[Raw documents] --> B[LLM extraction:<br/>entities + relationships]
B --> C[(Entity-relationship graph)]
C --> D[Community detection<br/>e.g. Leiden algorithm]
D --> E[LLM summarization<br/>per community]
E --> F[(Community reports,<br/>hierarchical)]
end
subgraph Online["Online: per query"]
G[User query] --> H{Local or global?}
H -->|entity-specific| I[Local search:<br/>fan out from matched entities]
H -->|corpus-wide| J[Global search:<br/>map-reduce over community reports]
I --> C
J --> F
I --> K[Answer]
J --> K
end
classDef default fill:#ffffff,stroke:#4338CA,stroke-width:2px,color:#0F172A,rx:8px,ry:8px;
classDef data fill:#EEF0F7,stroke:#0D9488,stroke-width:2px,color:#0F172A,rx:8px,ry:8px;
classDef process fill:#F7F8FC,stroke:#6366F1,stroke-width:2px,color:#0F172A,rx:8px,ry:8px;
classDef output fill:#4338CA,stroke:#4338CA,stroke-width:2px,color:#ffffff,rx:8px,ry:8px;
class C,F data;
class B,D,E,I,J process;
class K output;
Extraction uses an LLM to read each chunk and pull out entities (people, systems, organizations, concepts) and the relationships between them, written as subject-predicate-object triples. This is the step entity-rich semantic structuring covers in depth; extraction quality bounds everything downstream.
Community detection clusters the resulting graph into groups of densely connected entities, typically with the Leiden algorithm, producing a hierarchy: fine-grained communities nested inside broader ones. A community here roughly corresponds to a topic or sub-theme that emerged organically from how entities actually connect, not from a predefined taxonomy.
Community summarization has an LLM write a natural-language report for each community, describing what it is about and what the key entities and relationships within it are. These reports are what global search actually retrieves over; they are queryable summaries of the graph’s structure, not the raw graph itself.
# Scenario: building the graph once, offline, from a batch of incident
# postmortems, so that later "what are the themes" queries have summaries
# to retrieve rather than having to re-read every document at query time.
def build_graph(documents, llm):
triples = []
for doc in documents:
# 1. Extraction: pull (entity, relation, entity) triples per chunk
triples.extend(llm.extract_triples(doc))
graph = build_graph_from_triples(triples)
# 2. Community detection: cluster the graph into topical groups
communities = leiden_cluster(graph)
# 3. Summarization: one LLM-written report per community
reports = {
c_id: llm.summarize(community_subgraph(graph, c_id))
for c_id in communities
}
return graph, reports
Local Search vs. Global Search
The graph supports two structurally different query modes, and picking the wrong one is the most common way GraphRAG systems underperform.
| Local search | Global search | |
|---|---|---|
| Good for | Questions about a specific entity or its neighborhood | Questions about themes across the whole corpus |
| Mechanism | Fan out from matched entities to direct neighbors | Map-reduce over community reports |
| Example question | ”What dependencies does the billing service have?" | "What are the recurring root causes across all postmortems?” |
| Cost shape | Scales with neighborhood size, cheap per query | Scales with number of communities queried, can be expensive |
| Failure mode | Misses corpus-wide patterns | Misses precise, entity-specific facts |
Local search starts by matching the query to specific entities in the graph, then retrieves their immediate neighborhood: related entities, relationships, and the source text chunks those came from. It behaves like a targeted, structure-aware version of standard RAG.
Global search does not start from an entity at all. It queries across all (or a sampled subset of) community reports in parallel, has the model extract partial answers from each, and then reduces those partial answers into one response. This is a map-reduce over summaries, which is what makes “summarize the themes across everything” tractable: the model never has to read every source document, only the community reports that already compressed them.
DRIFT search, a later refinement, combines both: it starts with a global-style pass to establish which communities are relevant, then drills into local search within those communities for precision, improving both quality and efficiency over running either mode alone.
# Scenario: implementing the local/global split. The routing decision from
# the frontmatter example feeds into this: relational questions go local,
# corpus-wide questions go global.
def global_search(question, community_reports, llm):
# Map: extract a partial answer + relevance score from every report
partial_answers = [
llm.extract_partial_answer(question, report)
for report in community_reports
]
# Reduce: combine the relevant partial answers into one response
relevant = [a for a in partial_answers if a.relevance > 0]
return llm.reduce(question, relevant)
def local_search(question, graph, llm):
entities = llm.extract_entities(question)
neighborhood = graph.neighbors(entities, max_hops=1)
return llm.answer_with_context(question, neighborhood)
The Structure-Content Trade-off
Building the retrieval context for a graph query involves a real tension the field only recently named precisely: retrieving strictly by sub-question relevance maximizes content precision but tends to produce a disconnected, fragmented subgraph, since each sub-question pulls in whatever is locally relevant without regard for whether the pieces connect. Retrieving by matching the whole original question preserves structural coherence in the subgraph but tends to sacrifice precision, since broader matching pulls in less targeted content. The finding is that the best-performing systems sit at an intermediate point, blending both signals with a tunable weight rather than committing fully to either.
Try this: drag to either extreme and watch answer quality (indigo) sag even though one of precision or coherence is maxed out. The peak sits in the middle, which is the paper's actual finding: neither pure signal wins.
When the Graph Is Worth Building
Graph construction is not free: every document runs through LLM extraction, community detection, and LLM summarization before a single query is answered, and updates to the corpus require re-running parts of that pipeline. That cost is worth paying when the query workload actually needs what a graph provides, and not otherwise.
| Signal | Favors graph retrieval | Favors plain vector RAG |
|---|---|---|
| Query shape | Multi-hop, relational, “how is X connected to Y” | Single-fact lookup, “what does the doc say about X” |
| Scope | Corpus-wide summarization and theme extraction | Passage-level answers |
| Corpus structure | Dense, real relationships between named entities | Loosely related prose, few named entities |
| Update frequency | Low to moderate, batch rebuilds tolerable | High-frequency, streaming updates |
| Budget | Can absorb offline extraction and summarization cost | Needs the cheapest possible indexing path |
Reported comparisons back this up in both directions: graph-based retrieval reaches noticeably higher accuracy than flat retrieval on multi-hop benchmarks, and it can be dramatically more token-efficient than naive alternatives at query time because community reports are pre-compressed. But that efficiency is bought with upfront extraction and summarization cost that a pure vector index never pays, so a workload that is mostly single-fact lookups over a large, fast-changing corpus is often better served by hybrid search over flat chunks than by a graph it will rarely traverse for more than one hop.
# Scenario: a lightweight heuristic for deciding, per corpus, whether the
# graph-construction cost is likely to pay off before committing to it.
def graph_is_worth_it(corpus_stats: dict) -> bool:
return (
corpus_stats["named_entity_density"] > 0.15 # entities per 100 words
and corpus_stats["update_frequency_days"] > 7 # not streaming
and corpus_stats["multi_hop_query_share"] > 0.2 # real relational demand
)
What’s New (2025 to 2026)
GraphRAG moved from a single Microsoft Research paper to a standard reference architecture across production RAG systems in this window. DRIFT search’s combination of global-then-local passes addressed the original architecture’s biggest practical complaint, that global search alone was too expensive to run per query. The structure-content trade-off work formalized what practitioners had been discovering by trial and error: neither pure entity-fanout nor pure question-matching retrieval is optimal, and the best systems now expose that blend as a tunable parameter rather than hardcoding one strategy. And hybrid production architectures matured to the point of routing each query to whichever backend (vector, graph, or structured database) fits its shape, rather than forcing every query through one retrieval path, which is the pattern the routing code at the top of this entry reflects.
Limitations
Extraction quality is the ceiling on everything downstream: an LLM that misses a relationship or hallucinates an entity poisons the graph for every future query that would have traversed it, and errors compound silently since a broken edge does not announce itself the way a missing chunk does. Community summaries are themselves LLM-generated compressions, so global search inherits a second layer of summarization risk on top of the extraction risk. And graphs built once and queried for months drift from the corpus as it changes underneath them, which is why “toward robust GraphRAG” work on mitigating retrieval drift from imperfect knowledge graphs has become its own active area rather than a solved problem. Treat the graph as an index that needs the same staleness monitoring as any other, not a permanent structure.
How to Use: Route a query to graph traversal or vector search based on its shape
# Scenario: an internal knowledge assistant over product docs and an org
# chart. "Who owns billing reconciliation?" needs graph traversal
# (entity -> relationship -> entity). "What does our refund policy say
# about annual plans?" needs plain semantic retrieval over text.
import re
RELATION_WORDS = re.compile(
r"\bwho (owns|reports to|works on)\b|\bwhich team\b|\bhow (is|are) .* related to\b",
re.IGNORECASE,
)
def route(question: str, graph_index, vector_index):
if RELATION_WORDS.search(question):
# Multi-hop: walk the graph rather than hoping one chunk has the answer
entities = graph_index.extract_entities(question)
subgraph = graph_index.traverse(entities, max_hops=2)
return graph_index.answer_from_subgraph(question, subgraph)
else:
# Single-fact lookup: plain dense retrieval is cheaper and sufficient
chunks = vector_index.search(question, top_k=8)
return vector_index.answer_from_chunks(question, chunks)
Ready to build?
Leverage AI technologies to build your product stack
Superteams can help you build, deploy and launch AI application stacks using open source technologies — from architecture through to production.
Talk to Superteams