<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mark Gyles</title>
    <description>The latest articles on DEV Community by Mark Gyles (@mgyles).</description>
    <link>https://dev.to/mgyles</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1602559%2Ff82af600-3936-4f4a-a067-dfee7a3228d4.jpg</url>
      <title>DEV Community: Mark Gyles</title>
      <link>https://dev.to/mgyles</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mgyles"/>
    <language>en</language>
    <item>
      <title>Graph engineering is missing a graph</title>
      <dc:creator>Mark Gyles</dc:creator>
      <pubDate>Thu, 13 Aug 2026 13:08:08 +0000</pubDate>
      <link>https://dev.to/surrealdb/graph-engineering-is-missing-a-graph-6b4</link>
      <guid>https://dev.to/surrealdb/graph-engineering-is-missing-a-graph-6b4</guid>
      <description>&lt;p&gt;Author: &lt;a href="https://x.com/tobiemh" rel="noopener noreferrer"&gt;Tobie Morgan Hitchcock&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Graph engineering is having a moment.&lt;/p&gt;

&lt;p&gt;The definition going round is a good one, and I am not going to try to redefine it. Complex agents increasingly look like graphs: nodes do the work, edges decide what happens next, state moves between them. That is real, it matches what production actually looks like, and the people saying it are right.&lt;/p&gt;

&lt;p&gt;Almost nobody makes the next observation. Those agents are still fed context from flat vector chunks, a separate document store, a relational database and some ephemeral session memory. While the execution architecture became a graph, the data architecture underneath it did not.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Graph engineering is not just about graphing the agents. You also have to graph the context they operate on.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Or, less politely: you cannot graph-engineer the agent and leave its memory as a pile of chunks.&lt;/p&gt;

&lt;p&gt;This is my attempt to say what graph engineering has to mean if it is going to outlive the meme.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rename cycle
&lt;/h2&gt;

&lt;p&gt;We have been renaming the same struggle for a few years. Prompt engineering optimised instructions. Context engineering optimised what goes into the window. Harness engineering, loop engineering, and now graph engineering optimise how agents work together.&lt;/p&gt;

&lt;p&gt;I do not mind the labels. They appear when builders hit a wall. This wall is real. One agent loop in one chat window stops scaling the moment the work looks like a team: parallel reviewers, a fixer, a security check with veto power, a human gate before anything irreversible. That structure is a graph. It always was.&lt;/p&gt;

&lt;p&gt;Every one of those renames moved up the stack, and every one of them left the data layer where it was.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two graphs. Say which one you mean.
&lt;/h2&gt;

&lt;p&gt;When someone says graph engineering, they could mean one of three things and rarely say which:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A graph of loops&lt;/strong&gt;, which is really a claim about control flow: what you get when one loop stops being enough.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An execution graph.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A context graph.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The loop graph folds into the execution graph, so two are left standing, and most threads blur even those. Sometimes people mean both. They almost never say so.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fet6cf1xni6qvbzb0e475.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fet6cf1xni6qvbzb0e475.png" alt="execution graph" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The execution graph
&lt;/h3&gt;

&lt;p&gt;What runs next, in what order, with what rights. It determines what your agents &lt;strong&gt;do&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Nodes are work: a classifier, a tool call, a coding agent, a reviewer. Edges are transitions: success, fail, escalate, fan out. State is the scratchpad the system mutates. Verifiers sit on a separate path when the stakes are high. Stop rules exist so the thing does not burn tokens until the card declines.&lt;/p&gt;

&lt;p&gt;This is a state machine with probabilistic nodes inside it. LangGraph made the case earliest and loudest, and Google's ADK, Mastra, CrewAI, Microsoft's agent frameworks, or a boring job runner you wrote yourself all live on the same plane. Useful, necessary for a lot of production agents, and on its own not sufficient.&lt;/p&gt;

&lt;h3&gt;
  
  
  The context graph
&lt;/h3&gt;

&lt;p&gt;What is true, what is related, what was true when, and who said so. It determines what your agents &lt;strong&gt;know&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here the nodes are entities, documents, events, users and assets, and the edges between them carry properties of their own: since, confidence, source. Provenance, time, permissions and hybrid retrieval all matter, because the world is not only triples and it is not only embeddings.&lt;/p&gt;

&lt;p&gt;Classic knowledge graphs and GraphRAG both sit on this plane. It gets a paragraph in most threads and a whiteboard in almost none.&lt;/p&gt;

&lt;p&gt;So:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The execution graph engineers the graph of work. The context graph engineers the graph of knowledge.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most teams have engineered the first and are still running the second as a pile of chunks in a vector store.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where they meet: the turn
&lt;/h2&gt;

&lt;p&gt;Every serious agent turn is three moves: read, think, write. Read means traverse relations, pull vectors, load history and permissions. Think means the model reasons over a complete packet, not three partial dumps from three systems that disagree with each other. Write means decisions, new entities, and memories land somewhere the next turn can actually see them.&lt;/p&gt;

&lt;p&gt;If your execution graph is elegant and your write path is "maybe the vector DB, maybe the graph DB, maybe a JSON blob in object storage," the whiteboard never became system truth. You choreographed forgetting.&lt;/p&gt;

&lt;p&gt;That is the definition I use:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Graph engineering is how you design the execution graph of agent work and the context graph of durable knowledge, then keep them consistent across every turn.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The meme covers the execution graph. Production dies on the other two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Loops, graphs, context. Stack them.
&lt;/h2&gt;

&lt;p&gt;A loop is fine when one goal, one verifier, and one stop condition cover the job.&lt;/p&gt;

&lt;p&gt;An execution graph earns its keep when you need parallel specialists, explicit escalations, or policy gates.&lt;/p&gt;

&lt;p&gt;Context engineering is what you do on every node either way: put the right facts, tools, and state in front of the model at the right time. Widening the context window does not solve a wrong-context problem, it just raises the price of getting it wrong.&lt;/p&gt;

&lt;p&gt;Harnesses and frameworks host all of this. I am not here to sell you an orchestrator. We did not build SurrealDB to replace your control plane.&lt;/p&gt;

&lt;p&gt;We built it because the existing data planes under those nodes were a mess.&lt;/p&gt;

&lt;h2&gt;
  
  
  GraphRAG is one chapter, not the book
&lt;/h2&gt;

&lt;p&gt;People swap the words, but they should not.&lt;/p&gt;

&lt;p&gt;GraphRAG is a retrieval pattern: extract structure from a corpus, then use the graph, and often community summaries, to ground generation. It is a good technique, and one node in a larger system can run it just fine. You can also run GraphRAG with almost no multi-agent workflow at all. Microsoft's own documentation makes the honest version of the argument: vector-only retrieval struggles when answering a question requires connecting information across relationships.&lt;/p&gt;

&lt;p&gt;If graph engineering is going to mean anything durable, it is how the whole system moves, remembers, and stays correct. Retrieval is necessary, but it is not the architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retrieval is not three searches and a re-ranker
&lt;/h2&gt;

&lt;p&gt;Almost everyone ships the same pattern. Fire a vector search. Fire a keyword search. Maybe fire a graph query. Take fifty from each, throw the pile at a re-ranker, keep what floats, stuff it in the prompt.&lt;/p&gt;

&lt;p&gt;That pipeline has a structural flaw, and it is not latency. Each leg searches blind to the constraints the other legs know about. The vector index does not know this question is about one customer's contracts; it knows only that a question-shaped vector is near some passage-shaped vectors. So it returns fifty candidates and hopes. The keyword leg does the same. The graph leg returns a neighbourhood with no idea which parts are relevant to the sentence that was asked. Then a re-ranker, which can see the text and none of the structure, is asked to reconcile three lists that were each built without the others' information.&lt;/p&gt;

&lt;p&gt;You over-fetch because that is the only defence against three blind legs. Over-fetching grows your token bill. Then you pay a second time, because the pile you carried back is mostly padding, and padding in the context window is not free: it costs money on the way in and accuracy on the way out, since a longer prompt gives the model more chances to ground its answer in the wrong sentence.&lt;/p&gt;

&lt;p&gt;Run the similarity inside the traversal rather than beside it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuw6xvlko0vnmp8106qq8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuw6xvlko0vnmp8106qq8.png" alt="three blind legs" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If embeddings are a field on the entity, and full-text indexes sit on the same rows, and relations are edges between those same rows, then one query expresses the whole thing: start from what the question is about, walk the relations that matter, and score by similarity and text relevance as you go, with permissions and time bounds applied in the same pass. Roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;stated_by&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;provenance&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;works_at&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;acme&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;valid_until&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NONE&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;|&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The structure prunes before similarity scores, and similarity ranks within what the structure allows. You are not searching a corpus and hoping the answer's neighbourhood comes with it. You are searching a neighbourhood you already have reason to believe in.&lt;/p&gt;

&lt;p&gt;That buys two things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fewer tokens.&lt;/strong&gt; You stop over-fetching, because a constrained candidate set does not need a wide net to be safe. You carry back a small precise set instead of three large ones minus whatever the re-ranker threw away, and you stop paying for the same fact three times because three stores each returned their own copy of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Higher accuracy.&lt;/strong&gt; Every filter that was inexpressible in the vector leg (this entity, this scope, this time window, this permission, currently true rather than superseded) becomes expressible, because it is evaluated in the same statement as the similarity. Constraints you could only apply after ranking are now applied before it.&lt;/p&gt;

&lt;p&gt;Our own numbers say the same thing from the other direction. When we were first developing Spectron, our memory layer, we tried the obvious thing: reduce each extracted fact to a short sentence, embed it, and add it as another dense leg in the hybrid mix. It fixed roughly a hundred benchmark questions and broke roughly as many again, landing slightly negative and inside the noise. Meanwhile the two changes that clearly won were both structural: routing from a resolved entity into its typed facts, and expanding a hit into its conversational neighbours. Both worth a couple of points on their own.&lt;/p&gt;

&lt;p&gt;The lesson we took: reducing a typed fact to a sentence and matching it by cosine throws away exactly the structure that made extracting it worthwhile. A similarity list also cannot abstain. It always returns its k, so when the right answer is not there it returns something else with total confidence, and something else lands in the prompt. Structure can return nothing, which is frequently the correct answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  A context graph is not an extraction job
&lt;/h2&gt;

&lt;p&gt;The obvious objection: fine, &lt;em&gt;so I bolt a graph database onto my stack and run an extractor over my corpus.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You can. What you get is a pile of assertions in a vocabulary the model improvised one turn at a time, and the difference shows up months later as retrieval that cannot find things you know are in there.&lt;/p&gt;

&lt;p&gt;Here are the numbers from one conversation we ingested end to end. Of the attribute keys the extractor produced, &lt;strong&gt;85% were used exactly once&lt;/strong&gt;: 236 distinct keys across 325 facts, an average of 1.4 uses per key. Action verbs were nearly as bad, 80% used once. The two most common verbs in the whole set were &lt;code&gt;shared_image&lt;/code&gt; and &lt;code&gt;shared&lt;/code&gt;, describing the same act, counted separately.&lt;/p&gt;

&lt;p&gt;Now consider what that does to a query. &lt;code&gt;WHERE key = 'residence'&lt;/code&gt; finds nothing if this turn's extraction called it &lt;code&gt;lives_in&lt;/code&gt;, and the next one called it &lt;code&gt;home_city&lt;/code&gt;, and a third called it &lt;code&gt;current_residence&lt;/code&gt;. The structured path, the one that was supposed to make the graph worth building, stays dark. Retrieval falls through to similarity, and you are back to the pattern in the previous section, paying for a graph you cannot query.&lt;/p&gt;

&lt;p&gt;The interesting part is the control case. One relation family in that same measurement was &lt;strong&gt;not&lt;/strong&gt; fragmented: 4.3 uses per label instead of 1.4, only 40% singletons. It was the one family where the extraction prompt already showed the model the vocabulary the system had established, with an instruction to reuse an existing label and only mint a new one when nothing fits. Same model, same text, same run. The only difference was whether the graph told the extractor what it already knew.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F114530s6m70r4jsod51u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F114530s6m70r4jsod51u.png" alt="same model" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A context graph is a loop with itself, not a function over text.&lt;/strong&gt; Feed the established vocabulary back. Canonicalise predicates at write time so two spellings of one idea land in one bucket. Resolve mentions against entities that already exist, rather than minting a new node for every phrasing, because the same customer becoming five nodes means every traversal lies.&lt;/p&gt;

&lt;p&gt;Some hard-won specifics, since this is where the work actually is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is no correct base form a priori.&lt;/strong&gt; We tried folding inflections and the naive rules are landmines: strip a trailing &lt;code&gt;e&lt;/code&gt; and &lt;code&gt;care&lt;/code&gt; merges with &lt;code&gt;car&lt;/code&gt;; undouble a consonant and &lt;code&gt;roll&lt;/code&gt; merges with &lt;code&gt;role&lt;/code&gt;; strip a trailing &lt;code&gt;s&lt;/code&gt; and &lt;code&gt;news&lt;/code&gt; becomes &lt;code&gt;new&lt;/code&gt;. Any stemmer aggressive enough to bucket reliably produces non-words that then surface in your API responses. What worked was narrower and duller: only ever fold onto a spelling this graph already uses, never toward an invented lemma.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Semantic merging is a judgement about meaning, not a normalisation.&lt;/strong&gt; &lt;code&gt;home_city&lt;/code&gt; and &lt;code&gt;city_of_residence&lt;/code&gt; probably mean the same thing. "Use metric units" and "always answer in metric" probably do too. Fold them wrongly and you have discarded something a user explicitly said. We left that undone deliberately, and said so, rather than pretending a string function had settled it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time is two axes and they are not the same axis.&lt;/strong&gt; When a fact was stated is not when the thing happened. If you store one and render it as the other, "when did X happen" gets answered with when it was mentioned, confidently and wrongly. Relative phrases are worse: "last week" is a window, not an instant, and letting the answering model do calendar arithmetic at read time from whatever dates happened to be in its context is exactly the failure you are trying to remove. Resolve at write time, against the source's own date, and store what was actually stated. Store nothing when nothing was stated, so that "no event time" and "the event time equals the ingest time" stop being the same value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dates do not belong in the text you embed.&lt;/strong&gt; We had resolved windows interpolated into the fact sentence that was both the keyword document and the embedded vector. Those date tokens say nothing about what the fact means, and they pollute the retrieval key for every query. Dates live in columns. Renderers read columns. Text and columns cannot disagree if there is only one copy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Half of what extraction produces is not linked to anything.&lt;/strong&gt; In our measurement, 52% of action objects were free text rather than a resolved entity: the graph recorded that somebody did something to a string. That string is invisible to traversal. Extraction reported success. The graph got nothing it can walk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An edge nothing reads is not memory.&lt;/strong&gt; We shipped an alias edge, wrote a careful writer for it, and then pulled it, because no retrieval path traversed it. It was structure with no consequence.&lt;/p&gt;

&lt;p&gt;All of this decides whether the context graph means anything. Meaning cannot be extrapolated from a graph that was never normalised, because the meaning was discarded before the write. And none of it is a one-time import: entity resolution and temporal invalidation are standing jobs, running every turn, or "what is true" decays into "what was true in March."&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually breaks
&lt;/h2&gt;

&lt;p&gt;Skip the forty-agent overnight graph. That is how you max a bill and learn nothing.&lt;/p&gt;

&lt;p&gt;Here is what fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State lives only in the transcript.&lt;/strong&gt; You cannot debug it, audit it, or resume it cleanly if that is the only place it lives. IDs, permissions, and intermediate facts need a home outside the prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything is agentic.&lt;/strong&gt; Money movement, PII, deletes, external side effects: those edges should be code, not model judgement. Give the model freedom where exploration actually pays off, and encode the paths you already know are correct.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verifiers share the same spoiled context.&lt;/strong&gt; Agents on the same model, reading the same flawed packet, agree with each other at industrial scale, which is why verifier context needs to be separated and drawn from evidence outside the graph: tests that ran, money that moved, a human weighing in before the expensive push. Organised nonsense is still nonsense.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You drew a DAG and production needed cycles.&lt;/strong&gt; Retries, clarification, a human pausing and resuming the flow: these are not a design smell, they are how work actually works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your context graph is a pile of triples with no ontology.&lt;/strong&gt; Model the domain before you extract at scale. Schema is what stops you from lying to yourself about what the graph actually means, not overhead for its own sake.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Extraction is stateless.&lt;/strong&gt; Every turn invents its own vocabulary because nothing showed it the vocabulary that already exists. This is the single cheapest thing to fix and the one most people have not fixed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The graph is built once and never maintained.&lt;/strong&gt; Extraction is the easy day. Entities need resolving, or the same customer becomes five nodes and every traversal lies. Facts need expiry, or superseded beliefs sit next to current ones and both look true.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retrieval is three searches and a re-ranker.&lt;/strong&gt; Each leg is blind to the others' constraints, so you over-fetch to compensate, pay for the same fact three times, and hand a re-ranker the job of guessing what the query planner should have known.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write-back is optional.&lt;/strong&gt; The agent learns a preference, a failed approach, a new link between entities. If that write is not durable and visible under the same permissions as the read, the next node is guessing again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No stop rule.&lt;/strong&gt; Parallel agents without token, time, and tool budgets will happily burn through a budget without producing anything you can point to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape everyone ships
&lt;/h2&gt;

&lt;p&gt;Most "graph engineered" demos still look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6m3oihziwgc4zxyzbd6x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6m3oihziwgc4zxyzbd6x.png" alt="five separate systems" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Four taxes, every time.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Context leaks at the seams. Relationships, history, and metadata fragment whenever they cross a boundary.&lt;/li&gt;
&lt;li&gt;The same fact enters the prompt three times, and you pay for it three times.&lt;/li&gt;
&lt;li&gt;No query can be planned across the whole picture, so every retrieval is a wide guess narrowed after the fact.&lt;/li&gt;
&lt;li&gt;Ops multiplies: five configs, five monitors, five failure modes, and the glue quietly becomes the product.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You want the context graph and the durable data to be one thing. A context graph that cannot hold your application records, your documents, your vectors and your temporal state is a copy of your real data that you will spend the next year syncing.&lt;/p&gt;

&lt;p&gt;This is why I flinch when graph engineering is reduced to "use a graph database." A graph-only store solves a real slice, but agents need documents, vectors, time, session state, and above all the ability to ask one question that is simultaneously a traversal, a similarity search and a filter. Split those across stores and that question stops being expressible at any price.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like inside a bank
&lt;/h2&gt;

&lt;p&gt;Most of the graph-engineering conversation right now is about routing and reliability: how do I get five agents to hand work to each other without falling over. That is a startup's version of the problem.&lt;/p&gt;

