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 UPDATE on a documents row commits the corresponding HNSW entry for that row.

What “in-process” actually buys you

Two things specifically, and they are the same two things every in-process database call gets when the network is gone.

The first is a round trip. A single HTTP request from any Tier 2 client to a mongreldb-server over loopback measures p50 ~0.22 ms / p99 ~0.62 ms at ~4,000 ops/s for PUT /tables/{name}/put and POST /kit/txn commit on the published BENCHMARKS.md; 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 6.79 µs commit (group-committed, fsync’d) bound from the write-path numbers in the same file. The network is the dominant cost in the Tier 2 path; axum handler dispatch, JSON encode/decode, and the TCP round trip add up, while the engine work underneath is sub-10 µs.

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 “the database committed but the vector index lagged” 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.

The actual numbers

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 put path the published cargo bench -p mongreldb-core --bench write_path numbers report (put no-fsync at 618 ns, commit fsync at 6.79 µs, group commit at 686 µs for 1,000 rows). A k-nearest-neighbour query in the same transaction rides the same sub-10 µs filter path the cold-SQL-filter number (8.7 µs 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 BENCHMARKS.md when the HNSW build policy is committed, and we will not invent them in the meantime.

What this looks like from a where('ann', ...) call

The fluent query builder in mongreldb-php pushes where('ann', ...) to the same HNSW index through the same /kit/query path as a regular typed column. A query for ten nearest neighbours of a 384-dimensional embedding against a documents table, filtered by tags = 'engineering', looks like:

$rows = $db->table('documents')
    ->where('tags', 'engineering')
    ->whereAnn('embedding', $vector, k: 10)
    ->get();

That call, against a mongreldb-server running on the same host, is one HTTP round trip; against an in-process Tier 1 binding it is one function call into mongreldb-core. Both paths return the same rows with the same transaction isolation; the only difference is the round trip.

When this is the wrong shape

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’s vector, the same WHERE clause that filters by tags = 'engineering' also filters by approximate-nearest-neighbour distance, and the same /healthz 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.