Skip to main content

Knowledge Graph Search

Extract an entity and relationship graph from your documents, so agents can answer questions whose supporting facts are spread across several of them.

Why Use a Knowledge Graph

Vector search retrieves the passages that look most like the question. That works when one passage holds the answer, and fails when the answer is assembled from several.

Consider a knowledge base of company filings. "Which drugs does Aldous Corporation sell?" has its answer split in two: one document says Aldous acquired Bfarma Labs, another says Bfarma Labs makes Zalofen. The second document never mentions Aldous, so nothing about it looks like the question. In a corpus of any size it loses the top-k race to documents that merely repeat the question's wording.

A knowledge graph links the passages through the entities they share:

Aldous Corporation --[acquired]--> Bfarma Labs --[manufactures]--> Zalofen

Graph search starts from the entities your question mentions, walks those relationships, and returns the source passages behind every entity it reaches. The document is retrieved because it is connected to the question, not because it resembles it.

Overview

Turning the graph on changes two things:

  1. At ingest. Each chunk goes through an extra LLM pass that extracts entities (people, organizations, products, locations) and the relationships between them. Entities that appear in several documents are merged, so the graph spans the whole knowledge base rather than a single file.
  2. At search. A new retrieval mode, searchMode: 'graph', seeds entities from your query with vector search, expands their relationships, maps the reached entities back to their source chunks, and fuses those results with a standard hybrid search. Nothing is lost relative to hybrid search: the graph channel adds passages, it does not replace them.

On top of the entity graph, Squid builds a concept layer: document nodes, themes (topics) clustered from the entity graph, and facet trees derived from your context metadata. That layer powers the queryGraph() operations, which let an agent ask structural questions ("what is in this knowledge base", "which documents sit under this topic", "how are these two things connected") instead of only retrieving passages.

When to use it

SituationRecommendation
Answers span several documents linked by shared entities✅ Knowledge graph
"How is A connected to B", "what is involved with X"✅ Knowledge graph
Agent needs to describe or navigate the corpus, not just quote it✅ Knowledge graph, via queryGraph()
Exact tokens: error codes, SKUs, file namesKeyword search
One passage answers the questionDefault hybrid search is enough

Requirements

  • The knowledge base must be created with vectorDbType: 'mongoAtlas'. The vector store is immutable after creation, so a knowledge base on the default 'postgres' store cannot gain a graph later. See Creating a Knowledge Base.
  • graphRag itself is mutable, so you can turn the graph on and off on an existing Atlas knowledge base at any time.
  • Graph extraction runs an LLM over every chunk at ingest, which costs tokens on top of embedding. Budget for it before enabling the graph on a large corpus, and track the actual spend with getGraphStatus().
  • The graph APIs are available in both the TypeScript and Python SDKs. The examples on this page use TypeScript; the Python client exposes the same surface as get_graph_status(), rebuild_graph(), query_graph(), explore_graph(), and search_with_graph_context() on squid.ai().knowledge_base(...).

Quick Start

Knowledge base management requires an API key, so run it from backend code and expose it to your frontend through an executable.

Backend code
import { SquidService, executable } from '@squidcloud/backend';

export class KnowledgeGraphService extends SquidService {
@executable()
async createGraphKnowledgeBase(): Promise<void> {
await this.squid
.ai()
.knowledgeBase('filings-knowledgebase')
.upsertKnowledgeBase({
description: 'Company filings, acquisitions, and product lines',
embeddingModel: 'text-embedding-3-small',
chatModel: 'gpt-5.5',
metadataFields: [],
// Required: the graph is only built on the MongoDB Atlas vector store.
vectorDbType: 'mongoAtlas',
graphRag: {
enabled: true,
// Optional domain hint. Steers extraction toward the entity kinds you care about.
entityTypes: ['ORGANIZATION', 'PRODUCT', 'LOCATION'],
},
});
}

@executable()
async addFilings(): Promise<void> {
await this.squid
.ai()
.knowledgeBase('filings-knowledgebase')
.upsertContexts([
{
contextId: 'acquisition-2019',
type: 'text',
title: 'Acquisition',
text: 'Aldous Corporation acquired Bfarma Labs in 2019 for two billion dollars.',
},
{
contextId: 'product-line',
type: 'text',
title: 'Product line',
text: 'Bfarma Labs manufactures a migraine drug called Zalofen at its Dublin plant.',
},
]);
}
}

Entity extraction runs inline with ingestion, so no rebuild is needed to make the graph queryable. The indexing bookkeeping settles a moment after upsertContexts() resolves, so wait for it before asserting the graph is ready, as shown under Monitoring the graph.

Searching needs no API key and can run from the client:

Client code
// 'graph' is the default mode on a graph-enabled knowledge base, so searchMode is optional here.
const chunks = await squid.ai().knowledgeBase('filings-knowledgebase').search({
prompt: 'Which drugs does Aldous Corporation sell?',
searchMode: 'graph',
});

