AACsearch
GraphRAG

GraphRAG Overview

Graph-aware retrieval that adds entity and relation reasoning on top of Knowledge RAG. When to reach for it and how it differs from plain RAG.

GraphRAG is a retrieval mode for the Knowledge module that augments vector search with a knowledge graph built from the same chunks. Where plain RAG retrieves the k chunks closest to a query, GraphRAG additionally traverses entity-relation paths — letting it answer questions whose evidence is scattered across multiple documents.

It is not a separate product. GraphRAG runs over the same KnowledgeChunk table as Knowledge RAG, with two extra tables — GraphNode and GraphEdge — populated by an LLM pass at ingest time.

Status

CapabilityStatus
LLM-based entity resolution at ingest (resolveEntitiesFromChunks)✅ Available
LLM-based relation typing (extractRelationsFromChunks)✅ Available
Confidence-weighted edges with evidence chunk reference✅ Available
Community detection (Louvain) for clustering✅ Available
graphragExplain — answer with traversal path✅ Available
Mixed retrieval (vector + graph hop) in ask🟡 Beta — includeGraph: true option
Per-space graph rebuild without re-embedding⏳ Roadmap
Multi-hop reasoning beyond 2 hops⏳ Roadmap
Visual graph explorer in the dashboard⏳ Roadmap

When includeGraph is off, the ask endpoint behaves exactly like Knowledge RAG. When it's on, the system blends vector retrieval with a graph hop and concatenates the evidence.

When to use GraphRAG vs plain RAG

Question shapePick
"What is the password reset policy?"Plain RAG — single document, single chunk.
"List all integrations our SDK supports."Plain RAG — usually one canonical doc.
"How does our auth system handle SAML, and which teams own it?"GraphRAG — joins auth docs with team-roster docs.
"Which customer cases used the wallet API, and what payment provider were they on?"GraphRAG — multi-doc, multi-entity.
"Summarise everything about our PrestaShop connector."Plain RAG with topK: 10.
"What changed between v1 and v2 of the documents API?"GraphRAG with the changelog source.

The rule of thumb: if the answer requires joining facts from different documents, GraphRAG is worth the extra ingest cost. If the answer is already in one document, plain RAG is faster, cheaper, and just as accurate.

How it's built at ingest

For every IngestionJob, after chunking and embedding, buildGraphFromChunks (in packages/api/modules/knowledge/lib/graphrag.ts) does the following:

  1. Entity resolution. An LLM pass over each chunk extracts named entities with a canonical name and a semantic type ("Product", "Person", "API", "Concept", …). Implementation: resolveEntitiesFromChunks in entity-resolution.ts.
  2. Filter. Chunks with fewer than 2 entities are dropped — they cannot form a relation.
  3. Relation typing. A second LLM pass over the same chunks, given the resolved entities, extracts directed relations between them with a confidence score. Implementation: extractRelationsFromChunks in relation-typing.ts.
  4. Graph write. For each entity, upsertGraphNode either reuses an existing GraphNode (matched by (spaceId, canonicalName, nodeType)) or creates a new one. For each relation, createGraphEdge writes a new edge keyed by (fromNodeId, toNodeId, relationType) with the chunk id as evidenceChunkId so the answer can cite the originating passage.

The graph is append-only at ingest time — re-ingesting the same source adds new evidence edges but does not remove old ones. Per-source rebuild without re-embedding is on the roadmap.

Community detection

After enough edges accumulate, the Louvain algorithm clusters dense neighbourhoods into communities (listCommunities, getCommunity). Each community is a soft cluster of related concepts — useful for "what topics are in this space" overviews and for narrowing GraphRAG retrieval to a subgraph.

Communities are recomputed by the scheduled graphragCommunities job. See community-detection.ts for the Louvain implementation and the LLM-based naming pass that labels each cluster.

How a GraphRAG query runs

const result = await orpc.knowledge.graphragExplain.call({
  spaceId: "ks_…",
  question: "How does the wallet ledger handle promo balances?",
});

The query path:

  1. Anchor extraction. The question is embedded and the top-k chunks identified. The entities in those chunks become the anchor nodes.
  2. Graph hop. From each anchor, the system walks outgoing and incoming edges with relationType matching the question intent (typed by the LLM). The hop is currently 1 hop by default; 2 hops is opt-in.
  3. Evidence gather. For each visited edge, evidenceChunkId resolves back to the originating chunk text.
  4. Answer synthesis. Anchor chunks + traversed chunks are concatenated and passed to the LLM. The answer cites the originating documents.
  5. Path output. The response includes a path — the ordered list of (fromNode, relation, toNode) tuples — so the UI can render a "why this answer" explanation.

The response shape is { answer, sources, chunks, path: GraphPath[] }. path is what differentiates graphragExplain from ask.

When GraphRAG is the wrong tool

  • Tiny spaces. Fewer than ~20 documents — there aren't enough relations for the graph to add signal over plain retrieval.
  • Highly structured content. If every document is a self-contained policy or product card, the graph adds little.
  • Adversarial / noisy text. The LLM entity-resolution pass picks up junk entities from machine-translated text or low-quality OCR. Plain RAG is more robust.
  • Cost-sensitive workflows. The ingest-time graph build runs two extra LLM passes per chunk — meaningful at scale. Budget accordingly.

Cost shape at ingest

Per chunk, ingest cost is roughly:

embedding cost (always) + entity-resolution cost + relation-typing cost
   1×                       1×                       0.5–1× (only for chunks with ≥ 2 entities)

So a GraphRAG-enabled ingest is 2–3× the cost of a plain Knowledge ingest. Reserve credits accordingly; see CREDIT_RATES.knowledge_graph_build in entitlements/credit-rates.ts.

If cost is a concern, enable GraphRAG only on the spaces where multi-document questions actually arrive. A per-space flag (KnowledgeSpace.ragConfig.graphragEnabled) controls this; off by default.

Tenant isolation

The graph is namespaced by knowledgeSpaceId. GraphNode.canonicalName is unique within a space; "OpenAI" in space A and "OpenAI" in space B are distinct rows. Cross-space traversal is not allowed (Invariant 5).

On this page