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:

  1. Seed retrieval — find the most relevant documents by vector (or hybrid) similarity.
  2. 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.

Restored / mistyped edge collections are covered too. An edge collection that was created as a plain 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

OptionDefaultMeaning
hops2Max expansion depth.
direction"outbound"outbound | inbound | any.
decay0.6Score multiplier per hop (0–1). Contribution = seed_score · decayhops.
combine"max"max keeps the strongest path; sum adds every contribution (centrality-style).
include_seedstrueWhether the seeds themselves appear in the result (at hop 0).
limit20Max results after ranking.
max_frontier10000Caps vertices visited per seed (safety on high-fan-out graphs).
seed_collectionQualifies 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:

OptionDefaultMeaning
seed_mode"vector"vector (ANN on vector_index) or hybrid (vector + fulltext fusion).
seed_limit10How many seeds to retrieve (the k).
efHNSW ef_search for the vector leg.
fulltext_fieldRequired when seed_mode = "hybrid".
text_queryRequired 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:

FieldDescription
docThe full reached document.
idVertex id, "collection/key".
scoreCombined relevance (seed similarity × hop decay).
hopsMinimum hop distance from any seed (0 for seeds).
seedtrue if this document was one of the retrieved seeds.
seed_scoreThe seed's own similarity score (null for pure neighbors).
viaThe 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.

  1. Detect communities over an edge collection with the Louvain method.
  2. Summarize each community (keyword extraction by default, or an LLM when one is configured).
  3. 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 fieldDefaultMeaning
edge_collectionRequired. The graph to analyze.
resolution1.0Louvain resolution — higher yields more, smaller communities.
min_community_size3Drop communities smaller than this.
summarizefalseGenerate a title / summary / keywords per community.
max_communities50Cap on how many (largest) communities get summarized.
providerLLM 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 across content/text/summary/title, including under a doc wrapper).
  • 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/_to strings of the same form.
  • Make sure the seed_collection matches 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.