How it works

The conceptual model of polign_db: what lives in the bucket, how a write becomes durable and searchable, how reads are served hot or cold, and exactly what the consistency contract promises, and where its edges are.

The mental model

Most vector databases are servers that own your data: the index lives on their disks, and the cluster is the system of record. polign_db inverts that. The bucket is the database. Everything durable lives as objects in a bucket you own: the write log, the data, the search indexes. Serving nodes are stateless caches in front of it.

              writes                          reads
                │                               │
        ┌───────▼───────────────────────────────▼───────┐
        │                serving nodes                   │   stateless — kill, add,
        │        RAM tier · optional NVMe cache          │   or scale to zero freely
        └───────┬───────────────────────────────▲───────┘
                │ append                        │ ranged reads
        ┌───────▼───────────────────────────────┴───────┐
        │                  your bucket                   │   the system of record
        │      write log · segments · manifests          │   S3 · GCS · Azure · MinIO · R2
        └───────────────────────────────────────────────┘

Two consequences fall out of this shape:

There are two ways to run it. The default is a single in-memory binary with no bucket at all, and state persists to a local snapshot file. That's how you start on a laptop. Adding -store switches to the storage-backed deployment described on this page, where the bucket becomes the source of truth. Everything below describes the storage-backed mode; the guarantees are its guarantees.

The write path

A write (an upsert or delete) goes through three stages, each with a durable artifact in the bucket:

put ──▶ 1. write log (in the bucket) ──▶ 2. immutable segments ──▶ 3. compaction
            durable before acked           searchable in place        background merge
  1. Append to the write log. The log itself lives in the bucket. Batches of writes are stored as objects, sequenced by conditional PUTs, so no broker is involved. The client sees success only after the append is durable; if the append fails, the write is rejected rather than half-applied.
  2. Persist into segments. A background process drains the log into segments: immutable, self-contained blobs that hold vectors, a search graph, ids, and metadata, with an optional BM25 text index alongside. Each published batch of segments is recorded in a manifest; making it live is a single swap of a tiny pointer object. That pointer is the only object in the bucket that is ever overwritten. Everything else is written once, so a reader always sees a consistent snapshot and a crash mid-publish can't corrupt anything.
  3. Compact in the background. Compaction merges small segments and physically removes deleted entries; garbage collection reclaims blobs no longer referenced by the current manifest, with an age gate so a reader pinned to a recent snapshot never has an object deleted out from under it.

Updates and deletes are handled with tombstones: a newer entry hides every older copy of the same id, and compaction is what eventually removes the bytes. Nothing in the hot path rewrites data in place.

The read path

The defining property of the read path is that object storage is directly queryable. Segments are laid out so a query can search them where they are. A node fetches just the few blobs relevant to one query, in parallel, and runs the search over those bytes. No index build, no restore, no warm-up requirement.

Reads are served from one of three tiers, and heat, not configuration, decides which:

TierWhat's residentWhen it's used
Cold (the floor) KBs per collection: manifest, routing table, tombstones Always available, because any node can serve any collection it has never seen
Disk cache (optional) Recently fetched blobs on local NVMe Repeat reads pay a local read instead of an object-store fetch
Hot (earned) The collection's index in RAM Sustained query rate promotes a collection automatically; cooling demotes it

This is what makes many small collections cheap: a node keeps thousands of collections servable while holding almost nothing for each, and only the ones actually being queried earn RAM. A collection nobody queries costs only its object storage.

The trade is latency, and it's worth stating plainly: a cold search against same-region S3 measures ~144 ms typical (~354 ms p99), versus ~18 ms against a same-machine store, and a hot in-RAM collection is faster still. The full numbers, including where RAM-resident engines beat this design, are on the benchmarks page.

The consistency contract

These are the promises the storage-backed deployment makes. They hold across node crashes, node replacement, and cold reads:

  1. A write is durable before it is acknowledged. Success means the write is in the bucket's log, not in a node's memory awaiting a flush. A node dying the instant after acking loses nothing.
  2. Every collection has one write order. All of a collection's writes flow through one log partition keyed by the collection's name, so every consumer, whether the persistor, a replaying node, or a rebuild, observes the same order the writes were acknowledged in.
  3. Reads see acknowledged writes, even cold. Segments alone would lag the log by one flush interval. To close that gap, each serving node follows the log's tail into a small in-memory overlay and merges it over segment results. The rule that makes this correct: the overlay is authoritative for any id it knows. A write acked on one agent step is readable on the next, whether the collection is hot or cold.
The one documented lag

Keyword (BM25) search reads only segments. The freshness overlay covers vector reads, not text. So a document's text becomes keyword-searchable at the next segment flush, seconds later, while vector search over the same write is immediate. This is the contract's one freshness exception, and it is deliberate rather than accidental.

Two further caveats apply only to a cold-first node serving a resource it keeps no in-memory index for (memory stays log-only by design): listing such a resource fails with "not supported" (HTTP 501) rather than returning results, since its records live in segments — search or get by id instead — and a delete of an already-persisted id there is acknowledged optimistically, without a local existence check. Search and point reads honor the full contract everywhere.

Search three ways

One query endpoint serves three shapes, chosen by what the request contains:

Three index types, one dial

Each collection picks an index type, and all three sit behind the same API, so the choice is purely a speed-versus-memory trade-off:

IndexCharacterPick it when
Graph (HNSW) Fastest queries, most RAM The corpus fits comfortably in memory and latency is everything
Partitioned (default) Balanced; partitions map cleanly to bucket objects Almost always. It works from the first vector to large scale
Compressed (IVF-PQ) ~32× smaller in RAM; accuracy restored by re-checking a small candidate pool against exact vectors. Cold queries scan the compact codes first, cutting the bytes fetched from the bucket to a fraction of the exact path Memory is the binding constraint, or cold-serving cost is

Index maintenance never blocks serving. When a compressed index needs re-partitioning as the corpus grows, the rebuild runs out of band. A separate process reads the bucket, builds the new index, and publishes it as an immutable generation. A live node adopts it with one atomic swap: in-flight queries finish on the old index, new ones use the new. The same publish-and-adopt mechanism is how a replacement node boots quickly (load the latest generation, replay only the tail) instead of replaying history.

Failure and scaling model

The fine print

Design choices with real limits, stated up front:

Go deeper