GraphRAG: Structured Knowledge Graph Retrieval
GraphRAG leverages knowledge graphs—networks of entities and their relationships—to enable structured, reasoning-aware retrieval. Instead of searching flat documents, GraphRAG queries a graph of concepts (e.g., "Company X acquired Company Y in 2024") and traverses relationship edges to answer multi-step reasoning questions. This approach improves accuracy on multi-hop questions by 35–45% and enables complex queries that would fail with flat text retrieval (Shi et al., 2024).
What Is a Knowledge Graph?
A knowledge graph is a directed graph where nodes represent entities (people, organizations, places, concepts) and edges represent relationships between them. For example:
Node: "OpenAI"
├─ is_a_company
├─ founded_by → "Sam Altman"
├─ created → "ChatGPT"
├─ acquired_by → "Microsoft"
└─ employees → ["Ilya Sutskever", "Dario Amodei", ...]
Node: "ChatGPT"
├─ is_a_model
├─ created_by → "OpenAI"
├─ base_model → "GPT-4"
└─ release_date → "November 2022"
GraphRAG extracts entities and relationships from documents, builds this graph, and then answers user questions by traversing the graph—finding paths between entities that answer the query.
Building a Knowledge Graph from Documents
Extract entities and relationships using an LLM:
from anthropic import Anthropic
import json
client = Anthropic()
def extract_entities_and_relations(document: str) -> dict:
"""Extract entities and relationships from a document."""
extraction_prompt = """Extract all entities (people, companies, places, concepts)
and relationships from this document.
Return JSON: {
"entities": [{"name": "...", "type": "person|company|place|concept"}],
"relationships": [{"subject": "...", "relation": "...", "object": "..."}]
}"""
response = client.messages.create(
model="claude-opus-4-1",
max_tokens=1000,
system=extraction_prompt,
messages=[{"role": "user", "content": document}]
)
text = response.content[0].text
start = text.find('{')
end = text.rfind('}') + 1
return json.loads(text[start:end])
# Example: extract from a news article
article = """Microsoft announced on June 1, 2025 that it has invested $5B
in Anthropic to strengthen AI capabilities. Dario Amodei, CEO of Anthropic,
said this partnership will accelerate research on safer AI systems."""
graph_data = extract_entities_and_relations(article)
print("Entities:")
for entity in graph_data["entities"]:
print(f" {entity['name']} ({entity['type']})")
print("\nRelationships:")
for rel in graph_data["relationships"]:
print(f" {rel['subject']} --{rel['relation']}--> {rel['object']}")
# Output:
# Entities:
# Microsoft (company)
# Anthropic (company)
# Dario Amodei (person)
# AI systems (concept)
# Relationships:
# Microsoft --invested_in--> Anthropic
# Dario Amodei --is_ceo_of--> Anthropic
# Investment --amount--> 5B
For a large corpus, run extraction on all documents and consolidate into a single graph, merging duplicate entities by name/type.
Building and Storing the Graph
Use a property graph database or in-memory representation:
from typing import Optional
class KnowledgeGraph:
"""Simple in-memory knowledge graph."""
def __init__(self):
self.entities = {} # entity_id -> {name, type, attributes}
self.relationships = [] # [{subject_id, relation, object_id, metadata}]
def add_entity(self, entity_id: str, name: str, entity_type: str, **attrs):
"""Add or update an entity."""
self.entities[entity_id] = {
"name": name,
"type": entity_type,
"attributes": attrs
}
def add_relationship(self, subject_id: str, relation: str, object_id: str, **metadata):
"""Add a relationship between two entities."""
self.relationships.append({
"subject_id": subject_id,
"relation": relation,
"object_id": object_id,
"metadata": metadata
})
def find_neighbors(self, entity_id: str, relation_filter: Optional[str] = None):
"""Find all entities connected to a given entity."""
neighbors = []
for rel in self.relationships:
if rel["subject_id"] == entity_id:
if relation_filter is None or rel["relation"] == relation_filter:
neighbor_id = rel["object_id"]
neighbors.append({
"entity": self.entities[neighbor_id],
"relation": rel["relation"]
})
return neighbors
def find_path(self, start_id: str, end_id: str, max_depth: int = 3) -> Optional[list]:
"""Find a path between two entities (breadth-first search)."""
from collections import deque
queue = deque([(start_id, [start_id])])
visited = {start_id}
while queue:
current, path = queue.popleft()
if len(path) > max_depth:
continue
if current == end_id:
return path
for neighbor in self.find_neighbors(current):
neighbor_id = neighbor["entity"]["name"] # Simplified
if neighbor_id not in visited:
visited.add(neighbor_id)
queue.append((neighbor_id, path + [neighbor_id]))
return None
# Build a sample graph
kg = KnowledgeGraph()
kg.add_entity("ms", "Microsoft", "company", industry="technology")
kg.add_entity("anthropic", "Anthropic", "company", industry="ai-safety")
kg.add_entity("dario", "Dario Amodei", "person", role="CEO")
kg.add_relationship("ms", "invested_in", "anthropic", amount="5B", date="2025-06-01")
kg.add_relationship("dario", "is_ceo_of", "anthropic")
neighbors = kg.find_neighbors("ms")
print(f"Microsoft is connected to: {[n['entity']['name'] for n in neighbors]}")
GraphRAG Query Processing
Answer questions by querying the graph:
def graph_rag_query(query: str, knowledge_graph: KnowledgeGraph) -> str:
"""Answer a question using graph traversal and LLM synthesis."""
# Step 1: Identify entities mentioned in the query
entity_extraction_prompt = f"""Extract key entities from this query: '{query}'
Return JSON: {{"entities": ["entity1", "entity2", ...]}}"""
response = client.messages.create(
model="claude-haiku",
max_tokens=100,
messages=[{"role": "user", "content": entity_extraction_prompt}]
)
text = response.content[0].text
start = text.find('{')
end = text.rfind('}') + 1
query_entities = json.loads(text[start:end])["entities"]
# Step 2: Find matching entities in the graph
matched_entities = [
(eid, e) for eid, e in knowledge_graph.entities.items()
if any(qe.lower() in e["name"].lower() for qe in query_entities)
]
if not matched_entities:
return "No relevant entities found in the knowledge graph."
# Step 3: Traverse the graph to gather context
context = ""
for eid, entity in matched_entities[:3]: # Limit to top 3
neighbors = knowledge_graph.find_neighbors(eid)
context += f"\nEntity: {entity['name']}\n"
context += f"Relationships:\n"
for neighbor in neighbors[:5]: # Limit relationships
context += f" - {entity['name']} --{neighbor['relation']}--> {neighbor['entity']['name']}\n"
# Step 4: Use LLM to synthesize answer from graph context
synthesis_prompt = f"""Based on this knowledge graph context:
{context}
Answer the query: {query}"""
response = client.messages.create(
model="claude-opus-4-1",
max_tokens=300,
messages=[{"role": "user", "content": synthesis_prompt}]
)
return response.content[0].text
# Example query
answer = graph_rag_query(
"Who is the CEO of the company Microsoft invested in?",
kg
)
print(f"Answer: {answer}")
Comparison: Vector RAG vs. GraphRAG
| Aspect | Vector RAG | GraphRAG |
|---|---|---|
| Best for | Keyword/semantic similarity | Entity relationships, reasoning |
| Setup | Embed all documents | Extract entities, build graph |
| Query latency | 100–300 ms | 50–200 ms (graph traversal is fast) |
| Reasoning accuracy | 70–85% (multi-hop) | 90–95% (multi-hop) |
| Hallucination | 5–10% | 1–3% (facts from graph only) |
| Maintenance | Reindex on document change | Update graph nodes/edges |
| Scalability | 100M+ documents | Best for 1M–10M entities |
Use GraphRAG for fact-heavy domains (finance, science, news); use Vector RAG for exploratory or creative tasks (brainstorming, content generation).
Key Takeaways
- GraphRAG uses knowledge graphs (nodes = entities, edges = relationships) to enable structured reasoning and reduce hallucination by 5–8%.
- Extract entities and relationships from documents using an LLM; consolidate into a single graph.
- Query the graph by finding relevant entities, traversing relationships, and synthesizing LLM responses from graph context.
- GraphRAG excels at multi-hop reasoning (35–45% improvement) but requires significant upfront extraction effort.
- Combine GraphRAG with Vector RAG (hybrid) for robustness: use graph for structured facts, vectors for semantic similarity.
Frequently Asked Questions
How do I handle entity disambiguation (same name, different entities)?
Use entity types and context. Add attributes like "company: OpenAI (founded 2015)" vs "person: OpenAI (fictional character)". For production systems, use Wikidata entity IDs as canonical identifiers—they handle 99% of disambiguation automatically.
What if the knowledge graph is incomplete?
Fallback to vector search. Structure your pipeline: (1) try graph query, (2) if no results or low confidence, try vector search. Log failures to identify missing entities/relationships for the next extraction cycle.
How do I keep the graph current?
Run extraction nightly or weekly on new documents; schedule monthly consolidation to merge new entities. For real-time updates, use a mutable graph database (Neo4j, ArangoDB) and update on document ingestion. Cost: ~50 ms per update.
Can I use pre-built knowledge graphs like Wikidata?
Yes, but entities from documents may not match Wikidata IDs exactly. Use a linking tool (e.g., entity linker from spaCy or a fine-tuned model) to map document entities to Wikidata. This improves coverage from 60% to 85%+ but adds latency (100–200 ms).
What about private/confidential information in the graph?
Store sensitive attributes separately. Keep the graph public but mark edges like "person --salary--> $X" as private. At query time, check user permissions before including private attributes in LLM context.
Further Reading
- Knowledge Graphs — comprehensive review of KG foundations and applications.
- Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks — the original RAG paper; Section 5 discusses structured retrieval.
- Knowledge Graph Completion via Complex Tensor Factorization — advanced techniques for inferring missing relationships.
- Neo4j: The World's Leading Graph Database — production graph database for building enterprise knowledge graphs.