Graph RAG
Retrieve seed documents by similarity, then expand across your knowledge graph to pull in related context — the classic retrieve → traverse pattern, expressible in a single SDBQL query with GRAPH_RAG and NEIGHBORS.
Combine with auto-embeddings (set embedding_source on your vector index) so you only ever insert raw text.
What it does
Plain vector search returns the top-K most similar documents but ignores how they relate. Graph RAG combines two of SoliDB's primitives:
- Seed retrieval — find the most relevant documents by vector (or hybrid) similarity.
- Graph expansion — walk the edge collection outward from those seeds, scoring each reached document by how many hops away it is.
The result is a context set that is both semantically relevant (the seeds) and structurally connected (their neighbors) — ideal for feeding an LLM richer, linked context than similarity alone provides. Two building blocks make this fast and ergonomic: automatic edge indexes and the NEIGHBORS / GRAPH_RAG functions.
That covers local questions — "what relates to these documents?". For global questions — "what themes run through the whole graph?" — SoliDB also detects communities and summarizes them, which you then query with COMMUNITY_SEARCH.
Automatic edge indexes
Graph traversal and expansion need to find an edge's neighbors quickly. When you create an edge collection, SoliDB now automatically builds persistent, non-unique indexes on _from and _to:
POST /_api/database/mydb/collection { "name": "links", "type": "edge" }
# → creates indexes: _edge_from_idx (_from), _edge_to_idx (_to)
Without these, every traversal falls back to a full scan of the edge collection to build an in-memory adjacency map. With them, neighbor lookups are indexed — traversals, NEIGHBORS, and GRAPH_RAG all benefit.
document type (e.g. by a generic import) gets its _from/_to index created lazily on its first traversal, so it is indexed from then on. Existing user indexes on those fields are respected and never duplicated.
NEIGHBORS — expand from known seeds
NEIGHBORS is the graph-expansion primitive: given seed vertices you already have, it walks the edge collection outward and returns the reached documents scored by hop distance.
NEIGHBORS(edge_collection, seeds, options?)
seeds — an array of either "collection/key" strings, or objects { id: "coll/key", score: 0.9 } carrying a per-seed weight (default weight 1.0). Bare keys are qualified with options.seed_collection when provided.
Options
| Option | Default | Meaning |
|---|---|---|
| hops | 2 | Max expansion depth. |
| direction | "outbound" | outbound | inbound | any. |
| decay | 0.6 | Score multiplier per hop (0–1). Contribution = seed_score · decayhops. |
| combine | "max" | max keeps the strongest path; sum adds every contribution (centrality-style). |
| include_seeds | true | Whether the seeds themselves appear in the result (at hop 0). |
| limit | 20 | Max results after ranking. |
| max_frontier | 10000 | Caps vertices visited per seed (safety on high-fan-out graphs). |
| seed_collection | — | Qualifies bare-key seeds into "coll/key". |
Example
Function calls are used through a LET binding, then iterated with FOR (the same idiom as HYBRID_SEARCH):
LET res = NEIGHBORS("links", ["docs/a"], { hops: 2, direction: "outbound", decay: 0.5 })
FOR n IN res
RETURN { id: n.id, hops: n.hops, score: n.score, seed: n.seed }
[
{ "id": "docs/a", "hops": 0, "score": 1.0, "seed": true },
{ "id": "docs/b", "hops": 1, "score": 0.5, "seed": false },
{ "id": "docs/d", "hops": 1, "score": 0.5, "seed": false },
{ "id": "docs/c", "hops": 2, "score": 0.25, "seed": false }
]
Seed a scores 1.0 at hop 0; its direct neighbours b/d score 1.0 · 0.51 = 0.5; c (two hops away) scores 0.52 = 0.25.
GRAPH_RAG — retrieve, then expand
GRAPH_RAG is the full pipeline: it retrieves seeds by similarity from a document collection, then hands them to the same expansion engine as NEIGHBORS.
GRAPH_RAG(seed_collection, vector_index, edge_collection, query_vector, options?)
It accepts every NEIGHBORS option (hops, direction, decay, combine, limit…) plus seed-retrieval options:
| Option | Default | Meaning |
|---|---|---|
| seed_mode | "vector" | vector (ANN on vector_index) or hybrid (vector + fulltext fusion). |
| seed_limit | 10 | How many seeds to retrieve (the k). |
| ef | — | HNSW ef_search for the vector leg. |
| fulltext_field | — | Required when seed_mode = "hybrid". |
| text_query | — | Required when seed_mode = "hybrid". |
Example
LET res = GRAPH_RAG("docs", "emb", "links", @query_vector,
{ hops: 1, seed_limit: 2, direction: "outbound" })
FOR r IN res
RETURN { id: r.id, hops: r.hops, seed: r.seed, score: r.score }
[
{ "id": "docs/a", "hops": 0, "seed": true, "score": 1.0 },
{ "id": "docs/d", "hops": 0, "seed": true, "score": 0.994 },
{ "id": "docs/b", "hops": 1, "seed": false, "score": 0.6 }
]
The query vector retrieves a and d as seeds (cosine 1.0 and 0.994), then a 1-hop outbound expansion adds b (reached from a, score 1.0 · 0.6).
Hybrid seeding
LET res = GRAPH_RAG("docs", "emb", "links", @query_vector, {
seed_mode: "hybrid", fulltext_field: "text", text_query: "vector database",
seed_limit: 5, hops: 2
})
FOR r IN res RETURN r
Result shape
Both functions return an array of hits, ranked by score descending:
| Field | Description |
|---|---|
| doc | The full reached document. |
| id | Vertex id, "collection/key". |
| score | Combined relevance (seed similarity × hop decay). |
| hops | Minimum hop distance from any seed (0 for seeds). |
| seed | true if this document was one of the retrieved seeds. |
| seed_score | The seed's own similarity score (null for pure neighbors). |
| via | The seed responsible for this hit's strongest contribution. |
Global GraphRAG — communities & summaries
NEIGHBORS and GRAPH_RAG answer local questions ("what is connected to these documents?"). For global questions ("what themes exist across the whole graph?"), SoliDB detects communities — densely connected clusters of vertices — summarizes each, and lets you retrieve the relevant summaries.
- Detect communities over an edge collection with the Louvain method.
- Summarize each community (keyword extraction by default, or an LLM when one is configured).
- Retrieve the relevant summaries with
COMMUNITY_SEARCH.
1. Build communities
The build runs asynchronously and returns a request_id to poll:
POST /_api/database/{db}/graph/community/build
{ "edge_collection": "links", "min_community_size": 3, "summarize": true }
# → { "request_id": "...", "run_id": "run_...", "status": "pending" }
GET /_api/database/{db}/graph/community/build/{request_id} # poll: pending → done | failed
GET /_api/database/{db}/graph/communities?edge_collection=links # list detected communities
| Body field | Default | Meaning |
|---|---|---|
| edge_collection | — | Required. The graph to analyze. |
| resolution | 1.0 | Louvain resolution — higher yields more, smaller communities. |
| min_community_size | 3 | Drop communities smaller than this. |
| summarize | false | Generate a title / summary / keywords per community. |
| max_communities | 50 | Cap on how many (largest) communities get summarized. |
| provider | — | LLM provider (openai/anthropic/ollama/gemini); read from _env if omitted. |
Output is stored in _graph_communities (membership) and _community_summaries (title/summary/keywords), with the latest run per edge collection tracked in _graph_runs. Summaries use deterministic keyword extraction unless an LLM provider is configured (e.g. OPENAI_API_KEY in the database _env), in which case the model writes the summary with a keyword fallback on failure.
2. COMMUNITY_SEARCH
COMMUNITY_SEARCH(query_text, options?)
options: { run_id?, edge_collection?, limit: 5 }. When run_id is omitted it defaults to the latest run recorded for edge_collection.
LET res = COMMUNITY_SEARCH("vector database", { edge_collection: "links", limit: 3 })
FOR c IN res
RETURN { community_id: c.community_id, title: c.title, summary: c.summary, score: c.score }
[
{
"community_id": 0,
"title": "entities/c",
"summary": "A community of 3 connected entities. Key topics: database, vector, embedding, search.",
"score": 1.0
}
]
Returns the community summaries whose text best matches the query — the entry point for broad questions that no single document answers.
RAG_PIPELINE — a named retrieve→rerank pipeline
Store a retrieval pipeline once, run it by name. RAG_PIPELINE(name, query_vector, options?) loads a definition from the _rag_pipelines collection, runs GRAPH_RAG retrieval with its stored parameters, applies the configured rerank, and truncates to the limit.
Define a pipeline (one document keyed by name):
{
"_key": "faq",
"seed_collection": "docs",
"vector_index": "emb",
"edge_collection": "links",
"retrieve_options": { "hops": 1, "seed_limit": 20 },
"rerank": { "mode": "lexical", "field": "doc.content", "limit": 5 }
}
Then run it — pass text_query for the rerank stage, and override limit per call:
LET hits = RAG_PIPELINE("faq", @query_vector, { text_query: "how to index", limit: 5 })
FOR h IN hits RETURN h.doc
Returns the GRAPH_RAG hit shape ({ doc, score, hops, ... }), reranked and limited.
RERANK — reorder retrieved docs
RERANK(query, docs, options?) reorders an array of retrieved documents by relevance to query, most relevant first.
mode—"lexical"(default): query-token overlap, no LLM, no extra round-trip."llm": a chat model reorders the candidates, falling back to lexical on any failure.field— dotted path to each doc's text (auto-detected acrosscontent/text/summary/title, including under adocwrapper).limit,provider,model.
LET hits = VECTOR_SEARCH("docs", "emb", @query_vector, 20)
RETURN RERANK("hnsw index tuning", hits, { field: "doc.content", limit: 5 })
Notes & tips
- Vertex ids are
"collection/key"(no database prefix). Edges reference vertices through their_from/_tostrings of the same form. - Make sure the
seed_collectionmatches the collection your edges point into, or expansion finds nothing. combine: "max"(default) avoids over-ranking hub nodes; use"sum"when you want recall/centrality to matter.- Empty seed retrieval returns
[]— not an error. - Edge collections are auto-indexed (see above), so expansion is indexed out of the box.
- A community build is a point-in-time snapshot tagged by
run_id; re-run the build to refresh it after the graph changes. - For seed retrieval details, see Vector Search and Hybrid Search; for raw traversal, see Graph Queries.