Skip to content
New

Introducing Scale: SurrealDB Cloud for high availability and scale

Learn more

1/4

One query, not two stores: how vector + graph in SurrealDB makes agents more accurate

AI
Tutorial

Jul 14, 20267 min read

Martin Schaer

Martin Schaer

Show all posts

One query, not two stores: how vector + graph in SurrealDB makes agents more accurate

Our newsletter

Get tutorials, AI agent recipes, webinars, and early product updates in your inbox every two weeks

Most retrieval-augmented agents are quietly running a distributed join in application code. The embeddings live in a vector database, the relationships live in a graph database (or a relational one pretending to be one), and somewhere in the middle a Python function is fetching from both and trying to reconcile them. That reconciliation step – the fusion – is where accuracy goes to die.

This post is about avoiding that step. In SurrealDB the vectors and the graph live in the same engine and the same query, so an agent can ask one question and get one coherent answer instead of two ranked lists it has to stitch together itself.

TL;DR

  • A typical agentic RAG stack splits retrieval across a vector store and a graph/relational store, then fuses the results in app code. Fusion is lossy, slow, and a frequent source of wrong answers.

  • SurrealDB does vector KNN search and graph traversal in a single SurrealQL statement, so the join happens inside the database against consistent data — not in the agent's glue code.

  • This means the agent retrieves semantically relevant records and follows their relationships in one hop: "find the docs that match this question, then pull the entities they cite, their authors, and superseding versions."

  • One round trip, one transactional snapshot, one result set the model can reason over directly.

  • Everything below is copy-paste SurrealQL. Give it a try in Surrealist.

Picture a support agent answering a question over a corpus of incident reports, runbooks, and the services they describe. The standard architecture:

  1. Embed the user's question, query the vector store for the top-k similar documents.

  2. Take the IDs that come back, query the graph store for what those documents relate to — the services they mention, the team that owns them, the newer doc that superseded them.

  3. In application code, merge the two result sets, re-rank, dedupe, and hope the IDs still line up.

Step 3 is the problem. The vector store ranks by cosine similarity, a ranking that the graph store knows nothing about. So the agent's glue code has to invent a fusion heuristic — reciprocal rank fusion, a hand-tuned weight, or just "take the vector hits and look up their neighbors." Each of these:

  • Loses signal. Similarity scores and graph distance are reconciled with a heuristic that neither store endorses.

  • Drifts out of sync. The two stores are updated independently; the vector hit can point at a node the graph hasn't seen yet, or one that was deleted.

  • Adds latency and failure modes. Two network round trips, two clients, two timeout budgets, two things that can return stale data.

  • Leaks into the prompt. Whatever the fusion code got wrong, the model now reasons over. Garbage context produces confident, wrong answers.

The agent isn't inaccurate because the model is weak. It's inaccurate because it was handed a badly fused context.

In SurrealDB a record can carry an embedding vector and participate in graph edges. There's no separate "vector table" — the embedding is just a field on the same document record that the graph traverses.

-- A document carries its text, an embedding, and lives in the graph.
DEFINE TABLE document SCHEMAFULL;
DEFINE FIELD title     ON document TYPE string;
DEFINE FIELD body      ON document TYPE string;
DEFINE FIELD kind      ON document TYPE string
  ASSERT $value IN ["runbook", "incident", "design"];
DEFINE FIELD embedding ON document TYPE array<float>;

-- HNSW index over the embedding for fast approximate KNN.
DEFINE INDEX doc_vec ON document
  FIELDS embedding HNSW DIMENSION 768 DIST COSINE;

-- Entities the documents talk about, and people who wrote them.
DEFINE TABLE service SCHEMAFULL;
DEFINE FIELD name ON service TYPE string;

DEFINE TABLE person SCHEMAFULL;
DEFINE FIELD name ON person TYPE string;
DEFINE FIELD team ON person TYPE string;

Now the relationships — these are graph edges, defined once and traversable in both directions:

-- who wrote what
RELATE person:ada->wrote->document:runbook_payments;

-- what a document is about
RELATE document:incident_42->mentions->service:payments;

-- version history: a newer doc supersedes an older one
RELATE document:runbook_payments_v2->supersedes->document:runbook_payments;

The crucial point: the embedding on document and the wrote / mentions / supersedes edges are in the same database, under the same transaction. There is no second system to keep in sync.

Here is the whole two-store dance, collapsed into one statement. Find the documents most similar to the question's embedding, and in the same query pull their graph context — the services they mention, the author and team, and whether a newer version supersedes them:

