Knowledge Graphs and GraphRAG
Vector RAG retrieves similar chunks. Knowledge graphs retrieve related entities and traverse relationships. For domains where relationships are the answer (org charts, drug interactions, supply chains, fraud rings), graphs beat vectors.
When graphs beat vectors
Vector RAG works when answers live in passages of similar text. It fails when:
- The answer requires combining facts from disconnected sources (“Who reports to Alice’s manager?”).
- The query is structural (“All companies in our supply chain affected by the Taiwan earthquake”).
- Relationships matter more than text similarity (“Doctors who prescribed drug X to patients also taking drug Y”).
- Multi-hop reasoning (“What’s the parent company of the vendor of the subcomponent that failed?”).
For these, you need explicit relationships. Knowledge graphs store them.
Graph basics
A knowledge graph is nodes (entities) connected by edges (relationships):
(Alice:Person) -[:WORKS_AT]-> (Acme:Company)
(Acme:Company) -[:LOCATED_IN]-> (Berlin:City)
(Berlin:City) -[:IN_COUNTRY]-> (Germany:Country)
Both nodes and edges have labels (types) and properties (key-value attributes).
(Alice:Person {name: "Alice", age: 30})
-[:WORKS_AT {since: 2022, title: "Engineer"}]->
(Acme:Company {founded: 2010})
Storage options
| Strengths | Weaknesses | |
|---|---|---|
| Neo4j | mature, Cypher language, ecosystem | self-hosted ops, license costs at scale |
| Amazon Neptune | managed AWS service | proprietary, weaker community |
| TigerGraph | massive scale, fast | rare, niche |
| Memgraph | in-memory, fast, Cypher-compatible | newer |
| NetworkX (Python) | embedded, no server | not for production scale |
| Postgres + pgrouting / Apache AGE | reuse your SQL DB | less ergonomic, slower |
For most production GraphRAG: Neo4j or Memgraph. For prototyping: NetworkX.
Cypher — the query language
Neo4j’s query language (also supported by Memgraph and others). Reads like ASCII art:
// Find all people who work at companies in Germany
MATCH (p:Person)-[:WORKS_AT]->(c:Company)-[:LOCATED_IN]->(:City)-[:IN_COUNTRY]->(:Country {name: "Germany"})
RETURN p.name, c.name
Multi-hop relationships are first-class. Variable-length paths:
// Find Alice's manager chain (up to 5 levels)
MATCH (alice:Person {name: "Alice"})-[:REPORTS_TO*1..5]->(boss)
RETURN boss.name
Aggregations, filtering, ordering — all SQL-like:
MATCH (p:Person)-[:WORKS_AT]->(c:Company)
WHERE c.industry = "tech"
RETURN c.name, COUNT(p) AS employee_count
ORDER BY employee_count DESC
LIMIT 10
GraphRAG — the retrieval pattern
Microsoft’s GraphRAG paper (2024) popularized the pattern:
1. Build a knowledge graph from your corpus.
2. Extract entities and relationships using an LLM.
3. Generate community summaries (clusters of related entities).
4. At query time, retrieve relevant subgraphs (not chunks).
5. Pass the subgraph + community summaries as context to the LLM.
Stage 1: extraction
prompt = """Extract entities and relationships from the following text.
Return JSON with the structure:
{
"entities": [{"name": ..., "type": ...}],
"relationships": [{"source": ..., "target": ..., "type": ..., "description": ...}]
}
Text: {chunk}"""
result = llm.generate(prompt.format(chunk=chunk))
data = json.loads(result)
for ent in data["entities"]:
graph.merge_node(name=ent["name"], type=ent["type"])
for rel in data["relationships"]:
graph.add_edge(rel["source"], rel["target"], type=rel["type"], desc=rel["description"])
LLM-based entity/relationship extraction. Use Pydantic + structured output to enforce schema:
class Entity(BaseModel):
name: str
type: Literal["Person", "Company", "Location", "Event"]
class Relationship(BaseModel):
source: str
target: str
type: str
description: str
class Extraction(BaseModel):
entities: list[Entity]
relationships: list[Relationship]
Iterate over corpus chunks → graph grows incrementally.
Stage 2: community detection
Run a clustering algorithm (Leiden, Louvain) on the graph to find communities — groups of densely-connected nodes that probably represent topics or themes.
For each community, generate a summary via LLM:
for community in graph.communities():
members = [node.name for node in community.nodes]
edges = [(e.source, e.target, e.type) for e in community.edges]
summary = llm.summarize(f"Summarize this knowledge graph community: nodes={members}, edges={edges}")
community.summary = summary
Community summaries are pre-computed and indexed (often vector-embedded for retrieval).
Stage 3: query-time retrieval
def graphrag_query(question: str) -> str:
# 1. Extract entities mentioned in the question
entities = llm_extract_entities(question)
# 2. Find relevant communities
community_summaries = retrieve_communities(question, top_k=5)
# 3. Pull subgraph around the entities
subgraph = graph.subgraph_around(entities, hops=2)
# 4. Build context: community summaries + subgraph
context = format_for_llm(community_summaries, subgraph)
return llm.generate(f"Answer using:\n{context}\n\nQuestion: {question}")
The LLM gets a structured view of the data — entities and relationships — instead of (or alongside) text chunks.
Hybrid: vector + graph
Production GraphRAG usually pairs both:
def hybrid_retrieve(question):
vector_hits = vector_store.search(embed(question), top_k=5)
entities = extract_entities(question)
graph_subgraph = graph.expand(entities, hops=2)
return vector_hits, graph_subgraph
Vector retrieval finds relevant chunks; graph traversal finds connected facts. The LLM consumes both. Each fills the other’s gaps:
- Vector misses cross-document relationships → graph catches them.
- Graph misses unstructured nuance → vector catches it.
Tools
| Tool | Role |
|---|---|
| Neo4j | graph DB |
| Microsoft GraphRAG | end-to-end GraphRAG library (build + query) |
| LlamaIndex KnowledgeGraphIndex | graph-RAG via LlamaIndex |
| LangChain GraphCypherQAChain | LLM → Cypher → results |
| rdflib | RDF/SPARQL Python lib for academic-style graphs |
LangChain’s GraphCypherQAChain lets the LLM generate Cypher queries directly:
from langchain.chains import GraphCypherQAChain
from langchain_neo4j import Neo4jGraph
graph = Neo4jGraph(url="bolt://...", username="...", password="...")
chain = GraphCypherQAChain.from_llm(llm=ChatOpenAI(), graph=graph, verbose=True)
result = chain.invoke({"query": "Who is Alice's manager's manager?"})
The chain prompts the LLM with the schema and your question, the LLM emits Cypher, the chain executes it, then the LLM formulates a natural-language answer from the results.
Building the graph — challenges
Entity resolution
LLMs extract “Alice”, “Alice Johnson”, “A. Johnson” as separate entities. Resolve them:
def canonicalize(entity):
return embedding_match(entity, existing_entities, threshold=0.9)
Or LLM-assisted resolution as a separate pass.
Schema drift
LLM extracts “REPORTS_TO” sometimes, “MANAGED_BY” other times. Same relationship, different label.
Mitigation: provide a fixed schema in the extraction prompt:
ALLOWED_REL_TYPES = ["REPORTS_TO", "WORKS_AT", "LOCATED_IN"]
prompt = f"Use only these relationship types: {ALLOWED_REL_TYPES}..."
Or Pydantic enum types in the extraction schema.
Cost
LLM-based extraction is expensive at scale. A 1M-document corpus × $0.01 per extraction = $10k. Optimize:
- Cheaper extraction model (Haiku, GPT-4o-mini).
- Pre-filter with NER (spaCy) for entity candidates.
- Hybrid: rule-based for common patterns, LLM for ambiguous.
Update strategy
Graphs are mutable. New documents → new entities, possibly conflicting. Snapshots, lineage tracking, conflict resolution all become DB-engineering problems.
When NOT to use a knowledge graph
- Small / simple corpora. Vector RAG is much cheaper to build.
- Relationships don’t matter much. “Summarize this article” doesn’t need a graph.
- Latency-sensitive. Graph traversal + LLM hop is slow vs vector lookup.
- Schema unstable. Domains where relationships keep changing — graph maintenance is expensive.
Interview angle
- “When would you choose a knowledge graph over vector RAG?” — when relationships drive the answer (org charts, supply chains, drug interactions, fraud rings) or multi-hop reasoning is required. Vector RAG works on text similarity; graphs work on structure.
- “What’s GraphRAG?” — Microsoft’s pattern: extract entities + relationships from documents via LLM, build a knowledge graph, detect communities, summarize each, query by retrieving subgraphs + community summaries. Combines graph traversal with LLM synthesis.
- “How do you extract entities and relationships from unstructured text?” — LLM with a structured-output schema (Pydantic), bounded entity/relationship types, iterate over corpus chunks. Pre-filter with NER for cost. Entity resolution (dedup similar names) is a separate pass.
- “Hybrid graph + vector retrieval — why?” — vector finds passage-level relevant text; graph finds connected facts. They cover different failure modes. Production GraphRAG usually combines both.
- “How do you query a knowledge graph from an LLM?” —
GraphCypherQAChainpattern: provide the schema to the LLM, prompt it to generate Cypher, execute the Cypher, feed results back to the LLM for natural-language answer. Or precompute subgraphs and pass them as context. - “What goes wrong in production with GraphRAG?” — entity resolution (same person, different names), schema drift (same relationship, different labels), extraction cost at scale, graph maintenance as data changes. The graph is a maintained artifact, not a one-shot.