yo_vector/lib.rs
1//! The vector model: RaBitQ codes under partitions that update in place
2//! (`10`).
3//!
4//! A vector index is two decisions, and as of 2026 only one of them is still
5//! open. The quantiser is settled: RaBitQ won, and VectorChord, Lucene, which
6//! calls it BBQ, CockroachDB, turbopuffer and Zvec all landed on it against five
7//! different index structures inside eighteen months. [`Quantizer`] is that,
8//! and it is what makes a ten million vector collection a 1 GB index rather
9//! than a 30 GB one.
10//!
11//! The index is decided here by the update path. A graph index tombstones a
12//! delete, degrades as the tombstones pile up, and gets better again only when
13//! it is rebuilt. Redis shipped HNSW vector sets in 8.0 in May 2025 and they
14//! were still beta three minor releases later, which is the vendor's own
15//! evidence about how hard that path is. So this is partitions with the
16//! centroids resident and the codes in flat postings, which SPFresh showed can
17//! be split, merged and reassigned in place, and there is never a rebuild.
18//!
19//! # What is here so far
20//!
21//! The quantiser and the rotation it needs. [`Quantizer::encode`] writes the
22//! searchable form of a vector against the centroid of the partition it belongs
23//! to, and [`Quantizer::query`] prepares a query once so that measuring it
24//! against the codes in a partition is a scan over contiguous bytes.
25//!
26//! ```
27//! use yo_vector::{Bits, Quantizer};
28//!
29//! let q = Quantizer::new(128, Bits::One, 7);
30//! assert_eq!(q.code_bytes(), 16);
31//! ```
32//!
33//! A code is stored as bit planes rather than with each coordinate's bits next
34//! to each other, and the query is quantised and transposed the same way, so
35//! measuring one against the other is ANDs and popcounts rather than a float
36//! multiply per dimension. That is 20 nanoseconds a vector at 768 dimensions
37//! against a whole search budget of a millisecond, and 35 times what the same
38//! estimator costs with the query left in floats. `benches/rabitq.rs` runs both
39//! so the ratio is measured rather than remembered.
40//!
41//! [`Partitions`] is the index over those codes. A vector belongs to the
42//! partition whose centroid it is nearest, a partition's members are a flat run
43//! of codes, an insert is an append and a delete moves the last member into the
44//! hole. A search ranks the centroids, scans the nearest few postings, and then
45//! measures the best handful properly against the full precision vectors. It
46//! splits, merges and reassigns in bounded steps as it goes, which is SPFresh's
47//! LIRE, and it is why there is never a rebuild.
48//!
49//! The centroids are kept already rotated, which matters more than it sounds
50//! like it should. Preparing a query is mostly the rotation and it happens once
51//! per partition probed, so rotating the centroids once when they are built
52//! turns tens of rotations a search into one.
53//!
54//! A filter runs inside the posting scan rather than after it. Every member
55//! carries a tag word beside its code, and a scan that can reject a member
56//! before it measures one can keep going into further partitions until it has
57//! enough that pass. That widening is the whole point: a selective filter means
58//! the nearest partitions may hold nothing the caller asked for, and a search
59//! that does not go looking is a recall lottery. [`Signature`] packs arbitrary
60//! attribute values into that one word, and it is allowed to say yes when it
61//! should have said no but never the other way round, so the caller's own
62//! predicate stays the authority.
63//!
64//! [`muvera`] is late interaction retrieval on that same index. A ColBERT style
65//! model gives a document one vector per token and scores a query against it
66//! with Chamfer similarity, which normally means a second index over every
67//! token of every document and a scoring pass on top of it. MUVERA maps a set
68//! of token vectors to one fixed length vector whose dot product approximates
69//! Chamfer, so it costs an encode at write time, the index that is already
70//! here, and [`muvera::chamfer`] as the rerank. There is no second index.
71//!
72//! [`hnsw`] is the compatibility view. Clients pass `M`, `EF_CONSTRUCTION` and
73//! `EF_RUNTIME` and expect them to do something, because against Redis and
74//! valkey they do, and there is no graph here to point them at. So each one is
75//! mapped onto whatever it was actually for: build effort becomes the posting
76//! size, search beam becomes the probe and the rerank width, and `M` is the out
77//! degree of a graph that does not exist, so it is echoed back and changes
78//! nothing. A client that asked for HNSW and meant it can say so and be
79//! refused rather than quietly served something else.
80//!
81//! Writing any of this to a `.yo` file is the rest of M6.
82
83#![deny(missing_docs)]
84
85pub(crate) mod coarse;
86pub mod hnsw;
87pub mod muvera;
88pub mod partition;
89pub mod rabitq;
90pub mod rotate;
91
92pub use partition::{Any, Filter, Hit, Partitions, Signature, Tuning, Vectors};
93pub use rabitq::{Bits, Coded, Quantizer, Query};
94pub use rotate::Rotation;