The traversal reaches Zalofen from Aldous Corporation through Bfarma Labs, so the product-line chunk is retrieved even though it never mentions Aldous Corporation. You can see the chain it walked with searchWithGraphContext().

Do not expect a two document knowledge base to show a difference between the search modes. Every mode returns everything when there is nothing to compete with. The graph earns its keep once enough documents are competing for the top-k that a connected but dissimilar passage would otherwise be crowded out.

Configuration

Every field of graphRag except enabled is optional.

FieldTypeDescription
enabledbooleanTurns extraction and searchMode: 'graph' on. Required.
entityTypesstring[]Domain taxonomy hint injected into the extraction prompt, for example ['ORGANIZATION', 'PRODUCT']. Guides extraction rather than restricting it.
extractionModelAiChatModelSelectionChat model used for per-chunk extraction. Defaults to the server's graph model.
autoBuildDebounceMsnumberQuiet window before an automatic build runs. Positive integer, defaults to the server's 5 minute window.
conceptsobjectConcept layer configuration. concepts.facets selects which metadata fields become facets ('auto' by default, [] to disable), and concepts.pathFacets builds a navigable tree out of a hierarchical path field.

A path facet turns a folder path into a browsable hierarchy in the graph:

Backend code
await this.squid
.ai()
.knowledgeBase('filings-knowledgebase')
.upsertKnowledgeBase({
description: 'Company filings, acquisitions, and product lines',
embeddingModel: 'text-embedding-3-small',
chatModel: 'gpt-5.5',
metadataFields: [],
vectorDbType: 'mongoAtlas',
graphRag: {
enabled: true,
concepts: {
// Build a folder tree from the folderPath metadata stamped by folder-aware uploads.
pathFacets: [{ field: 'folderPath', type: 'path' }],
},
},
});

Upserting graphRag replaces the whole object, so fields you omit are lost. Read the current configuration with getKnowledgeBase() and spread it if you want to change a single field.

Enabling the Graph on an Existing Knowledge Base

Flipping enabled to true on an Atlas knowledge base that already holds content backfills the graph for you. Squid waits for the knowledge base to go quiet, then runs a structural build that extracts every context not yet graph indexed and builds the concept layer on top. You do not need to call rebuildGraph().

Backend code
const kb = this.squid.ai().knowledgeBase('filings-knowledgebase');
const existing = await kb.getKnowledgeBase();

// Spread the current configuration so enabling the graph does not drop any other field.
await kb.upsertKnowledgeBase({ ...existing!, graphRag: { enabled: true } });

Automatic builds

The concept layer (themes and facets) is derived from the whole corpus, so it is rebuilt in batches rather than per document. A build is scheduled automatically after graph activity: ingesting content, deleting content, or enabling the graph.

  • The build waits for a quiet window measured from the completion of the last piece of graph work. Ingestion that is still in flight keeps pushing the deadline out, so a long bulk upload produces one build at the end rather than one per document.
  • The window defaults to 5 minutes and is configurable per knowledge base with graphRag.autoBuildDebounceMs.
  • A sweep checks for due knowledge bases every minute, so an elapsed window is picked up within roughly a minute of expiring.
  • While a build is pending, getGraphStatus() reports nextAutoBuildAt. It moves forward as long as activity continues.

Entity extraction is not part of this schedule. It runs inline with ingestion, so searchMode: 'graph' works on freshly ingested documents before the concept layer catches up. What lags is the theme and facet structure that queryGraph() reads.

Forcing a build

Call rebuildGraph() when you do not want to wait for the quiet window. The default 'structural' mode is almost always what you want. See the warning on 'full' below.

Backend code
// Runs a structural build (the default): folds newly ingested documents into topics and
// facets now, reusing the extracted entities. Same build the automatic schedule runs.
await this.squid.ai().knowledgeBase('filings-knowledgebase').rebuildGraph();
ModeWhat it recomputesCost
'structural' (default)Entity resolution, themes, and facets. Extraction runs only for contexts never graph indexed.A small fraction of a full rebuild.
'full'Everything, including LLM re-extraction of every chunk.Re-extraction dominates. Same order as the initial ingest.
warning

A 'full' rebuild is rarely necessary, and it is both destructive and slow. It wipes the existing graph up front and then re-extracts every chunk with the LLM, so it costs tokens and time on the same order as the initial ingest, and until the job completes, searchMode: 'graph' and queryGraph() run against an incomplete graph. Extraction already runs inline with ingestion, so new or updated documents never need a full rebuild; a structural build folds them in. Request mode: 'full' explicitly only when the extraction itself must be redone, i.e. after changing entityTypes or extractionModel.

Only one rebuild runs per knowledge base at a time. Track it through getGraphStatus().buildJob.

