LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 196 articles completed

🛠️Computing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
📊Math & Statistics0/9
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningBayesian Inference from EvidenceStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
📚Preparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
🧮ML Algorithms & Evaluation0/15
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageStatistical Learning and GeneralizationConformal Prediction and UncertaintyClustering and PCAProbabilistic Generative ModelsCore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingCausal Inference and CounterfactualsPyTorch Training LoopsDataset Pipelines and Data Quality
📦Production ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
🧪Core LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
🧰Applied LLM Engineering0/25
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernancePrivacy-Preserving Machine LearningData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BPrompt Optimization with DSPyModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
🎓Portfolio Capstones0/9
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned ClassifierCapstone: Reproducible ML Study
🧠Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
🧬Advanced Training & Adaptation0/17
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabJAX for PyTorch ResearchersContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineMixed Precision TrainingDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningTraining Run OperationsReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight Interpolation
🤖Advanced Agents & Retrieval0/16
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryRecursive Language Models (RLM)Multi-Agent OrchestrationCapstone: Production Agent
⚡Inference & Production Scale0/31
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceAccelerator Architecture Field GuideCorrect GPU Profiling and BenchmarkingGPU Kernel Performance Engineering LabGPU Parallel Primitives LabGPU Compilers and Kernel LanguagesGEMM From Naive CUDA to Tensor CoresLow-Precision GPU ProgrammingGPU Runtime Execution LabAI-Generated Kernel EngineeringModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingGPU Failure OperationsDistributed Inference Data PlaneServing Benchmark EngineeringA/B Testing for LLMs
🏗️System Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning Agent System Design
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
🔬Project Deep Dives0/17
Deep Dive - vLLMDeep Dive - SkyRLDeep Dive - FlashAttentionDeep Dive - FlashInferDeep Dive - DeepGEMMDeep Dive - NCCLDeep Dive - MegatronDeep Dive - DeepSpeedDeep Dive - RayDeep Dive - MLflowDeep Dive - PyTorchDeep Dive - TransformersDeep Dive - SGLangDeep Dive - slimeDeep Dive - DeepEPDeep Dive - TinkerDeep Dive - Light-PEFT
Back to Topics
LearnAdvanced Agents & RetrievalGraphRAG & Knowledge Graphs
🔍HardRAG & Retrieval

GraphRAG & Knowledge Graphs

Answer one inference-api SLO question and one corpus-wide theme question from the same incident graph, then pay for reports only when vector top-k fails.

42 min read
Learning path
Step 122 of 196 in the full curriculum
Advanced RAG: HyDE & Self-RAGRAG Security & Access Control

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

An AI platform has 10,000 incident reports, runbooks, and trace summaries. An on-call engineer asks: "Why did inference-api breach the latency service-level objective (SLO) during release 2026.06.14?" Ordinary retrieval can find the write-up about that service.

Then an analyst asks: "What are the top three recurring reasons model-serving incidents breach latency SLOs?" A small top-k result set can look convincing while covering only a few incidents. Similarity doesn't by itself give you theme coverage over the whole corpus.

The previous chapter made retrieval-augmented generation (RAG) adaptive: rewrite the query, generate hypothetical evidence, critique retrieved context, and retry when evidence is weak. GraphRAG changes the representation layer. It indexes relationship structure and summaries alongside text so a system can retrieve connected evidence or corpus-level themes, at extra extraction and query cost.

The engineering question is when that tradeoff complements vector search and when it adds work you don't need.

Microsoft's GraphRAG architecture builds a graph index of entities and relationships, pregenerates hierarchical community reports, and embeds those artifacts for retrieval [1]Reference 1From Local to Global: A Graph RAG Approach to Query-Focused Summarization.https://arxiv.org/abs/2404.16130[2]Reference 2GraphRAG Indexing Overviewhttps://microsoft.github.io/graphrag/index/overview/[3]Reference 3GraphRAG Default Dataflowhttps://microsoft.github.io/graphrag/index/default_dataflow/. The same index supplies structured artifacts for entity-focused local context.

In the original paper's global-sensemaking evaluation on podcast and news corpora in the roughly 1 million token range, GraphRAG beat a vector RAG baseline on LLM-judge comprehensiveness (about 72-83% win rate) and diversity (about 62-82% win rate) [1]Reference 1From Local to Global: A Graph RAG Approach to Query-Focused Summarization.https://arxiv.org/abs/2404.16130. That's a result for that query class and those datasets, not a promise that every product question wants a graph.

A broader roadmap by Pan et al. places this work in the wider effort to combine LLMs with structured knowledge graphs [4]Reference 4Unifying Large Language Models and Knowledge Graphs: A Roadmap.https://arxiv.org/abs/2306.08302.

Same three incidents under three GraphRAG surfaces: vector top-k hits 3155 and 1842 but misses 2031, the entity graph reaches redis-cache and the maxmemory policy with citations, and a community report names the recurring cache-eviction theme.
The same three incidents look different on each surface. Vector top-k misses 2031 because it never names inference-api. The graph reaches that cache policy with citations. The community report names the theme, but still can't support a future-breach claim without evidence.

Vector search ranks chunks that resemble the question. A graph index adds typed entity relationships and summary layers for cases where evidence spans multiple incidents. Standard GraphRAG local search builds ranked mixed context from graph and text artifacts. If your product needs an explicit path walk, you have to implement that policy and test it. The graph isn't a free multi-hop oracle.

Why small top-k retrieval can be insufficient

The global query problem

Keep those 10,000 incident reports, runbooks, traces, and postmortems as the running corpus. First ask what shape of evidence each question needs.

Local queries (vector search handles well)

For a specific question about one issue, vector search usually earns its keep:

"What is the timeout for the embeddings API?" Retrieval path: top-5 similar documents, then answer from local context. This works because the needed evidence is concentrated in a few chunks.

The question is self-contained, so a few chunks about the API contract and runbook contain everything the system needs.

Global queries (small top-k retrieval can miss coverage)

For a question that must synthesize across the entire corpus, small top-k vector retrieval has a different problem:

"What are the top 3 recurring reasons model-serving incidents breach the latency SLO?" Failure mode: no single chunk contains a cross-corpus summary, and a top-5 result set is unlikely to represent themes across 10,000 documents. The resulting answer can be incomplete or misleading.

Before raising k, predict what changes: coverage may grow, but top-k still has no mechanism to count or summarize every theme in 10,000 documents.

Why similarity-only retrieval struggles

Vector search retrieves the most similar chunks, not the most representative set. For "top three recurring SLO reasons," no single incident write-up is the answer. The index has to turn many local mentions into structures that can be grouped and summarized. GraphRAG builds those broader units in four offline steps:

  1. Entity extraction across the entire corpus
  2. Relationship modeling between entities
  3. Community detection to find natural clusters of related information
  4. Hierarchical summarization at different levels of abstraction
FeatureVector Search (Standard RAG)GraphRAG
Available artifactsRanked text chunksText units, entities, relationships, community reports
Natural starting pointSpecific evidence lookup ("What is X?")Entity-focused or thematic analysis ("What are the trends?")
Context constructionSimilarity-ranked chunksRanked graph/text context or report map-reduce
Indexing workEmbedding and optional sparse indexAdditional extraction, clustering, reports, and embeddings
Query workRetrieval plus generationDepends on search mode; global search adds map-reduce calls

A concrete multi-hop example

Before naming graph pieces, predict what each route can see. Compare the two approaches on this tiny incident corpus:

Incident 1842: "inference-api release 2026.06.14 ran in us-east-1 and hit p95 latency after redis-cache began evicting hot keys." Incident 2031: "redis-cache in us-east-1 still uses the old maxmemory policy. The config migration is blocked by a compatibility test." Incident 3155: "inference-api breached the inference latency SLO twelve minutes after deploy. Rollback restored latency."

The decision question is:

"Why did inference-api breach the latency SLO, and is this likely to happen again?"

Illustrative vector-only result: Suppose retrieval finds Incident 3155 (mentions "inference-api" and "latency SLO") and Incident 1842 (mentions "inference-api" and "redis-cache"), but not Incident 2031 because it doesn't mention the service name.

That answer can identify the rollout and cache eviction while missing the unresolved maxmemory policy.

Now add graph structure. Suppose extraction produced these entities and relationships, with source-chunk provenance:

  • (inference-api) --[ran_in]--> (us-east-1)
  • (inference-api) --[depends_on]--> (redis-cache)
  • (inference-api) --[triggered_by]--> (release 2026.06.14)
  • (redis-cache) --[located_in]--> (us-east-1)
  • (redis-cache) --[has_issue]--> (old maxmemory policy)
  • (old maxmemory policy) --[blocked_by]--> (compatibility test)

An evidence-backed traversal can start at inference-api, follow depends_on to redis-cache, and retrieve evidence about both the eviction event and the unresolved maxmemory policy.

It may then answer: "inference-api breached the latency SLO after a rollout, and source material ties the incident to cache evictions plus an unresolved cache policy." It shouldn't predict another breach unless source evidence supports that claim.

This is relationship-heavy retrieval: useful evidence is connected through entities that weren't all present in the query.

Microsoft's standard local-search dataflow ranks graph and text artifacts into a context window; it isn't a promise that every answer executes a deterministic path. If explicit multi-hop paths matter, preserve edge provenance and test the traversal policy.

