Build a Knowledge Graph + RAG Hybrid Search System
Raunak Baliyan
30 October 2026 · 39 min read
Build a Knowledge Graph + RAG Hybrid Search System
1. Clarifying Requirements
1.1 Functional Requirements
The system ingests a heterogeneous enterprise corpus (documents, wikis, tickets, emails, PDFs, database records, meeting transcripts). It builds two linked representations of it:
a retrieval index of text chunks (vector and keyword), and
a knowledge graph of entities (people, products, systems, customers, contracts, incidents), their attributes, and typed relationships between them.
Users ask natural-language questions and receive grounded answers with citations to the source passages (and, where relevant, the graph path that supports the answer).
The system answers four distinct question types:
Lookup: "What is the SLA for the Atlas service?" Best served by chunk retrieval.
Entity-centric: "Who owns the billing pipeline and what incidents did it have last quarter?" Needs attributes and relations of a specific entity.
Multi-hop: "Which customers are affected by vendors that had a security incident in 2024?" Needs traversal across several relationships that no single document states.
Global/thematic: "What are the main recurring causes of outages across the platform?" Needs aggregation across thousands of documents, which top-k retrieval cannot do.
A query router chooses the retrieval strategy per question (vector-only, graph-assisted, multi-hop traversal, or global summarisation), and can combine strategies.
The system supports entity-aware navigation: entity pages, "related entities", and graph exploration alongside answers.
Every graph fact carries provenance: the chunk(s) and document(s) it was extracted from, extraction confidence, extraction model version, and timestamps.
The system keeps the graph and indexes fresh: new, edited, and deleted documents propagate to both structures incrementally, without full rebuilds.
The system enforces document-level access control across both chunks and graph facts. A user must never receive a fact, entity attribute, or relationship derived solely from documents they cannot read.
The system supports temporal facts ("owner as of March 2024") and conflicting facts (two documents disagree about an entity's attribute), surfacing conflicts rather than silently choosing.
Administrators manage the ontology (entity and relation types), entity-resolution rules and merges, extraction prompts, and quality dashboards.
1.2 Non-Functional Requirements
Answer quality over retrieval novelty. The graph is only worth its cost if it measurably improves multi-hop and global questions without hurting simple lookups. The design must include an evaluation harness that proves this, per question type.
Latency. Interactive queries target p95 ≤ ~3 s to first generated token. The retrieval phase (routing, entity linking, vector/keyword search, graph expansion, reranking) targets ≤ ~800 ms p95. Graph traversal must be bounded in depth, fan-out, and time.
Grounding and faithfulness. Every claim in an answer is traceable to retrieved evidence. Graph-derived claims must be backed by their source chunks, never by the graph alone, since extracted edges can be wrong.
Extraction is the dominant cost and the dominant quality risk. Running an LLM over the whole corpus is expensive, and LLM extraction produces errors (wrong entities, hallucinated relations, inconsistent naming). The design controls cost through tiering and caching, and controls quality through schema-guided extraction, confidence scores, and verification.
Entity resolution quality is critical. Over-merging collapses distinct entities (two customers named "Acme") and corrupts the graph. Under-merging fragments knowledge ("IBM", "I.B.M.", "International Business Machines") and breaks multi-hop paths. Both errors are visible in answers.
Security. Access control must hold even through multi-hop inference. A path of three individually-permitted facts is fine, but a path through a restricted document must be filtered. There must be no leakage through entity names, summaries, or community reports.
Freshness. New or changed documents become searchable within minutes (chunks first), and graph updates follow within a bounded delay. Deletions (including compliance deletions) propagate to chunks, graph facts, and derived summaries.
Scale and cost of storage. Hundreds of millions of chunks, tens of millions of entities, and hundreds of millions of edges, with embeddings on chunks, entities, and community summaries.
Operability. Pipelines are resumable, idempotent, and versioned. Re-extraction after a prompt or model change must be selective and cost-controlled, not a full re-run.
Auditability. For any answer, the system can show which chunks, entities, edges, and summaries were used, and which versions of extraction and ranking produced them.
2. Back-of-the-Envelope Estimation
Corpus size:
Assume 20M documents with an average of ~6,000 tokens each, about 120B tokens.
Chunked at ~600 tokens with overlap, that is roughly 200M chunks.
Daily churn of ~1-2% of documents (new, edited, deleted) is about 200K-400K documents/day, or ~3,000-5,000 chunks/hour at steady state, with bursts when a source is bulk-imported.
Embedding volume:
200M chunks × ~600 tokens = 120B tokens to embed initially. At typical embedding-model throughput on a modest GPU pool, this is a multi-day batch job, but it is far cheaper than LLM extraction.
Vector storage: 200M chunk vectors × 1,024 dims × 4 bytes ≈ ~820 GB raw, or roughly 200-400 GB after quantisation (int8 or product quantisation). Entity and community-summary vectors add a few percent.
Graph size:
Assume ~12-15 entity mentions per chunk, so ~3B mentions. After normalisation and resolution, perhaps 40-60M unique entities.
Relations extracted at ~3-5 per chunk, deduplicated and merged across evidence, give roughly 300-500M edges, each pointing to one or more evidence chunks.
Many edges have multiple evidence chunks, so the evidence-link table (edge → chunk) is larger than the edge table, perhaps 1-1.5B rows.
Graph storage at roughly 200-400 bytes per node and 150-300 bytes per edge (properties, provenance pointers): about 10-20 GB for nodes and 60-150 GB for edges, plus indexes. This is comfortably within a sharded graph database or a well-indexed relational store, so the graph is much smaller than the vector index.
The extraction cost, the real budget item:
Running an LLM extraction pass over every chunk means ~120B input tokens plus prompt overhead (schema and instructions, ~800 tokens per call, repeated 200M times = ~160B tokens) and ~250 output tokens per chunk (~50B output tokens).
Total is on the order of ~280B input and ~50B output tokens for a naive full pass. Even with a small, cheap model at about $0.15 per million input tokens and $0.60 per million output tokens, that is roughly $42K + $30K ≈ $70K per full extraction run, and several times that with a larger model.
Reducing the prompt overhead by batching several chunks per call (5-10 chunks) cuts the overhead tokens by ~5-8x and brings a full pass to roughly $35-45K on a small model.
Tiering (section 4.2) lets many chunks skip expensive extraction entirely: boilerplate, near-duplicates, and low-information chunks can be filtered out. Skipping 30-40% of chunks lowers the bill proportionally.
Re-extraction is the recurring cost. Changing the ontology or prompt means re-running a large share of this, which is why extraction output is cached by
(chunk_hash, extractor_version)and re-runs are selective.
Incremental processing load:
At 300K documents/day × ~6 chunks ≈ 1.8M chunks/day, extraction costs roughly $300-500/day on a small model, which is manageable and must be monitored.
Query load:
Assume 300 queries/sec at peak (internal search, assistants, and API consumers). Each query typically involves: 1 router call (small model or classifier), 1-2 vector searches, 1 keyword search, 1-3 graph expansions, a rerank over ~100 candidates, and one generation call.
At 300 QPS peak, that is ~600 vector queries/sec, ~300 keyword queries/sec, and ~450 graph expansions/sec, plus ~300 reranks/sec over 100 candidates (30,000 pair scores/sec) and 300 generation calls/sec.
With ~1,500 context tokens (chunks plus graph facts) and ~300 output tokens per answer, generation is about 450K input and 90K output tokens/sec at peak, so the generation LLM is the main online cost.
Global-query (community summary) costs:
Community detection over 50M entities yields perhaps 500K-2M communities across levels. Generating a summary for each with an LLM (~2,000 input tokens, ~300 output each) costs on the order of 2-4B input tokens, i.e. hundreds of dollars per build, but must be refreshed incrementally, since rebuilding after every update would be wasteful.
Entity resolution workload:
3B mentions normalised to 50M entities. Pairwise comparison is infeasible (50M² pairs), so candidate generation by blocking (shared normalised name tokens, embeddings, attributes) reduces the work to a manageable number of candidate pairs (~hundreds of millions), scored by a cheap model with only ambiguous cases going to an LLM or a human queue.
Super-node and fan-out risk:
Graph degree is heavy-tailed. A few entities ("the company", "Engineering", a major customer) may have millions of edges. Unbounded traversal from such a node is a latency and relevance disaster, so degree-aware expansion and per-hop budgets are mandatory (section 6.3).
Freshness SLOs:
Chunks and vectors searchable within ~2-5 minutes of an update, graph facts within ~15-30 minutes, community summaries within hours (batched), and deletions propagated to chunks within minutes and to graph and summaries within the hour.
3. Core APIs
Ask (primary query API)
POST /v1/ask
Body: {
question: "Which customers are affected by vendors that had a security incident in 2024?",
mode: "AUTO" | "VECTOR" | "GRAPH_ASSISTED" | "MULTI_HOP" | "GLOBAL",
filters: { sources: ["wiki","tickets"], time_range: { from: "2024-01-01", to: "2024-12-31" } },
options: { max_hops: 3, include_graph_paths: true, stream: true }
}
Response (streamed): {
answer: "…grounded answer with inline [1][2] citations…",
strategy_used: ["ENTITY_LINKING","MULTI_HOP"],
citations: [ { id: 1, doc_id, chunk_id, title, url, snippet, version } ],
graph_evidence: [ { path: ["VendorX","HAD_INCIDENT","INC-2217"], supporting_chunks: [..], confidence: 0.91 } ],
conflicts: [ { claim: "Owner of Atlas", sources: [..], note: "Sources disagree" } ],
confidence: "HIGH" | "MEDIUM" | "LOW"
}
Search (retrieval only, no generation)
POST /v1/search
Body: { query, top_k: 20, mode: "HYBRID", include_entities: true }
Response: { chunks: [...], entities: [ { entity_id, name, type, score } ], trace_id }
Entity APIs
GET /v1/entities/{entity_id}
Response: { name, type, aliases, attributes: [ { key, value, valid_from, valid_to, confidence, evidence: [chunk_ids] } ],
relations: [ { type, target, evidence_count, confidence } ], conflicts: [...] }
GET /v1/entities/{entity_id}/neighbors?relation=OWNS&depth=2&limit=50
POST /v1/entities/resolve Body: { mention: "Int'l Business Machines", type_hint: "ORG" }
Response: { entity_id, confidence, alternatives: [...] }
Ingestion
POST /v1/ingest/documents
Body: { source: "confluence", doc_id, content_ref, acl: { groups: [...] }, version, modified_at }
Response: { ingest_id, status: "QUEUED" }
DELETE /v1/ingest/documents/{doc_id} → triggers propagated deletion
Ontology and extraction administration
PUT /v1/admin/ontology Body: { entity_types, relation_types, constraints } → new ontology_version
POST /v1/admin/extraction/rerun Body: { scope: { doc_filter }, extractor_version, dry_run: true }
Response: { estimated_chunks: 4200000, estimated_cost_usd: 6100, estimated_hours: 9 }
Entity curation
POST /v1/admin/entities/merge Body: { entity_ids: [a, b], reason, approved_by }
POST /v1/admin/entities/split Body: { entity_id, partitions: [...], reason }
GET /v1/admin/resolution/review-queue?min_confidence=0.4&max_confidence=0.7
Evaluation and feedback
POST /v1/feedback Body: { trace_id, rating, issue: "WRONG_ENTITY"|"MISSING_SOURCE"|"UNSUPPORTED_CLAIM" }
POST /v1/admin/eval/run Body: { suite: "multi_hop_v3", config_candidate: {...} }
GET /v1/admin/eval/{run_id} → { metrics_by_question_type, regressions, cost_per_query }
4. High-Level Design
4.1 Why Hybrid: Each Representation Answers Different Questions
Components needed:
The design starts from an honest comparison of what each structure is good at, because building a knowledge graph is expensive and only justified where chunk retrieval alone fails.
Vector and keyword retrieval over chunks excels at questions whose answers live inside one or two passages, at finding exact identifiers (error codes, product names), and at paraphrase matching. It is cheap, robust to extraction errors, and always returns verbatim evidence. It struggles when the answer requires combining facts spread across many documents, because top-k similarity returns passages that look like the question rather than passages that connect to each other.
The knowledge graph excels at relational questions ("which X relate to Y through Z"), at aggregating facts about one entity from many documents, at disambiguation ("which Atlas?"), and at multi-hop reasoning where no document states the whole chain. It struggles with nuance and context (an extracted edge loses the qualifications in the surrounding text), is only as good as its extraction and entity resolution, and costs significantly more to build and maintain.
Community summaries over the graph make global questions answerable: instead of retrieving a handful of passages, the system retrieves pre-computed summaries of clusters of related entities and documents and synthesises across them.
The design principle: the graph is an index and a navigator, the chunks are the evidence. The graph helps find which chunks to read and how they relate, but final answers are grounded in the chunk text. Every graph edge points back to its evidence chunks, so a wrong extraction can be caught at generation time instead of silently becoming a wrong answer.
4.2 Ingestion Pipeline: From Raw Documents to Chunks, Entities, and Edges
Components needed:
Source connectors and normalisation. Connectors pull from each source with change tracking (modified time, version, ACLs) and emit normalised documents (text, structure, metadata). Parsing preserves structure (headings, tables, code blocks, lists), because structural context improves both chunking and extraction.
Structure-aware chunking. Chunks follow semantic boundaries (sections, paragraphs, table rows) with modest overlap, and each chunk carries its document ID, section path, position, ACL, version, and a content hash. The content hash drives all downstream caching and change detection.
Chunk-level indexing path (fast path). Chunks are embedded and written to the vector index and the keyword index within minutes. This path does not depend on graph extraction, so new content is searchable immediately even though the graph catches up later.
Graph-extraction path (slower, tiered). Extraction cost and quality are controlled through tiers:
Tier 0: filtering. Skip boilerplate (headers, footers, legal disclaimers), exact and near-duplicate chunks (cluster by content hash and MinHash), and chunks with too little informational content. Skipped chunks still exist in the vector and keyword indexes.
Tier 1: cheap structured extraction. Records from structured sources (CRM rows, ticket fields, org charts, service catalogs) map to entities and edges deterministically through mapping rules, with no LLM involved. These are the highest-precision edges and form the graph's backbone.
Tier 2: NER plus lightweight relation extraction. A fast model recognises entity mentions and typed spans, and a classifier proposes relations between mention pairs within a chunk. This covers the bulk of text.
Tier 3: LLM extraction for high-value content. Chunks in priority sources (contracts, incident reports, architecture documents), chunks where the cheaper tiers produced conflicting or low-confidence results, and chunks that mention many entities go to an LLM with a schema-guided prompt.
Schema-guided LLM extraction. The prompt provides the ontology (entity types, relation types, allowed subject-object type pairs, attribute lists), several few-shot examples, and the instruction to output strictly structured JSON, with each item tied to a quoted evidence span. Constraining to the ontology and requiring evidence quotes sharply reduces hallucinated relations. Several chunks are batched per call to amortise the prompt overhead, with chunk IDs attached to each extracted item.
Extraction validation. Each extracted item is checked: the evidence span must actually appear in the chunk (string or fuzzy match), the entity and relation types must satisfy the ontology constraints, and attribute values must pass type checks. Items failing validation are dropped, and items passing get a confidence score combining extractor confidence, validation signals, and agreement across tiers or across multiple chunks.
Extraction caching and versioning. Output is keyed by (chunk_content_hash, extractor_version, ontology_version). Unchanged chunks never re-extract, and a prompt change re-extracts only the targeted scope (section 6.2).
Entity resolution (section 4.3) then merges mentions into canonical entities, and graph writes are idempotent upserts keyed by canonical entity ID and by edge identity (subject, relation, object, valid_from), with evidence links appended.
Community detection and summarisation (batch). Periodically, the system runs community detection (Leiden or a similar algorithm) over the graph to produce hierarchical clusters, then generates a summary report per community (key entities, relationships, notable events, with citations to source chunks). Community summaries are embedded and indexed for global queries (section 4.5).
4.3 Entity Resolution: The Make-or-Break Component
Components needed:
Resolution turns 3B raw mentions into ~50M canonical entities. Errors here directly corrupt answers.
Normalisation and blocking. Mentions are normalised (case, punctuation, legal suffixes, common abbreviations) and assigned to blocks using several keys: normalised name tokens, phonetic keys, embedding nearest neighbours, and shared strong identifiers (email, domain, employee ID, repository slug). Comparison happens only within blocks, which reduces the candidate pair count from ~10¹⁵ to hundreds of millions.
Pairwise scoring. A model combines features: name similarity, type compatibility, shared attributes, co-occurring entities and relations, source-system identity (the same CRM record ID is a hard match), and embedding similarity of the mention contexts. The output is a match probability.
Decision zones.
High-confidence matches (above a high threshold) merge automatically.
High-confidence non-matches stay separate.
The ambiguous middle goes to a review queue (human curators or an LLM adjudicator with evidence), with the system continuing to treat the candidates as separate but linked ("possibly same as") until resolved.
Hard constraints override statistics. Different strong identifiers cannot merge. Two "Acme" entities with different tax IDs or CRM account numbers remain distinct however similar their names are. Type conflicts (a person versus a product) block merges. These rules prevent the most damaging over-merges.
Merges are reversible. Every merge records the original entities, evidence, and decision basis, so a wrong merge can be split later without data loss. The graph keeps MERGED_FROM lineage.
Context-aware linking at query time. The same machinery, in lighter form, links the entity mentions in a user's question to canonical entities (section 4.4), using the question's other terms, the user's context (team, recent activity), and popularity priors to disambiguate "Atlas".
4.4 Query Understanding and Routing
Components needed:
Query analysis. A fast model or classifier performs: intent classification, entity mention detection, time-range extraction, and question-type prediction (lookup, entity-centric, multi-hop, global, or comparison).
Entity linking. Detected mentions are resolved to candidate canonical entities by alias index plus embedding search, scored with context, with ambiguity preserved when the question doesn't disambiguate. Low-confidence links lead to either a clarifying question or a retrieval that covers the top candidates.
Routing policy. The router selects among strategies:
Vector/keyword only for simple lookup questions with no resolved entities, which is the cheapest and fastest path and the default when graph signals are weak.
Graph-assisted retrieval when linked entities exist: retrieve chunks via entity evidence links and neighbourhood expansion in addition to vector search.
Multi-hop traversal when the question contains relational structure ("customers affected by vendors with incidents"), executed as a bounded path search or a query plan over typed edges.
Global mode for thematic questions ("main causes across the platform"), retrieving community summaries at an appropriate hierarchy level and synthesising over them (map-reduce style when many summaries are relevant).
Hybrid combinations when signals are mixed, with results merged by fusion.
Safe fallback. If graph retrieval returns nothing useful (entities unresolved, no paths), the system falls back to vector and keyword retrieval so the graph never makes things worse than plain RAG. The router's choice and confidence are logged for evaluation.
4.5 Hybrid Retrieval, Graph Expansion, and Fusion
Components needed:
Seed retrieval (parallel). Three retrievers run concurrently:
Dense vector search over chunk embeddings (ANN with metadata and ACL filters).
Keyword/BM25 search for exact terms and identifiers.
Entity search over entity names, aliases, and entity-description embeddings, which yields seed entities.
Graph expansion from seeds. Starting from linked and retrieved entities (and the entities mentioned in top chunks), the system expands through the graph with strict controls:
Depth limits per question type (1-2 hops for entity-centric questions, up to 3-4 for multi-hop).
Relation-type filters guided by the question (traverse
OWNS,DEPENDS_ON,HAD_INCIDENT, notMENTIONED_WITH).Degree-aware expansion: super-nodes are expanded only through typed, filtered, score-ranked edges, never exhaustively (section 6.3).
A fan-out budget per hop and a global time budget, with early termination when enough evidence is gathered.
Path and subgraph scoring. Candidate paths and entities are scored by edge confidence, evidence count, recency, relation-type relevance to the question, semantic similarity between the path's evidence and the question, and path length (shorter paths preferred). The top paths and their evidence chunks become candidate context.
Evidence gathering. For each selected edge or entity attribute, the system fetches its supporting chunks (the provenance links), choosing the best-supported evidence. This is what turns graph results into citeable text.
Fusion. Chunk candidates from vector, keyword, and graph-derived evidence are merged with a rank-fusion method (reciprocal rank fusion with weights tuned per question type), deduplicated, and passed to a cross-encoder reranker that scores (question, chunk) pairs, producing the final ordered set. A diversity step prevents the context from being filled with near-duplicate passages, and ensures each hop of a multi-hop path has at least one supporting chunk.
Global retrieval path. For global questions the retrieval target is community summaries: vector search over summary embeddings at the appropriate level of the hierarchy, then (when many summaries qualify) a map-reduce synthesis, where each relevant summary yields partial findings that a final step merges, with citations propagated back to the underlying chunks.
4.6 Context Assembly, Generation, and Verification
Components needed:
Structured context assembly. The prompt contains: the question, a compact set of graph facts (entity, relation, entity, with evidence IDs), the ranked chunks with IDs and source metadata, and conflict flags where relevant. Graph facts give the model the connective structure, and chunks give it the verbatim support. The assembler respects the token budget by prioritising by score and ensuring coverage across hops.
Citation-bound generation. The model is instructed to answer only from supplied context, cite chunk IDs for every claim, state explicitly when the evidence is insufficient, and describe disagreement where conflicts exist. For multi-hop answers, it is asked to present the reasoning chain with each step cited, which also makes errors visible to users.
Post-generation verification. Each cited claim is checked against its cited chunk (entailment check), and graph-derived claims are checked against their evidence spans. Claims lacking support are removed or flagged, and the answer carries a confidence indicator. This closes the loop on extraction errors, since a hallucinated edge fails verification because its evidence chunk doesn't actually say it.
Answer composition for conflicts and time. When retrieved facts conflict (two sources give different owners), the answer presents both with sources and dates instead of choosing one, unless a principled rule applies (newer authoritative source supersedes). Time-scoped questions use temporal filtering on edges and attributes.
Graceful degradation. If the graph service is slow or down, the system answers from vector and keyword retrieval with a flag indicating reduced capability. If generation is slow, retrieval results can still be returned.
4.7 Access Control Across Chunks, Entities, Edges, and Summaries
Components needed:
Access control is harder with a graph, because derived structures can leak what the underlying documents protect.
ACLs propagate from documents to every derived artefact.
Chunks inherit their document's ACL.
Edges and attribute values carry the ACLs of their evidence chunks. A fact visible to a user must have at least one evidence chunk the user can read. Edge visibility is computed as "exists evidence the user may read", never as a property of the edge alone.
Entities are visible if any of their facts or mentions is visible, but their attributes and relations are shown only per the above rule, so an entity's existence does not reveal restricted facts.
Community summaries are the hardest case, since one summary blends many documents. They are built per ACL scope (for example, a public tier, and per-team or per-sensitivity tiers), or built only from content with a common access class, and surfaced to a user only if the user's access covers every source used. Blending restricted and unrestricted content into one summary is forbidden.
Query-time enforcement. Retrieval filters (vector, keyword, graph) include the user's group memberships. Graph traversal uses ACL-aware edge access so that traversal cannot step through a restricted edge, and aggregated counts and "related entities" lists are computed only from visible edges, to avoid side-channel leaks through numbers or existence.
Efficiency through ACL classes. Rather than per-document ACL lists, documents map to a bounded set of ACL classes (distinct permission sets), and filters operate on class IDs, which keeps filtered ANN search and graph traversal fast. Edge evidence references class IDs, so edge visibility checks are set lookups.
Permission changes and deletions. When a document's ACL changes or it is deleted, the change propagates to chunks, to evidence links (and therefore to edge visibility), and to summaries built from it, which are marked stale and rebuilt. Facts with no remaining evidence are removed from the graph (section 4.8).
4.8 Incremental Updates, Deletion, Time, and Conflicts
Components needed:
Change propagation. A document change event flows through the pipeline: new chunk hashes are diffed against the old ones, unchanged chunks keep their extraction, changed chunks are re-embedded and re-extracted, and removed chunks lose their evidence links. Edge and attribute evidence counts are decremented, and an edge disappears only when its last evidence link is removed.
Deletion with provenance makes cleanup tractable. Because every fact links to its evidence, "delete document D" has a precise graph consequence: remove D's chunks, remove evidence links to those chunks, delete facts that now have zero evidence, mark affected community summaries stale, and invalidate caches. This also supports compliance deletions with an auditable trail.
Temporal modelling. Edges and attributes carry valid_from and valid_to (derived from document dates, explicit dates in text, or source-system effective dates) alongside the observed_at time. An update such as "the new owner is X" closes the previous interval rather than overwriting it, so questions about past states remain answerable, and "current" queries filter to open intervals.
Conflict handling. When two sources assert incompatible values for the same functional attribute (single-valued relations such as HAS_OWNER), the graph stores both with provenance and a CONFLICTED marker. A resolution policy ranks sources by authority (a system-of-record beats a chat message), recency, and confidence. If the policy yields a clear winner, it is marked preferred while the alternatives remain visible, otherwise the conflict is surfaced to users and the curation queue.
Reprocessing and backfills. Ontology or extractor changes trigger a selective re-extraction of affected scopes, previewed with a cost estimate, run in priority order, and written under a new extractor version so old and new facts can be compared before the old version is retired.
Community summaries refresh incrementally. Changes mark the affected communities stale. A scheduler rebuilds them in batches, prioritising communities that are frequently queried or heavily changed. Full re-clustering runs on a slower cadence, since cluster membership changes slowly relative to document changes.
4.9 Evaluation, Feedback, and Continuous Improvement
Components needed:
A layered evaluation suite. Separate datasets per question type: lookup, entity-centric, multi-hop, global, and "no answer" questions. Metrics include retrieval recall of gold evidence, answer correctness, citation precision (do citations support claims), faithfulness, and latency and cost per query. Reporting by question type is essential because an improvement for multi-hop questions may hide a regression on simple lookups.
Component-level evaluation. Extraction precision and recall on a labelled sample (entities, relations, attributes), entity-resolution precision (wrong merges) and recall (missed merges), entity-linking accuracy on queries, and graph-path correctness. These localise failures: a wrong answer might be an extraction error, a resolution error, a retrieval miss, or a generation error, and each needs different fixes.
A/B testing and shadow runs. Routing policy changes, fusion weights, and extraction versions are tested on sampled traffic or offline against recorded queries before promotion, using the evaluation suite and live feedback signals.
Feedback loop. User feedback (wrong entity, unsupported claim, missing source) is categorised and routed: entity errors to the curation queue, missing evidence to retrieval analysis, and unsupported claims to verification tuning. Frequent query failures seed new evaluation cases.
Quality monitors in production. Dashboards track graph health (orphan entities, giant merged components, rising conflict rates), extraction error rates by source, entity-resolution review backlog, router distribution, fallback rates, and the share of answers where graph evidence was used and verified.
4.10 Putting It All Together
The system maintains two coupled indexes over one corpus: a chunk index (vector and keyword) for fast, robust text retrieval, and a provenance-linked knowledge graph with community summaries for relational and global reasoning. Ingestion runs a fast path that makes chunks searchable within minutes and a tiered extraction path that builds the graph at controlled cost, with deterministic mapping for structured sources, cheap models for bulk text, and LLM extraction for high-value content. Entity resolution with blocking, hard constraints, and reversible merges keeps the graph coherent. At query time, a router chooses among vector, graph-assisted, multi-hop, and global strategies, retrieval runs in parallel across vector, keyword, and entity indexes, bounded and degree-aware graph expansion discovers connected evidence, and fusion plus reranking produces a compact context where every graph fact is backed by source chunks. Generation is citation-bound and verified, ACLs propagate through every derived structure, updates and deletions flow through evidence links, and a layered evaluation harness shows, per question type, whether the graph actually earns its cost.
5. Database Design
5.1 SQL vs NoSQL
Vector store (Qdrant, Milvus, or pgvector at smaller scale) for chunk embeddings, entity-description embeddings, and community-summary embeddings, in separate collections. It must support quantisation, ACL-class filtering, and metadata filters (source, time, type) with filtered ANN search at ~600 queries/sec.
Search engine (OpenSearch/Elasticsearch) for BM25 over chunk text, entity names and aliases (including fuzzy and phonetic matching), and exact-identifier lookup. It also holds the alias index used by entity linking.
Graph database (Neo4j, JanusGraph on a distributed backend, or a graph layer over a sharded relational store) for entities and edges, with traversal queries bounded by depth and fan-out. Given 50M nodes and several hundred million edges, a sharded graph store or a relational adjacency model with proper indexes both work. The decisive requirements are efficient typed-edge traversal, edge-property filtering (confidence, validity, ACL class), and the ability to bound traversal cost. Heavy analytics (community detection, graph statistics) run in a batch engine over an export of the graph (Spark/GraphFrames or a graph-processing system) rather than on the serving store.
PostgreSQL for the source of truth on documents, chunks (metadata), extraction results, evidence links, entity resolution decisions and lineage, ontology versions, curation queues, and evaluation data. These are relational, transactional, and need constraints, versioning, and auditability.
Kafka for ingestion events, change propagation, extraction job queues, and cache invalidations, partitioned by document ID so a document's changes are processed in order.
Object storage for raw documents, parsed content, extraction outputs (cached by
chunk_hash), community-summary artefacts, and graph snapshots for batch analytics.Redis for hot caches: alias-to-entity lookups, entity neighbourhood caches for popular entities, query-result caches with ACL-aware keys, and per-request budgets.
Why the split matters: the graph database serves traversal, the vector store serves similarity, the search engine serves lexical matching, and PostgreSQL remains the authoritative, auditable record of what was extracted and why. If a serving index is corrupted or the extractor changes, it can be rebuilt from PostgreSQL and object storage without re-running every LLM call.
5.2 Database Schema
-- PostgreSQL (authoritative metadata, provenance, resolution, ontology)
documents (
doc_id TEXT PRIMARY KEY, source TEXT NOT NULL, uri TEXT, title TEXT,
version BIGINT NOT NULL, content_hash TEXT NOT NULL,
acl_class_id INT NOT NULL, -- bounded set of distinct permission sets
modified_at TIMESTAMPTZ, ingested_at TIMESTAMPTZ DEFAULT now(),
status TEXT DEFAULT 'ACTIVE' -- ACTIVE|DELETED
);
acl_classes (
acl_class_id INT PRIMARY KEY, principals TEXT[] NOT NULL, sensitivity TEXT
);
chunks (
chunk_id TEXT PRIMARY KEY, doc_id TEXT REFERENCES documents(doc_id),
seq INT NOT NULL, section_path TEXT, token_count INT,
content_hash TEXT NOT NULL, -- drives extraction caching
acl_class_id INT NOT NULL, text_ref TEXT, -- pointer to object storage
extraction_tier INT, -- 0 skipped, 1 deterministic, 2 NER, 3 LLM
indexed_at TIMESTAMPTZ
);
CREATE INDEX idx_chunks_doc ON chunks (doc_id, seq);
CREATE INDEX idx_chunks_hash ON chunks (content_hash);
ontology_versions (
ontology_version INT PRIMARY KEY, entity_types JSONB, relation_types JSONB,
constraints JSONB, created_at TIMESTAMPTZ DEFAULT now(), status TEXT
);
extraction_runs (
run_id UUID PRIMARY KEY, extractor_version TEXT NOT NULL, ontology_version INT,
scope JSONB, status TEXT, chunks_processed BIGINT, est_cost_usd NUMERIC(12,2), actual_cost_usd NUMERIC(12,2)
);
extracted_mentions (
mention_id UUID PRIMARY KEY, chunk_id TEXT REFERENCES chunks(chunk_id),
surface_form TEXT NOT NULL, entity_type TEXT, span_start INT, span_end INT,
extractor_version TEXT NOT NULL, confidence REAL, resolved_entity_id UUID
);
CREATE INDEX idx_mentions_chunk ON extracted_mentions (chunk_id);
CREATE INDEX idx_mentions_entity ON extracted_mentions (resolved_entity_id);
entities (
entity_id UUID PRIMARY KEY, canonical_name TEXT NOT NULL, entity_type TEXT NOT NULL,
strong_ids JSONB, -- email, domain, crm_id, repo slug (hard match keys)
aliases TEXT[], status TEXT DEFAULT 'ACTIVE', -- ACTIVE|MERGED|SPLIT
merged_into UUID, created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_entities_strong ON entities USING GIN (strong_ids);
entity_merge_log (
merge_id UUID PRIMARY KEY, surviving_entity UUID, merged_entities UUID[],
decision_basis JSONB, match_probability REAL, decided_by TEXT, -- MODEL|LLM|HUMAN
reversible_snapshot JSONB, decided_at TIMESTAMPTZ DEFAULT now()
);
edges (
edge_id UUID PRIMARY KEY,
subject_id UUID REFERENCES entities(entity_id), relation_type TEXT NOT NULL,
object_id UUID REFERENCES entities(entity_id),
confidence REAL, evidence_count INT DEFAULT 0,
valid_from TIMESTAMPTZ, valid_to TIMESTAMPTZ, observed_at TIMESTAMPTZ,
conflict_state TEXT DEFAULT 'NONE', -- NONE|CONFLICTED|SUPERSEDED
extractor_version TEXT, ontology_version INT,
UNIQUE (subject_id, relation_type, object_id, valid_from)
);
CREATE INDEX idx_edges_subject ON edges (subject_id, relation_type);
CREATE INDEX idx_edges_object ON edges (object_id, relation_type);
edge_evidence (
edge_id UUID REFERENCES edges(edge_id), chunk_id TEXT REFERENCES chunks(chunk_id),
evidence_span TEXT, extraction_confidence REAL, acl_class_id INT NOT NULL,
PRIMARY KEY (edge_id, chunk_id)
);
CREATE INDEX idx_evidence_chunk ON edge_evidence (chunk_id); -- supports deletion propagation
entity_attributes (
attr_id UUID PRIMARY KEY, entity_id UUID REFERENCES entities(entity_id),
key TEXT NOT NULL, value JSONB NOT NULL, valid_from TIMESTAMPTZ, valid_to TIMESTAMPTZ,
confidence REAL, conflict_state TEXT DEFAULT 'NONE'
);
attribute_evidence (
attr_id UUID REFERENCES entity_attributes(attr_id), chunk_id TEXT REFERENCES chunks(chunk_id),
acl_class_id INT NOT NULL, PRIMARY KEY (attr_id, chunk_id)
);
communities (
community_id UUID PRIMARY KEY, level INT NOT NULL, parent_id UUID,
acl_scope INT NOT NULL, -- summaries are built per permission scope
member_count INT, summary_ref TEXT, summary_version INT,
stale BOOLEAN DEFAULT false, built_at TIMESTAMPTZ
);
community_members (community_id UUID, entity_id UUID, PRIMARY KEY (community_id, entity_id));
query_traces (
trace_id UUID PRIMARY KEY, user_id TEXT, question_hash TEXT, strategy TEXT[],
retrieved_chunk_ids TEXT[], used_edge_ids UUID[], router_confidence REAL,
latency_ms JSONB, cost_usd NUMERIC(8,5), created_at TIMESTAMPTZ DEFAULT now()
);
curation_queue (
item_id UUID PRIMARY KEY, item_type TEXT, -- MERGE_CANDIDATE|CONFLICT|LOW_CONF_EDGE
payload JSONB, priority INT, status TEXT DEFAULT 'OPEN', assigned_to TEXT, created_at TIMESTAMPTZ DEFAULT now()
);
eval_cases (
case_id UUID PRIMARY KEY, question TEXT, question_type TEXT,
gold_answer TEXT, gold_evidence_chunk_ids TEXT[], gold_entity_ids UUID[]
);
Vector store point schema (chunk collection):
{ "id": "chunk_...", "vector": [ ... ],
"payload": { "doc_id": "...", "source": "wiki", "acl_class_id": 14, "doc_time": "2024-06-01",
"entity_ids": ["e1","e7"], "section_path": "Runbooks/Billing", "content_hash": "..." } }
Entity-description and community-summary collections follow the same pattern with entity_type, level, and acl_scope payload fields.
Graph store model (serving copy):
(:Entity {entity_id, name, type, aliases_ref})
(:Entity)-[:REL {type, confidence, evidence_count, valid_from, valid_to, acl_class_ids[], conflict}]->(:Entity)
Indexes: Entity(entity_id), Entity(type, name), REL(type), REL(valid_to)
Traversal always passes parameters: allowed_acl_classes, min_confidence, as_of_time, max_fanout
Redis key schema:
HASH alias:{normalized_alias} → [entity_id:prior_score, ...]
STRING nbr:{entity_id}:{acl_hash}:{rel} → cached neighbour list (TTL minutes)
STRING qcache:{acl_hash}:{question_hash} → cached answer + citations (TTL, invalidated on source change)
6. Design Deep Dive
6.1 Extraction Quality: Preventing a Graph Built on Hallucinated Edges
LLM extraction is the part most likely to silently degrade the system.
Failure modes. Typical errors: inventing a relation not stated in the text, attaching a relation to the wrong pair of entities in a sentence with several, losing negation or hedging ("is not the owner", "may be affected"), misreading roles, and inconsistent naming across chunks.
Evidence-bound extraction. Requiring each extracted item to include a verbatim evidence quote, then verifying the quote exists in the chunk and that both entity mentions fall in or near it, removes a large class of hallucinations mechanically, without another model call.
Ontology constraints as validation. A
HAS_OWNERedge must connect a Service to a Person or Team. Type-incompatible extractions are rejected. Cardinality constraints (single-valued attributes) trigger conflict handling instead of silent overwrite.Qualifier preservation. Edges carry modality and polarity (asserted, planned, negated, hypothetical) where the ontology supports it. Without this, "we plan to migrate to X" becomes
USES X. Where the ontology doesn't capture a qualifier, low confidence plus the always-present evidence chunk lets the generation step see the original wording.Confidence from agreement. An edge extracted independently from several chunks or from several tiers (a deterministic source and a text mention) is more trustworthy than a single-chunk, single-pass extraction. Confidence feeds both traversal scoring and display.
Calibrated sampling and measurement. A labelled sample per source type measures extraction precision and recall continuously. A sudden drop in a source's precision (a new document template, a format change) shows up in the dashboards, and extraction for that source can be throttled or routed to a different tier until fixed.
Why the final answer is robust anyway. Because generation is grounded in chunk text and verified against it (section 4.6), an extraction error generally results in a bad retrieval path that fails verification, not a confidently wrong answer. The graph's role is to find candidates, and the chunks remain the arbiter.
6.2 Controlling Extraction Cost and Making Re-Extraction Practical
At ~$35-70K per full pass, extraction cost shapes the architecture.
Tiering by value. Not every chunk deserves an LLM. Structured-source mappings are free and high precision. Bulk text goes through cheaper NER and relation models. LLM extraction is reserved for high-value sources, ambiguous cases, and content-dense chunks. Measuring marginal quality gain per tier (does the LLM add enough correct edges to justify its cost on this source type) guides the allocation.
Deduplication before extraction. Large corpora contain many duplicates and near-duplicates (forwarded emails, templated reports, repeated boilerplate). Content-hash and MinHash clustering extracts one representative and copies the results, with evidence links to every duplicate.
Prompt overhead reduction. Batching multiple chunks per call, using concise schemas, and caching the static prompt prefix (provider-side prompt caching) cut input token cost severalfold.
Caching by
(chunk_hash, extractor_version, ontology_version). Unchanged chunks never pay twice, and edits invalidate only affected chunks.Selective re-extraction. When an ontology change adds one relation type, running a targeted extraction (only chunks likely to mention it, found via keyword or classifier prefilter) is far cheaper than re-processing everything. A dry-run API (section 3) estimates cost before committing.
Incremental prompt migration. New extractor versions run in shadow on a sample, comparing precision and recall against the labelled set and against the current version, so a prompt change is adopted only when it demonstrably helps. Old and new facts coexist under versioned labels until the old version is retired.
Budgeting. Extraction spend has daily and per-source budgets with alerts, and the pipeline degrades by skipping lowest-value tiers (text stays searchable by vector and keyword) rather than failing ingestion when budgets are hit.
6.3 Graph Expansion Without Explosion: Super-Nodes, Fan-Out, and Relevance
Naive traversal is the most common way graph retrieval makes answers worse and slower.
The problem. Heavy-tailed degree distributions mean one hop from a popular entity ("Engineering", "Acme Corp", "Kubernetes") reaches millions of edges, and most are irrelevant. Two hops from such a node can reach the entire graph.
Degree-aware policy. Entities above a degree threshold are flagged as hubs. Expansion through a hub is allowed only along filtered relation types and ranked edges, with a small per-hub fan-out, and a path that passes through a hub is penalised unless the hub is itself an anchor from the question.
Relation-type guidance. The question's intent selects which edge types to follow. "Who is on call for billing" follows
ON_CALL_FORandOWNS, not genericRELATED_TOorMENTIONED_WITH. Generic co-occurrence edges are kept at low weight or excluded from default traversal.Beam search with scoring, not breadth-first enumeration. At each hop, the system keeps the top-K frontier nodes by a combined score of edge confidence, evidence count, semantic similarity of the edge evidence to the question, and recency. This bounds work to K × depth instead of degree^depth.
Time and budget caps. Each traversal has a wall-clock budget (for example 150-250 ms) and a node-visit budget. If the budget is hit, results so far are used, and the trace records the truncation so evaluation can see whether budgets are costing quality.
Precomputation for common patterns. Frequently used multi-hop patterns (service → dependencies → owners, customer → contracts → vendors) can be materialised as derived edges or cached neighbourhoods, refreshed incrementally, turning expensive traversals into lookups.
Query-time sub-graph extraction. For multi-hop questions, the router can build a typed query plan (match pattern over entity and relation types with filters) and execute it directly in the graph store, instead of heuristic expansion. This is more precise when the question's structure is clear, and the heuristic expansion remains the fallback when it is not.
Relevance versus completeness. For enumerative questions ("list all customers affected") bounded retrieval can't guarantee completeness. The system states when the result set was truncated, and for known high-value patterns it uses an exhaustive, paginated query path instead.
6.4 Entity Resolution at Scale: The Precision–Recall Balance and Its Consequences
Asymmetric costs. A wrong merge (two different customers fused) contaminates every fact attached to either entity and is visible in confidently wrong answers, a worse failure than a missed merge, which only splits knowledge. Thresholds therefore favour precision for automatic merges, with the ambiguous middle handled by "possibly same as" links and a review queue.
Strong-identifier rules dominate. Where sources provide identifiers (CRM account ID, domain, employee ID), they are authoritative. Entities from structured sources form anchor entities, and text mentions are linked to anchors, so the graph's backbone is clean before noisy text extraction attaches to it.
Candidate blocking quality. Blocks should be recall-oriented across several keys. Poor blocking misses true matches silently, so blocking recall is measured on labelled duplicates.
Context-based disambiguation. The same name resolves differently by surrounding context: "Atlas" near "Kubernetes cluster" versus "Atlas" near "customer contract". Co-occurring entities, document source, and team metadata supply features to the pairwise scorer and to query-time linking.
Transitivity and clustering hazards. Pairwise decisions can create giant clusters through chains of weak matches (A~B, B~C, C~D). Cluster-level checks (maximum cluster size, conflicting strong IDs within a cluster) catch these, and suspicious clusters are broken or queued for review.
Reversibility and lineage. Merges are logged with reversible snapshots, edge and attribute evidence stays attached to the original mention-level records, and splits re-assign evidence by mention. Curators fix errors without re-extraction.
Incremental resolution. New mentions are resolved against the existing entity index (fast alias and embedding lookup), and a periodic global re-clustering pass catches cross-batch matches that incremental resolution missed. Merges that change existing entity IDs emit events so caches and indexes follow.
Evaluation. Pairwise precision and recall, cluster purity, and downstream effect (multi-hop answer accuracy as a function of resolution quality) are tracked, since resolution quality is often the hidden determinant of graph usefulness.
6.5 Global Questions: Community Summaries and When They Are Worth It
Why top-k fails here. A question like "what themes recur across incident reports" has no small set of passages that answers it. Retrieval returns a handful of passages that happen to match the wording, producing a narrow, biased answer.
Hierarchical communities. Community detection partitions the graph into clusters at several granularities, from fine-grained (a service and its immediate dependencies) up to broad (an entire product area). A summary is generated for each community, describing its entities, relationships, events, and recurring themes, with citations to supporting chunks.
Query strategy by level. Broad questions retrieve higher-level summaries, and focused questions retrieve lower-level ones. For wide questions, a map-reduce step extracts partial findings from each relevant summary (parallel, cheap) and a reduce step merges them, with citation propagation so claims trace down to chunks.
Cost and staleness. Summaries cost LLM calls to build and go stale as content changes. Incremental refresh (rebuild only changed communities, prioritising by query frequency and churn), with staleness shown to the answer ("summary built on date X"), bounds cost. Full re-clustering happens on a slower cadence.
Access control. Because summaries blend documents, they are built per permission scope (section 4.7), which multiplies the build cost in organisations with many scopes. The design builds summaries for the large common scopes (company-wide, department-level) and, for narrow scopes, falls back to on-demand synthesis from visible chunks.
When to skip it. For corpora or tenants where global questions are rare, the community layer may not justify its cost. The router logs the share of global-type questions, and the layer can be enabled per tenant or corpus based on measured demand.
Alternatives for global questions. Pre-computed topic models, cluster-then-summarise over chunk embeddings (no graph), or LLM map-reduce over a sampled subset are cheaper alternatives. The evaluation harness compares them on the global-question set so the choice is evidence-based.
6.6 Security Through the Graph: Leakage Paths and How They Are Closed
Edge visibility derives from evidence. An edge is visible to a user only if at least one of its evidence chunks is readable by the user. Storing ACL class IDs on the evidence link makes this a set intersection performed inside the traversal, so restricted edges are never walked or counted.
Aggregate and existence leaks. Counts ("12 related incidents"), degree numbers, entity "related" lists, and even the existence of an entity can leak restricted information. Aggregates are computed only over visible edges, entities with no visible evidence are hidden entirely, and error messages and "no results" responses avoid distinguishing "doesn't exist" from "exists but not permitted" where that distinction would leak.
Summaries and derived text. As described in section 4.7, summaries are scoped, and entity descriptions generated from multiple documents are generated per scope or from public content only. Cached LLM outputs (summaries, entity descriptions, query-result caches) carry ACL scope in their keys and are invalidated when source permissions change.
Multi-hop inference leaks. Even when each fact is individually permitted, combining facts can reveal something sensitive (for example inferring a confidential acquisition from two benign facts). This is a policy question rather than a pure technical one. The system offers sensitivity labels on entity and relation types, and a policy can mark certain relation types or entity classes as excluded from multi-hop expansion for particular audiences.
Permission changes propagate. ACL updates flow to chunks, evidence links, caches, and summaries. Until propagation completes, the system fails closed for the changed documents (treat as restricted) rather than serving stale permissive access.
Deletion and right-to-be-forgotten. Provenance links make deletion tractable: removing a document removes its chunks, evidence links, now-unsupported facts, and marks summaries stale. Backups and derived caches are covered by retention and purge processes. The extraction cache keyed by chunk hash is purged for deleted content.
Audit. Query traces record which chunks and edges were used, and the access decision basis, supporting later review of whether a response was properly permitted.
6.7 Fusion, Routing, and Proving the Graph Earns Its Cost
Routing errors are costly in both directions. Sending a simple lookup through graph expansion wastes latency and may add irrelevant context, and sending a multi-hop question through plain vector search produces plausible but incomplete answers. The router's confidence is calibrated, low-confidence routing runs both strategies and fuses, and the decision is logged for evaluation.
Fusion weights are per question type. Reciprocal rank fusion with tuned weights between vector, keyword, and graph-derived candidates. Identifier-heavy queries weight keyword retrieval, relational queries weight graph evidence, and conceptual queries weight dense retrieval. Weights are tuned on the evaluation set and re-validated when the extractor or embedding model changes.
Reranking is the great equaliser. A cross-encoder reranker over the merged candidate pool means individual retrievers need good recall rather than perfect ranking, and it lets graph-derived chunks compete on relevance to the question instead of being included on structural grounds alone.
Ablation as a standing practice. The evaluation suite runs vector-only, vector+keyword, +graph-assisted, +multi-hop, and +global configurations on every question type, so the contribution (and cost) of each component is measured, not assumed. If the graph adds little for a tenant's workload, the system can reduce graph usage there and save cost.
Latency budgeting. The routing, retrieval, expansion, and reranking stages have individual time budgets (for example router 50 ms, parallel seed retrieval 150 ms, expansion 200 ms, rerank 150 ms), with graceful truncation, so the p95 target holds even when graph traversal is slow. Stages that exceed budgets are skipped and the answer notes reduced coverage.
Cost per query as a first-class metric. Router calls, rerank compute, and generation tokens are tracked per strategy, so the cost of multi-hop and global answers (more context, more calls) is visible against their quality gains, and policies like "use multi-hop only when router confidence is high" can be tuned.
Failure analysis taxonomy. Every failed evaluation case is attributed to a stage: extraction miss, resolution error, linking error, retrieval miss, ranking error, context-assembly truncation, or generation error. This is what turns evaluation from a score into an engineering roadmap.
Summary
The graph is an index and navigator, the chunks are the evidence. Chunk retrieval handles lookups robustly, the graph handles relational, entity-centric, and multi-hop questions, and community summaries handle global questions. Every graph fact links back to evidence chunks, so extraction errors fail verification instead of becoming confident wrong answers.
Ingestion has a fast path and a tiered slow path. Chunks are embedded and indexed within minutes, while graph extraction is tiered: deterministic mapping for structured sources, cheap NER and relation models for bulk text, and schema-guided LLM extraction for high-value or ambiguous content, with deduplication and batching to control the ~$35-70K full-pass cost.
Extraction is evidence-bound and validated. Verbatim evidence quotes, ontology type and cardinality constraints, qualifier handling, and agreement-based confidence remove most hallucinated edges mechanically, and results are cached by chunk hash and extractor version so re-extraction is selective and previewed with a cost estimate.
Entity resolution is the make-or-break component. Blocking, strong-identifier hard constraints, precision-favouring thresholds, review queues for the ambiguous middle, cluster-level sanity checks, and reversible, lineage-tracked merges keep the graph coherent, since wrong merges corrupt answers and missed merges break multi-hop paths.
A router selects among vector, graph-assisted, multi-hop, and global strategies, with safe fallback to plain hybrid retrieval and calibrated confidence, so the graph can only help and never degrade basic lookups.
Graph expansion is bounded and degree-aware. Beam search with scored frontiers, relation-type guidance, hub handling, time and node budgets, precomputed common patterns, and typed query plans prevent super-node explosion, and truncation is reported rather than hidden.
Retrieval fuses vector, keyword, and entity-graph evidence with per-question-type weights and a cross-encoder reranker, and generation is citation-bound with post-hoc claim verification, conflict disclosure, and graceful degradation when the graph is unavailable.
Access control propagates through every derived structure. Edge visibility derives from readable evidence, aggregates and existence are computed only over visible data, summaries are built per permission scope, ACL changes fail closed until propagated, and sensitivity policies can restrict multi-hop inference.
Updates, deletions, time, and conflicts are first-class. Evidence-count-based fact lifecycle makes deletion precise, validity intervals preserve history, conflicting facts are stored with provenance and resolved by authority and recency or surfaced, and community summaries refresh incrementally by staleness and query priority.
Evaluation proves the graph's value. Per-question-type suites, component-level extraction, resolution, and linking metrics, ablations of each retrieval layer, cost-per-query tracking, and a stage-attributed failure taxonomy show where the graph helps, where it only adds cost, and what to fix next.