LET $q = $question_embedding;  -- 768-d vector from your embedding model

SELECT
    title,
    kind,
    -- similarity score, straight from the vector index
    vector::distance::knn() AS score,
    -- graph context, fetched in the same pass:
    ->mentions->service.name        AS services,
    <-wrote<-person.name            AS authors,
    <-wrote<-person.team            AS owning_teams,
    -- is this doc stale? (does anything supersede it?)
    count(<-supersedes<-document) > 0 AS superseded
FROM document
WHERE embedding <|5,40|> $q          -- top-5 KNN, HNSW efSearch=40
ORDER BY score;

$question_embedding must to be injected by the client, or you could generate the embeddings inside SurrealQL with a custom function.

One round trip returns rows that are already the join: each semantically-matched document arrives with its similarity score and its relationships resolved. The agent never sees two lists. It sees the answer.

A few things worth calling out:

  • `` is the KNN operator: top 5 neighbors, HNSW search-list size 40. `vector::distance::knn()` reads back the distance computed by that same operator — no recomputation.

  • ->mentions->service.name and <-wrote<-person.name are graph traversals projected as fields. Arrows point in the direction of the edge; <- walks it backwards. The agent gets services and authors as arrays right on the row.

  • superseded lets the agent down-weight stale docs in context — a graph fact (version history) directly improving retrieval quality. You cannot express that in a vector store alone.

The accuracy win isn't a faster query (though it is faster). It's that the context handed to the model is internally consistent.

The ranking and the relationships agree. Because the traversal starts from the KNN hits inside one query, every relationship the agent sees belongs to a document that genuinely matched. There's no fusion heuristic guessing how to weigh "similar" against "connected" — the graph hangs off the ranked set directly.

No phantom or stale neighbors. The traversal runs against the same transactional snapshot as the vector search. A document can't match the embedding index while its edges point at a node that was already deleted, because both reads see the same committed state.

The graph can correct the vectors. The superseded flag above is a graph fact that improves a vector result — exactly the kind of cross-modal signal that's impossible when the two live apart. You can go further: filter KNN hits to only those ->mentions-> a service the user has access to, or boost docs <-wrote<- by a trusted team. Authorization and trust become part of retrieval, not a post-filter the agent has to remember to apply.

Agents often need expansion, not just lookup — "what's related to the things related to my hits." Because traversal composes, that's still one query. Here we retrieve similar incidents and pull in the runbooks for every service they touch:

SELECT
    title,
    vector::distance::knn() AS score,
    -- for each service this incident mentions, the runbooks about it
    ->mentions->service<-mentions<-document[WHERE kind = "runbook"].title
        AS related_runbooks
FROM document
WHERE kind = "incident"
  AND embedding <|3,40|> $q
ORDER BY score;

That ->mentions->service<-mentions<-document chain is a two-hop graph walk — incident → service → other documents — filtered inline to runbooks, all anchored to the vector-ranked incidents. In a split stack this is three queries and a fan-out join. Here it's a clause.

Honesty matters: a dedicated vector store with billions of vectors and a team tuning it full-time will out-scale a single SurrealDB node on raw vector throughput, and there are workloads where that's the right call. The point of this post is narrower and, for most agent builders, more relevant: the moment your retrieval needs both similarity and relationships, keeping them in separate stores forces a fusion step that quietly costs you accuracy. If your corpus fits comfortably in SurrealDB — and most agent corpora do — the single-store hybrid query is both simpler and more correct.

Spin up an instance, define the schema above, RELATE a few documents to services and authors, and run the hybrid query with a real embedding. The whole thing is one engine and one query language — no vector store to provision, no sync job to babysit, no fusion code to debug.

Related posts

Our newsletter

Get tutorials, AI agent recipes, webinars, and early product updates in your inbox every two weeks

SurrealDB

The context layer for AI agents.

Documents, graphs, vectors, time-series, and memory.
One transaction, one query, one deployment.

Explore with AI

Stay in the loop

Tutorials, AI agent recipes, and product updates, every two weeks.

Independently verified

SOC 2 Type 2

GDPR

Cyber Essentials Plus

ISO 27001

Trust Centre

Copyright © 2026 SurrealDB Ltd. Registered in England and Wales. Company no. 13615201

Registered address: 3rd Floor 1 Ashley Road, Altrincham, Cheshire, WA14 2DT, United Kingdom

Trading address: Huckletree Oxford Circus, 213 Oxford Street, London, W1D 2LG, United Kingdom