require-provenance-for-graph-paths.py
1from dataclasses import dataclass 2 3@dataclass(frozen=True) 4class Edge: 5 source: str 6 relation: str 7 target: str 8 source_chunks: tuple[str, ...] 9 10def cited_path(edges: list[Edge]) -> tuple[bool, list[str]]: 11 if any(not edge.source_chunks for edge in edges): 12 return False, [] 13 citations = sorted({chunk for edge in edges for chunk in edge.source_chunks}) 14 return True, citations 15 16path = [ 17 Edge("inference-api", "depends_on", "redis-cache", ("incident-1842",)), 18 Edge("redis-cache", "has_issue", "old maxmemory policy", ("incident-2031",)), 19] 20unsupported_path = path + [ 21 Edge("old maxmemory policy", "will_cause", "future SLO breach", ()), 22] 23 24print("supported path:", cited_path(path)) 25print("unsupported prediction:", cited_path(unsupported_path))
Output
1supported path: (True, ['incident-1842', 'incident-2031']) 2unsupported prediction: (False, [])

Entities, edges, and provenance

A knowledge graph represents information as a network of entities (nodes) and relationships (edges). A vector index stores chunks as isolated embeddings.

The graph's extra value is the connection between facts.

The basic unit is the triple: (Subject) - [Predicate] -> (Object). Edge direction should follow the meaning of the relation, not a generic left-to-right habit.

From our running example:

  • (inference-api) --[depends_on]--> (redis-cache)
  • (redis-cache) --[has_issue]--> (old maxmemory policy)
  • (old maxmemory policy) --[blocked_by]--> (compatibility test)

A live index is usually a property graph rather than bare triples. Nodes and edges carry metadata such as type, description, source_chunk_ids, timestamps, or confidence.

Those fields are what you filter, cite, and rank at query time.

Give every entity a stable identity such as (canonical_title, canonical_type), not a title-only key. Keep the source and target type on each relationship, resolve both endpoints through that key, and merge repeated edges only after resolution. If a malformed edge omits a type, accept the name only when it maps to one candidate; reject ambiguous or unknown references so a convenient first match can't connect the wrong nodes.

This graph structure mirrors how an on-call engineer traces an incident. When a query mentions redis-cache, typed relationships and provenance can separate service dependencies, region-specific incidents, config drift, and release side effects.

Service-name similarity alone can't do that.

Graph traversal isn't learned message passing

Following inference-api -> redis-cache -> old maxmemory policy traverses existing relationships and returns their cited source chunks. That lookup doesn't train a model or update a node embedding.

A graph neural network (GNN) adds a separate learned model that combines node features, edges, and usually neighboring information to predict properties such as incident impact or service relevance.[5]Reference 5Semi-Supervised Classification with Graph Convolutional Networkshttps://arxiv.org/abs/1609.02907

A message-passing neural network (MPNN) makes that distinction concrete: each layer computes messages from neighboring node features, aggregates them, and updates the destination node's learned representation.[6]Reference 6Neural Message Passing for Quantum Chemistryhttps://arxiv.org/abs/1704.01212

Suppose redis-cache and another dependency contribute scalar messages 0.4 and 0.8; mean aggregation produces 0.6, which becomes an input to the next inference-api update. That learned update can help rank candidates, but it doesn't establish source provenance or prove a predicted relationship is true.

Standard GraphRAG can extract entities, follow stored edges, cluster communities, and prepare reports without training either a GNN or an MPNN.

What makes a knowledge graph useful beyond storing triples?

Answer

The graph connects entities through typed relationships and provenance. Retrieval can then follow meaningful paths, filter by metadata, and explain which source chunks support the path.

The GraphRAG pipeline

Phase 1: Graph construction (indexing)

The indexing phase turns raw document chunks into a structured graph index.

Microsoft's default dataflow extracts entities and relationships, optionally extracts claims (covariates), uses entity resolution to identify descriptions of the same object, builds hierarchical communities, generates community reports, and embeds the artifacts used at query time [2]Reference 2GraphRAG Indexing Overviewhttps://microsoft.github.io/graphrag/index/overview/[3]Reference 3GraphRAG Default Dataflowhttps://microsoft.github.io/graphrag/index/default_dataflow/:

GraphRAG indexing flow where incident documents become text units, extraction merges entities and edges, Leiden clusters communities, community reports are written before queries, and embeddings plus optional claims become the retrieval surface.
GraphRAG spends before the first question. Text becomes graph structure, descriptions get merged, reports are written offline, and only then are retrieval artifacts embedded.

Step 1: Entity & relationship extraction

Extraction view where incident chunk 1842 becomes four typed entities: inference-api, redis-cache, us-east-1, and release 2026.06.14, with depends_on, ran_in, and triggered_by edges that all cite the same source chunk.
Chunk 1842 becomes four typed nodes and three cited edges. The maxmemory policy isn't in this chunk, so it must not appear as an edge here.

Start with one text unit. An extractor names entities and the relationships between them. Standard GraphRAG does this with an LLM. FastGraphRAG replaces that step with cheaper NLP heuristics so indexing costs less [7]Reference 7GraphRAG Indexing Methodshttps://microsoft.github.io/graphrag/index/methods/. A simple extraction prompt looks like this:

step-1-entity-and-relationship-extraction.py
1EXTRACTION_PROMPT = """ 2Extract all entities and relationships from the following text. 3 4Entities should include: services, datastores, regions, releases, incidents, and config issues. 5Relationships should include: ran_in, depends_on, triggered_by, located_in, has_issue, blocked_by. 6 7Text: {chunk_text} 8 9Output as JSON: 10{ 11 "entities": [ 12 {"name": "...", "type": "...", "description": "..."} 13 ], 14 "relationships": [ 15 { 16 "source": "...", 17 "source_type": "...", 18 "target": "...", 19 "target_type": "...", 20 "type": "...", 21 "description": "...", 22 "strength": 1.0 23 } 24 ] 25} 26"""

That prompt isn't a parser. Use structured output (JSON schema or tool calling) so a missing brace doesn't kill a 10,000-chunk index job. Keep the contract the same either way: typed entities, typed edge endpoints, and source chunk IDs. An edge that says only source: "gateway" is incomplete when both a Service and a Datastore use that title.

The runnable version below uses a deterministic extractor so you can test that contract without calling a model. When you wire a real extractor, replace extract_from_chunk() with a structured-output call and keep the same Entity and Relationship objects.

After per-chunk extraction, GraphRAG merges entities that share a title and type, concatenates their descriptions, then asks a model to compress that list into one description per entity and relationship [3]Reference 3GraphRAG Default Dataflowhttps://microsoft.github.io/graphrag/index/default_dataflow/. The merge below does the bookkeeping; the LLM summary is the step you'd swap in later. It uses a typed canonical key, carries endpoint types on every edge, and fails closed when an untyped name matches more than one entity.