Searching with the Graph

searchMode: 'graph' is the default mode on a graph-enabled knowledge base, so an omitted searchMode uses the graph. The other modes stay available and are documented under Searching a Knowledge Base.

Tune the traversal with graphOptions:

OptionDefaultLimitDescription
seedLimit825Entities seeded by vector search before expansion.
maxHops23How far the traversal expands from each seed.
includeGraphContextfalseAttaches the traversed subgraph to the response.
Client code
const chunks = await squid
.ai()
.knowledgeBase('filings-knowledgebase')
.search({
prompt: 'How is Aldous Corporation connected to Dublin?',
searchMode: 'graph',
graphOptions: { seedLimit: 12, maxHops: 3 },
});

Raising maxHops widens the net and pulls in weaker associations, so raise it only for genuinely multi-hop questions.

Scoping a search to a concept

graphFilter restricts a search to the documents under one concept of the graph, which is useful when the user has already narrowed the subject. It composes with every search mode and with contextMetadataFilter, so you can combine a thematic scope with a metadata filter.

Client code
const chunks = await squid
.ai()
.knowledgeBase('filings-knowledgebase')
.search({
prompt: 'What was the purchase price?',
// A theme name, a facet value, or a facet nodeId. Names are resolved server side.
graphFilter: { underConcept: 'Acquisitions' },
});

Facet scopes filter by exact metadata membership. Theme scopes are thematic, so they are recall-safe but fuzzy: a document outside the theme's current membership is excluded even if it is relevant. An unresolvable concept fails with CONCEPT_NOT_FOUND and the nearest matching names.

Inspecting the traversal

search() returns chunks only. To see which entities and relationships produced them, use searchWithGraphContext() with includeGraphContext, which is useful for debugging retrieval and for showing users why a passage was returned.

Client code
const response = await squid
.ai()
.knowledgeBase('filings-knowledgebase')
.searchWithGraphContext({
prompt: 'Which drugs does Aldous Corporation sell?',
searchMode: 'graph',
graphOptions: { includeGraphContext: true },
});

for (const entity of response.graphContext?.entities ?? []) {
console.log(`${entity.name} (${entity.type}): ${entity.description}`);
}
for (const rel of response.graphContext?.relationships ?? []) {
console.log(`${rel.source} --[${rel.type}]--> ${rel.target}`);
}

graphContext is present only when the search ran in graph mode with includeGraphContext: true.

Querying the Graph Directly

queryGraph() reads the graph's structure rather than retrieving passages. Every operation is a single round trip that returns a complete answer.

OpRequired inputReturns
overviewA rendered map of the knowledge base: its themes, facets, and counts.
resolverefNodes matching a name, across documents, entities, themes, and facets.
describerefOne node with its ancestors and immediate neighbors.
subtreerefThe node's subtree with document counts, to depth (default and cap 3).
docsUnderrefDocuments under the node's closure, with totalDocs reporting the full size.
conceptsOfcontextIdThe facets and themes one document belongs to.
neighborhoodrefThe entity subgraph around an entity, expanded by hops (default 1, max 2).
pathBetweenref and refBThe relationship chain connecting two nodes, or the concepts they share.
globalSummaryqueryThe knowledge base's theme summaries, ranked against a question.
Backend code
const kb = this.squid.ai().knowledgeBase('filings-knowledgebase');

// What is in this knowledge base?
const overview = await kb.queryGraph({ op: 'overview' });
console.log(overview.overview);

// How are two things connected?
const path = await kb.queryGraph({
op: 'pathBetween',
ref: 'Aldous Corporation',
refB: 'Zalofen',
});
if (path.pathBetween?.tier === 'entity') {
// An explanatory chain: Aldous Corporation acquired Bfarma Labs, which manufactures Zalofen.
for (const step of path.pathBetween.steps ?? []) {
console.log(`${step.from} --[${step.type}]--> ${step.to}`);
}
} else {
// No entity chain exists, so the answer falls back to the concepts both refs sit under.
console.log(path.pathBetween?.sharedConcepts);
}

ref accepts either a name, which Squid resolves server side by exact match, then alias, then similarity, or a facet nodeId. Only facet nodeIds and document contextIds are stable handles worth persisting. Theme and entity ids change on every rebuild, so resolve them by name each time.

Exploring the Graph

exploreGraph() returns a bounded slice of the entity graph for visualization: the highest degree entities and the relationships among them.

Backend code
const subgraph = await this.squid.ai().knowledgeBase('filings-knowledgebase').exploreGraph({
nodeLimit: 100, // Default 200, max 1000.
});

console.log(`${subgraph.nodes.length} nodes, ${subgraph.edges.length} edges`);
// topics maps each node's topicPath ids to names, so a visualization can group by theme.
console.log(subgraph.topics?.map((topic) => topic.name));