&lt;p&gt;Ask the same question inside a bank, an insurer or a global enterprise and the requirements change shape entirely. Every enterprise conversation I have had this year has landed on some version of the same list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shared context, not per-agent context.&lt;/strong&gt; Twenty agents across four teams need the same view of the same customer. If each one has its own vector store, you do not have twenty agents, you have twenty inconsistent opinions with an org chart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Permissions that travel with the data.&lt;/strong&gt; Not permissions checked at the API gateway and then discarded. If retrieval cannot enforce who may see which fact inside the query itself, then every prompt is a potential exfiltration path and every new agent is a new review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Provenance carried on the facts themselves.&lt;/strong&gt; Who said this, in which document, on what date, and is that source still authoritative. "The model said so" is not a defensible answer when the fact ends up in a regulatory filing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auditable retrieval.&lt;/strong&gt; Not just what the agent answered, but what it read to get there, and why those rows and not others. A re-ranker's opinion over three fused lists is close to unexplainable. A query with predicates in it explains itself.&lt;/p&gt;

&lt;p&gt;Underneath all of it sits temporal truth: what did we believe on the day the decision was made, not what do we believe now. In a regulated firm the record exists to answer that question, which means facts need validity intervals and reads need to resolve as of a point in time rather than as of now. And the graph needs a boundary in both directions, with control over what is allowed into it and over what is allowed out of it into a prompt.&lt;/p&gt;

&lt;p&gt;None of that is served by a prettier flowchart, and none of it is served by a vector store with a metadata filter bolted on. It is served by the context graph being a real database: transactional, permissioned, temporal, and auditable. The current discourse is not covering this, and it is what enterprises are buying.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we set out to build
&lt;/h2&gt;

&lt;p&gt;I am biased. I will say that up front.&lt;/p&gt;

&lt;p&gt;We built SurrealDB as a multi-model engine: documents, graphs, vectors, time-series, full-text, relational, and auth in one system with one query language over them. The reason has nothing to do with graphs being fashionable. It is that the useful query is almost never one shape. It is "things related to this, that resemble that, that were true then, that this user may see," and every system that stores those four facts separately can only answer it by approximation and reassembly.&lt;/p&gt;

&lt;p&gt;On the context side that means schema as ontology. Edges carry confidence, time, and source as fields rather than afterthoughts. Vectors live on the entities they came from, so similarity is an operator inside a traversal rather than a separate service you join against afterwards. Traversal, similarity, full-text, and filters resolve in one statement, planned together, instead of three round trips and a sync cron you forgot about until it pages you at 2am. Write-back lands transactionally under the same permission model you read from, which matters, but it is the floor rather than the pitch.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcs3ab6rfyn31gmykll51.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcs3ab6rfyn31gmykll51.png" alt="Where the layers actually sit" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Spectron is the memory layer we built on that engine: entity extraction, predicate normalisation, entity resolution, temporal facts, and hybrid retrieval that is one query rather than three. Most of the engineering effort there has gone into what happens between the text arriving and the graph existing, rather than into retrieval algorithms. When the session ends, the context window forgets, but the system should not.&lt;/p&gt;

&lt;p&gt;We are also building the comparison, because I would not take my word for it either: the same multi-agent application twice, once on an orchestrator plus a vector store plus a relational database plus a session cache, and once on the same orchestrator with a single context graph underneath. Same agents, same tasks, same model. Measured on context accuracy, tokens consumed, latency, network hops and lines of glue.&lt;/p&gt;

&lt;p&gt;The control plane stays yours. We are complementary on purpose. The question I would ask is not whose orchestrator you use, but whether your retrieval can express a traversal and a similarity search in the same breath, and whether anything understood your data before it became a graph.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist I would actually use
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Scope
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;One recurring job with a real success metric, not a general assistant.&lt;/li&gt;
&lt;li&gt;Someone owns the ontology, someone owns the workflow, and early on that can be the same person.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Execution graph
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Label nodes: code / single LLM call / tool / full agent.&lt;/li&gt;
&lt;li&gt;Edges only where work actually flows.&lt;/li&gt;
&lt;li&gt;A verifier path with fresh context for high-risk output.&lt;/li&gt;
&lt;li&gt;A human gate on irreversible actions.&lt;/li&gt;
&lt;li&gt;Explicit stops and budgets.&lt;/li&gt;
&lt;li&gt;Cycles where retries and clarification are real.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Before the context graph exists
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Entity and relation types written down before bulk extraction.&lt;/li&gt;
&lt;li&gt;Extraction shown the vocabulary the graph already uses, with an instruction to reuse it.&lt;/li&gt;
&lt;li&gt;Predicates canonicalised at write time, folding only onto spellings the graph already has.&lt;/li&gt;
&lt;li&gt;Mentions resolved to existing entities, with the unresolved rate measured rather than assumed.&lt;/li&gt;
&lt;li&gt;Event time and mention time stored as separate columns, resolved at write time, absent when nothing was stated.&lt;/li&gt;
&lt;li&gt;Nothing in your embedded text that is not part of what the fact means.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Context graph
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Provenance on facts and edges.&lt;/li&gt;
&lt;li&gt;Temporal validity, and a job that actually retires superseded beliefs.&lt;/li&gt;
&lt;li&gt;A retrieval path that constrains before it ranks, explainable in one sentence.&lt;/li&gt;
&lt;li&gt;Write-back after every turn for memories, entities, decisions.&lt;/li&gt;
&lt;li&gt;Permissions enforced inside retrieval, not at the gateway.&lt;/li&gt;
&lt;li&gt;Graph statistics on a dashboard: predicates per fact, unresolved mentions, duplicate entities. If you cannot see fragmentation, you will not fix it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Reality checks
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Outside-the-graph evidence for ship decisions.&lt;/li&gt;
&lt;li&gt;Evals that include multi-hop and time questions.&lt;/li&gt;
&lt;li&gt;Cost and latency budgets per node type.&lt;/li&gt;
&lt;li&gt;Replayable state when something fails.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Grow the graph when the work forces you, not when a meme does.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would ignore
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Renaming your orchestrator and calling it strategy.&lt;/li&gt;
&lt;li&gt;Forty agents overnight.&lt;/li&gt;
&lt;li&gt;Treating GraphRAG, context graphs, and execution graphs as the same slide.&lt;/li&gt;
&lt;li&gt;Bigger context windows as a substitute for structure.&lt;/li&gt;
&lt;li&gt;A better re-ranker as a substitute for a query that could have been planned properly.&lt;/li&gt;
&lt;li&gt;Holy wars about anyone's database. The architectural gap is enough, and the market does not need another feud.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Graph engineering became the new buzzword because one chatty loop is not enough for real work. Good. Draw the execution graph. Be honest about where the model decides and where code decides.&lt;/p&gt;

&lt;p&gt;Then do the half most threads skip. Understand the data before you write it: one vocabulary, resolved entities, real event times, facts that expire. Put the vectors on those entities rather than in another system, so retrieval is one query that traverses and ranks together instead of three searches arguing through a re-ranker. Make permissions and provenance properties of the data rather than of the API in front of it.&lt;/p&gt;

&lt;p&gt;The label will get renamed again. It always does, and I would not build a roadmap on the word. But the two graphs are not a naming fashion. Every serious agent system has an execution graph and a context graph whether or not anyone drew the second one, and the gap between them is where the accuracy goes, where the token bill comes from, and where the audit fails.&lt;/p&gt;

&lt;p&gt;Frameworks draw the graph of work. Something has to hold the graph of knowledge, keep it true, and survive the write-back. That is the problem we set out to solve with SurrealDB, and with Spectron, the memory layer we built on top of it. If your agent topology is getting serious and your context layer is still five separate systems you are hoping stay in sync, that is the seam to fix.&lt;/p&gt;

</description>
      <category>graphengineering</category>
      <category>ai</category>
      <category>database</category>
      <category>agents</category>
    </item>
    <item>
      <title>Introducing Scale: SurrealDB Cloud, built for high availability and scale</title>
      <dc:creator>Mark Gyles</dc:creator>
      <pubDate>Thu, 02 Jul 2026 15:53:29 +0000</pubDate>
      <link>https://dev.to/surrealdb/introducing-scale-surrealdb-cloud-built-for-high-availability-and-scale-2ib</link>
      <guid>https://dev.to/surrealdb/introducing-scale-surrealdb-cloud-built-for-high-availability-and-scale-2ib</guid>
      <description>&lt;p&gt;Author: &lt;a href="https://x.com/tobiemh" rel="noopener noreferrer"&gt;Tobie Morgan Hitchcock&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Today we're launching Scale, a new tier of SurrealDB Cloud built for the workloads you can't afford to have go down.&lt;/p&gt;

&lt;p&gt;Our first tier, Start, was designed for building and shipping fast. Scale is designed for what happens next: production traffic, uptime commitments, and the kind of resilience your users never notice because there are always available nodes. It's the tier for teams running SurrealDB as the scalable context layer behind real applications and AI agents in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get with Scale
&lt;/h2&gt;

&lt;p&gt;Scale is about one thing: keeping your database available and consistent under real-world conditions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Highly-available, fault-tolerant clusters.&lt;/strong&gt; Scale runs your database as a multi-node cluster designed to survive node and infrastructure failures without dropping writes or losing consistency. A single point of failure is no longer a single point of downtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiple availability-zone deployment.&lt;/strong&gt; Your cluster is distributed across multiple availability zones, so even the loss of an entire zone doesn't take your database with it. Traffic keeps flowing while the cluster recovers in the background.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Horizontal scale.&lt;/strong&gt; As demand grows, Scale grows with it. Add capacity by scaling out across nodes rather than being capped by the size of a single machine. Start with three nodes, and keep adding to scale your application or agent's needs.&lt;/p&gt;

&lt;p&gt;See more information about SurrealDB Cloud Scale architecture &lt;a href="https://surrealdb.com/docs/manage/cloud/architecture#scale" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built on SurrealDS
&lt;/h2&gt;

&lt;p&gt;Scale is powered by &lt;a href="https://surrealdb.com/platform/surrealds" rel="noopener noreferrer"&gt;SurrealDS&lt;/a&gt;, SurrealDB's distributed storage engine and the foundation that makes all of this possible.&lt;/p&gt;

&lt;p&gt;SurrealDS is a new generation distributed storage architecture, rethought from first principles. Instead of coupling storage to compute on a single box or to a proprietary cloud tier, SurrealDS embeds consensus directly in SurrealDB nodes and separates the two layers cleanly. Here's what that architecture gives you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture overview
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Compute and storage separation.&lt;/strong&gt; Scale compute for QPS and storage for capacity independently, so you provision for the dimension that's actually under pressure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No single leader.&lt;/strong&gt; Each availability-zone node writes locally, writes scale horizontally, and transactions commit once a quorum acknowledges them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Multi-write nodes.&lt;/strong&gt; Every write node in the cluster can accept and coordinate transactions - there's no bottleneck routing all writes through one leader.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reduced operational overhead.&lt;/strong&gt; Consensus is embedded directly in SurrealDB nodes, eliminating external coordination services.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fewer network dependencies.&lt;/strong&gt; A single broadcast replaces multi-hop coordination, reducing latency.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lower latency than leader-based replication.&lt;/strong&gt; With no single leader and each AZ node writing locally, transactions avoid the extra round trips of traditional leader-based systems.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Future roadmap
&lt;/h2&gt;

&lt;p&gt;Several SurrealDS capabilities are on the near-term roadmap and will land in the Scale tier as they ship:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Object-storage backed.&lt;/strong&gt; Persist transactional data directly to commodity object storage - storage costs drop and capacity scales far beyond any single disk.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cross-region replication.&lt;/strong&gt; Data flows through object storage rather than between nodes, significantly reducing data-transfer costs while keeping regions in sync.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Instant recovery.&lt;/strong&gt; A crashed node restores from object storage, with recovery time almost instant regardless of dataset size.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Instant branching.&lt;/strong&gt; Clone a petabyte-scale database in seconds for development, testing, or experimentation.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Get started
&lt;/h2&gt;

&lt;p&gt;If you're running SurrealDB in production, or getting ready to, Scale gives you the availability and fault-tolerance your workloads need, on the storage architecture no one else has.&lt;/p&gt;

&lt;p&gt;Explore the tier in &lt;a href="https://surrealdb.com/cloud" rel="noopener noreferrer"&gt;SurrealDB Cloud&lt;/a&gt;, and dig into the engine underneath at &lt;a href="https://surrealdb.com/platform/surrealds" rel="noopener noreferrer"&gt;SurrealDS&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>surrealdb</category>
      <category>surrealdbcloud</category>
      <category>cloud</category>
      <category>database</category>
    </item>
    <item>
      <title>SurrealDB is now available on the Nebius AI Cloud Marketplace</title>
      <dc:creator>Mark Gyles</dc:creator>
      <pubDate>Thu, 18 Jun 2026 15:33:29 +0000</pubDate>
      <link>https://dev.to/surrealdb/surrealdb-is-now-available-on-the-nebius-ai-cloud-marketplace-34fj</link>
      <guid>https://dev.to/surrealdb/surrealdb-is-now-available-on-the-nebius-ai-cloud-marketplace-34fj</guid>
      <description>&lt;p&gt;Author: &lt;a href="https://x.com/tobiemh" rel="noopener noreferrer"&gt;Tobie Morgan Hitchcock&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We're excited to announce that SurrealDB is now officially available on the Nebius AI Cloud Marketplace.&lt;/p&gt;

&lt;p&gt;This launch reflects our commitment to meeting developers and AI teams where they build, and deepens our partnership with Nebius to make SurrealDB easier to adopt, deploy, and scale on one of the fastest-growing AI cloud platforms in the world.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Nebius?
&lt;/h2&gt;

&lt;p&gt;Nebius is a full-stack AI cloud built for the demands of modern AI workloads. Powered by the latest GPU and high-performance networking infrastructure, with managed platform, serverless AI, and storage built for the entire AI lifecycle, Nebius gives teams the infrastructure to train, tune, and serve models at scale.&lt;/p&gt;

&lt;p&gt;As an NVIDIA Reference Platform Cloud Partner with infrastructure across Europe and the US, Nebius pairs bare-metal performance with the operational simplicity and cost efficiency that AI teams need to move from experiment to production. It's a natural home for SurrealDB - giving you a unified data layer right alongside the compute powering your AI and agentic applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why SurrealDB?
&lt;/h2&gt;

&lt;p&gt;SurrealDB combines document, graph, and relational capabilities in one unified database - designed for agent memory, context layers, modern applications, real-time systems, and AI-powered workloads.With flexible schema, SurrealQL, and built-in real-time features, SurrealDB enables you to build semantic and context layers powered by knowledge graphs for agent memory and multi-agentic workflows. &lt;/p&gt;

&lt;p&gt;SurrealDB simplifies your technology stack, reduces Total Cost of Ownership and operational complexity, and allows you to ship products and features in days rather than weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get started
&lt;/h2&gt;

&lt;p&gt;SurrealDB is available now on the &lt;a href="https://console.nebius.com/project-e00j0yxxpr003gvmj7t4vd/applications/surrealdb" rel="noopener noreferrer"&gt;Nebius AI Cloud Marketplace&lt;/a&gt;. We can't wait to see what you build, join our &lt;a href="https://discord.com/invite/surrealdb" rel="noopener noreferrer"&gt;Discord community&lt;/a&gt; and let us know how you get on!&lt;/p&gt;

</description>
      <category>surrealdb</category>
      <category>database</category>
      <category>nebius</category>
      <category>ai</category>
    </item>
    <item>
      <title>SurrealDB 3.x by the numbers</title>
      <dc:creator>Mark Gyles</dc:creator>
      <pubDate>Fri, 29 May 2026 19:20:49 +0000</pubDate>
      <link>https://dev.to/surrealdb/surrealdb-3x-by-the-numbers-39ao</link>
      <guid>https://dev.to/surrealdb/surrealdb-3x-by-the-numbers-39ao</guid>
      <description>&lt;p&gt;Author: &lt;a href="https://x.com/tobiemh" rel="noopener noreferrer"&gt;Tobie Morgan Hitchcock&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  One engine, multi-workloads, full durability.
&lt;/h2&gt;

&lt;p&gt;You can explore the full results, methodology, and per-database breakdowns at &lt;a href="https://surrealdb.com/benchmarks" rel="noopener noreferrer"&gt;surrealdb.com/benchmarks&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we ran these
&lt;/h2&gt;

&lt;p&gt;Database benchmarks are notoriously easy to game, and difficult to get right. Different hardware, different durability settings, different client libraries, a workload that happens to suit one engine's indexing strategy - any of those will tilt the numbers. So we did three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Ran every database on the same hardware - an AMD Ryzen Threadripper 9970X (32C/64T), 128 GiB DDR5, NVMe storage, Ubuntu 24.04.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Used the same open-source harness - crud-bench - with each workload translated into each engine's native query language so no database is penalised for an unfamiliar dialect.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Configured every engine for production-grade durability - fsync on, snapshot isolation, no in-memory shortcuts (except where explicitly noted for embedded comparisons).&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We also went out of our way to give every database a fair shot. Rather than running each engine on its out-of-the-box defaults, we used&amp;nbsp;&lt;strong&gt;optimised configurations across the board&lt;/strong&gt;&amp;nbsp;- the same kind of tuning a production team would apply before going live. That meant raising connection and worker pool limits to match the 128-client load, sizing buffer pools, page caches, and shared memory to take advantage of the available 128 GiB of RAM, enabling parallel query execution and prepared-statement caching where supported, setting WAL and checkpoint intervals to values recommended by each project's own performance guides, and turning on the indexes and storage engines (InnoDB, WiredTiger, RocksDB-backed stores, etc.) that each database's documentation recommends for OLTP workloads. The goal was to make sure no engine was held back by a conservative default - if a database underperforms here, it isn't because we left it on its laptop-friendly starter config.&lt;/p&gt;

&lt;p&gt;Workloads run with 128 clients issuing 48 concurrent queries each, against datasets of a single table with 5 - 15 million rows of mixed-type records (strings, integers, floats, UUIDs, datetimes, booleans, large text fields, geospatial data, and nested objects and arrays).&lt;/p&gt;

&lt;h2&gt;
  
  
  About last time
&lt;/h2&gt;

&lt;p&gt;We owe a word on durability. The previous round of benchmark results ran with&amp;nbsp;&lt;code&gt;fsync&lt;/code&gt;&amp;nbsp;disabled for every engine - leaving each database's writes in the OS page cache rather than flushed to disk. Every database in the comparison ran with the same setting, so nothing was being "fudged" relative to the other engines, but we didn't make the setting explicit, and the headline numbers ended up describing a workload that most production deployments would not likely run.&lt;/p&gt;

&lt;p&gt;This round is different.&amp;nbsp;&lt;strong&gt;Every database in these benchmarks runs with full disk durability enabled&lt;/strong&gt;&amp;nbsp;-&amp;nbsp;&lt;code&gt;fsync&lt;/code&gt;&amp;nbsp;on, WAL flushed on every commit, no buffered writes hiding behind the page cache. The configuration files for each engine are checked into the &lt;a href="https://github.com/surrealdb/crud-bench" rel="noopener noreferrer"&gt;crud-bench repository&lt;/a&gt;&amp;nbsp;so anyone can audit them. The numbers above are what each engine sustains when every committed transaction is on disk before the client gets an acknowledgement. That's slower than the cache-friendly numbers you'll find in some marketing posts, ours included, but it's the only honest way to compare databases that are going to outlive a power outage.&lt;/p&gt;