step-1-entity-and-relationship-extraction-2.py
1from collections import defaultdict 2from dataclasses import dataclass, field 3 4@dataclass 5class Entity: 6 name: str 7 type: str 8 description: str 9 source_chunk_ids: list[int] = field(default_factory=list) 10 aliases: list[str] = field(default_factory=list) 11 12@dataclass 13class Relationship: 14 source: str 15 source_type: str 16 target: str 17 target_type: str 18 type: str 19 description: str 20 confidence: float 21 source_chunk_ids: list[int] = field(default_factory=list) 22 23def canonicalize(value: str) -> str: 24 return "".join(character for character in value.casefold() if character.isalnum()) 25 26def entity_key(name: str, entity_type: str) -> tuple[str, str]: 27 """Identify an entity by canonical title and canonical type.""" 28 return canonicalize(name), canonicalize(entity_type) 29 30def extract_from_chunk(chunk_id: int, chunk: str) -> tuple[list[Entity], list[Relationship]]: 31 entities: list[Entity] = [] 32 relationships: list[Relationship] = [] 33 34 if "inference-api" in chunk: 35 entities.append(Entity("inference-api", "Service", "Inference API service", [chunk_id])) 36 if "redis-cache" in chunk: 37 entities.append(Entity("Redis Cache", "Datastore", "Shared cache dependency", [chunk_id])) 38 if "us-east-1" in chunk: 39 entities.append(Entity("US East 1", "Region", "Production region", [chunk_id])) 40 if "release 2026.06.14" in chunk: 41 entities.append(Entity("Release 2026.06.14", "Release", "Inference API deployment", [chunk_id])) 42 if "old maxmemory policy" in chunk: 43 entities.append(Entity("Old Maxmemory Policy", "ConfigIssue", "Cache eviction policy risk", [chunk_id])) 44 if "compatibility test" in chunk: 45 entities.append(Entity("Compatibility Test", "TestGate", "Migration-blocking test", [chunk_id])) 46 47 entity_keys = {entity_key(entity.name, entity.type) for entity in entities} 48 if {entity_key("inference-api", "Service"), entity_key("Redis Cache", "Datastore")} <= entity_keys: 49 relationships.append( 50 Relationship( 51 source="inference-api", source_type="Service", 52 target="Redis Cache", target_type="Datastore", 53 type="depends_on", description="Service uses cache", 54 confidence=1.0, source_chunk_ids=[chunk_id], 55 ) 56 ) 57 if {entity_key("inference-api", "Service"), entity_key("US East 1", "Region")} <= entity_keys: 58 relationships.append( 59 Relationship( 60 source="inference-api", source_type="Service", 61 target="US East 1", target_type="Region", 62 type="ran_in", description="Service ran in region", 63 confidence=0.9, source_chunk_ids=[chunk_id], 64 ) 65 ) 66 if {entity_key("inference-api", "Service"), entity_key("Release 2026.06.14", "Release")} <= entity_keys: 67 relationships.append( 68 Relationship( 69 source="inference-api", source_type="Service", 70 target="Release 2026.06.14", target_type="Release", 71 type="triggered_by", description="Latency followed release", 72 confidence=0.8, source_chunk_ids=[chunk_id], 73 ) 74 ) 75 if {entity_key("Redis Cache", "Datastore"), entity_key("US East 1", "Region")} <= entity_keys: 76 relationships.append( 77 Relationship( 78 source="Redis Cache", source_type="Datastore", 79 target="US East 1", target_type="Region", 80 type="located_in", description="Cache region", 81 confidence=0.8, source_chunk_ids=[chunk_id], 82 ) 83 ) 84 if {entity_key("Redis Cache", "Datastore"), entity_key("Old Maxmemory Policy", "ConfigIssue")} <= entity_keys: 85 relationships.append( 86 Relationship( 87 source="Redis Cache", source_type="Datastore", 88 target="Old Maxmemory Policy", target_type="ConfigIssue", 89 type="has_issue", description="Eviction policy risk", 90 confidence=0.8, source_chunk_ids=[chunk_id], 91 ) 92 ) 93 if {entity_key("Old Maxmemory Policy", "ConfigIssue"), entity_key("Compatibility Test", "TestGate")} <= entity_keys: 94 relationships.append( 95 Relationship( 96 source="Old Maxmemory Policy", source_type="ConfigIssue", 97 target="Compatibility Test", target_type="TestGate", 98 type="blocked_by", description="Migration gate", 99 confidence=0.7, source_chunk_ids=[chunk_id], 100 ) 101 ) 102 103 return entities, relationships 104 105def deduplicate_entities(entities: list[Entity]) -> list[Entity]: 106 merged: dict[tuple[str, str], Entity] = {} 107 for entity in entities: 108 key = entity_key(entity.name, entity.type) 109 if key not in merged: 110 merged[key] = Entity( 111 name=entity.name, 112 type=entity.type, 113 description=entity.description, 114 source_chunk_ids=sorted(set(entity.source_chunk_ids)), 115 aliases=list(entity.aliases), 116 ) 117 continue 118 existing = merged[key] 119 existing.source_chunk_ids = sorted(set(existing.source_chunk_ids + entity.source_chunk_ids)) 120 if entity.name != existing.name and entity.name not in existing.aliases: 121 existing.aliases.append(entity.name) 122 if entity.description not in existing.description: 123 existing.description = f"{existing.description}; {entity.description}" 124 return [merged[key] for key in sorted(merged)] 125 126def index_entities( 127 entities: list[Entity], 128) -> tuple[dict[tuple[str, str], Entity], dict[str, list[Entity]]]: 129 by_key = {entity_key(entity.name, entity.type): entity for entity in entities} 130 by_name: dict[str, dict[tuple[str, str], Entity]] = defaultdict(dict) 131 for entity in entities: 132 key = entity_key(entity.name, entity.type) 133 by_name[canonicalize(entity.name)][key] = entity 134 for alias in entity.aliases: 135 by_name[canonicalize(alias)][key] = entity 136 return by_key, {name: list(candidates.values()) for name, candidates in by_name.items()} 137 138def resolve_entity( 139 name: str, 140 entity_type: str | None, 141 by_key: dict[tuple[str, str], Entity], 142 by_name: dict[str, list[Entity]], 143) -> Entity: 144 """Resolve a typed reference, or reject an ambiguous untyped reference.""" 145 if entity_type: 146 entity = by_key.get(entity_key(name, entity_type)) 147 if entity is None: 148 raise ValueError(f"Unknown typed entity reference: {name!r} ({entity_type})") 149 return entity 150 151 candidates = by_name.get(canonicalize(name), []) 152 if not candidates: 153 raise ValueError(f"Unknown entity reference: {name!r}") 154 if len(candidates) > 1: 155 candidate_types = ", ".join(sorted({candidate.type for candidate in candidates})) 156 raise ValueError( 157 f"Ambiguous entity reference {name!r}; endpoint type required ({candidate_types})" 158 ) 159 return candidates[0] 160 161def resolve_relationships( 162 relationships: list[Relationship], 163 entities: list[Entity], 164) -> list[Relationship]: 165 """Resolve both endpoints by typed identity and merge duplicate edges.""" 166 by_key, by_name = index_entities(entities) 167 merged: dict[tuple[tuple[str, str], tuple[str, str], str], Relationship] = {} 168 for relationship in relationships: 169 source = resolve_entity(relationship.source, relationship.source_type, by_key, by_name) 170 target = resolve_entity(relationship.target, relationship.target_type, by_key, by_name) 171 edge_key = ( 172 entity_key(source.name, source.type), 173 entity_key(target.name, target.type), 174 canonicalize(relationship.type), 175 ) 176 resolved = Relationship( 177 source=source.name, 178 source_type=source.type, 179 target=target.name, 180 target_type=target.type, 181 type=relationship.type, 182 description=relationship.description, 183 confidence=relationship.confidence, 184 source_chunk_ids=sorted(set(relationship.source_chunk_ids)), 185 ) 186 if edge_key not in merged: 187 merged[edge_key] = resolved 188 continue 189 existing = merged[edge_key] 190 existing.source_chunk_ids = sorted( 191 set(existing.source_chunk_ids + resolved.source_chunk_ids) 192 ) 193 existing.confidence = max(existing.confidence, resolved.confidence) 194 if resolved.description not in existing.description: 195 existing.description = f"{existing.description}; {resolved.description}" 196 return [merged[key] for key in sorted(merged)] 197 198def extract_graph_elements(chunks: list[str]) -> tuple[list[Entity], list[Relationship]]: 199 all_entities: list[Entity] = [] 200 all_relationships: list[Relationship] = [] 201 202 for chunk_id, chunk in enumerate(chunks): 203 entities, relationships = extract_from_chunk(chunk_id, chunk) 204 all_entities.extend(entities) 205 all_relationships.extend(relationships) 206 207 deduped_entities = deduplicate_entities(all_entities) 208 return deduped_entities, resolve_relationships(all_relationships, deduped_entities) 209 210chunks = [ 211 "inference-api hit p95 latency in us-east-1 because redis-cache started evicting hot keys during release 2026.06.14.", 212 "redis-cache in us-east-1 still uses the old maxmemory policy. Config migration is blocked by a compatibility test.", 213] 214 215entities, relationships = extract_graph_elements(chunks) 216relationship_types = {rel.type for rel in relationships} 217 218print( 219 "entity identities:", 220 ", ".join(sorted(f"{entity.type}:{entity.name}" for entity in entities)), 221) 222print("relationships:", ", ".join(sorted(relationship_types))) 223print( 224 "resolved edges:", 225 ", ".join( 226 sorted(f"{rel.source} -[{rel.type}]-> {rel.target}" for rel in relationships) 227 ), 228) 229 230ambiguous_entities = deduplicate_entities( 231 [ 232 Entity("gateway", "Service", "API gateway", [8]), 233 Entity("gateway", "Datastore", "Gateway state store", [9]), 234 ] 235) 236try: 237 resolve_relationships( 238 [ 239 Relationship( 240 source="gateway", 241 source_type="", 242 target="gateway", 243 target_type="Service", 244 type="depends_on", 245 description="Untrusted untyped source", 246 confidence=0.5, 247 source_chunk_ids=[10], 248 ) 249 ], 250 ambiguous_entities, 251 ) 252except ValueError as error: 253 print("ambiguous endpoint rejected:", error)
Output
1entity identities: ConfigIssue:Old Maxmemory Policy, Datastore:Redis Cache, Region:US East 1, Release:Release 2026.06.14, Service:inference-api, TestGate:Compatibility Test 2relationships: blocked_by, depends_on, has_issue, located_in, ran_in, triggered_by 3resolved edges: Old Maxmemory Policy -[blocked_by]-> Compatibility Test, Redis Cache -[has_issue]-> Old Maxmemory Policy, Redis Cache -[located_in]-> US East 1, inference-api -[depends_on]-> Redis Cache, inference-api -[ran_in]-> US East 1, inference-api -[triggered_by]-> Release 2026.06.14 4ambiguous endpoint rejected: Ambiguous entity reference 'gateway'; endpoint type required (Datastore, Service)

Why should extracted relationships store source chunk IDs and endpoint types?

Answer

Graph paths still need evidence and unambiguous endpoints. Source chunk IDs let the answer cite the text that produced each edge, while endpoint types let the resolver distinguish same-titled entities such as a Service and a Datastore. If an edge omits a type and the title has multiple candidates, reject the edge instead of guessing.

Step 2: Build the knowledge graph

Once entities and relationships are extracted and merged, store them as a directed property graph. Nodes hold type and description; edges hold relation type, weight, and source chunk IDs.

Later retrieval can follow relationships that never appeared together in one chunk.

The next function takes those lists and builds a small adjacency structure. It keeps typed canonical IDs internally and uses display names only when printing a result. Production GraphRAG writes the same facts to tables (entities, relationships, text units) rather than requiring a graph database [8]Reference 8GraphRAG Outputshttps://microsoft.github.io/graphrag/index/outputs/:

