Expand description
The vector model: RaBitQ codes under partitions that update in place
(10).
A vector index is two decisions, and as of 2026 only one of them is still
open. The quantiser is settled: RaBitQ won, and VectorChord, Lucene, which
calls it BBQ, CockroachDB, turbopuffer and Zvec all landed on it against five
different index structures inside eighteen months. Quantizer is that,
and it is what makes a ten million vector collection a 1 GB index rather
than a 30 GB one.
The index is decided here by the update path. A graph index tombstones a delete, degrades as the tombstones pile up, and gets better again only when it is rebuilt. Redis shipped HNSW vector sets in 8.0 in May 2025 and they were still beta three minor releases later, which is the vendor’s own evidence about how hard that path is. So this is partitions with the centroids resident and the codes in flat postings, which SPFresh showed can be split, merged and reassigned in place, and there is never a rebuild.
§What is here so far
The quantiser and the rotation it needs. Quantizer::encode writes the
searchable form of a vector against the centroid of the partition it belongs
to, and Quantizer::query prepares a query once so that measuring it
against the codes in a partition is a scan over contiguous bytes.
use yo_vector::{Bits, Quantizer};
let q = Quantizer::new(128, Bits::One, 7);
assert_eq!(q.code_bytes(), 16);A code is stored as bit planes rather than with each coordinate’s bits next
to each other, and the query is quantised and transposed the same way, so
measuring one against the other is ANDs and popcounts rather than a float
multiply per dimension. That is 20 nanoseconds a vector at 768 dimensions
against a whole search budget of a millisecond, and 35 times what the same
estimator costs with the query left in floats. benches/rabitq.rs runs both
so the ratio is measured rather than remembered.
Partitions is the index over those codes. A vector belongs to the
partition whose centroid it is nearest, a partition’s members are a flat run
of codes, an insert is an append and a delete moves the last member into the
hole. A search ranks the centroids, scans the nearest few postings, and then
measures the best handful properly against the full precision vectors. It
splits, merges and reassigns in bounded steps as it goes, which is SPFresh’s
LIRE, and it is why there is never a rebuild.
The centroids are kept already rotated, which matters more than it sounds like it should. Preparing a query is mostly the rotation and it happens once per partition probed, so rotating the centroids once when they are built turns tens of rotations a search into one.
They are read in full on every search, which sounds like the obvious thing to
fix on a collection with thousands of them and is not. src/rank.rs is the
measurement: coding them the way their members are coded is three to nine
times slower than reading them, because reading them is already going at
memory speed and the estimator that would replace it is not.
src/probe.rs is where that decision gets its context. It splits a query
into ranking the centroids, preparing the query against each partition
probed, and scanning the postings, and it is what says which of the three is
worth working on at a given dimension and collection size.
Collection is the piece above that, and it is the one both doors reach.
Partitions deals in ids and knows nothing about the key a client wrote a
vector under, what metric the collection was opened with, or where the full
precision vectors live, because none of those are the index’s business.
db.vectors() in a Rust program and VADD off a socket both need all three,
so they are answered once here rather than twice above.
A filter runs inside the posting scan rather than after it. Every member
carries a tag word beside its code, and a scan that can reject a member
before it measures one can keep going into further partitions until it has
enough that pass. That widening is the whole point: a selective filter means
the nearest partitions may hold nothing the caller asked for, and a search
that does not go looking is a recall lottery. Signature packs arbitrary
attribute values into that one word, and it is allowed to say yes when it
should have said no but never the other way round, so the caller’s own
predicate stays the authority. That predicate has a place to live too:
Filter::exact sees the member’s id and runs only on members the tag let
through that are near enough to be ranked, which is what lets an expression
over a JSON string be the real answer without the scan ever reading one.
muvera is late interaction retrieval on that same index. A ColBERT style
model gives a document one vector per token and scores a query against it
with Chamfer similarity, which normally means a second index over every
token of every document and a scoring pass on top of it. MUVERA maps a set
of token vectors to one fixed length vector whose dot product approximates
Chamfer, so it costs an encode at write time, the index that is already
here, and muvera::chamfer as the rerank. There is no second index.
hnsw is the compatibility view. Clients pass M, EF_CONSTRUCTION and
EF_RUNTIME and expect them to do something, because against Redis and
valkey they do, and there is no graph here to point them at. So each one is
mapped onto whatever it was actually for: build effort becomes the posting
size, search beam becomes the probe and the rerank width, and M is the out
degree of a graph that does not exist, so it is echoed back and changes
nothing. A client that asked for HNSW and meant it can say so and be
refused rather than quietly served something else.
image is how any of it survives a restart. A collection is written down
as the format’s 10 section 2 says, one chain per partition under a
checkpoint, and read back without a single vector being requantised. The
vectors themselves are not in there, because they are already records of kind
3 in the log, so a load takes the shape and the codes from the image and the
vectors from whatever the caller points it at.
Re-exports§
pub use collection::Collection;pub use collection::Match;pub use image::Restored;pub use image::Stored;pub use partition::Any;pub use partition::Filter;pub use partition::Hit;pub use partition::Partitions;pub use partition::Signature;pub use partition::Tuning;pub use partition::Vectors;pub use partition::Work;pub use quant::Quant;pub use quant::Squeezed;pub use rabitq::Bits;pub use rabitq::Coded;pub use rabitq::Quantizer;pub use rabitq::Query;pub use rotate::Rotation;
Modules§
- collection
- A collection of vectors under keys, which is what both doors reach.
- hnsw
- HNSW as a compatibility view, not as an index (
10section 7). - image
- Writing a collection down, and reading it back without rebuilding it.
- muvera
- MUVERA: a set of token vectors as one vector, so late interaction retrieval
runs on the index that is already here (
10section 6). - partition
- The partition index, and the in place update protocol that means it never
has to be rebuilt (
10sections 2, 4 and 5). - quant
- How a vector set squeezes a vector before it stores it.
- rabitq
- RaBitQ, the quantiser the searchable form of a vector is written in
(
10section 3). - rotate
- The random rotation every vector goes through before it is quantised
(
10section 3).