&lt;h2&gt;
  
  
  &amp;nbsp;How far SurrealDB has come
&lt;/h2&gt;

&lt;p&gt;The biggest story is internal. Across three major releases, we've fundamentally rebuilt the query, parser, and storage layers:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo8rdhc9mhnypvtd651en.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo8rdhc9mhnypvtd651en.png" alt="how far SurrealDB has come" width="800" height="170"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Between SurrealDB 2.x and 3.x alone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;31% faster mean CRUD throughput&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;58% faster batch operations&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;11,894% faster non-indexed full-table scans&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;136% faster indexed queries&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tail latency improvements of 27% (CRUD), 32% (batches), 59% (indexed), and 99% (scans)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The scan number is not a typo. The SurrealDB 3.x query planner and storage engine eliminates the per-row decoding overhead that dominated earlier versions, which is why a workload that used to take minutes now completes in seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we compare to other databases
&lt;/h2&gt;

&lt;p&gt;SurrealDB is a durable, transactional, multi-model database, so the comparisons that matter most are against the primary databases people actually evaluate it against - Postgres for relational, MongoDB for document, Neo4j for graph. Here's how the same workload looks across those three categories, run on the same hardware with each engine on a tuned production-grade configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  vs. PostgreSQL (and MySQL)
&lt;/h3&gt;

&lt;p&gt;CRUD throughput (ops/s) and bulk-read metrics:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsrc8zkre8pfllgqwdrhp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsrc8zkre8pfllgqwdrhp.png" alt="vs. PostgreSQL and MySQL" width="800" height="285"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;SurrealDB is faster than Postgres on every write operation - roughly&amp;nbsp;&lt;strong&gt;1.5× faster creates, 1.3× faster updates, and 1.8× faster deletes&lt;/strong&gt;&amp;nbsp;- while Postgres still edges ahead on raw single-record reads. Against MySQL the gap widens dramatically: SurrealDB is&amp;nbsp;&lt;strong&gt;5 - 7× faster on writes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Averaged across creates, updates, and deletes,&amp;nbsp;&lt;strong&gt;SurrealDB delivers ~1.5× the write throughput of Postgres&lt;/strong&gt;&amp;nbsp;- the headline number the benchmarks page leads the Relational category with - and beats Postgres by ~1.5× on full-table counts. Postgres' query planner is 30 years old and still ahead on indexed predicate filtering; we're not pretending otherwise, and we're working on closing that gap in 3.1.&lt;/p&gt;

&lt;h3&gt;
  
  
  vs. MongoDB (and ArangoDB)
&lt;/h3&gt;

&lt;p&gt;CRUD throughput (ops/s) and bulk-read metrics:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fammier1thf3i9smmihcx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fammier1thf3i9smmihcx.png" alt="vs. MongoDB and ArangoDB" width="800" height="235"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the closest race. MongoDB still leads on single-record writes, while SurrealDB is&amp;nbsp;&lt;strong&gt;~1.3× faster on reads&lt;/strong&gt;&amp;nbsp;- and the picture flips on heavier workloads. On predicate filter scans against unindexed tables - the headline figure the benchmarks page uses for the Document category -&amp;nbsp;&lt;strong&gt;SurrealDB is roughly 2.7× faster than MongoDB&lt;/strong&gt;, with consistently lower mean and p99 latency. Against ArangoDB's document engine, SurrealDB is between&amp;nbsp;&lt;strong&gt;100× and 150× faster on writes&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  vs. Neo4j (and ArangoDB)
&lt;/h3&gt;

&lt;p&gt;CRUD throughput (ops/s) and bulk-read metrics:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4s2ttjs7y6alh38f9nev.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4s2ttjs7y6alh38f9nev.png" alt="vs. Neo4j and ArangoDB" width="800" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;SurrealDB outperforms Neo4j across every CRUD operation -&amp;nbsp;&lt;strong&gt;roughly 2 - 3.5× faster&amp;nbsp;on writes&lt;/strong&gt; and&amp;nbsp;&lt;strong&gt;1.5× faster on reads&lt;/strong&gt;&amp;nbsp;- while running the same graph traversals through the same engine that handles documents and tables.&lt;/p&gt;

&lt;p&gt;The gap on filtered scans is even more dramatic. Across indexed predicate filter queries - the headline metric the benchmarks page uses for the Graph category -&amp;nbsp;&lt;strong&gt;SurrealDB is roughly 35× faster than Neo4j&lt;/strong&gt;. No separate database, no separate query language, no separate operational story.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reference point: Redis and KeyDB
&lt;/h3&gt;

&lt;p&gt;For raw key-value throughput, we also ran SurrealDB's in-memory engine (with append-only persistence) against Redis and KeyDB:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvy0ei2goby1x5h5mza5f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvy0ei2goby1x5h5mza5f.png" alt="Redis and KeyDB" width="798" height="197"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's roughly&amp;nbsp;&lt;strong&gt;3× faster than Redis on writes, updates, and deletes&lt;/strong&gt;, while offering durable, snapshot-isolated transactions and a full query language Redis doesn't have. Redis still wins on large 1,000-record batch operations and edges ahead on single-record reads.&lt;/p&gt;

&lt;h3&gt;
  
  
  Embedded mode (vs. SQLite)
&lt;/h3&gt;

&lt;p&gt;SurrealDB's embedded engine runs the same SurrealQL on the same disk format as the server.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F87t5kfwba1xj02u5v5cc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F87t5kfwba1xj02u5v5cc.png" alt="vs. SQLite" width="799" height="273"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Against SQLite, SurrealDB is roughly&amp;nbsp;&lt;strong&gt;85× faster on creates, 110× faster on updates, and 75× faster on deletes&lt;/strong&gt;, with single-record reads in the same ballpark. On predicate filter scans against unindexed tables it's around&amp;nbsp;&lt;strong&gt;6.5× faster&lt;/strong&gt;&amp;nbsp;than SQLite, and on indexed filter scans, around&amp;nbsp;3× faster.&lt;/p&gt;

&lt;p&gt;The full breakdowns - including p50, p95, and p99 latencies, batch sizes from 100 to 1,000 rows, indexed and non-indexed predicate filters, and full-text search - are on the &lt;a href="https://surrealdb.com/benchmarks" rel="noopener noreferrer"&gt;benchmarks page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing the remaining gaps
&lt;/h2&gt;

&lt;p&gt;The numbers above are an honest snapshot, not a finish line. There are still workloads where Redis, MongoDB, and Postgres beat us - large batch operations vs. Redis, single-record writes vs. Mongo, indexed predicate filtering vs. Postgres - and we know exactly where each gap comes from. Closing those gaps is the central focus of the SurrealDB 3.1 cycle.&lt;/p&gt;

&lt;p&gt;What we're actively working on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Batch path rewrites to bring 100-row and 1,000-row batched ops closer to Redis throughput, including better client-side pipelining and a leaner server-side batch executor.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A smarter query planner with cost-based optimisation, predicate pushdown into the storage engine, and richer index selectivity statistics - the work that gets us to parity with Postgres on indexed filter scans.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Storage layer improvements for the document workload - more compact in-place updates, sharper write amplification, and tighter integration between the key encoding and the document path resolver - which is where Mongo currently has the edge on single-record writes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Vector and graph traversal optimisations as those workloads land in the benchmark suite, so the multi-model story holds up at the same rigour as CRUD.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal isn't "fastest at one thing." It's to be&amp;nbsp;&lt;strong&gt;fastest, or competitive, across every workload that matters&lt;/strong&gt;, while keeping the one property no specialist engine can match: a single query language - SurrealQL - that handles relational, document, graph, key-value, time-series, vector, and full-text search data in the same database, with the same transactional guarantees, on the same disk format - and the same engine, whether you're running it embedded inside an application, on a single server, at the edge close to your users, or distributed across hundreds of nodes for horizontal scale. We don't think you should have to choose between Postgres, Mongo, Neo4j, and Redis - or between a database that runs on a developer laptop and one that runs across a global fleet. We think a single database should run all four shapes of workload at production speed, anywhere it needs to live, and the SurrealDB 3.x numbers above are the strongest evidence yet that it can.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this matters for AI agents
&lt;/h3&gt;

&lt;p&gt;There's a reason we keep pushing on this combination of data models, and it's not historical accident.&amp;nbsp;&lt;strong&gt;Agent memory is multi-model by nature&lt;/strong&gt;.&amp;nbsp;A useful AI agent needs structured facts about the world (relational), semi-structured context and tool outputs (document), entity and event relationships (graph), embeddings for semantic recall (vector), keyword and BM25 retrieval over its corpus (full-text search), episodic and temporal context (time-series), and fast session and cache state (key-value) - and it needs all of that in a single transactionally consistent store, because the moment those shapes live in separate databases, you've built a glue-code problem that breaks every time the schema changes or the model is updated.&lt;/p&gt;

&lt;p&gt;Agents also need that memory&amp;nbsp;&lt;strong&gt;close to where they run&lt;/strong&gt;. An agent reasoning inside a browser, a phone, an in-vehicle system, or a per-tenant edge worker can't afford a round trip to a central database for every retrieval. The fact that SurrealDB runs as an embedded engine on the same disk format as the distributed server - with the same query language and the same transactional guarantees - is what makes it usable as the memory layer for agents that move fluidly between local, edge, and centralised deployments. Faster CRUD, faster scans, and faster indexed lookups aren't just abstract benchmark wins; they're how many tools an agent can call, how much context it can recall per turn, and how many concurrent agents a single host can support. That's the workload SurrealDB 3.x is built for, and it's the workload the next round of benchmarks - covering vector search, graph traversal, and full-text retrieval - will measure head-on.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;We intend to expand the benchmarks to also cover&amp;nbsp;&lt;strong&gt;CockroachDB, TiDB, MongoDB, and Aerospike&lt;/strong&gt;&amp;nbsp;for distributed comparisons, which we'll publish in a future round. We also plan to expand the workload set to include graph traversals vector search, and full-text search, two areas where the single-engine, multi-model design of SurrealDB shows its biggest advantages.&lt;/p&gt;

&lt;p&gt;Until then: the harness is open source, the results are reproducible, and we'd love for you to run them on your own hardware and tell us what you find.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://surrealdb.com/benchmarks" rel="noopener noreferrer"&gt;See the full benchmarks&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>surrealdb</category>
      <category>database</category>
      <category>benchmarks</category>
      <category>news</category>
    </item>
    <item>
      <title>SurrealDB 3.1: stability, DiskANN, and a new release process</title>
      <dc:creator>Mark Gyles</dc:creator>
      <pubDate>Thu, 28 May 2026 08:43:39 +0000</pubDate>
      <link>https://dev.to/surrealdb/surrealdb-31-stability-diskann-and-a-new-release-process-20cj</link>
      <guid>https://dev.to/surrealdb/surrealdb-31-stability-diskann-and-a-new-release-process-20cj</guid>
      <description>&lt;p&gt;Author: &lt;a href="https://github.com/tobiemh" rel="noopener noreferrer"&gt;Tobie Morgan Hitchcock&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three months after 3.0 went GA, we're excited to announce that SurrealDB 3.1 is now available. This is the first minor release in the 3.x series. It builds on the foundations we shipped in 3.0 with a focus on stability, a second approximate-nearest-neighbour index in DiskANN, and a substantial round of security hardening. Alongside the release, we're also rolling out a change to how we develop and ship SurrealDB.&lt;/p&gt;

&lt;p&gt;The full list of changes is in the &lt;a href="https://surrealdb.com/releases/3.1.0" rel="noopener noreferrer"&gt;3.1 release notes&lt;/a&gt;. Below are the highlights.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's new in 3.1
&lt;/h2&gt;

&lt;h3&gt;
  
  
  DiskANN approximate-nearest neighbour index
&lt;/h3&gt;

&lt;p&gt;SurrealDB now ships DiskANN as a second ANN index type, sitting alongside HNSW. DiskANN trades a different set of memory and recall characteristics and is well suited to larger-than-memory vector workloads, which has been a recurring request from teams building production agent and search systems on SurrealDB.&lt;/p&gt;

&lt;p&gt;The introduction of DiskANN also drove an end-to-end overhaul of the ANN warm-lookup path. Both HNSW and DiskANN benefit from this work, with measurable improvements to warm-cache latency on the existing HNSW index for free.&lt;/p&gt;

&lt;p&gt;You can pick the index type at definition time with the new &lt;code&gt;DEFINE INDEX ... DISKANN&lt;/code&gt; syntax. See the release notes for the full set of options.&lt;/p&gt;

&lt;h3&gt;
  
  
  GraphQL: aliases, cursor pagination, and multi-model filtering
&lt;/h3&gt;

&lt;p&gt;SurrealDB v3.1 brings a significant upgrade to the GraphQL surface, making it more expressive, more predictable, and easier to integrate with modern GraphQL clients.&lt;/p&gt;

&lt;p&gt;The generated schema now follows Apollo conventions by default, with singular fetch, plural list queries, and &lt;code&gt;createX&lt;/code&gt; / &lt;code&gt;updateX&lt;/code&gt; / &lt;code&gt;deleteX&lt;/code&gt; mutations, so you always know what to expect without consulting per-database config. Fields and tables can now carry &lt;code&gt;GRAPHQL_ALIAS&lt;/code&gt; and &lt;code&gt;GRAPHQL_DEPRECATED&lt;/code&gt; clauses directly in their definitions, letting you decouple your SurrealQL identifiers from the names your API consumers see:&lt;/p&gt;

&lt;p&gt;Schema&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;DEFINE&lt;/span&gt; &lt;span class="n"&gt;FIELD&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;person&lt;/span&gt; &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;
  &lt;span class="n"&gt;GRAPHQL_ALIAS&lt;/span&gt; &lt;span class="nv"&gt;"firstName"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;DEFINE&lt;/span&gt; &lt;span class="n"&gt;FIELD&lt;/span&gt; &lt;span class="n"&gt;legacy_score&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt; &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
  &lt;span class="n"&gt;GRAPHQL_DEPRECATED&lt;/span&gt; &lt;span class="nv"&gt;"Use `rank` instead"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cursor pagination arrives in this release too. Each table now gets a &lt;code&gt;&amp;lt;plural&amp;gt;Connection&lt;/code&gt; query returning &lt;code&gt;edges&lt;/code&gt;, &lt;code&gt;pageInfo&lt;/code&gt;, and a lazily-evaluated &lt;code&gt;totalCount&lt;/code&gt;, so you only pay for the count when you ask for it. Offset pagination via &lt;code&gt;limit&lt;/code&gt; and &lt;code&gt;start&lt;/code&gt; remains fully supported alongside it.&lt;/p&gt;

&lt;p&gt;On the multi-model side, GraphQL queries can now reach across SurrealDB's full data model. Full-text search, vector similarity, and time-series aggregation are all directly queryable through the GraphQL API, unlocking AI-native and analytics-heavy workloads without touching SurrealQL.&lt;/p&gt;

&lt;p&gt;This release also closes a range of reported issues: stale schema caches after DDL changes, invalid filter identifiers on nested array fields, missing &lt;code&gt;id&lt;/code&gt; range and &lt;code&gt;in&lt;/code&gt; filters, and &lt;code&gt;count()&lt;/code&gt;-based predicates in &lt;code&gt;WHERE&lt;/code&gt; clauses, all resolved and covered by integration tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Improved in-memory performance
&lt;/h3&gt;

&lt;p&gt;SurrealDB's in-memory backend now uses a datastore based on optimistic lock coupling. This improves performance by allowing readers to acquire lock-free access, only retrying at the end if a modification has occurred in the meantime. This allows readers to proceed without blocking writers, and vice versa.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stability and correctness
&lt;/h3&gt;

&lt;p&gt;The bulk of the 3.1 work is bug fixes and stabilisation of the 3.0 line. This continues the stream of fixes that shipped in v3.0.1 through v3.0.5, with a substantial number of additional issues addressed across the query engine, indexing, and storage layers. If you upgraded from 2.x to 3.0 and hit any rough edges, there's a strong chance the relevant fix is in 3.1.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security hardening
&lt;/h3&gt;

&lt;p&gt;3.1 closes a batch of previously reported security vulnerabilities, alongside a set of previously undiscovered issues that we found internally. The undiscovered ones come from an increased investment in LLM-assisted security review, in line with broader industry practice. Mozilla wrote a good summary of this approach recently. The combined result is a notably larger security section in this release than in previous ones.&lt;/p&gt;

&lt;p&gt;The full security section is itemised in the release notes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enterprise: audit logging and slow-query pipeline
&lt;/h3&gt;

&lt;p&gt;Customers on the Enterprise tier get two new operational tools in 3.1:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A structured audit log capturing authentication events, schema changes, and other actions of interest to compliance and security teams.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A slow-query pipeline that surfaces queries exceeding a configurable threshold, complete with their plan and execution metadata, to help diagnose performance issues in production.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both have been on Enterprise customer wishlists for a while, and we're pleased to have them shipping in 3.1.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other quality of life additions
&lt;/h3&gt;

&lt;p&gt;Among the many changes mentioned in the 3.1 release notes, some of the changes that you won't want to miss are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A built-in MCP server for AI tools and IDEs,Unified OpenTelemetry metrics and logging pipeline,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Unified OpenTelemetry metrics and logging pipeline,W3C trace propagation across HTTP and WebSocket,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;W3C trace propagation across HTTP and WebSocket,Full ALTER coverage for every DEFINE statement,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Full ALTER coverage for every DEFINE statement,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;And continued executor and index improvements beyond the in-memory engine.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  A new release process
&lt;/h3&gt;

&lt;p&gt;3.1 is also the first release we've shipped under a new development workflow, and we want to be transparent about what's changed and why.&lt;/p&gt;

&lt;p&gt;For most of SurrealDB's history, we've developed in public on the main &lt;code&gt;surrealdb/surrealdb&lt;/code&gt; repository. When a security vulnerability needed addressing, we'd cut a temporary private fork, fix the issue there, cut a release, patch the SurrealDB Cloud fleet, and then make the release public. The intent was good. Development happened where the community could see it, and security fixes were embargoed only as long as they needed to be. In practice, though, this added real overhead to every security response. Cutting a fresh private fork for each issue, keeping it in sync with main, and merging the fix back when going public all took time we'd rather have spent on the fix itself.&lt;/p&gt;

&lt;p&gt;Starting with 3.1, we've moved all development to a private repository. The public &lt;code&gt;surrealdb/surrealdb&lt;/code&gt; repo remains the source of truth for releases, issues, and the source code that users read. Day-to-day commits now happen privately.&lt;/p&gt;

&lt;p&gt;The cadence looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;We develop and tag releases in the private repository.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When a release is ready, we announce it publicly and ship binaries and Docker images on the usual schedule.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Roughly 1 week after each release, we sync the private repository back to the public &lt;code&gt;surrealdb/surrealdb&lt;/code&gt; repo.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The week between release and public sync gives us a private window to triage any issues or vulnerabilities reported against the new version before the corresponding commits become visible. We can ship a fix and roll it out across SurrealDB Cloud before the underlying issue is public. This is the same model used by most database and infrastructure projects at comparable scale, and it lets us close security issues as quickly as they're reported.&lt;/p&gt;