step-2-build-the-knowledge-graph.py
1from collections import defaultdict 2from dataclasses import dataclass, field 3 4@dataclass 5class Entity: 6 name: str 7 type: str 8 description: str 9 source_chunk_ids: list[int] = field(default_factory=list) 10 11@dataclass 12class Relationship: 13 source: str 14 source_type: str 15 target: str 16 target_type: str 17 type: str 18 description: str 19 confidence: float 20 source_chunk_ids: list[int] = field(default_factory=list) 21 22def canonicalize(value: str) -> str: 23 return "".join(character for character in value.casefold() if character.isalnum()) 24 25def entity_key(name: str, entity_type: str) -> tuple[str, str]: 26 return canonicalize(name), canonicalize(entity_type) 27 28def build_knowledge_graph( 29 entities: list[Entity], 30 relationships: list[Relationship], 31) -> dict: 32 """Store a directed property graph as nodes plus typed outbound edges.""" 33 nodes = { 34 entity_key(entity.name, entity.type): { 35 "name": entity.name, 36 "type": entity.type, 37 "description": entity.description, 38 "source_chunk_ids": entity.source_chunk_ids, 39 } 40 for entity in entities 41 } 42 out_edges: dict[tuple[str, str], list[dict]] = defaultdict(list) 43 for rel in relationships: 44 source_key = entity_key(rel.source, rel.source_type) 45 target_key = entity_key(rel.target, rel.target_type) 46 if source_key not in nodes or target_key not in nodes: 47 raise ValueError(f"relationship references unknown entity: {rel.source} -> {rel.target}") 48 out_edges[source_key].append( 49 { 50 "target": target_key, 51 "type": rel.type, 52 "description": rel.description, 53 "weight": rel.confidence, 54 "source_chunk_ids": rel.source_chunk_ids, 55 } 56 ) 57 return {"nodes": nodes, "out_edges": dict(out_edges)} 58 59entities = [ 60 Entity("inference-api", "Service", "Inference API service", [0]), 61 Entity("Redis Cache", "Datastore", "Shared cache dependency", [0]), 62 Entity("US East 1", "Region", "Production region", [0, 1]), 63] 64relationships = [ 65 Relationship( 66 "inference-api", "Service", "Redis Cache", "Datastore", 67 "depends_on", "Service uses cache", 1.0, [0] 68 ), 69 Relationship( 70 "inference-api", "Service", "US East 1", "Region", 71 "ran_in", "Service ran in region", 0.9, [0] 72 ), 73] 74 75graph = build_knowledge_graph(entities, relationships) 76edge_tuples = sorted( 77 (graph["nodes"][source]["name"], edge["type"], graph["nodes"][edge["target"]]["name"]) 78 for source, edges in graph["out_edges"].items() 79 for edge in edges 80) 81depends_on = next( 82 edge["weight"] 83 for edge in graph["out_edges"][entity_key("inference-api", "Service")] 84 if edge["type"] == "depends_on" 85) 86 87print("nodes:", sorted(node["name"] for node in graph["nodes"].values())) 88print("edges:", edge_tuples) 89print("US East 1 node type:", graph["nodes"][entity_key("US East 1", "Region")]["type"]) 90print("depends_on weight:", depends_on)
Output
1nodes: ['Redis Cache', 'US East 1', 'inference-api'] 2edges: [('inference-api', 'depends_on', 'Redis Cache'), ('inference-api', 'ran_in', 'US East 1')] 3US East 1 node type: Region 4depends_on weight: 1.0

Step 3: Community detection (Leiden algorithm)

Retrieval walks the directed property graph. Community detection usually clusters an undirected (often weighted) view of the same nodes: ignore edge type for clustering, then keep it for later context packing.

In this corpus, dense clusters become themes such as cache pressure versus eval flakiness. The groups come from connection density, not keyword matching.

The Leiden algorithm finds those clusters. Traag et al. prefer it to classic Louvain because Louvain can emit disconnected communities (nodes in the same group with no path between them). Leiden adds a refinement phase and guarantees connected communities [9]Reference 9From Louvain to Leiden: guaranteeing well-connected communities.https://arxiv.org/abs/1810.08473. That removes a structural defect before report generation.

Leiden still can't fix bad extraction or unsupported edges. Microsoft's GraphRAG pipeline applies hierarchical Leiden recursively until communities hit a size threshold, which gives it coarse and fine-grained views of the same corpus [3]Reference 3GraphRAG Default Dataflowhttps://microsoft.github.io/graphrag/index/default_dataflow/.

One quality function Leiden can optimize is modularity (QQQ). In plain terms: are there more edges inside each group than chance would predict in this extracted graph? A high QQQ means the clustering is strong on that graph. It doesn't prove the extractor captured a real topic.

For an unweighted, undirected graph:

Q=12m∑i,j[Aij−kikj2m]δ(ci,cj)Q = \frac{1}{2m} \sum_{i,j} \left[ A_{ij} - \frac{k_i k_j}{2m} \right] \delta(c_i, c_j)Q=2m1​∑i,j​[Aij​−2mki​kj​​]δ(ci​,cj​)

AijA_{ij}Aij​ is 1 if nodes iii and jjj share an edge and 0 otherwise. kik_iki​ and kjk_jkj​ are node degrees. mmm is the number of undirected edges. δ(ci,cj)\delta(c_i, c_j)δ(ci​,cj​) is 1 when both nodes sit in the same community. Put every node in one community and QQQ is identically 0, which is a useful sanity check.

What does high modularity mean in a GraphRAG entity graph?

Answer

Entities inside the same community connect more densely than random chance would predict in the extracted graph. Validate the entity and edge evidence before treating that cluster as a real topic.

Score two candidate partitions of the same undirected graph. Production GraphRAG runs hierarchical Leiden; scoring QQQ by hand shows what that algorithm is optimizing, using only the standard library.

step-3-score-community-modularity.py
1from collections import defaultdict 2 3def modularity( 4 nodes: list[str], 5 undirected_edges: list[tuple[str, str]], 6 community: dict[str, str], 7) -> float: 8 """Newman modularity Q for an unweighted undirected graph.""" 9 adjacency: dict[str, set[str]] = defaultdict(set) 10 degree: dict[str, int] = {node: 0 for node in nodes} 11 for source, target in undirected_edges: 12 adjacency[source].add(target) 13 adjacency[target].add(source) 14 degree[source] += 1 15 degree[target] += 1 16 two_m = 2 * len(undirected_edges) 17 total = 0.0 18 for i in nodes: 19 for j in nodes: 20 if community[i] != community[j]: 21 continue 22 a_ij = 1.0 if j in adjacency[i] else 0.0 23 total += a_ij - (degree[i] * degree[j]) / two_m 24 return total / two_m 25 26nodes = [ 27 "inference-api", 28 "redis-cache", 29 "us-east-1", 30 "old-maxmemory", 31 "compatibility-test", 32 "eval-harness", 33 "flaky-judge", 34] 35edges = [ 36 ("inference-api", "redis-cache"), 37 ("inference-api", "us-east-1"), 38 ("redis-cache", "us-east-1"), 39 ("redis-cache", "old-maxmemory"), 40 ("old-maxmemory", "compatibility-test"), 41 ("eval-harness", "flaky-judge"), 42] 43one_bucket = {node: "all" for node in nodes} 44split = { 45 "inference-api": "cache", 46 "redis-cache": "cache", 47 "us-east-1": "cache", 48 "old-maxmemory": "cache", 49 "compatibility-test": "cache", 50 "eval-harness": "eval", 51 "flaky-judge": "eval", 52} 53 54print("one community Q:", round(modularity(nodes, edges, one_bucket), 3)) 55print("cache vs eval Q:", round(modularity(nodes, edges, split), 3))
Output
1one community Q: 0.0 2cache vs eval Q: 0.278

The clusters roll up in layers. Global search can start at a high report, then drill into the cache branch without paying for every eval leaf.

Hierarchy where inference-api, redis-cache, and the maxmemory policy cluster into a cache-latency community, eval-harness and a flaky judge cluster into a quality community, and both roll up into corpus-level reports that global GraphRAG search can map-reduce.
Communities compress many local entities into a few higher-level reports. Global search starts high, then drills down only when that branch is relevant to the question.

Step 4: Community summarization

The final indexing step compresses one community's entities, descriptions, and relationships into a report. Global search can map over that report later [1]Reference 1From Local to Global: A Graph RAG Approach to Query-Focused Summarization.https://arxiv.org/abs/2404.16130[3]Reference 3GraphRAG Default Dataflowhttps://microsoft.github.io/graphrag/index/default_dataflow/:

