How it works
nidus holds dense vectors plus typed metadata in a single on-disk directory and answers nearest-neighbour queries over cosine (the default), dot, or Euclidean. Scoring is exact by default — every in-scope vector is compared — and you can opt into an approximate index (HNSW or IVF) for larger collections. There is no query planner and no background thread — the whole thing is a RAM-resident matrix, an optional in-RAM index, and a small amount of write glue.
The storage model
Section titled “The storage model”A store is a directory:
<dir>/ manifest names the live segments (the first is `data`); the atomic commit point data append-only, fixed-stride, row-major f32 matrix (header pins dimension) log append-only framed op stream: [len][bincode(Op)][crc32] — the commit record lock O_EXCL writer-exclusion lock file seg-… additional immutable segments, once a store grows past the seal thresholddatais the vectors: a flatf32matrix with a fixed stride (the pinned dimension), row-major, never rewritten in place. New rows are appended. A store holds one or more such segments presented as a single dense row space; by default it is justdata(see Storage → Segments).manifestnames the live segments and pins the dimension/metric — a tiny CRC-checked object, replaced atomically when a segment is sealed or compacted.logis the commit record: an append-only stream of framed, CRC32-checked, bincode-encoded operations (CreateCollection,Upsert,Delete, …). This is what makes a write durable.lockexcludes concurrent writers via anO_EXCLlock file — pure std, noflock, no FFI.
open reads data into RAM and replays log into an in-RAM index:
collection → { id → (row, attrs) }The index is fully reproducible from the two files, so it is never itself
persisted. After open, search never touches disk — it sweeps the in-RAM
matrix. (The opt-in Config::mmap
mode trades this for capacity: cold segments are mapped from disk and paged in on
touch, so a store can outgrow RAM.)
Upsert
Section titled “Upsert”A batch upsert is a fixed sequence designed so a crash can never corrupt the store:
- Append the new vectors to
data. - fsync
data. - Append the committing
Upsertrecords tolog. - fsync
log.
The log append is the commit point. A vector that made it into data but
whose Upsert record never landed in log is simply ignored on the next open —
it is an unreferenced row, reclaimed by compaction.
Upsert is all-or-nothing: any failure mid-batch rolls data and log back
to the entry marks, so a caught ENOSPC leaves the store exactly as it was.
Search
Section titled “Search”Search scores (cosine, dot, or Euclidean) over a
Scope — one collection, a named subset, or the whole
store — merged into a single ranking. By default it is exact (every in-scope row is
scored); with Config::ann set it instead
walks an approximate index for a candidate set and applies the same scope/filter/rerank
to those. The exact path is:
- Resolve the scope to a set of candidate rows.
- Apply the metadata
Filter(before any dot product — cheap rows are discarded first). - Score each surviving row against the query with a dot product. Because
vectors are unit-normalized on insert,
score = dot(v, q)is cosine similarity in[-1, 1]. - Keep the top-k in a bounded heap, optionally dropping anything below
min_score.
Scoping the whole store in one call is sound because every collection shares one embedding space — one dimension is pinned for the life of the store, so all vectors are directly comparable.
The scoring kernel is plain safe Rust the optimizer can vectorize: an 8-lane chunked dot product, an allocation-free top-k scan, and a storage-order (prefetcher-friendly) sweep of the matrix. See Performance for the numbers.
What it deliberately is not
Section titled “What it deliberately is not”- Exact by default. The default search compares every in-scope vector — 100%
recall, by construction. Approximate indexing (HNSW/IVF) is opt-in via
Config::annwhen you want speed over exactness at larger scale. - Not a database. No SQL, no joins, no transactions across calls.
- Not async. The hot path is CPU-bound; the library API is synchronous (see Embedding in a host app).
- In-process by default. You embed it and call methods directly; when you
want it over the wire,
nidus serveexposes the whole store over HTTP.
None of those are walls — they are seams, additive over the same append-only format. Several have since shipped as opt-in modes: an ANN index, scalar/binary quantization, and memory-mapped larger-than-RAM stores. Each stays off by default, so the simple exact-in-RAM store is what you get until you opt in.