&lt;p&gt;A few practical notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Issues, PRs, and discussions stay on the public repo&lt;/strong&gt;. Keep filing them on &lt;code&gt;surrealdb/surrealdb&lt;/code&gt;. We watch and respond to them daily.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Security reports&lt;/strong&gt;. Use the GitHub Security Advisory flow on the public repo, or email &lt;code&gt;security@surrealdb.com&lt;/code&gt;. Both reach us privately, and are the right route for anything you don't want disclosed publicly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Community PRs&lt;/strong&gt;. We're still very keen for community contributions. They get reviewed against the public repo as before, and brought into the private tree as part of the normal cycle.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We don't expect this to be visible to most users in day-to-day use. If you follow commit-by-commit development on the public repo, the experience will change: you'll see batched updates around each release rather than a continuous stream. We think the security benefit is worth that tradeoff, and we plan to keep working this way for the foreseeable future.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;SurrealDB 3.1 is available now. Head to the &lt;a href="https://surrealdb.com/install" rel="noopener noreferrer"&gt;install page&lt;/a&gt; to grab it for your platform.&lt;/p&gt;

&lt;p&gt;If you're running on SurrealDB Cloud, instances are being rolled to 3.1 as part of our standard release cycle. No action needed.&lt;/p&gt;

&lt;p&gt;Read the &lt;a href="https://surrealdb.com/releases/3.1.0" rel="noopener noreferrer"&gt;full release notes&lt;/a&gt; for the complete list of changes, and come say hello on &lt;a href="https://discord.gg/surrealdb" rel="noopener noreferrer"&gt;Discord&lt;/a&gt; if you hit anything along the way.&lt;/p&gt;

</description>
      <category>surrealdb</category>
      <category>database</category>
      <category>news</category>
      <category>graphql</category>
    </item>
    <item>
      <title>Kreuzberg &amp; SurrealDB: from unstructured documents to hybrid retrieval</title>
      <dc:creator>Mark Gyles</dc:creator>
      <pubDate>Tue, 12 May 2026 12:35:55 +0000</pubDate>
      <link>https://dev.to/surrealdb/kreuzberg-surrealdb-from-unstructured-documents-to-hybrid-retrieval-3657</link>
      <guid>https://dev.to/surrealdb/kreuzberg-surrealdb-from-unstructured-documents-to-hybrid-retrieval-3657</guid>
      <description>&lt;p&gt;Author: &lt;a href="https://x.com/IgnacioPaz87" rel="noopener noreferrer"&gt;Ignacio Paz&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We’re excited to share a new partner integration: &lt;code&gt;kreuzberg-surrealdb&lt;/code&gt;, a connector that bridges the Kreuzberg document intelligence framework directly into SurrealDB. This integration was created by the Kreuzberg team and we are excited to have this functionality available now in SurrealDB.&lt;/p&gt;

&lt;p&gt;Kreuzberg extracts, chunks, and generates embeddings from 88+ document formats, while SurrealDB provides a multi-model database for AI applications, combining documents, graphs, vectors, and full-text search in a single system.&lt;/p&gt;

&lt;p&gt;Together, they make it easy to build document search and RAG pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the integration does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0mi2ox6g7h6vmbu2d7x3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0mi2ox6g7h6vmbu2d7x3.png" alt="document extraction" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kreuzberg-surrealdb&lt;/code&gt; handles the full ingestion workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automatic schema setup&lt;/li&gt;
&lt;li&gt;Content deduplication using SHA-256 hashing&lt;/li&gt;
&lt;li&gt;Storage and indexing in SurrealDB&lt;/li&gt;
&lt;li&gt;Documents ready for search immediately after ingest&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The integration supports two modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DocumentConnector:indexes full documents for BM25 keyword search.&lt;/li&gt;
&lt;li&gt;DocumentPipeline:chunks documents, generates embeddings, and enables semantic and hybrid search using HNSW vector indexes and Reciprocal Rank Fusion.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why it matters
&lt;/h2&gt;

&lt;p&gt;Building document search systems often requires combining multiple tools for extraction, chunking, embeddings, and storage.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;kreuzberg-surrealdb&lt;/code&gt;, the entire workflow runs through a &lt;strong&gt;single integration&lt;/strong&gt;—no schema boilerplate, no duplicate ingestion, and built-in support for &lt;strong&gt;keyword&lt;/strong&gt;, &lt;strong&gt;semantic&lt;/strong&gt;, and &lt;strong&gt;hybrid search&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get started
&lt;/h2&gt;

&lt;p&gt;See how to get started in &lt;a href="https://surrealdb.com/docs/build/integrations/ai-frameworks/kreuzberg?utm_source=kreuzberg_blog&amp;amp;utm_medium=blog&amp;amp;utm_campaign=kreuzberg_blog" rel="noopener noreferrer"&gt;SurrealDB Docs: Kreuzberg Integration&lt;/a&gt;, and check out our example of &lt;a href="https://surrealdb.com/blog/how-to-build-a-knowledge-graph-for-ai?utm_source=kreuzberg_blog&amp;amp;utm_medium=blog&amp;amp;utm_campaign=kreuzberg_blog#parsing-unstructured-data" rel="noopener noreferrer"&gt;How to build a knowledge graph for AI with SurrealDB and Kreuzberg&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>surrealdb</category>
      <category>kreuzberg</category>
      <category>database</category>
      <category>integration</category>
    </item>
    <item>
      <title>Schema migrations in SurrealDB: A local dev workflow</title>
      <dc:creator>Mark Gyles</dc:creator>
      <pubDate>Fri, 08 May 2026 12:42:16 +0000</pubDate>
      <link>https://dev.to/surrealdb/schema-migrations-in-surrealdb-a-local-dev-workflow-1ijm</link>
      <guid>https://dev.to/surrealdb/schema-migrations-in-surrealdb-a-local-dev-workflow-1ijm</guid>
      <description>&lt;p&gt;Author: &lt;a href="https://github.com/itsezc" rel="noopener noreferrer"&gt;Chiru Boggavarapu&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This post walks through a proper migration workflow for local development using &lt;strong&gt;SurrealKit&lt;/strong&gt;, an official tool from the SurrealDB team that handles schema sync, rollouts, seeding, and testing.&lt;/p&gt;

&lt;p&gt;If you've spent any time working with SurrealDB, you know that with great flexibility comes the responsibility of managing your schema carefully. You're iterating quickly, adding tables, refining field definitions, and removing what you no longer need, and before long your local database can drift out of sync with what your code expects.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is SurrealKit?
&lt;/h2&gt;

&lt;p&gt;SurrealKit is a CLI tool that manages your SurrealDB schema through &lt;code&gt;.surql&lt;/code&gt; files. You define your schema as files, and SurrealKit keeps your database in sync with them.&lt;/p&gt;

&lt;p&gt;It has two main modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sync&lt;/strong&gt;: a fast, declarative approach for local and dev environments. Your files are the source of truth. Add something and it gets created, change it and it gets updated, remove it and it gets deleted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollout&lt;/strong&gt;: a more controlled migration path for shared or production databases, with planning, staged execution, and rollback support.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For local dev, you'll mostly live in &lt;code&gt;sync&lt;/code&gt;. Rollouts come into play when you're pushing to shared environments.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;Cargo is the Rust package manager. It downloads a Rust package's dependencies, compiles the packages, and makes distributable packages.&lt;/p&gt;

&lt;p&gt;Install via Cargo&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo &lt;span class="nb"&gt;install &lt;/span&gt;surrealkit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or grab a prebuilt binary from the GitHub releases page if you don't want to compile it.&lt;/p&gt;

&lt;p&gt;Initialise a project&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surrealkit init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a &lt;code&gt;/database&lt;/code&gt; directory with the scaffolding you need. SurrealKit connects to your database using environment variables, so add these to your &lt;code&gt;.env&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;.env&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;SURREALDB_HOST=localhost:8000&lt;/span&gt;
&lt;span class="s"&gt;SURREALDB_NAME=myapp&lt;/span&gt;
&lt;span class="s"&gt;SURREALDB_NAMESPACE=development&lt;/span&gt;
&lt;span class="s"&gt;SURREALDB_USER=root&lt;/span&gt;
&lt;span class="s"&gt;SURREALDB_PASSWORD=secret&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It supports both &lt;code&gt;DATABASE_HOST&lt;/code&gt; and &lt;code&gt;PUBLIC_DATABASE_HOST&lt;/code&gt; variants, which is handy if you're working in a SvelteKit or similar setup where env vars are split by visibility.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Local Dev Workflow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Writing Your Schema
&lt;/h3&gt;

&lt;p&gt;Schema files live in &lt;code&gt;database/schema/&lt;/code&gt;. Each file is a &lt;code&gt;.surql&lt;/code&gt; file with your table definitions, indexes, access rules — whatever you'd normally write in SurrealQL.&lt;/p&gt;

&lt;p&gt;The structure is entirely up to you. You might have one file per table, or group related things together. Either way, SurrealKit tracks what's in those files and reconciles it with what's actually in your database.&lt;/p&gt;

&lt;h3&gt;
  
  
  Syncing Changes
&lt;/h3&gt;

&lt;p&gt;When you've made a change and want to apply it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surrealkit &lt;span class="nb"&gt;sync&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. SurrealKit diffs your schema files against the current database state and applies what's changed. If you deleted a table definition from your files, it removes it from the database too. Everything stays in sync.&lt;/p&gt;

&lt;p&gt;For active development sessions, watch mode is where you'll spend most of your time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surrealkit &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--watch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This watches your &lt;code&gt;database/schema/&lt;/code&gt; directory and resyncs automatically whenever you save a file. It handles deletions too — if you remove a definition, it gets cleaned up. No manual intervention needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Moving to Shared Environments
&lt;/h2&gt;

&lt;p&gt;The sync approach is great for local databases you own completely. But the moment you're working against a shared or staging database, you need more control. That's what rollouts are for.&lt;/p&gt;

&lt;h3&gt;
  
  
  Baseline First
&lt;/h3&gt;

&lt;p&gt;Before your first rollout on an existing database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surrealkit rollout baseline
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This records the current state so SurrealKit knows what it's working from.&lt;/p&gt;

&lt;h3&gt;
  
  
  Plan → Start → Complete
&lt;/h3&gt;

&lt;p&gt;When you're ready to ship a schema change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surrealkit rollout plan &lt;span class="nt"&gt;--name&lt;/span&gt; add_user_indexes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates a TOML manifest in &lt;code&gt;database/rollouts/&lt;/code&gt; describing exactly what will change. You can review it, commit it, get it reviewed. Treat it like a pull request for your schema.&lt;/p&gt;

&lt;p&gt;When you're ready to apply, you'll reference the manifest by its full filename. SurrealKit names these with a timestamp prefix: &lt;code&gt;20260302153045&lt;/code&gt; translates to 2nd March 2026 at 15:30:45. This is the time the plan was generated, and it's there so rollouts always have a guaranteed sort order. If two people generate a plan on the same day, the timestamps keep them distinct and sequential. You'll see the full name in your &lt;code&gt;database/rollouts/&lt;/code&gt; directory after running &lt;code&gt;plan&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When you're ready to apply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surrealkit rollout start 20260302153045__add_user_indexes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs the non-destructive part of the migration. Once your application is deployed and you're confident everything is working:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surrealkit rollout &lt;span class="nb"&gt;complete &lt;/span&gt;20260302153045__add_user_indexes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This finishes the migration, including any cleanup of legacy objects.&lt;/p&gt;

&lt;p&gt;If something goes wrong mid-rollout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surrealkit rollout rollback 20260302153045__add_user_indexes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also validate a manifest without touching the database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surrealkit rollout lint 20260302153045__add_user_indexes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And check what state a rollout is in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surrealkit rollout status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SurrealKit tracks rollout state in the database itself, so it's always resumable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Seeding
&lt;/h2&gt;

&lt;p&gt;SurrealKit has a built-in seeding system for populating your local database with test data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surrealkit seed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seed files live alongside your schema in the &lt;code&gt;/database&lt;/code&gt; directory. Useful for onboarding new team members quickly — they clone the repo, init SurrealKit, run sync and seed, and they're good to go.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;p&gt;One of the more interesting features is the declarative testing framework. You write TOML test suites in &lt;code&gt;database/tests/suites/&lt;/code&gt; and run them with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surrealkit &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each suite runs in an isolated ephemeral namespace, so tests can't interfere with each other or your actual data. You can test SQL assertions, permission rules, schema metadata, and even HTTP API endpoints.&lt;/p&gt;

&lt;p&gt;A simple example, checking that a guest can't create an order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"security_smoke"&lt;/span&gt;

&lt;span class="nn"&gt;[[cases]]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"guest_cannot_create_order"&lt;/span&gt;
&lt;span class="py"&gt;kind&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"sql_expect"&lt;/span&gt;
&lt;span class="py"&gt;actor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"guest"&lt;/span&gt;
&lt;span class="py"&gt;sql&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"CREATE order CONTENT { total: 10 };"&lt;/span&gt;
&lt;span class="py"&gt;allow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="py"&gt;error_contains&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"permission"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also test permission matrices across a whole table at once, which is handy for verifying your access rules haven't regressed after a schema change.&lt;/p&gt;

&lt;p&gt;For CI, add &lt;code&gt;--json-out&lt;/code&gt; to get machine-readable output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surrealkit &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--json-out&lt;/span&gt; database/tests/report.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command exits non-zero on any failure, so it integrates cleanly with GitHub Actions and other CI/CD systems seamlessly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Putting It Together
&lt;/h2&gt;

&lt;p&gt;Here's what a typical day looks like with this setup:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pull the latest changes&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;surrealkit sync&lt;/code&gt; to update your local DB&lt;/li&gt;
&lt;li&gt;Edit schema files in &lt;code&gt;database/schema/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;surrealkit sync --watch&lt;/code&gt; while you work&lt;/li&gt;
&lt;li&gt;When you're done, run &lt;code&gt;surrealkit test&lt;/code&gt; to make sure nothing broke&lt;/li&gt;
&lt;li&gt;Commit your schema files alongside your code&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For production changes, swap step 4 for &lt;code&gt;surrealkit rollout plan&lt;/code&gt; and follow the plan → start → complete flow.&lt;/p&gt;

&lt;p&gt;Would love to hear how you get on with SurrealKit. &lt;a href="https://discord.com/invite/surrealdb" rel="noopener noreferrer"&gt;Jump into Discord&lt;/a&gt; and let us know how it goes or if you need a hand along the way.&lt;/p&gt;

</description>
      <category>surrealdb</category>
      <category>surrealkit</category>
      <category>database</category>
      <category>schema</category>
    </item>
    <item>
      <title>New SurrealDB docs search using hybrid search and HNSW/BM25 reranking</title>
      <dc:creator>Mark Gyles</dc:creator>
      <pubDate>Fri, 01 May 2026 15:32:31 +0000</pubDate>
      <link>https://dev.to/surrealdb/new-surrealdb-docs-search-using-hybrid-search-and-hnswbm25-reranking-51jd</link>
      <guid>https://dev.to/surrealdb/new-surrealdb-docs-search-using-hybrid-search-and-hnswbm25-reranking-51jd</guid>
      <description>&lt;p&gt;Author: &lt;a href="https://x.com/mithridates" rel="noopener noreferrer"&gt;Dave MacLeod&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Trying to produce relevant search results is one of the hardest things to get right when building an app. Searching can be done in a lot of ways: basic text search, full-text search, vector search, and often even graph and geospatial.&lt;/p&gt;

&lt;p&gt;This post introduces an example that includes two of these models: full-text and vector search together. These two tend to involve quite a bit of SurrealQL code, because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;To use full-text search, you first have to define how you want text to be split up and modified. Do you want it to be case-sensitive? Split by whitespace, or some other way? Modified to root forms based on the language of the text? (And so on...)&lt;/li&gt;
&lt;li&gt;To use vector search, you have to generate embeddings for pieces of text and know how to query them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What makes this example special is that it is the search function you use every time you type some text into the "Search the docs" part of the SurrealDB documentation! This functionality was recently implemented, and because it's open source we are able to show you the ins and outs of how it was done.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Doc Search Is Implemented
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/surrealdb/docs.surrealdb.com/blob/main/search/README.md" rel="noopener noreferrer"&gt;README&lt;/a&gt; in the &lt;a href="https://github.com/surrealdb/docs.surrealdb.com/tree/main/search" rel="noopener noreferrer"&gt;/search&lt;/a&gt; section of the docs repo goes into a good amount of detail. &lt;/p&gt;

&lt;p&gt;In addition, you can see the mechanics of the search itself inside  this &lt;a href="https://github.com/surrealdb/docs.surrealdb.com/blob/main/src/utils/search.ts" rel="noopener noreferrer"&gt;search.ts file&lt;/a&gt; in another section.&lt;/p&gt;

&lt;p&gt;The entire implementation is done using SurrealDB, along with Bun to deploy the website and an OpenAI API key. You can go to &lt;a href="https://github.com/surrealdb/docs.surrealdb.com/tree/main/search#local-development" rel="noopener noreferrer"&gt;this part of the page&lt;/a&gt; to deploy it locally and test it out. &lt;/p&gt;

&lt;p&gt;Here is what the final result looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgsy92u2q866ijtmk97w5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgsy92u2q866ijtmk97w5.png" alt="How Doc Search Is Implemented" width="800" height="679"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Since the README file already goes into detail about how to do the deployment yourself, in this post we will instead take a closer look at the SurrealQL statements themselves to see how they work and how they might help you do something similar in your own tools and apps.&lt;/p&gt;




&lt;h2&gt;
  
  
  The role SurrealDB Plays
&lt;/h2&gt;