step-4-community-summarization.py
1from dataclasses import dataclass 2from typing import Protocol 3 4@dataclass(frozen=True) 5class Entity: 6 name: str 7 description: str 8 9@dataclass(frozen=True) 10class Relationship: 11 source: str 12 type: str 13 target: str 14 15class CommunitySummarizer(Protocol): 16 def summarize(self, prompt: str) -> str: ... 17 18class FakeCommunitySummarizer: 19 def summarize(self, prompt: str) -> str: 20 return ( 21 "Cache latency cluster: inference-api depends on redis-cache, " 22 "and cache evictions recur when the old maxmemory policy remains active." 23 ) 24 25def summarize_community( 26 community_entities: list[Entity], 27 community_relationships: list[Relationship], 28 level: int, 29 summarizer: CommunitySummarizer, 30) -> str: 31 entity_lines = "\n".join( 32 f"- {entity.name}: {entity.description}" for entity in community_entities 33 ) 34 relationship_lines = "\n".join( 35 f"- {rel.source} {rel.type} {rel.target}" for rel in community_relationships 36 ) 37 prompt = ( 38 f"Community level: {level}\n" 39 f"Entities:\n{entity_lines}\n" 40 f"Relationships:\n{relationship_lines}\n" 41 "Summarize the main operational pattern." 42 ) 43 return summarizer.summarize(prompt) 44 45summary = summarize_community( 46 [Entity("Redis Cache", "Shared cache dependency")], 47 [Relationship("Redis Cache", "has_issue", "Old Maxmemory Policy")], 48 level=0, 49 summarizer=FakeCommunitySummarizer(), 50) 51 52print(summary)
Output
1Cache latency cluster: inference-api depends on redis-cache, and cache evictions recur when the old maxmemory policy remains active.

Authorization continuity for graph artifacts

Community reports compress many text units into one artifact. If any unit in that community is confidential, a global-search answer that reads the report can leak facts the user isn't allowed to see, even when chunk-level vector search is correctly ACL-filtered.

Carry grants through the whole index, including derived artifacts beyond leaf chunks:

  1. Text units: every chunk keeps tenant + principal ACL metadata (same contract as enterprise RAG).
  2. Entities and edges: an edge is queryable only when the caller is allowed to see both endpoints' supporting evidence (or the edge fails closed).
  3. Community reports: never materialize a mixed-privilege community report for global search. Either:
    • build reports only inside a single ACL equivalence class, or
    • recompute a per-principal report view from allowed text units at query time, or
    • drop the report and fall back to local search on authorized entities.
  4. Revocation: when a document's ACL shrinks, reindex affected edges and community reports. Tombstoning the leaf chunk alone isn't enough if a summary still quotes it.

Worked fail-closed sketch. Three text units land in one Leiden community:

Text unitACLFact in unit
tu-public-sloall-staffp95 latency SLO is 200 ms
tu-security-keysecurity-onlyproduction key rotation requires break-glass approval
tu-public-runbookall-staffrestart cache before scale-out

A global community report over all three would compress the restricted rotation policy into staff-visible theme text. Fail-closed choices for a non-security principal:

ChoiceReport built fromMap-reduce admission
Equivalence-class reportsonly all-staff unitsadmit public report; never the mixed report
Per-principal recomputeunits the caller may readbuild a view without tu-security-key
Drop reportnonefall back to local search on authorized entities only

Don't map-reduce the mixed report and scrub afterward: the restricted fact is already in context.

The next lesson, RAG Security & Access Control, deepens prefilter vs post-filter vector search and operationalizes graph-artifact authorization. Graph modes need the same fail-closed habit: authz before map-reduce, not after the summary is already written into context.

Why can community reports leak data even when vector search is ACL-aware?

Answer

Reports summarize many chunks. If a community mixes public and restricted text units, global search can surface restricted facts through the summary without ever returning the restricted chunk as a retrieval hit.

Phase 2: Query processing (runtime)

At runtime, GraphRAG exposes Basic Search, Local Search, Global Search, and DRIFT Search. Local search builds mixed context for entity-centric questions, while global search synthesizes over community reports.

Basic Search is a text-unit vector baseline. DRIFT (Dynamic Reasoning and Inference with Flexible Traversal) starts from community reports, then uses local-search follow-ups [10]Reference 10GraphRAG Local Searchhttps://microsoft.github.io/graphrag/query/local_search/[11]Reference 11GraphRAG Global Searchhttps://microsoft.github.io/graphrag/query/global_search/[12]Reference 12GraphRAG DRIFT Searchhttps://microsoft.github.io/graphrag/query/drift_search/.

Local search (specific questions)

Local search isn't just named-entity matching plus one-hop neighbors. In Microsoft's docs, it maps the query into semantically related entities first.

It then ranks a mixed context from connected entities, relationships, community reports, linked text units, and optionally covariates if claim extraction is on [10]Reference 10GraphRAG Local Searchhttps://microsoft.github.io/graphrag/query/local_search/.

The runnable example below is a bounded neighborhood walk for intuition. For the incident question, predict that two hops from inference-api reach Redis Cache and Old Maxmemory Policy. The walk expands candidates; ranking and provenance still decide what enters context. It isn't a copy of GraphRAG's context builder or ranking policy:

local-search-specific-questions.py
1class EntityStore: 2 def __init__(self, entity_names: list[str]) -> None: 3 self.entity_names = entity_names 4 5 def similarity_search(self, query: str, k: int = 10) -> list[str]: 6 query_lower = query.lower() 7 matches = [ 8 entity for entity in self.entity_names if entity.lower() in query_lower 9 ] 10 return matches[:k] 11 12def neighbors(graph: dict, node: str) -> set[str]: 13 names = {edge["target"] for edge in graph["out_edges"].get(node, [])} 14 for source, edges in graph["out_edges"].items(): 15 for edge in edges: 16 if edge["target"] == node: 17 names.add(source) 18 return names 19 20def expand_entity_neighborhood(graph: dict, entity_names: list[str], hops: int = 1) -> set[str]: 21 nodes: set[str] = set(entity_names) 22 frontier: set[str] = set(entity_names) 23 for _ in range(hops): 24 next_frontier: set[str] = set() 25 for node in frontier: 26 next_frontier.update(neighbors(graph, node)) 27 nodes.update(next_frontier) 28 frontier = next_frontier 29 return nodes 30 31def collect_relationships(graph: dict, nodes: set[str]) -> list[tuple[str, str, str]]: 32 return sorted( 33 (source, edge["type"], edge["target"]) 34 for source, edges in graph["out_edges"].items() 35 if source in nodes 36 for edge in edges 37 if edge["target"] in nodes 38 ) 39 40def local_search( 41 query: str, 42 entity_store: EntityStore, 43 graph: dict, 44 text_units: dict[str, list[str]], 45 community_reports: dict[str, list[str]], 46) -> str: 47 mapped_entities = entity_store.similarity_search(query, k=10) 48 neighborhood_entities = sorted(expand_entity_neighborhood(graph, mapped_entities, hops=2)) 49 relationships = collect_relationships(graph, set(neighborhood_entities)) 50 context = "\n".join( 51 [ 52 f"Entities: {neighborhood_entities}", 53 f"Relationships: {relationships}", 54 f"Text units: { {entity: text_units.get(entity, []) for entity in neighborhood_entities} }", 55 f"Reports: { {entity: community_reports.get(entity, []) for entity in neighborhood_entities} }", 56 ] 57 ) 58 return f"Answer using local GraphRAG context:\n{context}" 59 60graph = { 61 "out_edges": { 62 "inference-api": [{"target": "Redis Cache", "type": "depends_on"}], 63 "Redis Cache": [{"target": "Old Maxmemory Policy", "type": "has_issue"}], 64 } 65} 66 67answer = local_search( 68 "Why did inference-api breach the latency SLO?", 69 EntityStore(["inference-api", "Redis Cache", "Old Maxmemory Policy"]), 70 graph, 71 text_units={"inference-api": ["inference-api breached the latency SLO after release 2026.06.14."]}, 72 community_reports={"Redis Cache": ["Cache eviction reports point to maxmemory policy risk."]}, 73) 74 75print(answer)
Output
1Answer using local GraphRAG context: 2Entities: ['Old Maxmemory Policy', 'Redis Cache', 'inference-api'] 3Relationships: [('Redis Cache', 'has_issue', 'Old Maxmemory Policy'), ('inference-api', 'depends_on', 'Redis Cache')] 4Text units: {'Old Maxmemory Policy': [], 'Redis Cache': [], 'inference-api': ['inference-api breached the latency SLO after release 2026.06.14.']} 5Reports: {'Old Maxmemory Policy': [], 'Redis Cache': ['Cache eviction reports point to maxmemory policy risk.'], 'inference-api': []}

The actual context-builder problem includes a token budget: relevant entities, relationships, text units, and reports compete for room in one generation request. A production route needs ranking and provenance, not unbounded expansion.

pack-ranked-local-context-under-a-budget.py
1from dataclasses import dataclass 2 3@dataclass(frozen=True) 4class Candidate: 5 source: str 6 score: float 7 tokens: int 8 citation: str 9 10def pack_context(candidates: list[Candidate], token_budget: int) -> list[Candidate]: 11 selected: list[Candidate] = [] 12 used = 0 13 for candidate in sorted(candidates, key=lambda item: item.score, reverse=True): 14 if used + candidate.tokens <= token_budget: 15 selected.append(candidate) 16 used += candidate.tokens 17 return selected 18 19candidates = [ 20 Candidate("relationship: depends_on", 0.98, 30, "incident-1842"), 21 Candidate("text: maxmemory issue", 0.91, 55, "incident-2031"), 22 Candidate("report: cache overview", 0.62, 80, "community-7"), 23] 24selected = pack_context(candidates, token_budget=90) 25 26print("selected:", [item.source for item in selected]) 27print("citations:", [item.citation for item in selected]) 28print("tokens:", sum(item.tokens for item in selected))
Output
1selected: ['relationship: depends_on', 'text: maxmemory issue'] 2citations: ['incident-1842', 'incident-2031'] 3tokens: 85

Global search (broad questions)

For questions that need cross-corpus coverage, GraphRAG uses a map-reduce over community reports at a chosen hierarchy level. The docs batch reports and produce rated intermediate points during map.