Pass topicId to restrict the slice to one theme. Theme ids come from getGraphStatus().topics or from a previous exploreGraph() response, and are only valid until the next rebuild.

Monitoring the Graph

getGraphStatus() reports build state, coverage, and cost.

Backend code
const status = await this.squid.ai().knowledgeBase('filings-knowledgebase').getGraphStatus();

console.log(`${status.contextsIndexed}/${status.contextsTotal} contexts graph indexed`);
console.log(`${status.entityCount} entities, ${status.relationshipCount} relationships`);
console.log(`Graph ingest cost so far: $${status.ingestUsage?.estCostUsd ?? 0}`);

if (status.buildJob?.status === 'in_progress') {
console.log(`Build running since ${new Date(status.buildJob.startedAt!).toISOString()}`);
} else if (status.structureStale) {
console.log(`${status.staleDocCount} documents changed since the last build`);
}
if (status.nextAutoBuildAt) {
console.log(`Next automatic build around ${new Date(status.nextAutoBuildAt).toISOString()}`);
}

Fields worth knowing:

FieldMeaning
enabledWhether the graph is on for this knowledge base.
contextsIndexed / contextsTotalExtraction coverage. Equal values mean every context is in the graph.
entityCount / relationshipCountSize of the entity graph.
topics / facetsThe concept layer, populated once a build has run.
structureStale / staleDocCountDocuments graph indexed since the last concept build. Themes lag content until the next build.
buildJobThe most recent build: status, startedAt, and error when it failed.
lastStructureBuildAtWhen a build last completed successfully. Absent if none ever has.
nextAutoBuildAtWhen the pending automatic build is due. Absent when none is scheduled.
ingestUsage / lastRebuildUsageLLM tokens and estimated cost, split per model under byModel.

To wait for a graph to be fully ready, poll until contextsIndexed === contextsTotal and buildJob is not 'in_progress'.

Using the Graph from the Console

The Squid Console exposes the same feature without code:

  • Create a knowledge base with Vector Store set to mongoAtlas, then turn on the Knowledge Graph toggle. The toggle stays disabled for any other store, including Server default, because the store cannot be changed after creation. A knowledge base created with Server default that resolves to Atlas can have the graph enabled afterwards by editing it.
  • The knowledge base page shows a Knowledge Graph card with the last build time, how many documents changed since it, the estimated time of the next automatic build, and a Build now button that runs a structural build immediately.

Agent Behavior

Connected agents pick up the graph on their own. On a graph-enabled knowledge base:

  • The knowledge base search tool offers 'graph' alongside the other modes and treats it as the default, so multi-hop questions use graph retrieval without any prompting on your part.
  • The tool description carries a compact overview of the graph, giving the agent the corpus themes and counts before it searches.
  • The agent gets a queryKnowledgeGraph tool backed by the operations above, so it can navigate the corpus structure rather than only retrieving passages.

See Agent abilities for connecting a knowledge base to an agent.

Error Handling

ErrorCauseFix
CONCEPT_NOT_FOUNDA queryGraph() ref matched nothing. The message lists the nearest names.Resolve names with op: 'resolve' first, or use one of the suggested names.
TOPIC_NOT_FOUNDAn exploreGraph() topicId is unknown, usually because a rebuild churned the ids.Re-read theme ids from getGraphStatus() after each rebuild instead of persisting them.
JOB_ALREADY_EXISTSA rebuild is already running for this knowledge base.Wait for getGraphStatus().buildJob to leave 'in_progress'.
Graph calls succeed but return emptyThe knowledge base is not on mongoAtlas, or graphRag.enabled is false. Graph operations are no-ops there.Check getGraphStatus().enabled and the knowledge base's vectorDbType.
buildJob.status === 'failed'The build hit an error, recorded in buildJob.error.Fix the underlying cause and rerun rebuildGraph(). The next automatic build also retries.

Disabling graphRag deletes the knowledge base's graph data. Re-enabling it rebuilds from scratch, at full extraction cost.

Best Practices

  • Decide on the graph before creating the knowledge base. vectorDbType is immutable, so a 'postgres' knowledge base can never gain one.
  • Pass entityTypes that match your domain. Extraction is markedly more reliable for proper nouns such as organizations, people, and locations than for generic product categories.
  • Enable the graph before bulk ingesting, so extraction happens inline instead of requiring a full rebuild afterwards.
  • Leave maxHops at its default. Raise it for questions that are genuinely several hops deep, and expect looser results when you do.
  • Watch ingestUsage.estCostUsd on a small corpus first to project the cost of enabling the graph over everything.
  • Widen autoBuildDebounceMs for knowledge bases under continuous ingestion, so builds do not chase a moving target, and shorten it when you want themes to refresh quickly after small updates.
  • Resolve theme names, not ids, across rebuilds. Only facet nodeIds and document contextIds are stable.