Skip to main content

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

AspectVector RAGGraphRAG
Best forKeyword/semantic similarityEntity relationships, reasoning
SetupEmbed all documentsExtract entities, build graph
Query latency100–300 ms50–200 ms (graph traversal is fast)
Reasoning accuracy70–85% (multi-hop)90–95% (multi-hop)
Hallucination5–10%1–3% (facts from graph only)
MaintenanceReindex on document changeUpdate graph nodes/edges
Scalability100M+ documentsBest 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