Reduce then keeps the highest-value points for the final answer [1]Reference 1From Local to Global: A Graph RAG Approach to Query-Focused Summarization.https://arxiv.org/abs/2404.16130[11]Reference 11GraphRAG Global Searchhttps://microsoft.github.io/graphrag/query/global_search/:

Diagram showing Query, Map report batches, Reduce top points, and Cited answer.
Query, Map report batches, Reduce top points, and Cited answer.

The sketch below does that map-reduce on four community reports from the incident corpus. Predict its shape first: map keeps latency-related points, then reduce returns the top three:

global-search-broad-questions.py
1from dataclasses import dataclass 2 3@dataclass(frozen=True) 4class RatedPoint: 5 text: str 6 rating: int 7 8def batch_reports(reports: list[str], batch_size: int = 2) -> list[list[str]]: 9 return [reports[index:index + batch_size] for index in range(0, len(reports), batch_size)] 10 11def map_report_batch(query: str, report_batch: list[str]) -> list[RatedPoint]: 12 points: list[RatedPoint] = [] 13 query_lower = query.lower() 14 asks_about_latency = any(term in query_lower for term in ("latency", "slo", "miss", "delay")) 15 for report in report_batch: 16 report_lower = report.lower() 17 if asks_about_latency and any(term in report_lower for term in ("latency", "slo", "queue", "eviction")): 18 points.append(RatedPoint(report, rating=9)) 19 elif "security" in query_lower and "credential" in report_lower: 20 points.append(RatedPoint(report, rating=6)) 21 return points 22 23def select_top_points(points: list[RatedPoint], top_k: int) -> list[RatedPoint]: 24 return sorted(points, key=lambda point: point.rating, reverse=True)[:top_k] 25 26def global_search( 27 query: str, 28 reports_by_level: dict[int, list[str]], 29 level: int, 30) -> str: 31 """Answer dataset-level questions with map-reduce over community reports.""" 32 batches = batch_reports(reports_by_level[level], batch_size=2) 33 mapped_points = [ 34 point 35 for batch in batches 36 for point in map_report_batch(query, batch) 37 ] 38 top_points = select_top_points(mapped_points, top_k=3) 39 bullets = "\n".join(f"- {point.text}" for point in top_points) 40 return f"Top recurring themes for '{query}':\n{bullets}" 41 42reports_by_level = { 43 1: [ 44 "Latency breaches cluster around cache eviction storms.", 45 "GPU queue starvation delays batch inference jobs.", 46 "Evaluation failures cluster around flaky judge prompts.", 47 "SLO breaches recur after cache config drift.", 48 ] 49} 50 51answer = global_search( 52 "What are the main reasons services miss the latency SLO?", 53 reports_by_level, 54 level=1, 55) 56 57print(answer)
Output
1Top recurring themes for 'What are the main reasons services miss the latency SLO?': 2- Latency breaches cluster around cache eviction storms. 3- GPU queue starvation delays batch inference jobs. 4- SLO breaches recur after cache config drift.

The level argument hides a cost problem. Static global search maps over every community report at the chosen level, so cost scales with that whole level even when most communities are irrelevant.

Dynamic community selection starts near the top of the hierarchy, rates each report against the query, prunes irrelevant branches, and expands only sub-communities under surviving reports.

On an AP News evaluation, Microsoft reported about 77% lower token cost at community level 1 with LLM-judge quality comparable to static search (win rates near 50%). Letting the search continue to level 3 improved comprehensiveness and empowerment (about 59% and 60% win rate vs static level 1) but raised cost about 34% because more reports entered map-reduce [13]Reference 13GraphRAG: Improving global search via dynamic community selection.https://www.microsoft.com/en-us/research/blog/graphrag-improving-global-search-via-dynamic-community-selection/.

Rating calls add work, so benchmark dynamic selection against static search on your workload.

DRIFT search

DRIFT sits between those modes. The primer compares the query with the top-k most similar community reports, drafts a broad answer, and emits follow-up questions.

Follow-up steps run local search to refine those questions, then the engine ranks the resulting question-answer tree [12]Reference 12GraphRAG DRIFT Searchhttps://microsoft.github.io/graphrag/query/drift_search/. Use it when a specific entity question still needs community-level breadth, and measure it against local search rather than assuming it's always better.

Hybrid graph-vector architecture

Don't treat vector search and GraphRAG as mutually exclusive. The GraphRAG stack already mixes graph structure with embeddings during both indexing and query-time context building [2]Reference 2GraphRAG Indexing Overviewhttps://microsoft.github.io/graphrag/index/overview/[10]Reference 10GraphRAG Local Searchhttps://microsoft.github.io/graphrag/query/local_search/. One hybrid architecture looks like this:

Hybrid router that sends an embeddings-timeout fact lookup to text chunks, the inference-api SLO question to local graph context, and a top-three recurring-reasons question to community-report map-reduce, then returns one cited answer.
Keep the timeout question on text. Spend local graph context on the inference-api investigation. Pay for report map-reduce only when the analyst asks for corpus-wide themes.

A router is a policy to evaluate, not a guarantee. Measure supported-answer accuracy, p95 latency, and spend for each query class. In a workload dominated by direct evidence lookups, a text route may cover most requests; an analyst-heavy workload may justify more graph/report queries.

release-graph-routes-from-evaluation.py
1from dataclasses import dataclass 2 3@dataclass(frozen=True) 4class RouteResult: 5 route: str 6 query_class: str 7 supported_accuracy: float 8 p95_ms: int 9 10def release_route( 11 results: list[RouteResult], 12 query_class: str, 13 minimum_accuracy: float, 14 maximum_p95_ms: int, 15) -> str: 16 eligible = [ 17 result for result in results 18 if result.query_class == query_class 19 and result.supported_accuracy >= minimum_accuracy 20 and result.p95_ms <= maximum_p95_ms 21 ] 22 return max( 23 eligible, 24 key=lambda result: (result.supported_accuracy, -result.p95_ms), 25 ).route 26 27results = [ 28 RouteResult("basic", "fact_lookup", 0.96, 95), 29 RouteResult("local", "fact_lookup", 0.96, 240), 30 RouteResult("basic", "corpus_trend", 0.61, 92), 31 RouteResult("global", "corpus_trend", 0.91, 580), 32] 33 34print("fact route:", release_route(results, "fact_lookup", 0.90, 200)) 35print("trend route:", release_route(results, "corpus_trend", 0.90, 700))
Output
1fact route: basic 2trend route: global

One subtle detail: GraphRAG the technique doesn't require a dedicated graph database. Microsoft's reference implementation writes structured output tables to disk and builds query context from those artifacts directly. A graph database like Neo4j or Neptune becomes useful when you need custom traversals, shared KG infrastructure, or analyst-facing graph queries outside the stock pipeline [8]Reference 8GraphRAG Outputshttps://microsoft.github.io/graphrag/index/outputs/.

Query expansion and context bridging

Beyond simple routing, the knowledge graph layer can enrich vector results after initial retrieval.

Query expansion uses entities found in initial vector results to expand the search query, finding semantically related but textually distinct content.

Context bridging matters when two retrieved chunks don't directly connect. If edges retain supporting chunks, a traversal can identify intermediate entities for retrieval and citation.

For example, if one chunk mentions "inference-api depends on redis-cache" and another mentions "redis-cache still uses the old maxmemory policy," a policy can inspect inference-api -> redis-cache -> old maxmemory policy, then fetch source text before making a claim about the latency incident.

Result ranking can boost text results with short, supported paths to query entities. Treat this as a ranker feature to evaluate, not proof that a nearby node supports the answer.

What the extra index costs

Indexing cost

The biggest barrier to a standard GraphRAG index is the offline bill. Compared with vector-only RAG, its standard pipeline adds LLM-heavy graph extraction, summarization, community report generation, and multiple embedding passes [1]Reference 1From Local to Global: A Graph RAG Approach to Query-Focused Summarization.https://arxiv.org/abs/2404.16130[3]Reference 3GraphRAG Default Dataflowhttps://microsoft.github.io/graphrag/index/default_dataflow/.

Whether the resulting query quality justifies that work is an evaluation question.

Mitigation strategies

To keep indexing affordable, start cheap. Microsoft recommends fast, inexpensive models while you learn the system [14]Reference 14GraphRAG Getting Startedhttps://microsoft.github.io/graphrag/get_started/. Their indexing-methods docs estimate that graph extraction (entity and relationship extraction plus their summarization) is roughly 75% of standard indexing cost [7]Reference 7GraphRAG Indexing Methodshttps://microsoft.github.io/graphrag/index/methods/.

If your use case is mostly global summarization, FastGraphRAG can cut that further. It replaces LLM-based entity extraction with NLP noun-phrase extraction (NLTK or spaCy) and defines relationships by entity co-occurrence inside a text unit. The graph is noisier and less reusable outside GraphRAG, but indexing is much cheaper [7]Reference 7GraphRAG Indexing Methodshttps://microsoft.github.io/graphrag/index/methods/.

LazyGraphRAG in practice

Microsoft Research introduced LazyGraphRAG in November 2024 [15]Reference 15LazyGraphRAG: Setting a New Standard for Quality and Costhttps://www.microsoft.com/en-us/research/blog/lazygraphrag-setting-a-new-standard-for-quality-and-cost/. It targets a central cost concern with standard GraphRAG: paying for an LLM-driven index before knowing how often graph-assisted queries will run.