&lt;p&gt;As the README mentions, the search functionality uses hybrid search:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Hybrid search for the SurrealDB documentation. Combines BM25 full-text search with OpenAI vector embeddings, fused via Reciprocal Rank Fusion (RRF) inside SurrealDB.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://surrealdb.com/docs/learn/data-models/vector-search/hybrid-search" rel="noopener noreferrer"&gt;Hybrid search&lt;/a&gt; allows you to combine full-text search with vector search, giving you the best of both worlds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vector search&lt;/strong&gt; is useful because it is capable of going beyond the outward appearance of words to get to their semantic meaning. For example, the term lead in the following paragraph is used to mean two different things. We can recognize that as humans, and LLM models are also able to do the same, returning the output as an embedding (a large array of numbers representing semantic space).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The project &lt;strong&gt;lead&lt;/strong&gt; frowned and took a hard look at the results. They were clear as day. There was far too much lead content in the water. That's why the town had gotten sick!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;However, full-text search is useful too because it excels at slicing and modifying text into tokens. To show what that looks like, here's what the full-text analyzer looks like that is used in the docs search.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;DEFINE&lt;/span&gt; &lt;span class="n"&gt;ANALYZER&lt;/span&gt; &lt;span class="n"&gt;OVERWRITE&lt;/span&gt; &lt;span class="k"&gt;simple&lt;/span&gt;
    &lt;span class="n"&gt;TOKENIZERS&lt;/span&gt; &lt;span class="n"&gt;blank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;camel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;punct&lt;/span&gt;
    &lt;span class="n"&gt;FILTERS&lt;/span&gt; &lt;span class="n"&gt;snowball&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;english&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The easiest way to understand what all the tokenizers and filters do in an analyzer is by putting a string into the &lt;a href="https://surrealdb.com/docs/reference/query-language/functions/database-functions/search#searchanalyze" rel="noopener noreferrer"&gt;search::analyze()&lt;/a&gt; function, which returns an array. To avoid showing a huge array in this blog post, we'll call &lt;code&gt;.join(' ')&lt;/code&gt; on the output to join them back into a single string.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;search&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="k"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;"simple"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="nv"&gt;"The project lead frowned and took a hard look at the results. They were clear as day. There was far too much lead content in the water. That's how the town had gotten sick!"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As the output shows, using this analyzer will allow you to match on terms like "FroWNinG" which will turn into "frown" before being compared against a search string.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"the project lead frown and took a hard look at the result . they were clear as day . there was far too much lead content in the water . that ' s how the town had gotten sick !"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So if vector search and full-text search each have their advantages, how can you combine the two? It would be nice if you could somehow plug them into a function that does this.&lt;/p&gt;

