<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Vector-Search on MongrelDB</title><link>https://www.mongreldb.com/articles/tags/vector-search/</link><description>Recent content in Vector-Search on MongrelDB</description><image><title>MongrelDB</title><url>https://www.mongreldb.com/logo.png</url><link>https://www.mongreldb.com/logo.png</link></image><generator>Hugo</generator><language>en-US</language><lastBuildDate>Thu, 09 Jul 2026 12:30:00 -0500</lastBuildDate><atom:link href="https://www.mongreldb.com/articles/tags/vector-search/index.xml" rel="self" type="application/rss+xml"/><item><title>Sub-Millisecond HNSW at the Storage Layer</title><link>https://www.mongreldb.com/articles/2026/07/sub-millisecond-hnsw-at-the-storage-layer/</link><pubDate>Thu, 09 Jul 2026 12:30:00 -0500</pubDate><guid>https://www.mongreldb.com/articles/2026/07/sub-millisecond-hnsw-at-the-storage-layer/</guid><description>MongrelDB&amp;#39;s HNSW index lives in the same engine as the rows it covers, so vector search shares the same WAL, transaction, and /kit/query path as the rest of your data.</description><content:encoded><![CDATA[<p>Vector search usually means operating a second system, and that means writing the same row twice: once to the database, once to the vector service, and the two writes have to look like one through some pattern of outbox tables, CDC consumers, or dual-write transactions. We chose to skip the second system; the engine runs HNSW in-process, the same WAL that protects your rows protects your vector index, and the same transaction model that commits an <code>UPDATE</code> on a <code>documents</code> row commits the corresponding HNSW entry for that row.</p>
<h2 id="what-in-process-actually-buys-you">What &ldquo;in-process&rdquo; actually buys you</h2>
<p>Two things specifically, and they are the same two things every in-process database call gets when the network is gone.</p>
<p>The first is a round trip. A single HTTP request from any Tier 2 client to a <code>mongreldb-server</code> over loopback measures p50 ~0.22 ms / p99 ~0.62 ms at ~4,000 ops/s for <code>PUT /tables/{name}/put</code> and <code>POST /kit/txn</code> commit on the published <a href="https://github.com/visorcraft/MongrelDB/blob/main/BENCHMARKS.md"><code>BENCHMARKS.md</code></a>; the equivalent call into the engine through the in-process Tier 1 binding (Rust, PyO3 for Python, NAPI for TypeScript / Node) drops the per-call cost into single-digit microseconds, with the same <code>6.79 µs commit</code> (group-committed, fsync&rsquo;d) bound from the write-path numbers in the same file. The network is the dominant cost in the Tier 2 path; <code>axum</code> handler dispatch, JSON encode/decode, and the TCP round trip add up, while the engine work underneath is sub-10 µs.</p>
<p>The second is transactional consistency. The HNSW graph and the row it covers get the same atomic-write semantics; either both are durable, or both are not. There is no &ldquo;the database committed but the vector index lagged&rdquo; state for the engine to defend against because the engine is one engine. CDC pipelines, outbox consumers, and dual-write coordinators exist in application code only because two systems need to look like one; they stop existing when the two systems collapse back into one.</p>
<h2 id="the-actual-numbers">The actual numbers</h2>
<p>The HNSW graph is a column-level index in the same engine files as the rows it indexes. A vector insert inside a transaction rides the same single-digit-microsecond <code>put</code> path the published <code>cargo bench -p mongreldb-core --bench write_path</code> numbers report (<code>put</code> no-fsync at <code>618 ns</code>, <code>commit</code> fsync at <code>6.79 µs</code>, group commit at <code>686 µs</code> for 1,000 rows). A <code>k</code>-nearest-neighbour query in the same transaction rides the same sub-10 µs filter path the cold-SQL-filter number (<code>8.7 µs</code> at N=1M) tracks. The HTTP loopback path costs the same 0.22 ms p50 / 0.62 ms p99 as every other Tier 2 op, because the dominant cost in that path is the network, not the index; HNSW-specific benchmark numbers will land in <code>BENCHMARKS.md</code> when the HNSW build policy is committed, and we will not invent them in the meantime.</p>
<h2 id="what-this-looks-like-from-a-whereann--call">What this looks like from a <code>where('ann', ...)</code> call</h2>
<p>The fluent query builder in <code>mongreldb-php</code> pushes <code>where('ann', ...)</code> to the same HNSW index through the same <code>/kit/query</code> path as a regular typed column. A query for ten nearest neighbours of a 384-dimensional embedding against a <code>documents</code> table, filtered by <code>tags = 'engineering'</code>, looks like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span>$rows <span style="color:#f92672">=</span> $db<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">table</span>(<span style="color:#e6db74">&#39;documents&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">where</span>(<span style="color:#e6db74">&#39;tags&#39;</span>, <span style="color:#e6db74">&#39;engineering&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">whereAnn</span>(<span style="color:#e6db74">&#39;embedding&#39;</span>, $vector, <span style="color:#a6e22e">k</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">10</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">get</span>();
</span></span></code></pre></div><p>That call, against a <code>mongreldb-server</code> running on the same host, is one HTTP round trip; against an in-process Tier 1 binding it is one function call into <code>mongreldb-core</code>. Both paths return the same rows with the same transaction isolation; the only difference is the round trip.</p>
<h2 id="when-this-is-the-wrong-shape">When this is the wrong shape</h2>
<p>If your workload has a single vector index that already exceeds what fits in the memory of one server, and you genuinely need sharded vector search across dozens of machines, an HNSW graph that lives inside one engine process is the wrong shape; you want a vector service that is built to shard, and we will not pretend otherwise. One engine process means one HNSW graph in memory, and that is the limit on a single-machine deployment. What you get by staying inside one process is that the row that gets updated with a new title is the same transaction that updates the row&rsquo;s vector, the same <code>WHERE</code> clause that filters by <code>tags = 'engineering'</code> also filters by approximate-nearest-neighbour distance, and the same <code>/healthz</code> endpoint that tells you the database is up also tells you the vector index is up; for most applications that is a much better set of defaults than another microservice to operate, and when a workload outgrows it the same client code paths through the same wire format keep working against a vector service with no application-side rewrite.</p>
]]></content:encoded></item></channel></rss>