A knowledge graph represents entities, relationships, and claims as a connected model. It becomes valuable for AI when the application must traverse explicit relationships, preserve evidence lineage, enforce domain constraints, or aggregate facts across a corpus. It is not synonymous with a graph database, and GraphRAG is not a universal replacement for lexical, vector, or hybrid retrieval.
This guide designs the graph as a governed derived view: every claim points back to a source span and revision, every retrieval path inherits authorization, and every GraphRAG mode must beat a simpler baseline on the target workload.
Table of Contents
- Key Takeaways
- Knowledge Graph, Graph Database, and Ontology
- Production Architecture
- Model Claims and Provenance
- Control Extraction and Entity Resolution
- Choose RDF, Property Graph, or Relational Storage
- Route GraphRAG by Query Type
- Execute Graph Queries Safely
- Preserve Authorization and Evidence
- Evaluate Each Layer Separately
- Operate Updates, Deletions, and Rollbacks
- Common Failure Modes
- Frequently Asked Questions
- References
Key Takeaways
- A knowledge graph is a representation of knowledge; a graph database is one implementation choice.
- Store extracted statements as versioned claims with provenance, not as timeless truth.
- Treat relation extraction and entity resolution as measurable, reversible data pipelines.
- Use GraphRAG only where graph context beats lexical, vector, or hybrid baselines.
- Never execute unrestricted LLM-generated Cypher or trust query-supplied authorization fields.
- A graph path shows what retrieval used. It does not reveal hidden model reasoning or prove correctness.
- Evaluate extraction, resolution, graph quality, retrieval, generation, security, and lifecycle independently.
Knowledge Graph, Graph Database, and Ontology
A knowledge graph is a connected representation of entities and claims, while a graph database is software for storing and querying graph-shaped data. Keeping those concepts separate prevents storage choices from defining the knowledge model.
| Concept | What it defines | What it does not guarantee |
|---|---|---|
| Knowledge graph | Entities, claims, relationships, identifiers, and semantics | Truth, completeness, or a specific database |
| Graph database | Storage, indexes, transactions, and traversal queries | Ontology quality or source reliability |
| RDF graph | A set of subject-predicate-object triples identified with IRIs | Rich inference unless an entailment regime is added |
| Property graph | Nodes and relationships with labels and properties | Shared semantics across systems |
| Ontology | Vocabulary, classes, properties, and sometimes constraints or rules | Correct instance data |
| GraphRAG | Retrieval architectures that use graph-derived context | Better answers on every query |
The stable W3C RDF 1.1 Concepts Recommendation defines an RDF graph as a set of triples and an RDF dataset as a default graph plus zero or more named graphs. RDF 1.2 work may add capabilities, but a production contract should not describe a Working Draft as a final standard.
Property graphs make traversal-oriented application data convenient. RDF emphasizes global identifiers, graph exchange, and explicit semantics. Relational databases remain excellent for transactions, constraints, and bounded joins. A production system may use more than one model, provided identity, source lineage, and deletion semantics remain consistent.
Production Architecture
A production knowledge graph separates source records, extraction, resolution, graph publication, retrieval, and answer generation. This separation lets teams reprocess one layer without silently changing every downstream answer.
The offline path creates derived artifacts. The online path retrieves only artifacts the caller may access. The application should be able to answer four questions for every generated claim:
- Which source spans support it?
- Which extractor and resolver versions produced the graph artifacts?
- Which authorization decision allowed retrieval?
- Which graph and index snapshots were active?
If any answer is missing, the system has an observability gap rather than an explainability feature.
Model Claims and Provenance
The safest graph model treats an extracted edge as a claim supported by evidence, not as an unconditional fact. This matters when sources conflict, change over time, or apply to different scopes.
{
"claim_id": "clm_01J...",
"subject_id": "org:acme",
"predicate": "HEADQUARTERED_IN",
"object_id": "place:singapore",
"source_document_id": "doc_842",
"source_revision": "sha256:8f1...",
"source_span": {"start": 418, "end": 476},
"extractor_version": "relations-2026-08-01",
"valid_from": "2026-04-01",
"valid_to": null,
"tenant_id": "tenant_17",
"policy_scope": ["research"],
"state": "candidate"
}
The source document and revision are the citation boundary. Community summaries, embeddings, resolved entities, and graph edges are derived aids. They must point back to that boundary rather than cite one another recursively.
The following dependency-free validator demonstrates a minimum ingestion contract:
from dataclasses import dataclass
from typing import Optional
ALLOWED_RELATIONS = {"HEADQUARTERED_IN", "OWNS", "DEPENDS_ON"}
@dataclass(frozen=True)
class Claim:
subject_id: str
predicate: str
object_id: str
source_document_id: str
source_revision: str
span_start: int
span_end: int
extractor_version: str
tenant_id: str
valid_to: Optional[str] = None
def validate_claim(claim: Claim, source_length: int) -> None:
if claim.predicate not in ALLOWED_RELATIONS:
raise ValueError("relationship type is not in the versioned vocabulary")
if not (0 <= claim.span_start < claim.span_end <= source_length):
raise ValueError("source span is outside the source revision")
required = (
claim.subject_id,
claim.object_id,
claim.source_document_id,
claim.source_revision,
claim.extractor_version,
claim.tenant_id,
)
if any(not value.strip() for value in required):
raise ValueError("claim lineage and tenant scope are required")
example = Claim(
subject_id="org:acme",
predicate="HEADQUARTERED_IN",
object_id="place:singapore",
source_document_id="doc_842",
source_revision="sha256:8f1",
span_start=10,
span_end=42,
extractor_version="relations-v3",
tenant_id="tenant_17",
)
validate_claim(example, source_length=100)
print("valid")
# Output: valid
In production, also record confidence, review state, temporal scope, ingestion time, policy labels, and supersession links. Confidence is a ranking signal, not permission to publish unsupported claims.
Control Extraction and Entity Resolution
Extraction and entity resolution are noisy inference tasks, so they need explicit schemas, test sets, review queues, and rollback. A generic pretrained language model with an invented label list is not a relation extractor.
Use a constrained contract:
- Define a versioned relationship vocabulary and the valid subject/object types.
- Require source spans for every candidate entity and relation.
- Reject unknown predicates instead of creating new edge types at runtime.
- Resolve entities with stable identifiers and auditable match features.
- Route low-confidence or high-impact merges to review.
- Keep candidate claims separate from accepted and rejected claims.
Entity resolution deserves its own release gate. A harmful merge combines two different entities and can contaminate many paths; a harmful split creates duplicate identities and hides evidence. Track pairwise precision/recall alongside harmful merge and split rates, segmented by entity type and language.
Do not overwrite conflicts. Preserve claim-level source and temporal scope so retrieval can expose disagreement or select the claim valid for a specific time.
Choose RDF, Property Graph, or Relational Storage
Choose a storage model from workload and governance requirements, not a vendor-size slogan. Benchmark representative data, traversals, updates, and authorization filters before committing.
| Decision factor | RDF store | Property graph | Relational or hybrid |
|---|---|---|---|
| Shared identifiers and standards-based exchange | Strong fit | Requires conventions | Requires mapping |
| Ontology and explicit entailment | Strong fit with selected semantics | Usually application-managed | Usually application-managed |
| Traversal-heavy application queries | Capable; test SPARQL workload | Often ergonomic | Good for bounded relationships |
| Transactional records and strict constraints | Product-dependent | Product-dependent | Strong default |
| Existing operational platform | May add a new stack | May add a new stack | Often lowest incremental cost |
| Fine-grained authorization | Must be designed and tested | Must be designed and tested | Mature options, model-dependent |
| Portability | RDF standards help exchange | Query dialects and features vary | SQL is broad; extensions vary |
For database selection, evaluate query language compatibility, transaction and consistency guarantees, index behavior, traversal fan-out, bulk and incremental ingestion, backup/restore, observability, policy enforcement, managed versus self-hosted operations, and measured total cost. Product names alone do not answer those questions.
Route GraphRAG by Query Type
GraphRAG is a family of graph-enhanced retrieval designs, and each query mode serves a different task. Route by evaluated query class instead of sending every request through the most expensive pipeline.
Microsoft's GraphRAG query documentation distinguishes:
- Basic Search: a baseline vector RAG path.
- Local Search: combines text chunks with graph entities, relationships, and related reports.
- Global Search: map-reduce synthesis over community reports for corpus-level questions.
- DRIFT Search: expands local search with community information.
The original GraphRAG paper evaluates global sensemaking on particular corpora and criteria. It does not prove that graph retrieval dominates every task. Independent WildGraphBench results similarly indicate that benefits depend on query type: graph methods can help multi-fact aggregation, while BM25 remains competitive for straightforward lookup and graph aggregation can lose fine detail.
| Query class | Start with | Promote to graph retrieval when |
|---|---|---|
| Exact identifier or quoted fact | Lexical/BM25 | Entity aliases or linked records improve recall |
| Local semantic question | Vector or hybrid | Neighbor relations add supported evidence |
| Connected multi-fact question | Hybrid baseline | Bounded traversal raises evidence coverage |
| Corpus-wide themes | Aggregation baseline | Community reports improve completeness at acceptable cost |
| Transactional lookup | Authoritative database/API | Usually do not route through GraphRAG |
The router should use deterministic features where possible: requested operation, identified entities, expected aggregation scope, and policy. An LLM may classify intent, but its output still needs schema validation and a safe fallback.
Execute Graph Queries Safely
Safe graph retrieval maps validated intent to pre-reviewed query templates. It never sends arbitrary model-generated query text to the database.
from dataclasses import dataclass
from enum import Enum
from typing import Any
class Intent(str, Enum):
ENTITY_PROFILE = "entity_profile"
DEPENDENCY_NEIGHBORS = "dependency_neighbors"
@dataclass(frozen=True)
class QueryPlan:
cypher: str
parameters: dict[str, Any]
TEMPLATES = {
Intent.ENTITY_PROFILE: """
MATCH (e:Entity {tenant_id: $tenant_id, id: $entity_id})
WHERE e.policy_scope IN $allowed_scopes
RETURN e.id AS id, e.name AS name, e.type AS type
LIMIT 1
""",
Intent.DEPENDENCY_NEIGHBORS: """
MATCH (e:Entity {tenant_id: $tenant_id, id: $entity_id})
-[r:DEPENDS_ON]->(n:Entity {tenant_id: $tenant_id})
WHERE e.policy_scope IN $allowed_scopes
AND n.policy_scope IN $allowed_scopes
RETURN n.id AS id, n.name AS name, r.claim_id AS claim_id
ORDER BY n.id
LIMIT $row_limit
""",
}
def build_plan(
intent: Intent,
*,
entity_id: str,
authoritative_tenant_id: str,
authoritative_scopes: list[str],
requested_limit: int = 25,
) -> QueryPlan:
if not entity_id or len(entity_id) > 128:
raise ValueError("invalid entity identifier")
if not authoritative_scopes:
raise PermissionError("no authorized policy scope")
return QueryPlan(
cypher=TEMPLATES[intent],
parameters={
"entity_id": entity_id,
"tenant_id": authoritative_tenant_id,
"allowed_scopes": authoritative_scopes,
"row_limit": min(max(requested_limit, 1), 100),
},
)
plan = build_plan(
Intent.DEPENDENCY_NEIGHBORS,
entity_id="service:checkout",
authoritative_tenant_id="tenant_17",
authoritative_scopes=["operations"],
requested_limit=500,
)
print(plan.parameters["row_limit"])
# Output: 100
The Neo4j Cypher injection guidance explains why literal values should be parameters: query text and values are compiled separately. That control is necessary but incomplete.
Apply all of these controls:
- Derive tenant, object, and purpose scope from trusted identity and policy services.
- Use read-only, least-privilege database roles.
- Allowlist query templates, labels, relationship types, and sortable properties.
- Bound traversal depth, fan-out, returned rows, time, and memory.
- Parameterize literal values.
- Redact database errors before they reach a model or client.
- Log template ID, policy decision, snapshot, parameters after redaction, latency, and row count.
Parameters prevent literal-value injection. They do not authorize a query. Dynamic labels, relationship types, property names, and query clauses cannot be made safe merely by placing user text in a parameter.
Preserve Authorization and Evidence
Authorization must produce the same decision across source text, vector chunks, graph claims, and generated summaries. Otherwise, the graph becomes a side channel around document permissions.
Apply policy before and during retrieval:
- Resolve caller identity, tenant, purpose, and object grants from authoritative services.
- Filter candidate indexes by those grants.
- Enforce tenant and policy scope inside graph templates.
- Recheck every source span before prompt assembly.
- Reject generated citations that point outside the authorized evidence set.
- Record the policy decision and graph/index snapshot for audit.
Never let the model invent tenant_id, allowed_scopes, or an access-control predicate. Query text is not an identity source.
Community reports and other graph summaries require the same controls as their source documents. A summary that combines allowed and denied documents cannot be safely returned by hiding its citations; it must be rebuilt for the permitted evidence scope or excluded.
Evaluate Each Layer Separately
A single answer score cannot reveal whether a regression came from extraction, entity resolution, retrieval, generation, or policy. Use layered gates and keep a simpler retrieval baseline in every release evaluation.
| Layer | Example measures | Critical failure |
|---|---|---|
| Extraction | Entity and relation precision/recall/F1, source-span validity, vocabulary violations | Unsupported or untraceable claim |
| Resolution | Pairwise precision/recall/F1, harmful merge rate, harmful split rate | Cross-entity contamination |
| Graph quality | Provenance coverage, stale-edge rate, orphan rate, duplicate-entity rate | Accepted edge without current evidence |
| Retrieval | Evidence Recall@K, path/evidence coverage, citation coverage | Authorized evidence missed or denied evidence returned |
| Generation | Correctness, claim support, completeness, abstention, contradiction handling | Unsupported material claim |
| Security | Denied-content retrieval rate, cross-tenant leakage, template bypass attempts | Any unauthorized disclosure |
| Operations | p50/p95 latency, indexing cost, refresh lag, update amplification | SLO or deletion deadline breach |
This dependency-free example calculates evidence recall and citation precision without conflating them:
def retrieval_metrics(
required_evidence: set[str],
retrieved_evidence: list[str],
cited_evidence: list[str],
) -> dict[str, float]:
retrieved = set(retrieved_evidence)
cited = set(cited_evidence)
recall = len(required_evidence & retrieved) / max(len(required_evidence), 1)
citation_precision = len(cited & retrieved) / max(len(cited), 1)
return {
"evidence_recall": round(recall, 3),
"citation_precision": round(citation_precision, 3),
}
print(retrieval_metrics(
required_evidence={"span:A", "span:B"},
retrieved_evidence=["span:A", "span:C"],
cited_evidence=["span:A", "span:X"],
))
# Output: {'evidence_recall': 0.5, 'citation_precision': 0.5}
Segment results by query class, language, tenant, entity type, graph depth, and source freshness. Report quality alongside latency and cost. A gain in corpus-level synthesis does not justify routing direct lookups through a slower, less precise path.
For a broader release-gate design, use the production RAG evaluation guide. The advanced GraphRAG engineering guide goes deeper on extraction contracts and baseline experiments, while the semantic search guide covers hybrid retrieval and authorization filtering.
Operate Updates, Deletions, and Rollbacks
A production graph is a versioned derived view, so updates and deletions must propagate through every derivative. Updating only the source document leaves stale claims, embeddings, community reports, and caches active.
Maintain a dependency manifest:
source revision
-> segments
-> candidate claims
-> resolved entities
-> accepted graph snapshot
-> lexical/vector indexes
-> community assignments
-> generated summaries
-> retrieval and answer caches
For each source change:
- Create a new immutable source revision.
- Re-extract affected spans and compare claim deltas.
- re-run entity resolution only for impacted candidates.
- Publish a new graph/index snapshot atomically.
- Invalidate summaries and caches that depend on changed claims.
- Keep the previous snapshot available for rollback.
For deletion, tombstone the source immediately, block it from retrieval, remove or withdraw dependent claims, rebuild affected summaries, purge caches, and verify propagation within a defined deadline. Track deletion coverage and lag as release metrics.
Common Failure Modes
The most damaging failures cross layer boundaries and therefore remain invisible to a single answer-quality metric.
| Failure | Why it happens | Production control |
|---|---|---|
| Knowledge graph treated as truth | Extracted edges lose source context | Claim nodes, source spans, review states, temporal scope |
| Distinct entities are merged | Weak identity features or global thresholds | Type-specific thresholds, review queue, rollback |
| One entity is split | Alias and multilingual matching gaps | Stable IDs, alias provenance, pairwise evaluation |
| GraphRAG replaces every baseline | Architecture chosen before workload evaluation | Query-class router and baseline gates |
| Path shown as explanation | Retrieval artifact confused with reasoning | Label paths as evidence; cite original spans |
| Generated Cypher is executed | Model output crosses the trust boundary | Pre-reviewed templates and strict parameters |
| Tenant filter comes from the prompt | Query data is mistaken for authorization | Authoritative identity and policy service |
| Deleted source still affects answers | Derived artifacts lack dependency lineage | Deletion manifest, propagation SLO, verification |
| Community summary becomes authority | Generated text is cited instead of sources | Summary-to-span lineage and citation checks |
Frequently Asked Questions
Is every knowledge graph based on RDF?
No. RDF is a standardized graph data model based on triples and IRIs, but knowledge graphs can also use property graphs, relational representations, search indexes, or combinations of them. The representation should preserve identity, semantics, provenance, and lifecycle requirements regardless of storage.
Is an ontology required?
Not always. A small application may start with a constrained entity and relationship schema. An ontology becomes useful when teams need shared semantics, interoperability, inference rules, or formal constraints. An ontology does not validate source truth or eliminate entity-resolution errors.
Can GraphRAG reduce hallucinations?
It can provide better evidence for some tasks, but it cannot guarantee factual output. Hallucination risk still depends on extraction quality, retrieval coverage, authorization, prompt construction, model behavior, and claim-level verification. Measure supported-claim rate and abstention rather than assuming the graph fixes generation.
How deep should graph traversal be?
There is no universal depth. Use fixed, allowlisted templates for the relationships the product actually needs, then benchmark coverage, fan-out, latency, and false associations. Unbounded or model-selected depth increases cost, leakage risk, and irrelevant paths.
When should a team avoid GraphRAG?
Avoid it when direct database lookup, BM25, vector, or hybrid retrieval already satisfies quality and latency targets; when source permissions cannot be propagated to graph artifacts; or when the team cannot operate extraction, entity resolution, updates, deletion, and layered evaluation.
References
- RDF 1.1 Concepts and Abstract Syntax — W3C Recommendation
- GraphRAG Query Overview — Microsoft
- From Local to Global: A Graph RAG Approach to Query-Focused Summarization
- WildGraphBench: Benchmarking Graph-Based Retrieval-Augmented Generation
- Protecting against Cypher Injection — Neo4j
Summary
Knowledge graphs create durable AI value when they make identity, relationships, provenance, policy, and lifecycle explicit. Build the graph as a versioned derivative of authoritative sources, keep extraction and resolution reversible, route GraphRAG only where evaluation proves value, and execute graph queries through bounded templates. The result is not automatic truth or hidden-reasoning visibility; it is a more inspectable retrieval system with measurable evidence and governance contracts.