&lt;p&gt;And it turns out that SurrealDB has exactly that! It is called &lt;a href="https://surrealdb.com/docs/reference/query-language/functions/database-functions/search#searchrrf" rel="noopener noreferrer"&gt;search::rrf()&lt;/a&gt; and was added during the 3.0 beta period. Here is what it looks like in practice.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ── Reciprocal Rank Fusion ──&lt;/span&gt;
&lt;span class="c1"&gt;-- Combines the four ranked lists into a single ranking.&lt;/span&gt;
&lt;span class="c1"&gt;-- RRF scores each result as: sum(1 / (k + rank_in_list))&lt;/span&gt;
&lt;span class="c1"&gt;-- across all lists the result appears in.&lt;/span&gt;
&lt;span class="c1"&gt;--   arg 1: array of ranked lists to fuse&lt;/span&gt;
&lt;span class="c1"&gt;--   arg 2: k=60 (smoothing constant, standard RRF default)&lt;/span&gt;
&lt;span class="c1"&gt;--   arg 3: limit=80 (max candidates to consider)&lt;/span&gt;
&lt;span class="n"&gt;LET&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;fused&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;search&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;rrf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;page_ft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;page_vs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;section_ft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;section_vs&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; 
    &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="mi"&gt;80&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can probably guess, it is fusing the results of previous queries for full-text search on pages and sections (&lt;code&gt;$page_ft, $section_ft&lt;/code&gt;) together with vector searches on pages and sections (&lt;code&gt;$page_vs, $section_vs&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Full-text searches are done using the &lt;code&gt;@@&lt;/code&gt; operator (the "matches" operator). This operator works as long as you have a index on a field that uses a &lt;code&gt;FULLTEXT ANALYZER&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;DEFINE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;OVERWRITE&lt;/span&gt; &lt;span class="n"&gt;page_ft_title&lt;/span&gt;       &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="n"&gt;FIELDS&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;       &lt;span class="n"&gt;FULLTEXT&lt;/span&gt; &lt;span class="n"&gt;ANALYZER&lt;/span&gt; &lt;span class="k"&gt;simple&lt;/span&gt; &lt;span class="n"&gt;BM25&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;DEFINE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;OVERWRITE&lt;/span&gt; &lt;span class="n"&gt;page_ft_breadcrumb&lt;/span&gt;  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="n"&gt;FIELDS&lt;/span&gt; &lt;span class="n"&gt;breadcrumb&lt;/span&gt;  &lt;span class="n"&gt;FULLTEXT&lt;/span&gt; &lt;span class="n"&gt;ANALYZER&lt;/span&gt; &lt;span class="k"&gt;simple&lt;/span&gt; &lt;span class="n"&gt;BM25&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;DEFINE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;OVERWRITE&lt;/span&gt; &lt;span class="n"&gt;page_ft_description&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="n"&gt;FIELDS&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="n"&gt;FULLTEXT&lt;/span&gt; &lt;span class="n"&gt;ANALYZER&lt;/span&gt; &lt;span class="k"&gt;simple&lt;/span&gt; &lt;span class="n"&gt;BM25&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;DEFINE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;OVERWRITE&lt;/span&gt; &lt;span class="n"&gt;page_ft_content&lt;/span&gt;     &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="n"&gt;FIELDS&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;     &lt;span class="n"&gt;FULLTEXT&lt;/span&gt; &lt;span class="n"&gt;ANALYZER&lt;/span&gt; &lt;span class="k"&gt;simple&lt;/span&gt; &lt;span class="n"&gt;BM25&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;DEFINE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;OVERWRITE&lt;/span&gt; &lt;span class="n"&gt;page_ft_path&lt;/span&gt;        &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="n"&gt;FIELDS&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;        &lt;span class="n"&gt;FULLTEXT&lt;/span&gt; &lt;span class="n"&gt;ANALYZER&lt;/span&gt; &lt;span class="k"&gt;simple&lt;/span&gt; &lt;span class="n"&gt;BM25&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In between these two &lt;code&gt;@&lt;/code&gt; operators you can insert a number to access the &lt;code&gt;search::score()&lt;/code&gt; for each field. This will make sense once you take a look at these two queries. Each of them is querying multiple fields, each of which will return a score thanks to &lt;code&gt;BM25&lt;/code&gt; defined in the field. To know which field is which, you put a reference number in between the &lt;code&gt;@&lt;/code&gt; operators like &lt;code&gt;@0@&lt;/code&gt; and &lt;code&gt;@2@&lt;/code&gt;. They can then be added together or multiplied or whatever logic you would like to use.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;LET&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;page_ft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt;
            &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;"page"&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;page_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;breadcrumb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;search&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;search&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;search&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;search&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;search&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ft_score&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt;
            &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;
            &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;
            &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;breadcrumb&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;
            &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;
            &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;
        &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ft_score&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
        &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;LET&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;section_ft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 
        &lt;span class="k"&gt;SELECT&lt;/span&gt;
            &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;"section"&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"#"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;anchor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;page_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;collection&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;breadcrumb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;search&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;search&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;search&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ft_score&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;section&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt;
            &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;
            &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;breadcrumb&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;
            &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;
        &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ft_score&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
        &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The vector search query also uses its own index. Note that you don't specifically need an index to use vector search, as you can go with a &lt;a href="https://surrealdb.com/docs/reference/query-language/functions/database-functions/vector#vectorsimilaritycosine" rel="noopener noreferrer"&gt;brute force function&lt;/a&gt; such as &lt;code&gt;vector::similarity::cosine()&lt;/code&gt; instead. However, an &lt;code&gt;HNSW&lt;/code&gt; index instead can speed up the process dramatically if you don't mind sacrificing a bit of precision. Here the index is defined with a dimension of 1536 to match the length of the array returned by OpenAI's text-embedding-3-small model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- HNSW index for approximate nearest-neighbour vector search.&lt;/span&gt;
&lt;span class="c1"&gt;-- Dimension 1536 matches OpenAI text-embedding-3-small output.&lt;/span&gt;
&lt;span class="c1"&gt;-- Cosine distance is the standard metric for normalised text embeddings.&lt;/span&gt;
&lt;span class="n"&gt;DEFINE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;OVERWRITE&lt;/span&gt; &lt;span class="n"&gt;page_embedding_hnsw&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="n"&gt;FIELDS&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt;
    &lt;span class="n"&gt;HNSW&lt;/span&gt; &lt;span class="n"&gt;DIMENSION&lt;/span&gt; &lt;span class="mi"&gt;1536&lt;/span&gt; &lt;span class="n"&gt;DIST&lt;/span&gt; &lt;span class="n"&gt;COSINE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Having an &lt;code&gt;HNSW&lt;/code&gt; index also lets you use the &lt;code&gt;&amp;lt;||&amp;gt;&lt;/code&gt; operator (the KNN, or K-nearest neighbour) operator when performing the query. The comments above the code explain what each of the two numbers mean. Here, the &lt;code&gt;|30,100|&lt;/code&gt; syntax means to return 30 neighbours among up to 100 candidates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ── Page vector search ──&lt;/span&gt;
&lt;span class="c1"&gt;-- Finds the 30 pages whose embeddings are closest to the&lt;/span&gt;
&lt;span class="c1"&gt;-- query embedding. The &amp;lt;|30,100|&amp;gt; syntax means: return 30&lt;/span&gt;
&lt;span class="c1"&gt;-- neighbours, exploring up to 100 candidates in the HNSW&lt;/span&gt;
&lt;span class="c1"&gt;-- graph (higher = more accurate but slower).&lt;/span&gt;
&lt;span class="n"&gt;LET&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;page_vs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;"page"&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;page_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;breadcrumb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;knn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;|&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;qvec&lt;/span&gt;
    &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
    &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- ── Section vector search ──&lt;/span&gt;
&lt;span class="c1"&gt;-- Same as page vector search but for H2 sections.&lt;/span&gt;
&lt;span class="c1"&gt;-- Pulls the parent page's path and collection via the&lt;/span&gt;
&lt;span class="c1"&gt;-- record link (page.path, page.collection).&lt;/span&gt;
&lt;span class="n"&gt;LET&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;section_vs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;"section"&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"#"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;anchor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;page_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;collection&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;breadcrumb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;knn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;section&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;|&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;qvec&lt;/span&gt;
    &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
    &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So that is how the magic happens. Well, part of it. Be sure to check out the rest of the code which shows what happens after the SurrealDB query returns. For example, as the database is unaware of which collections it is searching through, some modifications are made on the SDK side to give SDK docs less precedence compared to more core docs. This is something that you could perhaps do on the database side if you wanted (starting with &lt;code&gt;DEFINE TABLE sdk_page&lt;/code&gt; instead of &lt;code&gt;DEFINE TABLE page&lt;/code&gt; for example) but that will always depend on your own case.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ──────────────────────────────────────────────────────────&lt;/span&gt;
&lt;span class="c1"&gt;// Post-retrieval boosting&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// After RRF fusion, we apply multiplicative boosts to adjust&lt;/span&gt;
&lt;span class="c1"&gt;// rankings based on signals the database query can't capture:&lt;/span&gt;
&lt;span class="c1"&gt;// title similarity, content type (page vs section), source&lt;/span&gt;
&lt;span class="c1"&gt;// collection, and comparison-query detection.&lt;/span&gt;
&lt;span class="c1"&gt;// ──────────────────────────────────────────────────────────&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Non-SDK doc collections get a small ranking boost because
 * generic queries like "authentication" should prefer the
 * core concept page over an SDK API reference page that
 * happens to mention auth as one of many methods.
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CORE_COLLECTIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;doc-surrealdb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;doc-surrealql&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;doc-tutorials&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;doc-cloud&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;doc-surrealist&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;doc-surrealml&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;doc-surrealkv&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;doc-integrations&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Simplifying the Example
&lt;/h2&gt;

&lt;p&gt;Since you might not have the head space at the moment to play around with the existing code demonstrated in this blog post, let's finish up with the same pattern as above in a very simplified form. This will let you copy and paste the example into the online &lt;a href="https://app.surrealdb.com/?_gl=1*n737cr*_gcl_aw*R0NMLjE3NzQ2MjU1MDcuQ2owS0NRancxWmpPQmhDbUFSSXNBRER1RlRDVV9VN2JENUdhMFQzTm5mV0paMzNOVkxiTDNROVFOSTlUckhRUHV0V2FVN1l2czJ0MlNGQWFBc3hCRUFMd193Y0I.*FPAU*NTM0Mzg1Mzg1LjE3NzA2NTk1MTg.*_ga*NDAyMzA0NTY4LjE3NzEyNDU1Nzg.*_ga_J1NWM32T1V*czE3Nzc2NDUxMDkkbzQxMyRnMSR0MTc3NzY0NTE5NCRqNjAkbDAkaDQwOTUwMzMzMA..*_fplc*OVhscHhYNEtSdG1MUlk4bUJqSnJVaTVXSDBNSTEyUG1DMFUlMkY3VXoycHhQbFVUaHFGY1dvR2JUdHFuVkhXWEY0JTJGTkYlMkZnUDVVajNoalZ3V095cG93cGd2N0c2ZzdMZk5hM1hLS2xxMXYyNG42R3hUZ0UlMkY0SXdsUnFWak9DWkElM0QlM0Q." rel="noopener noreferrer"&gt;Surrealist UI&lt;/a&gt; to give it a try yourself.&lt;/p&gt;

&lt;p&gt;The content below holds all the same features: a full-text analyzer, a full-text and vector index, a full-text and vector query, and a final query that combines the two into hybrid search results. However, it has all been simplified to a single field for text content and embeddings, and a single document table that is being searched on. In addition, the embeddings have been cut down to an array of just ten numbers in length. An array that small is very imprecise, but just long enough to serve for our simple example.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Define an analyzer&lt;/span&gt;
&lt;span class="n"&gt;DEFINE&lt;/span&gt; &lt;span class="n"&gt;ANALYZER&lt;/span&gt; &lt;span class="n"&gt;OVERWRITE&lt;/span&gt; &lt;span class="k"&gt;simple&lt;/span&gt; &lt;span class="n"&gt;TOKENIZERS&lt;/span&gt; &lt;span class="n"&gt;blank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;camel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;punct&lt;/span&gt; &lt;span class="n"&gt;FILTERS&lt;/span&gt; &lt;span class="n"&gt;SNOWBALL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;en&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Attach it to an index&lt;/span&gt;
&lt;span class="n"&gt;DEFINE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;OVERWRITE&lt;/span&gt; &lt;span class="n"&gt;ft&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt; &lt;span class="n"&gt;FIELDS&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="n"&gt;FULLTEXT&lt;/span&gt; &lt;span class="n"&gt;ANALYZER&lt;/span&gt; &lt;span class="k"&gt;simple&lt;/span&gt; &lt;span class="n"&gt;BM25&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Create a vector index too&lt;/span&gt;
&lt;span class="n"&gt;DEFINE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;OVERWRITE&lt;/span&gt; &lt;span class="n"&gt;page_embedding_hnsw&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt; &lt;span class="n"&gt;FIELDS&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;HNSW&lt;/span&gt; &lt;span class="n"&gt;DIMENSION&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="n"&gt;DIST&lt;/span&gt; &lt;span class="n"&gt;COSINE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Add some sample data&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;one&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;   &lt;span class="nv"&gt;"Elves and halflings and witches and such"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0019&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0142&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0080&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0664&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0173&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0109&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0066&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0045&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0204&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0087&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;two&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;   &lt;span class="nv"&gt;"Is that a wizard?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                        &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0010&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0051&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0207&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0787&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0061&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0127&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0172&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0097&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0077&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0120&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;three&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"Databases are used to store information"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0317&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0112&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0118&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0348&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0061&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0114&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0310&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0117&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0034&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0195&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;NONE&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;-- A sample query and its embedding&lt;/span&gt;
&lt;span class="n"&gt;LET&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;witch_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"witches"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;LET&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;witch_embed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0059&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0081&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0475&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0020&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0295&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0183&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0170&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0048&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0286&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;-- Get the full-text score&lt;/span&gt;
&lt;span class="n"&gt;LET&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;fts_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt;
            &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;search&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ft_score&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;witch_text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Get the vector score&lt;/span&gt;
&lt;span class="n"&gt;LET&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;vector_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;knn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;|&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;witch_embed&lt;/span&gt;
    &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Combine the results as a hybrid score&lt;/span&gt;
&lt;span class="k"&gt;search&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;rrf&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;fts_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;vector_score&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can see in the output that while only one document had a match on the word "witches" (and thus a value for ft_score), the vector search portion has been able to conclude that the sentence about wizards is semantically a bit closer than the one about databases. This would be a lot more accurate of course if the embedding were longer than 10 elements, but even at this length we can still see its potential.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Elves and halflings and witches and such"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"distance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.2789615948854415&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ft_score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.4782197177410126&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"document:one"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"rrf_score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.024691358024691357&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Is that a wizard?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"distance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.385041442417419&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"document:two"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"rrf_score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.012195121951219513&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Databases are used to store information"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"distance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.3876749269204385&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"document:three"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"rrf_score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.012048192771084338&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hopefully the simplified example has made it easy to follow the logic we use in our own doc search and has given you some ideas for how to implement patterns yourself. Have any questions or comments about hybrid search? Feel free to &lt;a href="https://surrealdb.com/docs/build/deployment/surrealdb-cloud/getting-started/create-an-instance" rel="noopener noreferrer"&gt;Create a free SurrealDB Cloud instance today to get started&lt;/a&gt;, and drop by &lt;a href="https://discord.com/invite/surrealdb" rel="noopener noreferrer"&gt;our Discord community&lt;/a&gt; to discuss anything on your mind with the SurrealDB community and staff.&lt;/p&gt;

</description>
      <category>surrealdb</category>
      <category>database</category>
      <category>devrel</category>
      <category>hybridsearch</category>
    </item>
    <item>
      <title>How to get near-perfect, deterministic accuracy from your AI agents</title>
      <dc:creator>Mark Gyles</dc:creator>
      <pubDate>Thu, 23 Apr 2026 22:23:21 +0000</pubDate>
      <link>https://dev.to/surrealdb/how-to-get-near-perfect-deterministic-accuracy-from-your-ai-agents-56ma</link>
      <guid>https://dev.to/surrealdb/how-to-get-near-perfect-deterministic-accuracy-from-your-ai-agents-56ma</guid>
      <description>&lt;p&gt;Author: &lt;a href="https://www.linkedin.com/in/mpenaroza/" rel="noopener noreferrer"&gt;Matthew Penaroza&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I have spent a lot of time working on large-scale agent architectures with some of the largest organizations in the world, and the single most common mistake I see teams make is assuming their accuracy problems are model problems. They almost never are. Your agent is typically not struggling because the LLM is weak. It is struggling because your retrieval layer is feeding it bad context. Fix your retrieval, and accuracy jumps. Make your retrieval learn from its own results, and accuracy becomes deterministic. Here is what that looks like in practice.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; - Agent accuracy problems are almost always retrieval problems, not model problems. At scale, vector search alone is not enough. You need structured filters, graph traversal, temporal constraints, and vector similarity composed in a single atomic query to get to ~90–95% accuracy. To close the remaining gap to ~99%+, you need reasoning graphs and retrieval graphs: structured graph edges that capture the agent's logic and retrieval patterns, feeding back into the pipeline so the system learns which approaches produce correct outcomes. Both require a multi-model database with ACID transactions across every data model, which is exactly what we designed SurrealDB to be.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The problem: why agent accuracy hits a ceiling
&lt;/h2&gt;

&lt;p&gt;Every agent follows the same loop: retrieve context, reason over it, act. The quality of the action is bounded by the quality of the context. I have seen perfectly capable models produce terrible outputs because the context they were given was noisy, and I have seen mid-tier models produce excellent outputs because the context was precise. The model matters less than most teams think.&lt;/p&gt;

&lt;p&gt;At small scale, none of this is a problem. If you have a few thousand documents, vector search returns good results, everything fits in a single database, and accuracy looks great. The problems emerge when data grows into the millions or billions of records. Suddenly more candidates look semantically similar, top-k selection becomes unstable, unrelated chunks bleed into the context window, and your agent starts hallucinating. The RAG survey literature (Gao et al., 2023) documents these failure modes extensively. Vector similarity is a useful ranking signal. It is not a control plane.&lt;/p&gt;

&lt;p&gt;What makes it worse is the typical production stack. Most teams are running Postgres for state, Neo4j for relationships, Pinecone for vectors, MongoDB for documents, sometimes Elasticsearch and a memory layer on top. Five or six independent systems, each with its own consistency model. When the graph database is 200ms behind the relational store, or the vector index has not ingested a document that was just written, your agent is reasoning over a version of reality that does not exist. It does not know the data is stale. It just makes a worse decision. And in multi-step workflows, each bad retrieval compounds into the next.&lt;/p&gt;

&lt;p&gt;So the question worth asking is not "which model should we use?" It is: how do you guarantee that the context reaching the model is valid, complete, current, and precise, even at scale? In my experience, the answer has two layers. The first gets you to roughly 90–95%. The second closes the gap to 99%+.&lt;/p&gt;

&lt;h2&gt;
  
  
  The accuracy staircase
&lt;/h2&gt;

&lt;p&gt;Each layer compounds on the last. Skip any step and accuracy hits a ceiling.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa5lmoyx1h1gwz8fg7zvz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa5lmoyx1h1gwz8fg7zvz.png" alt="The accuracy staircase" width="800" height="894"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting to ~90–95%: scope-first retrieval
&lt;/h2&gt;

&lt;p&gt;The idea is straightforward: instead of running similarity search against everything and cleaning up the results, you constrain the candidate set before ranking. Define what counts as a valid result first. Rank inside that scope second.&lt;/p&gt;

&lt;p&gt;Think about what a single agent retrieval step actually needs, and what goes wrong without each layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structured filters&lt;/strong&gt; narrow by type, permissions, and tenancy. Without them, I have watched a support agent retrieve internal engineering postmortems and serve them verbatim to a customer, because the postmortem was semantically similar to the customer's question. The vector search was technically correct. The result was a data leak.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Temporal constraints&lt;/strong&gt; ensure the data is current. Without them, an agent recommending products will happily surface items that were discontinued months ago, because the old product description is still the closest embedding match. The customer clicks through to a dead link. Trust erodes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Graph traversal&lt;/strong&gt; follows real relationships: this customer owns this product, which has this known issue, which maps to this knowledge base article. Without it, your agent retrieves articles about similar-sounding problems on entirely different products. The answer reads plausibly but is wrong in the specifics, which is the worst kind of hallucination because it is hard to catch.&lt;/p&gt;

&lt;p&gt;Only after all of that scoping should vector similarity and full-text search run, ranking the remaining candidates by relevance. By this point, the candidate pool is already valid. Similarity is just picking the best among the valid.&lt;/p&gt;

&lt;p&gt;The problem is you cannot compose these operations across a fragmented stack. In a multi-database architecture, each is an independent query against an independent system. The application layer stitches the results together and hopes the data was consistent across all of them. It never fully is. You need a database that handles all of this natively, in a single query, against a single consistent snapshot. That is why we built SurrealDB the way we did: structured filters, temporal constraints, graph traversal, vector similarity, full-text search, and business rule enforcement all compose in one SurrealQL transaction.&lt;/p&gt;

&lt;p&gt;Here is what that looks like. One query, every scoping layer, against one consistent snapshot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;knn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;vec_dist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;search&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ft_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;knn&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;65&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;search&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;35&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;blend_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;search&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;highlight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;em&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;/em&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;content_snippet&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;knowledge_base&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'support'&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;tenant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;tenant&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;agent_principal&lt;/span&gt; &lt;span class="n"&gt;INSIDE&lt;/span&gt; &lt;span class="n"&gt;allowed_principals&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;owns&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;has_issue&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;knowledge_base&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;content_embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;|&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;query_embedding&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;query_text&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;blend_score&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single statement does what would take four or five round-trips across a fragmented stack: permission checks, temporal filtering, graph traversal through the customer's actual product relationships, and hybrid vector + full-text ranking. One query. One transaction. One consistent snapshot. The line that does the most work is the graph traversal: &lt;code&gt;$customer-&amp;gt;owns-&amp;gt;product-&amp;gt;has_issue-&amp;gt;knowledge_base&lt;/code&gt;. That is not a join. It is a native graph traversal across edges, and it means the agent only sees knowledge base articles connected to the specific products this customer actually owns. Everything else is excluded before similarity even runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two reasons fragmented stacks kill accuracy
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvx7ib5z59fa66hlpf13l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvx7ib5z59fa66hlpf13l.png" alt="Two reasons fragmented stacks kill accuracy" width="800" height="1019"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Microsoft's GraphRAG work (Edge et al., 2024) demonstrated the same principle at the retrieval level: combining graph structure with similarity search significantly outperforms vector-only approaches. The CoALA framework (Sumers et al., 2024) describes it architecturally: agent memory must be consistent and accessible within a single reasoning cycle. A multi-model database that handles all data models under one transaction boundary is the simplest way I know to actually do both.&lt;/p&gt;

&lt;p&gt;This gets you to roughly 90–95% accuracy on large corpora. The consistency gaps are gone. The retrieval noise is way down. For many use cases, this is enough.&lt;/p&gt;

&lt;p&gt;But for agents that need to operate autonomously, make consequential decisions, or run in production without human oversight, the last 5–10% is where the difference between "mostly works" and "deterministic" lives. Closing that gap requires a different kind of mechanism.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting to ~99%+: reasoning graphs and retrieval graphs
&lt;/h2&gt;

&lt;p&gt;Scope-first retrieval fixes the shape of the funnel. Reasoning and retrieval graphs fix the funnel itself. They are the mechanism that turns a static retrieval pipeline into one that learns from its own results, and in my experience they are the single biggest differentiator between agents that are "pretty good" and agents that are genuinely deterministic.&lt;/p&gt;

&lt;p&gt;Every time an agent runs a retrieval pipeline and makes a decision, two graph structures get written in the same transaction as the action: a &lt;strong&gt;reasoning graph&lt;/strong&gt; (the agent's decision and the evaluated edges for each piece of evidence it considered) and a &lt;strong&gt;retrieval graph&lt;/strong&gt; (the operational details of how context was retrieved).&lt;/p&gt;

&lt;p&gt;I want to be specific about what these are. They are not log entries. They are not analytics events. They are graph edges stored in the same database as the data they describe, connecting the agent, the action, the customer, and the evidence records the agent evaluated. Written atomically with the agent's action and fully traversable with the same query language you use for everything else. ACID-guaranteed edges in the same graph your agent operates on. This is exactly why SurrealDB is a graph-native multi-model database: the reasoning and retrieval graphs have to be part of the same graph as the documents, the relationships, the vector embeddings, and the agent state, all under one transaction boundary. If they are not, the causal link between "what was retrieved," "how the agent reasoned about it," and "what it decided" breaks.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;reasoning graph&lt;/strong&gt; is a set of edges radiating from the agent. A &lt;strong&gt;decided&lt;/strong&gt; edge connects the agent to the action, carrying confidence and the eventual outcome. But the reasoning itself is not a field on that edge. Each step of the agent's logic is its own &lt;strong&gt;evaluated&lt;/strong&gt; edge connecting the agent directly to the evidence it considered: &lt;code&gt;agent-&amp;gt;evaluated-&amp;gt;kb:4821&lt;/code&gt; with a verdict (used or rejected), the reason, and how much it moved the agent's confidence. The reasoning is not a string or an array. It is a set of traversable edges connecting the agent to every piece of evidence it touched. The &lt;strong&gt;retrieval graph&lt;/strong&gt; is a &lt;code&gt;retrieved_via&lt;/code&gt; edge from the action back to the customer, carrying which scope filters were applied, the graph path traversed, the candidate count before and after scoping, whether the top result was relevant, and the retrieval latency.&lt;/p&gt;

&lt;h3&gt;
  
  
  Anatomy of the Reasoning and Retrieval Graphs
&lt;/h3&gt;

&lt;p&gt;Every reasoning step and every retrieval detail is a graph edge. Traversable, not buried in strings.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9f8wtnejx29cff55d4q1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9f8wtnejx29cff55d4q1.png" alt="Anatomy of the Reasoning and Retrieval Graphs" width="800" height="742"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The reason the reasoning has to be a graph, not a field, is that the pipeline planner needs to traverse individual evaluation steps across thousands of runs. If &lt;code&gt;kb:2201&lt;/code&gt; gets rejected in 94% of firmware queries, the planner can only discover that by traversing evaluated edges, not by parsing strings in a log. In SurrealQL, this is what the actual transaction looks like. The action is a node. The reasoning and retrieval are edges, created with &lt;code&gt;RELATE&lt;/code&gt;, connecting the agent to each piece of evidence and the action back to the customer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;support_action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;escalate_7291&lt;/span&gt; &lt;span class="n"&gt;CONTENT&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'escalate_to_engineering'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;acme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;9913&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="n"&gt;RELATE&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;support_tier2&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;decided&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;support_action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;escalate_7291&lt;/span&gt; &lt;span class="n"&gt;CONTENT&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;94&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="n"&gt;RELATE&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;support_tier2&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;evaluated&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;kb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4821&lt;/span&gt; &lt;span class="n"&gt;CONTENT&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;support_action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;escalate_7291&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'used'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Identical symptoms on same SKU'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;confidence_delta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="n"&gt;RELATE&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;support_tier2&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;evaluated&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;kb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2201&lt;/span&gt; &lt;span class="n"&gt;CONTENT&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;support_action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;escalate_7291&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'rejected'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Wrong product line'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;confidence_delta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="n"&gt;RELATE&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;support_tier2&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;evaluated&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;kb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5540&lt;/span&gt; &lt;span class="n"&gt;CONTENT&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;support_action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;escalate_7291&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'rejected'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Issue resolved, no longer active'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;confidence_delta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="n"&gt;RELATE&lt;/span&gt; &lt;span class="n"&gt;support_action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;escalate_7291&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;retrieved_via&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;acme&lt;/span&gt; &lt;span class="n"&gt;CONTENT&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;scope_filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'support'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'acme'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'30d'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="n"&gt;graph_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'customer:acme-&amp;gt;owns-&amp;gt;product:x100-&amp;gt;has_issue-&amp;gt;kb'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;candidates_before&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2847331&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;candidates_after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;top_result_relevant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;latency_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The decision logic is not buried in strings. Every reasoning step is an edge in the graph. The &lt;code&gt;decided&lt;/code&gt; edge connects the agent to the action. Each &lt;code&gt;evaluated&lt;/code&gt; edge connects the agent to a specific piece of evidence it considered, carrying the verdict, the reason, and the confidence delta. The &lt;code&gt;retrieved_via&lt;/code&gt; edge connects the action back to the customer with the retrieval metadata. The evaluated edges are not magic. Your agent framework already evaluates candidates and produces a decision. The &lt;code&gt;RELATE&lt;/code&gt; statements just persist what the agent already computed. The only design requirement is that your agent emits structured evaluations per candidate rather than a single monolithic response.&lt;/p&gt;

&lt;p&gt;That graph structure gives you something most teams have never had: you can replay any agent run, traverse the exact reasoning it followed, and identify exactly where and why it went wrong. But the real value is what happens when the reasoning and retrieval graphs feed back into the system. And they feed back in two different ways, serving two different purposes.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;pipeline planner&lt;/strong&gt; traverses the reasoning and retrieval graphs to decide &lt;strong&gt;what context the agent sees&lt;/strong&gt;. This is structural optimization. The planner can start from &lt;code&gt;kb:2201&lt;/code&gt; and traverse all evaluated edges pointing at it across every run. If it discovers that &lt;code&gt;kb:2201&lt;/code&gt; gets rejected 94% of the time for firmware queries, it deprioritizes or excludes it from the candidate set for that query type. It can walk &lt;code&gt;agent:support_tier2-&amp;gt;evaluated-&amp;gt;*&lt;/code&gt; to see every piece of evidence this agent has ever considered, grouped by action, filtered by verdict. It learns which retrieval patterns, graph paths, and scope filters correlate with correct outcomes and tightens the funnel accordingly.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;LLM itself&lt;/strong&gt; receives relevant past reasoning edges as context to guide &lt;strong&gt;how it thinks about the candidates it receives&lt;/strong&gt;. This is behavioral optimization. When the agent retrieves context for a firmware issue on &lt;code&gt;product:x100&lt;/code&gt;, the system can also pull in past evaluated edges for queries in the same product category: "Last time an agent evaluated a firmware issue on this product line, it used &lt;code&gt;kb:4821&lt;/code&gt; (identical symptoms, same SKU) and rejected &lt;code&gt;kb:2201&lt;/code&gt; (wrong product line). That decision was correct." The agent is not just getting better candidates. It is getting guidance on how to reason about those candidates, grounded in what actually worked before.&lt;/p&gt;

&lt;p&gt;Those two mechanisms work on different layers. The pipeline planner tightens the funnel so fewer, better candidates reach the agent. The past reasoning edges sharpen how the agent evaluates whatever reaches it. Together, they compress errors from both directions. This is different from a context graph, which stores world facts (this customer owns this product). The reasoning and retrieval graphs extend the context graph with behavioral facts: how agents reasoned, what worked, what did not. The pipeline planner and the LLM both walk all three layers.&lt;/p&gt;

&lt;h3&gt;
  
  
  The graph feedback loop
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This is the critical shift.&lt;/strong&gt; The reasoning and retrieval graphs turn retrieval from a static pipeline into a feedback loop. The funnel does not just narrow the data. It narrows itself. Every agent run produces decided, evaluated, and retrieval edges. Those edges inform the next pipeline plan. The scoping gets tighter. The graph paths get more precise. And because both graphs are written in the same transaction as the action, you have a causally consistent, traversable record of every decision the agent ever made, every piece of evidence it considered, and the retrieval path that produced it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4bekos5qtp4z4jij8ah5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4bekos5qtp4z4jij8ah5.png" alt="The graph feedback loop" width="800" height="727"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let me walk through what this looks like concretely. On the first run for a given query type, the retrieval funnel starts with 2.8 million candidates and narrows to 14. The agent evaluates the top candidates, creating an evaluated edge for each one with a verdict and reason. It makes a decision. The decision turns out to be correct. The decided edge, the evaluated edges, and the retrieval edges all persist in the graph.&lt;/p&gt;

&lt;p&gt;On the next similar query, both mechanisms kick in. The pipeline planner traverses past retrieval and reasoning edges. It knows the successful graph path. It knows which filter combination produced a good reduction ratio. It knows &lt;code&gt;kb:2201&lt;/code&gt; was rejected last time for being the wrong product line, so it excludes it from the candidate set entirely. The funnel goes from 2.8 million to 9 instead of 14. Meanwhile, the agent receives past evaluated edges as context alongside the candidates: "For similar firmware queries, &lt;code&gt;kb:4821&lt;/code&gt; was used because it described identical symptoms on the same SKU. &lt;code&gt;kb:5540&lt;/code&gt; was rejected because the issue was already resolved." The agent is not just seeing better candidates. It is seeing how a previous agent successfully reasoned about similar candidates.&lt;/p&gt;

&lt;p&gt;After a hundred similar queries, the graph contains a rich web of evaluated edges for that query type. The planner can see, across all runs, which evidence records consistently get used versus rejected, which rejection reasons recur, and which evaluation patterns correlate with correct outcomes. The LLM, on every new run, gets the distilled reasoning patterns from the most relevant past decisions. The funnel is no longer exploring. The reasoning is no longer starting from scratch. Both layers are applying learned patterns. And the patterns are not summaries or statistics computed offline. They are live graph topology that gets traversed on every run.&lt;/p&gt;

&lt;p&gt;This is what "deterministic" means in practice. Not that the LLM produces identical tokens every time. It means that given the same type of query, the system follows the same learned retrieval pattern, produces the same quality of context, and arrives at the same caliber of decision. The variability gets squeezed out of the data layer, which is where it was causing problems in the first place. The model can still be creative where creativity helps. But the retrieval is locked down.&lt;/p&gt;

&lt;h3&gt;
  
  
  The full retrieval funnel
&lt;/h3&gt;

&lt;p&gt;Putting it all together, here is the complete pipeline from raw candidates to agent context, with graphs feeding back into pipeline planning. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feuk9mceyalc1m8w2nu4q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feuk9mceyalc1m8w2nu4q.png" alt="The full retrieval funnel" width="800" height="1099"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After the agent acts, the decided, evaluated, and retrieval edges are written back, feeding into the next cycle of pipeline planning. The funnel self-tightens with every run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the graphs break in a fragmented stack
&lt;/h3&gt;

&lt;p&gt;There is one more thing worth making concrete, because it is easy to nod along with "you need one database" without feeling why.&lt;/p&gt;

&lt;p&gt;Say your reasoning and retrieval edges write to Postgres, your vector embeddings live in Pinecone, and your graph relationships are in Neo4j. The pipeline planner needs to query: "for this query type, which graph paths and filter combinations correlated with correct decisions in the last 200 runs?" That query needs to join reasoning edges and retrieval edges in Postgres with graph topology in Neo4j and vector metadata in Pinecone. What transaction boundary covers all three systems? There is not one. So the planner is reading behavioral data that may or may not reflect what actually happened, referencing graph paths that may have changed since the edges were written, and correlating against outcomes that landed in a different system at a different time. The feedback loop is broken before it starts. You are building a learning system on top of eventually consistent quicksand.&lt;/p&gt;

&lt;p&gt;That is why this architecture needs a multi-model graph database. Not because fewer databases is aesthetically nicer, but because the reasoning and retrieval edges need to live in the same graph as the entities they reference. The feedback loop is a graph traversal: walk from an agent through its decided edges, through its evaluated edges to each piece of evidence, follow the retrieval edges, compare outcomes. That only works when the reasoning and retrieval edges, the documents, the graph relationships, the vector embeddings, and the agent state all live in the same engine with the same transactional guarantees. SurrealDB handles all of this natively. This is the exact problem we designed it to solve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Supporting research
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Cognitive Architectures for Language Agents (CoALA)&lt;/strong&gt; — Sumers, Yao, Narasimhan, Griffiths · 2024. Formalizes agent memory into episodic, semantic, and procedural types coordinated through a single decision loop. Reasoning and retrieval graphs map directly to CoALA's episodic memory: structured records of past actions that inform future decisions. The framework makes clear that scattering memory types across separate systems works against reliable agent behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From Local to Global: A Graph RAG Approach&lt;/strong&gt; — Edge, Trinh, Larson, Truitt (Microsoft Research) · 2024. Demonstrated that combining knowledge graphs with retrieval-augmented generation significantly outperforms vector-only retrieval for complex queries requiring multi-hop reasoning and structured context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retrieval-Augmented Generation: A Survey&lt;/strong&gt; — Gao, Xiong, Gao et al. · 2023. Identifies consistent failure modes in RAG at scale: top-k instability, chunk blending, precision degradation. Recommends constraining the candidate set before similarity search using structural filters and graph traversal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Graph Retrieval-Augmented Generation: A Survey&lt;/strong&gt; — Peng, Zhu, Liu et al. · 2024. First comprehensive overview of GraphRAG, formalizing how graph-based indexing, retrieval, and generation improve accuracy and context-awareness over similarity-only approaches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Near-perfect agent accuracy comes from two things working together. A retrieval pipeline that scopes candidates using structural filters, temporal constraints, graph traversal, and business rules before similarity ranking, all within a single ACID transaction. That gets you to roughly 90–95% on large corpora by eliminating the consistency gaps and retrieval noise that cause most agent failures.&lt;/p&gt;

&lt;p&gt;And reasoning and retrieval graphs stored as edges in the same transaction as the agent's action, feeding back into the pipeline so the funnel tightens with every run. That is what closes the gap to 99%+.&lt;/p&gt;

&lt;p&gt;Neither works if your data models are split across separate systems. The scope query cannot compose and the feedback loop breaks unless everything shares one transaction boundary. That is why we built SurrealDB.&lt;/p&gt;

&lt;p&gt;On the first run, 2.8 million candidates narrowed to 14. A few hundred runs later, it narrows to 9, then 6, each time more precise, each time learning from the last. That is what deterministic accuracy looks like in practice: agents that do not just perform well on average, but get better with every decision they make.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Sumers, T.R., Yao, S., Narasimhan, K., &amp;amp; Griffiths, T.L. (2024). Cognitive Architectures for Language Agents. &lt;em&gt;Transactions on Machine Learning Research&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Edge, D., Trinh, H., Larson, J., &amp;amp; Truitt, S. (2024). From Local to Global: A Graph RAG Approach to Query-Focused Summarization. &lt;em&gt;arXiv:2404.16130&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Gao, Y., Xiong, Y., Gao, X., et al. (2023). Retrieval-Augmented Generation for Large Language Models: A Survey. &lt;em&gt;arXiv:2312.10997&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Peng, B., Zhu, Y., Liu, Y., et al. (2024). Graph Retrieval-Augmented Generation: A Survey. &lt;em&gt;arXiv:2408.08921&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>surrealdb</category>
      <category>agents</category>
      <category>database</category>
    </item>
    <item>
      <title>What's New in Surrealist 3.8</title>
      <dc:creator>Mark Gyles</dc:creator>
      <pubDate>Wed, 22 Apr 2026 09:15:32 +0000</pubDate>
      <link>https://dev.to/surrealdb/whats-new-in-surrealist-38-2c7h</link>
      <guid>https://dev.to/surrealdb/whats-new-in-surrealist-38-2c7h</guid>
      <description>&lt;p&gt;Authors: &lt;a href="https://github.com/ItsMateo" rel="noopener noreferrer"&gt;Matthew McFadden&lt;/a&gt; &amp;amp; &lt;a href="https://github.com/macjuul" rel="noopener noreferrer"&gt;Julian Mills&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We're excited to announce the release of Surrealist &lt;code&gt;3.8&lt;/code&gt;! This version introduces exciting new features such as a fully redesigned navigation experience, improved query formatting, and much more. Let's dive into what's new 🎉&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you are using Surrealist Desktop and this version does not appear automatically, please download the latest version manually.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Highlights
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Redesigned Navigation Experience
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv48h8sw931g1f0olhhcf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv48h8sw931g1f0olhhcf.png" alt="overview" width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We have redesigned the overview page to give you a cleaner and more intuitive experience. This new flow merges the organisations page into the overview page so that organisations are right at your fingertips when opening the app.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0a1e26k9zt1m9qdtcx41.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0a1e26k9zt1m9qdtcx41.png" alt="individual" width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Additionally, the organisation view has been fully redesigned from the ground up! This new sidebar navigation approach allows organisations to show more information than the previous tabs approach, making it easier to find your instances and view information at a glance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quality of Life Improvements
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy0iyj4vxen8h8ao2yma4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy0iyj4vxen8h8ao2yma4.png" alt="quality of life improvements" width="768" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This release also contains loads of quality of life improvements to make your experience using Surrealist even better! These include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An improved query formatting system with advanced configuration options for max line length, indentation mode, and indentation size&lt;/li&gt;
&lt;li&gt;A new request timer in the Query view which shows you the total time the request took for all queries&lt;/li&gt;
&lt;li&gt;A button to clear all notifications, rather than having to close each notification individually&lt;/li&gt;
&lt;li&gt;Advanced query result exporting in the Query view which allows you to export your query results as JSON or in CSV format&lt;/li&gt;
&lt;li&gt;A button to clear live query history&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Full Changelog
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Redesigned overview page&lt;/li&gt;
&lt;li&gt;Redesigned organisation view from the ground up&lt;/li&gt;
&lt;li&gt;Improved namespace and database management&lt;/li&gt;
&lt;li&gt;Added a new query results exporter to export query results in JSON or CSV format&lt;/li&gt;
&lt;li&gt;Added a button to clear live query history&lt;/li&gt;
&lt;li&gt;Added a new and improved query formatting system&lt;/li&gt;
&lt;li&gt;Added a new clear notifications button&lt;/li&gt;
&lt;li&gt;Added a query runtime to the Query view to show how long your whole request has been running&lt;/li&gt;
&lt;li&gt;Fixed an issue with the macOS menu bar items not working&lt;/li&gt;
&lt;li&gt;Fixed an issue with macOS keybinds not working&lt;/li&gt;
&lt;li&gt;Fixed an issue with the escape key exiting full screen on macOS&lt;/li&gt;
&lt;li&gt;Fixed an issue with the Designer view causing crashes&lt;/li&gt;
&lt;li&gt;Fixed an issue with stale data causing crashes in the Explorer view&lt;/li&gt;
&lt;li&gt;Fixed an issue with syntax highlighting being the wrong color in light mode when searching&lt;/li&gt;
&lt;li&gt;Fixed missing horizontal and vertical scrollbars in the Explorer view&lt;/li&gt;
&lt;li&gt;Fixed an issue with record selections persisting across tables&lt;/li&gt;
&lt;li&gt;Fixed an issue where graph relations caused Designer view to crash&lt;/li&gt;
&lt;li&gt;Fixed an issue where large namespaces and databases would cause scrolling&lt;/li&gt;
&lt;li&gt;Fixed an issue with decimal points being truncated that were below 0.000&lt;/li&gt;
&lt;li&gt;Fixed an issue with record ids rendering in light mode when the app is in dark mode&lt;/li&gt;
&lt;li&gt;Fixed an issue where some error messages were not showing in the record inspector&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;We hope you enjoy these new features and improvements! As always, we appreciate your feedback and suggestions for future releases. &lt;a href="https://discord.com/invite/surrealdb" rel="noopener noreferrer"&gt;Join the SurrealDB Discord&lt;/a&gt; - engage with the community and receive support.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://app.surrealdb.com/?_gl=1*1t0eg6h*_gcl_aw*R0NMLjE3NzQ2MjU1MDcuQ2owS0NRancxWmpPQmhDbUFSSXNBRER1RlRDVV9VN2JENUdhMFQzTm5mV0paMzNOVkxiTDNROVFOSTlUckhRUHV0V2FVN1l2czJ0MlNGQWFBc3hCRUFMd193Y0I.*FPAU*NTM0Mzg1Mzg1LjE3NzA2NTk1MTg.*_ga*NDAyMzA0NTY4LjE3NzEyNDU1Nzg.*_ga_J1NWM32T1V*czE3NzY4NDcxMTMkbzM1OSRnMSR0MTc3Njg0ODYxNiRqNjAkbDAkaDYyNTk1Njc2Nw..*_fplc*YlptRUpsMDR5MU5nT2MyOCUyRmM5V2FGOXNQZnc2V0FKdGhVYktRbmw5N3k5JTJCSDA4cExKam8lMkJ0Z2Q4SDFyVHlnTDVQWkQwZm1Cc1MlMkJORDQ3QXFQWlFOTERwVk9CbUtmcU14cFk3R2QzbTMwZDk0bjhvSlNFNW12bkF2a1NoREElM0QlM0Q." rel="noopener noreferrer"&gt;Get started with Surrealist for free today&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>surrealdb</category>
      <category>surrealist</category>
      <category>database</category>
      <category>web</category>
    </item>
    <item>
      <title>Graph RAG does not need a graph database. It needs a database that does everything.</title>
      <dc:creator>Mark Gyles</dc:creator>
      <pubDate>Tue, 07 Apr 2026 15:57:35 +0000</pubDate>
      <link>https://dev.to/surrealdb/graph-rag-does-not-need-a-graph-database-it-needs-a-database-that-does-everything-117k</link>
      <guid>https://dev.to/surrealdb/graph-rag-does-not-need-a-graph-database-it-needs-a-database-that-does-everything-117k</guid>
      <description>&lt;p&gt;Author: &lt;a href="https://www.linkedin.com/in/mpenaroza/" rel="noopener noreferrer"&gt;Matthew Penaroza&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How SurrealDB compares to Neo4j, Amazon Neptune, and ArangoDB for production graph RAG.&lt;/p&gt;

&lt;p&gt;Graph RAG is the right idea. Using relationships between entities to scope and improve retrieval produces better results than vector similarity alone. The research is clear on this. What the research does not address is where those operations execute, and that turns out to be the question that actually matters in production. Here is what happens when you try to build production graph RAG across a typical multi-database stack, and what changes when every operation composes in a single system.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap: what graph RAG actually needs in production
&lt;/h2&gt;

&lt;p&gt;Most graph RAG implementations follow the pattern from Microsoft's GraphRAG paper: extract entities from documents, build a knowledge graph of those entities and their relationships, then traverse that graph at query time to pull relevant context. The graph is derived from the content. It is an inferred structure that helps you find documents that are semantically connected through shared entities.&lt;/p&gt;

&lt;p&gt;That works for document discovery over a static corpus. It does not work for a production agent that needs to serve a specific customer, respect access control, enforce tenancy, check data freshness, and ground its answers in real business relationships rather than inferred ones.&lt;/p&gt;

&lt;p&gt;A production graph RAG pipeline needs all of the following in a single retrieval step:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ground truth relationships.&lt;/strong&gt; This customer purchased this product. This product has this known defect. This defect maps to these knowledge base articles. These are not inferred from document content. They are transactional facts from your purchase system, your CRM, your product catalog. The graph traversal that scopes retrieval needs to follow these real edges, not entity co-occurrence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LLM-inferred relationships.&lt;/strong&gt; This knowledge base article documents a fix for this issue. This article is related to that article. These connections are useful and hard to create manually at scale. An extraction pipeline reads the article and the issue description and decides they are connected. This is exactly what the GraphRAG pattern provides, and it is valuable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structured filters.&lt;/strong&gt; Type constraints, tenant isolation, permission checks. Not every document that is relevant is valid for this agent to return to this customer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Temporal constraints.&lt;/strong&gt; The article was updated within 30 days. The product has not been discontinued. The issue is still active.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hybrid retrieval.&lt;/strong&gt; Vector similarity and full-text search, blended into a single relevance score, running against the candidates that survived everything above.&lt;/p&gt;

&lt;p&gt;None of these are controversial. Every serious production RAG system needs them. The question is where they execute and whether they compose.&lt;/p&gt;

&lt;h2&gt;
  
  
  The comparison: the same pipeline in SurrealQL and Cypher
&lt;/h2&gt;

&lt;p&gt;Neo4j is the most established graph database. Since version 5.11, it has native vector indexes. It has full-text indexes. It can store properties on nodes and relationships. There is no technical reason you cannot build this entire pipeline in Neo4j.&lt;/p&gt;

&lt;p&gt;Here is the retrieval operation in SurrealQL. Same scoping, graph traversal, hybrid search, and result set as you would build in Cypher, but expressed as a single statement with co-equal predicates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;knn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;vec_dist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;search&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ft_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;knn&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;65&lt;/span&gt;
    &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;search&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;35&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;blend&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;search&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;highlight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;em&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;/em&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;snippet&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;knowledge_base&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'support'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;tenant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;tenant&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;agent_principal&lt;/span&gt; &lt;span class="n"&gt;INSIDE&lt;/span&gt; &lt;span class="n"&gt;allowed_principals&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt;
    &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;owns&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;
      &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;has_issue&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;knowledge_base&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;content_embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;|&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;COSINE&lt;/span&gt;&lt;span class="o"&gt;|&amp;gt;&lt;/span&gt;
    &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;query_embedding&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;query_text&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;blend&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One statement. Co-equal predicates. One consistent snapshot.&lt;/p&gt;

&lt;p&gt;In SurrealQL, the graph traversal, structured filters, vector search, and full-text search are co-equal predicates in a single &lt;code&gt;WHERE&lt;/code&gt; clause. They compose the same way boolean conditions always compose: with &lt;code&gt;AND&lt;/code&gt;. The query reads like a single thought. You can rearrange the predicates, add new ones, or remove them, and the statement still works.&lt;/p&gt;

&lt;p&gt;In Cypher, the same operations require separate procedural steps. Vector search is a &lt;code&gt;CALL db.index.vector.queryNodes()&lt;/code&gt; block. Full-text search is another &lt;code&gt;CALL db.index.fulltext.queryNodes()&lt;/code&gt; block. The graph traversal is a &lt;code&gt;MATCH&lt;/code&gt; pattern. The metadata filters are a &lt;code&gt;WHERE&lt;/code&gt; clause. The blending is manual arithmetic in the &lt;code&gt;RETURN&lt;/code&gt;. You are orchestrating five steps and correlating their results yourself. It works, but it reads like a script, not a query.&lt;/p&gt;

&lt;p&gt;The readability gap is the obvious problem. The less obvious one is memory. Each procedural step in the Cypher pipeline &lt;code&gt;YIELD&lt;/code&gt;s a result set into the execution context, and those intermediate results stay in memory while the next step runs. The vector &lt;code&gt;CALL&lt;/code&gt; yields 100 nodes with scores. Those 100 nodes sit in memory while the &lt;code&gt;MATCH&lt;/code&gt; clause runs graph pattern matching against them. The &lt;code&gt;MATCH&lt;/code&gt; can fan out fast: if a customer owns 5 products, each product has 10 known issues, and each issue maps to 20 knowledge base articles, the pattern expands to 1,000 intermediate rows before any &lt;code&gt;WHERE&lt;/code&gt; filter trims them. All materialized. All held. Then the full-text &lt;code&gt;CALL&lt;/code&gt; runs and yields another result set that needs to be correlated with everything already in memory.&lt;/p&gt;

&lt;p&gt;At production scale, this is not a theoretical concern. Combinatorial expansion in multi-hop &lt;code&gt;MATCH&lt;/code&gt; patterns is one of the most common causes of out-of-memory failures in Neo4j. The intermediate state accumulates faster than the downstream filters can prune it, and the query planner cannot optimize across the boundaries between &lt;code&gt;CALL&lt;/code&gt; blocks and &lt;code&gt;MATCH&lt;/code&gt; clauses because they are procedurally separate operations. You end up tuning heap sizes, adding &lt;code&gt;LIMIT&lt;/code&gt; hints mid-pipeline, or splitting queries into multiple round-trips to keep memory under control. All of which is managing a problem that does not need to exist.&lt;/p&gt;

&lt;p&gt;In SurrealQL, because all predicates are co-equal in one &lt;code&gt;WHERE&lt;/code&gt; clause, the engine sees every constraint upfront. It can start with the most selective predicate and narrow early before touching the expensive operations. There is no intermediate materialization between steps because there are no steps. The result set is built once, not accumulated across a pipeline.&lt;/p&gt;

&lt;p&gt;This matters for day-to-day development too. When retrieval logic is a single composable statement, you can modify it, extend it, and reason about it as a unit. When it is a multi-step procedure, every change requires understanding the data flow between steps. Adding a new filter means figuring out which step it belongs to and whether it changes what gets passed to the next step. Removing a constraint means tracing its effects through the pipeline. The cognitive overhead scales with the number of operations, and production retrieval pipelines have a lot of operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The deeper problem: your graph database is not your system of record
&lt;/h2&gt;

&lt;p&gt;Neo4j is almost never where the data originates. The purchase happens in Postgres. The ticket gets created in a CRM. The permission change happens in an auth service. Neo4j receives all of that via sync jobs, CDC pipelines, or batch imports. It is a read replica of graph-shaped data. The same is true of Neptune and, in most deployments, ArangoDB. The graph database is downstream of the systems that produce the data your agent needs to reason over.&lt;/p&gt;

&lt;p&gt;That is why the consistency problem exists in the first place. The graph is always behind the source of truth because it is not the source of truth. A permission gets revoked in the auth service, but the graph database does not see it for 50ms and the vector index does not see it for 200ms. An article gets updated in the CMS, but the vector store still has embeddings from the old version for up to 5 minutes. Each system is internally consistent. The inconsistency lives in the spaces between them. Your agent is reasoning over multiple realities simultaneously and does not know it.&lt;/p&gt;

&lt;p&gt;SurrealDB is designed as a transactional system of record. The purchase record, the customer relationship, the product graph, the knowledge base articles, the embeddings, the agent's memory, and the LLM-inferred edges from extraction pipelines all live in the same database as first-class data. The ground truth edges are not synced from somewhere else. They are the transaction records:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- The purchase record IS the graph edge&lt;/span&gt;
&lt;span class="n"&gt;RELATE&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4821&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;purchased&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;drip_coffee_maker&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt;
  &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ORD-29481'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;purchased_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="k"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'transaction'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Product-to-known-issue edge (from engineering)&lt;/span&gt;
&lt;span class="n"&gt;RELATE&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;drip_coffee_maker&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;has_issue&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;product_issue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;gasket_defect&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt;
  &lt;span class="n"&gt;severity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'moderate'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;confirmed_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="s1"&gt;'2026-02-15'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'engineering'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The moment that transaction commits, &lt;code&gt;customer:4821-&amp;gt;purchased-&amp;gt;product:drip_coffee_maker&lt;/code&gt; is traversable. There is no sync job. There is no CDC pipeline. There is no replication lag. An agent querying one millisecond later sees it.&lt;/p&gt;

&lt;p&gt;The LLM-inferred edges write to the same database. An extraction pipeline reads a knowledge base article and the issue description, decides they are connected, and creates the edge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;RELATE&lt;/span&gt; &lt;span class="n"&gt;product_issue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;gasket_defect&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;documented_in&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;kb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;fix_base_leaks&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt;
  &lt;span class="k"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'llm_extraction'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;94&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'claude-sonnet-4'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;verified_by&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NONE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A single traversal now crosses both: &lt;code&gt;$customer-&amp;gt;purchased-&amp;gt;product-&amp;gt;has_issue-&amp;gt;documented_in-&amp;gt;knowledge_base&lt;/code&gt;. The first two hops are ground truth from the transaction. The last hop is LLM-inferred from the extraction pipeline. Every predicate in the query evaluates against the same ACID snapshot.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is not a convenience feature. It eliminates an entire category of infrastructure: the sync jobs, the CDC pipelines, the cache invalidation, the eventual consistency windows. Every one of those is a place where accuracy silently degrades. A database that is both the system of record and the query engine removes the gaps where those failures live.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The landscape: how other graph databases handle this
&lt;/h2&gt;

&lt;p&gt;SurrealDB is not the only database attempting to unify these operations. Every major graph database has recognized that vector search matters for graph RAG, and each has added some form of it. The question is not whether they support it. It is whether the operations compose, and what breaks when they do not.&lt;/p&gt;

&lt;h3&gt;
  
  
  Neo4j
&lt;/h3&gt;

&lt;p&gt;Neo4j is the most established graph database and the most natural comparison. As of v2026.01, Neo4j introduced a native &lt;code&gt;SEARCH&lt;/code&gt; clause with in-index filtering as a preview feature. Before this, vector search could not be pre-filtered at all. You ran the vector index, got results, and filtered afterward. For years, this was a blocking issue for teams building multi-tenant RAG, and community threads going back to 2023 show developers hitting this wall repeatedly.&lt;/p&gt;

&lt;p&gt;The new &lt;code&gt;SEARCH&lt;/code&gt; clause is a step forward, but the &lt;code&gt;WHERE&lt;/code&gt; subclause inside &lt;code&gt;SEARCH&lt;/code&gt; supports only a subset of the full Cypher &lt;code&gt;WHERE&lt;/code&gt; clause. Full-text search is still a separate procedure call that cannot compose with the &lt;code&gt;SEARCH&lt;/code&gt; clause in a single statement. The graph traversal is still a separate &lt;code&gt;MATCH&lt;/code&gt; pattern. So even with the improvement, you are still orchestrating procedural steps and correlating results between them, with the memory accumulation problem described earlier in this article. Neo4j is actively closing the gap, but as of today the composition is partial.&lt;/p&gt;

&lt;p&gt;Where Neo4j remains the clear winner is deep graph analytics. The Graph Data Science library (community detection, centrality algorithms, pathfinding, node similarity at scale) is purpose-built and has no equivalent in SurrealDB. If your primary workload is analytical graph algorithms rather than transactional agent retrieval, Neo4j is the right tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  Amazon Neptune
&lt;/h3&gt;

&lt;p&gt;Amazon Neptune is the default choice for teams already on AWS, and it illustrates the composition problem at the product level. Neptune is actually two separate products. Neptune Database is a transactional graph database with no native vector search. Neptune Analytics is an analytics engine with vector search and graph algorithms but designed for analytical workloads, not transactional retrieval. AWS's own "unified" graph solution is itself a two-database architecture.&lt;/p&gt;

&lt;p&gt;Neptune Analytics has vector search, but with fundamental constraints. Vector index updates are explicitly not ACID compliant: changes to vector embeddings are non-atomic and become visible to concurrent queries immediately, even if the query that wrote them fails later. If a bulk load with embeddings fails midway, you end up with a partial set of embeddings and need to retry the entire operation. You can only create one vector index per graph, and it must be specified at graph creation time. There is no native full-text search. And because Neptune Analytics is an analytics engine, not a transactional store, it is designed for loading data from S3 or snapshotting from Neptune Database, not for serving live agent queries against data that changes in real time.&lt;/p&gt;

&lt;h3&gt;
  
  
  ArangoDB
&lt;/h3&gt;

&lt;p&gt;ArangoDB is the closest multi-model competitor to SurrealDB's architecture. It supports documents, graphs, key-value, full-text search (via ArangoSearch), and as of version 3.12.4, vector search via FAISS integration. On paper, it has every primitive under one roof.&lt;/p&gt;

&lt;p&gt;ArangoDB's vector index requires a &lt;code&gt;--vector-index&lt;/code&gt; startup flag that permanently alters the RocksDB storage engine and cannot be reversed. Since v3.12.6, ArangoDB has supported attribute pre-filtering on vector searches — you can place &lt;code&gt;FILTER&lt;/code&gt; operations between &lt;code&gt;FOR&lt;/code&gt; and &lt;code&gt;SORT&lt;/code&gt; to narrow candidates before the vector index runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FOR doc IN knowledge_base
  FILTER doc.tenant == @tenant
  FILTER doc.type == 'support'
  SORT APPROX_NEAR_COSINE(doc.embedding, @query_emb) DESC
  LIMIT 10
  RETURN doc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This addresses the multi-tenant filtering problem for attribute-based constraints and is a genuine improvement over earlier versions. Where the composition breaks down is the full retrieval pipeline. ArangoSearch (the full-text engine) uses a separate &lt;code&gt;SEARCH&lt;/code&gt; operation with its own syntax and optimizer path. Graph traversals use &lt;code&gt;FOR v, e, p IN ... GRAPH&lt;/code&gt; with a different iteration model. Vector search requires iterating over a collection directly with &lt;code&gt;APPROX_NEAR_*&lt;/code&gt; functions. Composing all three — traversing a customer's product graph to scope candidates, running vector similarity against those scoped candidates, and blending in full-text relevance scores — requires orchestrating these as separate operations within AQL rather than expressing them as co-equal predicates in a single statement.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The pattern across all three is the same. Each database has added vector search as a capability. None of them have made it compose natively with graph traversal, structured filters, and full-text search in a single atomic statement. The operations exist. The composition does not. That composition is the specific thing SurrealDB was designed to provide.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Honest tradeoffs: when SurrealDB is not the right choice
&lt;/h2&gt;

&lt;p&gt;If your primary workload is deep graph analytics (community detection, centrality algorithms, pathfinding over billions of edges), Neo4j's Graph Data Science library or TigerGraph's analytical engine are purpose-built for that. SurrealDB does not have an equivalent analytics library.&lt;/p&gt;

&lt;p&gt;The honest question is whether your current retrieval accuracy is good enough. If your agents are producing correct results at an acceptable rate and the failure modes described in this article are not showing up in production, there is no reason to migrate. The architecture you have is working.&lt;/p&gt;

&lt;p&gt;If you are trying to push accuracy higher and you are hitting a ceiling, the pattern is almost always the same: the individual operations work, but they do not compose. The vector search returns good candidates. The graph traversal follows the right paths. The filters check the right fields. But they run separately, against different snapshots, with intermediate results accumulating in memory, and the accuracy loss lives in the gaps between them. That is not a problem you can solve by tuning parameters. It is a structural limitation of running these operations across systems that were not designed to compose them. Closing that gap requires a database where graph traversal, vector search, full-text search, structured filters, and permission checks are co-equal predicates in a single atomic statement. That is what SurrealDB provides.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Every graph database covered in this article can store graphs, run traversals, and return results. Most of them can now do vector search. The thing none of them can do is compose all of it in a single atomic statement against a single consistent snapshot while also being the transactional system of record for the data your agent reasons over.&lt;/p&gt;

&lt;p&gt;That is not a feature gap. It is an architectural one. And it is the gap where retrieval accuracy goes to die.&lt;/p&gt;

</description>
      <category>graphrag</category>
      <category>rag</category>
      <category>surrealdb</category>
      <category>database</category>
    </item>
    <item>
      <title>Exponential cost traps in database architectures: how SurrealDB breaks the cycle</title>
      <dc:creator>Mark Gyles</dc:creator>
      <pubDate>Thu, 02 Apr 2026 12:33:01 +0000</pubDate>
      <link>https://dev.to/surrealdb/exponential-cost-traps-in-database-architectures-how-surrealdb-breaks-the-cycle-4ane</link>
      <guid>https://dev.to/surrealdb/exponential-cost-traps-in-database-architectures-how-surrealdb-breaks-the-cycle-4ane</guid>
      <description>&lt;h2&gt;
  
  
  Exponential cost traps in database architectures: how SurrealDB breaks the cycle
&lt;/h2&gt;

&lt;p&gt;In the fast-paced world of application development, database choices can make or break an organisation's scalability and budget. Traditional polyglot persistence (relying on multiple specialised databases) often leads to exponential cost growth as systems expand. What starts as a simple setup spirals into a complex, inefficient infrastructure riddled with data duplication, ETL pipelines, and operational silos. SurrealDB, with its multi-model architecture unifying document, graph, vector, full-text, time-series, geospatial, and relational capabilities, offers a consolidated alternative that eliminates these pitfalls. By storing data once and querying it across models, SurrealDB can reduce overall costs by multiple factors, particularly in storage-heavy environments where cloud block storage is in use.&lt;/p&gt;

&lt;p&gt;To understand the value of consolidation, consider the common trajectory of database sprawl. Below, we explore a real-world-inspired story of how costs escalate exponentially, helping readers identify if their setup is heading toward inefficiency. This narrative highlights the compounding effects of architectural decisions, setting the stage for how SurrealDB provides a streamlined, cost-effective path forward.&lt;/p&gt;

&lt;h2&gt;
  
  
  A story of exponentially increasing costs: from simplicity to sprawl
&lt;/h2&gt;

&lt;p&gt;Many teams begin with modest needs but face rapid growth that exposes the limitations of single-model databases. Let's trace a typical app's evolution, illustrating how costs outpace traffic due to added complexity, redundancy, and management overhead.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Launching the app: a simple start
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Sentiment: "We start simple: a single database, fast and cheap to run."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Your app is new, with straightforward transactional requirements. You deploy a single MySQL instance on AWS, adding one replica for high availability. It's low cost and easy to manage. Database expenses are minimal, infrastructure is straightforward, and personnel needs are light.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;​&lt;/th&gt;
&lt;th&gt;​&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Database Costs:&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traffic Handled:&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infra Cost:&lt;/td&gt;
&lt;td&gt;Minimal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Personnel Required:&lt;/td&gt;
&lt;td&gt;1–2 engineers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temp cost:&lt;/td&gt;
&lt;td&gt;None yet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complexity cost:&lt;/td&gt;
&lt;td&gt;None yet&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MySQL (Primary)&lt;/li&gt;
&lt;li&gt;MySQL (Failover Replica)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. First wave of growth: scaling reads
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Sentiment: "Our first scaling challenge is reads. Replicas help, but cost growth slightly outpaces traffic."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;User traffic surges, dominated by reads. You add 8 read replicas and scale the writer to a larger instance (where per-vCPU costs rise disproportionately). Costs begin increasing slightly faster than traffic due to over-provisioning and load balancing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;​&lt;/th&gt;
&lt;th&gt;​&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Database costs:&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traffic handled:&lt;/td&gt;
&lt;td&gt;Increased&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infra cost:&lt;/td&gt;
&lt;td&gt;Rising&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Personnel required:&lt;/td&gt;
&lt;td&gt;Still low, but monitoring grows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temp cost:&lt;/td&gt;
&lt;td&gt;None yet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complexity cost:&lt;/td&gt;
&lt;td&gt;None yet&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MySQL Writer&lt;/li&gt;
&lt;li&gt;Load Balancer&lt;/li&gt;
&lt;li&gt;8x MySQL Read Replicas&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Write traffic explodes: sharding begins
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Sentiment: "As writes explode, we shard the system, but migrations are expensive and disruptive."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Writes overwhelm the single writer. You shard into four MySQL instances, over-provisioning each for spikes. A data access layer handles routing by key, and the migration temporarily doubles costs. Complexity introduces temporary and ongoing expenses.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;​&lt;/th&gt;
&lt;th&gt;​&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Database costs:&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traffic handled:&lt;/td&gt;
&lt;td&gt;Substantial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infra cost:&lt;/td&gt;
&lt;td&gt;Accelerating&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Personnel required:&lt;/td&gt;
&lt;td&gt;Increasing (sharding expertise needed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temp cost:&lt;/td&gt;
&lt;td&gt;Spike from migration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complexity cost:&lt;/td&gt;
&lt;td&gt;Emerging&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;4x MySQL Shards (each with Writer + Multiple Read Replicas)&lt;/li&gt;
&lt;li&gt;Load Balancers&lt;/li&gt;
&lt;li&gt;CDC (Change Data Capture) for transitioning from old architecture&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Analytics demands: the first pipeline
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Sentiment: "Analytics needs have driven us to add a pipeline and duplicate data with added complexity as a result.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Product teams demand dashboards and real-time insights. You build an Extract, Transform, Load (ETL) pipeline to an analytical database such as ClickHouse, duplicating data via Change Data Capture (CDC). Applications now query multiple systems, and engineers must manage query routing while maintaining consistency across stores.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;​&lt;/th&gt;
&lt;th&gt;​&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Database costs:&lt;/td&gt;
&lt;td&gt;Very high&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traffic handled:&lt;/td&gt;
&lt;td&gt;Advanced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infra cost:&lt;/td&gt;
&lt;td&gt;Ballooning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Personnel required:&lt;/td&gt;
&lt;td&gt;Still increasing (pipeline maintenance)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temp cost:&lt;/td&gt;
&lt;td&gt;Pipeline setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complexity cost:&lt;/td&gt;
&lt;td&gt;Significant&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Existing MySQL Shards&lt;/li&gt;
&lt;li&gt;CDC + ETL&lt;/li&gt;
&lt;li&gt;ClickHouse&lt;/li&gt;
&lt;li&gt;Application querying multiple databases&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. AI ambitions: vector database enters
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Sentiment: "AI ambitions require another new system, and more pipelines. The architecture is growing fast."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;AI agents need RAG (Retrieval-Augmented Generation) support, querying across systems. You spin up a vector DB like Weaviate or Pinecone, with new pipelines for replication. Data is consolidated in S3 for training, adding sharding as usage grows.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;​&lt;/th&gt;
&lt;th&gt;​&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Database costs:&lt;/td&gt;
&lt;td&gt;Extreme&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traffic handled:&lt;/td&gt;
&lt;td&gt;AI-enhanced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infra cost:&lt;/td&gt;
&lt;td&gt;Exponential&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Personnel required:&lt;/td&gt;
&lt;td&gt;Specialised teams&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temp cost:&lt;/td&gt;
&lt;td&gt;AI integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complexity cost:&lt;/td&gt;
&lt;td&gt;Overwhelming&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Existing Setup&lt;/li&gt;
&lt;li&gt;Additional ETL + Kafka&lt;/li&gt;
&lt;li&gt;Weaviate&lt;/li&gt;
&lt;li&gt;S3 for data lakes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6. Keeping it all running: growing ops
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Sentiment: "Now we need a large ops team just to stay afloat. Costs are scaling exponentially, not linearly."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ongoing tasks include re-sharding MySQL, scaling MongoDB/Weaviate/ClickHouse, upgrades across systems, node recoveries, and pipeline fixes. Teams manage credentials, security, and optimisations in isolation. Ops cycles dominate, slowing innovation and ballooning personnel costs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;​&lt;/th&gt;
&lt;th&gt;​&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Database costs:&lt;/td&gt;
&lt;td&gt;Off the charts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traffic handled:&lt;/td&gt;
&lt;td&gt;Peak&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infra cost:&lt;/td&gt;
&lt;td&gt;Exponential&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Personnel required:&lt;/td&gt;
&lt;td&gt;Large teams&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temp cost:&lt;/td&gt;
&lt;td&gt;Constant disruptions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complexity cost:&lt;/td&gt;
&lt;td&gt;Debilitating&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All systems with balancing, re-sharding, upgrades, and recoveries&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why the cost curve went exponential
&lt;/h3&gt;

&lt;p&gt;Exponential costs are rarely caused by user growth alone. Instead, they often stem from an architecture that acts as a tax on every new login. Between the headaches of manual sharding, the hidden drain of data duplication, and the hours lost to rigid schema changes, many teams end up subsidizing a fragmented mess. If your infrastructure spend is climbing faster than your traffic, you are leaking resources rather than scaling. Moving toward a unified database simplifies the stack and returns your cost curve to a manageable, linear state.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hidden costs of database sprawl: ETL, layers, and operational overhead
&lt;/h2&gt;

&lt;p&gt;Building on this story, polyglot architectures create fragile ETL pipelines for data movement between systems like MySQL and ClickHouse. These demand resources for processing, debugging, and schema handling. Operational burdens include multiple query languages, fragmented backups, and custom synchronisation.&lt;/p&gt;

&lt;p&gt;SurrealDB eliminates this by natively supporting all models in one platform with SurrealQL. No ETL needed. Data stays unified, reducing dependencies and accelerating development, as seen in replacements of PostgreSQL + Neo4j + RabbitMQ stacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  EBS storage: the #1 cost driver in self-hosted databases
&lt;/h2&gt;

&lt;p&gt;As the story shows, EBS volumes (and Azure and GCP’s equivalent) multiply with replicas and duplications, becoming the dominant expense. With EBS volumes not being eligible for discounts, costs compound in sprawls: 3x replication per cluster, plus copies across databases, over-provisioning, and snapshots typically result in massive redundant data duplication&lt;/p&gt;

&lt;p&gt;For example, in a 50-node cluster with 4TB EBS volumes on AWS, storage will be $28,098 monthly, over twice the $12,577 for EC2 (factoring in discounts). Polyglot setups exacerbate this exponential growth.&lt;/p&gt;

&lt;h2&gt;
  
  
  SurrealDB's compression edge: reducing data size by 1/3 for dramatic savings
&lt;/h2&gt;

&lt;p&gt;SurrealDB's storage engine (TiKV) delivers 70-80% better compression than PostgreSQL's, typically shrinking datasets to one-third. For the 4TB example, this cuts EBS to ~1.33TB, dropping costs to under $10,000, a 65% savings. In addition to the raw savings, smaller data reduces I/O, snapshots, and data transfer cost, countering the sprawl's redundancy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Eliminating data duplication: organization-wide cost reductions
&lt;/h2&gt;

&lt;p&gt;Storing data once avoids multi-system duplication. A single cluster with 3× replication replaces the primary and secondary copies spread across multiple database systems. Eliminating ETL removes staging costs, while unified data views simplify compliance and backups, reducing S3 storage and egress fees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Linear scalability: ensuring cost-effective growth
&lt;/h2&gt;

&lt;p&gt;Unlike most other database systems, SurrealDB scales linearly via TiKV, with less than a 5% performance deviation coefficient per node. You get roughly the same increase in capability and performance when adding a node to a 9-node cluster as when adding a node to a 100-node cluster. This keeps costs predictable and prevents exponential cost growth which is typical in sprawling platforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: escape the exponential trap with SurrealDB
&lt;/h2&gt;

&lt;p&gt;The story of sprawl reveals how polyglot decisions lead to exponential costs through duplication, complexity, and ops overhead. SurrealDB's "one database to rule them all" reverses this, slashing EBS-driven expenses via consolidation and compression. For teams in inefficient systems, migrating promises simplicity and transformative savings.&lt;/p&gt;

</description>
      <category>surrealdb</category>
      <category>database</category>
      <category>webdev</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