A June 2025 editor's note on that post says the method is available in Microsoft Discovery and as an Azure Local public preview. Check those product pages before assuming it's in the open-source GraphRAG CLI.

LazyGraphRAG defers LLM use to query time. Its index uses NLP noun-phrase extraction and graph statistics for community structure, without entity summaries or precomputed community reports.

In Microsoft's reported experiment, indexing cost matched the vector RAG setup and was about 0.1% of full GraphRAG indexing cost [15]Reference 15LazyGraphRAG: Setting a New Standard for Quality and Costhttps://www.microsoft.com/en-us/research/blog/lazygraphrag-setting-a-new-standard-for-quality-and-cost/. Treat that as a benchmark result, not a constant for every corpus.

At query time, LazyGraphRAG blends vector similarity with community structure and exposes a relevance test budget that trades query cost for quality.

In the same write-up, a low-budget setting was comparable to GraphRAG global search on global queries at more than 700 times lower query cost, and a higher budget (about 4% of global-search query cost at community level 2) beat the tested alternatives on both local and global criteria [15]Reference 15LazyGraphRAG: Setting a New Standard for Quality and Costhttps://www.microsoft.com/en-us/research/blog/lazygraphrag-setting-a-new-standard-for-quality-and-cost/. Those comparisons used LLM-as-judge win rates on 100 synthetic queries over 5,590 AP news articles.

Precomputed community reports aren't the only candidate for global sensemaking. If the corpus changes frequently or global queries are rare, benchmark a lazy approach against standard GraphRAG.

If reusable community reports are a product output, the standard index provides artifacts the lazy path intentionally omits.

Use observed quality and workload volume to compare indexing strategies rather than choosing from architecture labels:

choose-index-strategy-from-workload.py
1from dataclasses import dataclass 2 3@dataclass(frozen=True) 4class Strategy: 5 name: str 6 index_cost: float 7 query_cost: float 8 supported_accuracy: float 9 10def choose_strategy( 11 strategies: list[Strategy], query_count: int, accuracy_floor: float 12) -> tuple[str, float]: 13 passing = [ 14 strategy for strategy in strategies 15 if strategy.supported_accuracy >= accuracy_floor 16 ] 17 winner = min( 18 passing, 19 key=lambda strategy: strategy.index_cost + query_count * strategy.query_cost, 20 ) 21 total_cost = winner.index_cost + query_count * winner.query_cost 22 return winner.name, total_cost 23 24# Illustrative measured values from one product evaluation, not vendor benchmarks. 25strategies = [ 26 Strategy("lazy", index_cost=1.0, query_cost=1.7, supported_accuracy=0.91), 27 Strategy("precomputed_reports", index_cost=200.0, query_cost=0.8, supported_accuracy=0.93), 28] 29 30print("few queries:", choose_strategy(strategies, query_count=10, accuracy_floor=0.90)) 31print("many queries:", choose_strategy(strategies, query_count=500, accuracy_floor=0.90))
Output
1few queries: ('lazy', 18.0) 2many queries: ('precomputed_reports', 600.0)

What part of LazyGraphRAG makes its reported indexing cost close to the vector RAG comparison?

Answer

It skips LLM entity and relationship summarization and doesn't precompute community reports. Its index uses NLP noun-phrase extraction plus graph statistics, while LLM work happens at query time. The exact cost ratio still depends on the evaluated corpus and configuration.

Query cost

Indexing pays the bulk of the computational expense, but runtime queries can still cost more than standard vector search. The query strategy determines how much more:

  • Local search: Usually moderate. You still have to build a mixed context from entity embeddings, graph neighborhoods, text units, and community reports, but the response path is much narrower than global search [10]Reference 10GraphRAG Local Searchhttps://microsoft.github.io/graphrag/query/local_search/.
  • Global search: Potentially expensive. Cost grows with the number of community-report batches you need to map over and the hierarchy level you choose [11]Reference 11GraphRAG Global Searchhttps://microsoft.github.io/graphrag/query/global_search/.

Mitigation strategies

To manage these runtime costs, standard GraphRAG precomputes community reports at multiple hierarchical levels. At query time, evaluate whether a coarse level answers the question adequately before paying for more detailed reports. Lower levels tend to yield more thorough responses, but they can also increase report volume and LLM work [11]Reference 11GraphRAG Global Searchhttps://microsoft.github.io/graphrag/query/global_search/.

Graph maintenance

Knowledge graphs aren't static; they must evolve as the underlying document corpus changes. Keeping the graph in sync with a live, mutating dataset adds engineering complexity.

Current GraphRAG releases expose explicit update flows and standard-update / fast-update methods. The output tables also include fields used for incremental update merges [16]Reference 16GraphRAG CLI Referencehttps://microsoft.github.io/graphrag/cli/[8]Reference 8GraphRAG Outputshttps://microsoft.github.io/graphrag/index/outputs/.

  • Additions: When new documents arrive, the system must extract entities and relationships, then merge them into the existing graph. New connections can shift community structure, requiring reclustering and regeneration of affected reports.
  • Deletions: Removing a document isn't as simple as deleting a row in a database. The system must trace and remove nodes or edges supported solely by that document. If removed evidence changes graph structure, communities and reports may need recomputation.
  • Strategy: Even with update support, a deployment may prefer scheduled refreshes for structural artifacts. Entity merges, community boundaries, and report summaries can shift when new documents arrive, so online reclustering is harder to operate than plain vector re-indexing.

When to use GraphRAG

GraphRAG isn't a default replacement for retrieval. It adds indexing and maintenance for analytical workloads. Label real queries before committing to the architecture:

ScenarioStart with vector RAGAdd GraphRAG when
Fact lookup ("What is the embeddings API timeout?")Strong, cheap baselineExtra structure rarely pays off
Entity investigation ("Why did inference-api breach the SLO?")Works if evidence sits in similar chunksLinked entities or reports recover missing hops
Global summary ("What are the top 3 SLO reasons?")Small top-k underrepresents themesCommunity reports are built for this class
Relationship-heavy question ("How does Redis config drift affect inference latency?")Needs expansion or luckGraph-linked evidence can help; test path policy
Corpus change rateFrequent edits, direct lookupsStable corpus, reusable reports, or rare global queries that still matter

The pragmatic path: start with vector search. Add a graph layer only when you can show a failure that reports or cited paths fix:

  1. Users ask questions that need synthesis across many documents
  2. You need to trace influence chains (what depends on what) with provenance
  3. Global sensemaking is a core use case, not an edge case
  4. The corpus is large and stable enough that indexing cost can amortize

Failures after the 200-document sandbox

A 200-document prototype hides different classes of failure. Once it leaves the sandbox, diagnose each trap as symptom, cause, and fix.

Mistake 1: "Use a bigger context window"

  • Symptom: You stuff 100,000 tokens into a long-context model and ask for a summary. The output misses key themes or contradicts itself.

  • Cause: A 1M token context window doesn't automatically solve global sensemaking. You still have to decide what to include, very large prompts are expensive to run, and models can still underuse information buried in the middle of long contexts [17]Reference 17Lost in the Middle: How Language Models Use Long Contextshttps://aclanthology.org/2024.tacl-1.9/.

  • Fix: Benchmark selective retrieval and structured indexing against long-context prompting. Standard GraphRAG is one candidate when repeated global questions justify precomputed reports; it isn't required for every long document.

Mistake 2: Treating GraphRAG as a replacement for vector search

  • Symptom: You replace your entire vector index with a knowledge graph. Simple lookups become slow and expensive.

  • Cause: GraphRAG is complementary, not substitutive. Local search uses entity-description embeddings and linked text units as part of context construction. Global search adds report synthesis for questions that require coverage across many documents.

  • Fix: Use a hybrid router. Route simple fact lookups to vector search, entity questions to local search, and global summaries to global search.

Mistake 3: Ignoring entity resolution

  • Symptom: Your graph has three separate nodes for RedisCache, redis-cache, and redis.internal. Queries that should traverse through the cache dependency fail because the path is broken.

  • Cause: Entity extraction from unstructured text is inherently noisy. Skipping deduplication leaves near-duplicate nodes that fragment the graph.

  • Fix: Use (canonical title, canonical type) as the entity key, carry both endpoint types on every edge, and reject unknown or ambiguous references. Candidate aliasing can use embeddings or string distance within one tenant and environment, but stable identifiers, source provenance, and a human-reviewed policy must authorize each merge. A model suggestion alone can't authorize a cross-tenant or cross-environment merge.

Mistake 4: Extracting every noun as a node

  • Symptom: The graph balloons to millions of nodes, most of them useless. Common words like "service," "alert," and "team" become nodes with thousands of meaningless edges.

  • Cause: Without a schema, the extractor treats every noun as an entity.

  • Fix: Define an ontology (a typed schema) before extraction. Restrict nodes to domain-relevant types like Service, Datastore, Region, Incident, ConfigIssue, and Runbook. Filter low-information entities in a post-processing step.

Mistake 5: Underestimating indexing cost

  • Symptom: The prototype works on 200 documents, then the first full-corpus run burns budget on extraction, summarization, and embedding passes.

  • Cause: Standard GraphRAG does much more than embed chunks. It asks models to extract entities, extract relationships, summarize repeated descriptions, generate community reports, and embed several downstream artifacts.

  • Fix: Measure indexing cost before committing to the architecture. Start with a small representative corpus, use inexpensive models while tuning prompts, consider FastGraphRAG for global-summary-heavy workloads, and set a refresh cadence instead of pretending every graph update is free.

Mistake 6: Ignoring graph maintenance

  • Symptom: Search results become stale or paths break after documents are added, deleted, or corrected.

  • Cause: A graph index has structure. New evidence can merge entities, change edge weights, move community boundaries, and invalidate old community reports.

  • Fix: Treat graph refresh as a product requirement. Track provenance with text_unit_ids, run incremental update flows where they are good enough, and schedule full rebuilds when entity resolution or community boundaries drift.

Try it yourself

Stay with the three incidents from the opening. Work this on paper before reading the sketch.

Queries

  1. "What is the timeout for the embeddings API?"
  2. "Why did inference-api breach the latency SLO, and is this likely to happen again?"
  3. A staff user (not security) asks for "top recurring operational themes." One Leiden community mixed tu-public-slo, tu-security-key, and tu-public-runbook.

For each query, name the search mode you'd run first, which artifacts you'd admit into context, and what you must not claim.

Solution sketch

  1. Basic / vector search. The timeout is a local fact. Don't pay for community map-reduce.

  2. Local search with provenance. Map to inference-api, pack cited neighbors (depends_on redis-cache from 1842, has_issue maxmemory from 2031) under the token budget. Cite those two facts. Don't predict another SLO breach: the will_cause edge has no source chunks.

  3. Fail closed before map-reduce. Drop the mixed community report. Use an all-staff equivalence-class report, recompute a per-principal view without tu-security-key, or fall back to authorized local search. Don't generate the mixed report and scrub the answer afterward.

When the graph earns its cost

Start by labeling 30 real queries as fact lookup, entity-specific investigation, or corpus-wide trend.

Use that table as a release gate: ship GraphRAG only for the bucket where vector retrieval fails and graph/report context improves cited-answer quality within your latency budget.

Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.An analyst asks for the top three recurring SLO-breach causes across 10,000 incidents. A vector route returns five highly similar write-ups from one service family. Which route addresses the unmet requirement?

Correct answer: Use global GraphRAG over authorized community reports, because similarity top-k doesn't guarantee representative corpus coverage.

The question asks for cross-corpus themes, not the nearest incidents. Community reports provide broader summary units that global search can map and reduce, subject to authorization and evidence checks.

2.A stable incident corpus needs a standard GraphRAG index for repeated global questions. Which offline workflow produces auditable graph and report artifacts without turning every noun into a node?

Correct answer: Extract typed ontology-limited entities and cited edges, resolve (title,type) identities, run hierarchical Leiden, write reports, and embed query artifacts.

Schema limits control graph noise, typed identities prevent collisions, provenance keeps relationships auditable, and communities become report-level retrieval units.

3.A query asks why inference-api breached its SLO. Evidence spans a service incident, a cache-eviction record, and a maxmemory note that omits the service name. Which context builder fits the entity-specific question?

Correct answer: Map to inference-api, rank linked entities, edges, text units, and reports, then pack cited local context within budget.

Local search can bridge the named service to connected cache evidence without paying for corpus-wide synthesis. Ranking, budget, and provenance stop neighborhood expansion from becoming an unsupported dump.

4.A path has cited depends_on and has_issue edges, then an uncited will_cause -> future breach edge. The user asks whether another breach will happen. Which answer boundary is defensible?

Correct answer: State the cited dependency and policy issue, but withhold every future-breach claim because its final edge lacks evidence.

Support is edge-specific. The cited subpath establishes observed structure and an unresolved issue, not a causal prediction. Graph reachability can't manufacture provenance for the final claim.

5.A Service and a Datastore both have title gateway, and one extracted edge omits its endpoint type. Which resolver behavior protects graph identity?

Correct answer: Reject the untyped reference, report both candidate types, and require an endpoint type before writing the edge.

The identity key includes canonical title and type. A name-only lookup is safe only when it has one candidate; failing closed prevents a relationship from silently connecting the wrong nodes.

6.The only source text supporting redis-cache has_issue old-maxmemory-policy is deleted. A community report also summarizes that issue. Which update preserves graph integrity?

Correct answer: Remove facts supported only by that source, then recompute affected communities and regenerate every dependent report.

Deletion propagates through derived structure. Leaving an unsupported edge or report would keep deleted evidence queryable even after its leaf vector disappeared.

7.Two strategies pass a 0.90 accuracy floor. Lazy costs 1.0 to index and 1.7 per query; precomputed reports cost 200.0 to index and 0.8 per query. Which minimizes total cost at 10 and 500 queries?

Correct answer: At 10, choose lazy for 18.0; at 500, choose precomputed reports for 600.0 after amortizing index cost.

Lazy totals 18.0 and 851.0; reports total 208.0 and 600.0. Since both pass quality, workload volume determines when lower per-query cost repays the larger index.

8.Fact lookup gates are accuracy at least 0.90 and p95 at most 200 ms; trend gates use 0.90 and 700 ms. Basic/local fact results are 0.96/95 and 0.96/240; basic/global trend results are 0.61/92 and 0.91/580. Which routes ship?

Correct answer: Ship basic for facts and global for trends, because those are the only routes clearing both class-specific gates.

Local fact lookup fails 200 ms despite matching accuracy; basic trend retrieval fails 0.90 despite low latency. Routes earn release per query class by satisfying both constraints.

9.A corpus changes weekly, global questions are rare, and no product feature consumes reusable community reports. Which graph-assisted candidate best matches this workload for an initial benchmark?

Correct answer: LazyGraphRAG, because cheap noun-phrase and graph-statistic indexing defers LLM work until a global query arrives.

The workload doesn't amortize repeated report generation. Lazy indexing matches that cost shape, but release still depends on measured global-query quality and total spend against simpler routes.

10.A staff user's leaf retrieval is ACL-filtered, but global search admits a community report built from public incidents and a security-only key-rotation unit. Which control preserves authorization continuity?

Correct answer: Reject the mixed report; use ACL-equivalent reports, per-principal recompute, or authorized local context instead.

Derived summaries carry facts and grants from their inputs. Authorization must run before map-reduce, and revocation must refresh reports rather than only tombstoning leaf embeddings.

10 questions remaining.

Next Step
Continue to RAG Security & Access Control

GraphRAG puts vector candidates, entities, and community reports on one retrieval surface. The next chapter asks who may read that surface: row-level security, document <span data-glossary="acl">ACLs</span>, and per-user filters must run before protected evidence reaches model context.

PreviousAdvanced RAG: HyDE & Self-RAG
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

From Local to Global: A Graph RAG Approach to Query-Focused Summarization.

Edge, D., et al. · 2024 · arXiv preprint

https://arxiv.org/abs/2404.16130

GraphRAG Indexing Overview

Microsoft GraphRAG Documentation · 2025

https://microsoft.github.io/graphrag/index/overview/

GraphRAG Default Dataflow

Microsoft GraphRAG Documentation · 2025

https://microsoft.github.io/graphrag/index/default_dataflow/

Unifying Large Language Models and Knowledge Graphs: A Roadmap.

Pan, S., et al. · 2024 · arXiv preprint

https://arxiv.org/abs/2306.08302

Semi-Supervised Classification with Graph Convolutional Networks

Kipf, T. N. & Welling, M. · 2016 · ICLR 2017

https://arxiv.org/abs/1609.02907

Neural Message Passing for Quantum Chemistry

Gilmer, J., Schoenholz, S. S., Riley, P. F., Vinyals, O., & Dahl, G. E. · 2017 · ICML 2017

https://arxiv.org/abs/1704.01212

GraphRAG Indexing Methods

Microsoft GraphRAG Documentation · 2025

https://microsoft.github.io/graphrag/index/methods/

GraphRAG Outputs

Microsoft GraphRAG Documentation · 2025

https://microsoft.github.io/graphrag/index/outputs/

From Louvain to Leiden: guaranteeing well-connected communities.

Traag, V.A., Waltman, L., & van Eck, N.J. · 2019 · Scientific Reports

https://arxiv.org/abs/1810.08473

GraphRAG Local Search

Microsoft GraphRAG Documentation · 2025

https://microsoft.github.io/graphrag/query/local_search/

GraphRAG Global Search

Microsoft GraphRAG Documentation · 2025

https://microsoft.github.io/graphrag/query/global_search/

GraphRAG DRIFT Search

Microsoft GraphRAG Documentation · 2025

https://microsoft.github.io/graphrag/query/drift_search/

GraphRAG: Improving global search via dynamic community selection.

Li, B., Trinh, H., Edge, D., & Larson, J. · 2025 · Microsoft Research Blog

https://www.microsoft.com/en-us/research/blog/graphrag-improving-global-search-via-dynamic-community-selection/

GraphRAG Getting Started

Microsoft GraphRAG Documentation · 2025

https://microsoft.github.io/graphrag/get_started/

LazyGraphRAG: Setting a New Standard for Quality and Cost

Larson, J. & Truitt, S. (Microsoft Research) · 2024

https://www.microsoft.com/en-us/research/blog/lazygraphrag-setting-a-new-standard-for-quality-and-cost/

GraphRAG CLI Reference

Microsoft GraphRAG Documentation · 2025

https://microsoft.github.io/graphrag/cli/

Lost in the Middle: How Language Models Use Long Contexts

Liu, N.F., et al. · 2024 · TACL 2024

https://aclanthology.org/2024.tacl-1.9/

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.