pub struct VecqIndex { /* private fields */ }Expand description
A quantized vector database in memory.
Each vector is stored as padded_dim / 2 bytes of 4-bit Lloyd-Max codes
(computed after RHDH rotation) plus one f32 correction factor. The score
against an f32 query is an unbiased estimate of the cosine similarity
after undoing the per-vector quantization scale.
Vectors can be stored anonymously via VecqIndex::add or under a
caller-chosen u64 key via VecqIndex::add_keyed. Keyed vectors can be
removed in place (tombstoned); tombstoned slots keep their storage but are
skipped by searches and dropped by VecqIndex::compact and by
VecqIndex::to_bytes. Slot indices stay stable until a
compaction, so integrators can treat a slot as a transient handle while
keys are the durable identity.
Implementations§
Source§impl VecqIndex
impl VecqIndex
Sourcepub fn to_bytes(&self) -> Vec<u8> ⓘ
pub fn to_bytes(&self) -> Vec<u8> ⓘ
Serialize the index to bytes. Plain indexes emit format version 1.3; residual indexes emit 1.4 (extra residual scale + code blocks).
Tombstoned slots are skipped: the output always holds the live vectors
in slot order, so a round-trip through bytes has the same effect as
VecqIndex::compact on disk without disturbing in-memory slot
indices. Keys of live keyed slots are stored in the keyed-slot table
and are fully restored by VecqIndex::from_bytes.
Sourcepub fn from_bytes(bytes: &[u8]) -> Result<Self, Error>
pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error>
Parse an index from bytes produced by [to_bytes] (a v1.3 file) or a
legacy v1 / v1.1 / v1.2 file (which carry no key table).
Source§impl VecqIndex
impl VecqIndex
Sourcepub fn new(dim: usize, seed: u64) -> Self
pub fn new(dim: usize, seed: u64) -> Self
Create an empty index for dim-dimensional unit vectors.
seed must be persisted with the index for cross-platform determinism.
Sourcepub fn with_working_dim(dim: usize, working_dim: usize, seed: u64) -> Self
pub fn with_working_dim(dim: usize, working_dim: usize, seed: u64) -> Self
Create an empty index over the leading working_dim dimensions of
dim-dimensional vectors (Matryoshka truncation).
Vectors and queries are always passed at full dim length; the index
truncates them to working_dim before normalization and the RHDH
rotation (truncating after rotation would not be equivalent, since the
transform mixes dimensions). Scores are computed in the
working_dim-dimensional space and are only comparable with indexes
built with the same working_dim and seed.
Sourcepub fn with_residual(dim: usize, seed: u64) -> Self
pub fn with_residual(dim: usize, seed: u64) -> Self
Create an empty index with second-pass residual codes (issue #23).
Doubles the code storage (~2x padded/2 bytes + one extra f16 scale per vector) in exchange for a finer distance estimate: the residual left by the first Lloyd pass is itself Lloyd-quantized and added to the score. Recall improves most on noise-dominated data; scan cost roughly doubles. Composable with the keyed and cascade layers.
Sourcepub fn set_bits(&mut self, bits: u8) -> &mut Self
pub fn set_bits(&mut self, bits: u8) -> &mut Self
Set the Lloyd-Max code width: 4 (legacy, max compression 5.98x),
5 (default sweet spot, 4.79x @ recall@10 0.983), or 6 (recall ≈
residual at 25% less storage, single-pass). Must be called on an
empty index before the first add.
Sourcepub fn bytes_per_vector(&self) -> usize
pub fn bytes_per_vector(&self) -> usize
Stored code bytes per vector at the configured width (bit-packed
LSB-first; exact since padded % 8 == 0).
Sourcepub fn is_residual(&self) -> bool
pub fn is_residual(&self) -> bool
Whether this index carries second-pass residual codes.
pub fn is_empty(&self) -> bool
Sourcepub fn slots(&self) -> usize
pub fn slots(&self) -> usize
Total slots in use, including tombstoned ones
(slots() == len() + tombstones()).
Sourcepub fn tombstones(&self) -> usize
pub fn tombstones(&self) -> usize
Number of tombstoned slots awaiting VecqIndex::compact.
pub fn dim(&self) -> usize
Sourcepub fn working_dim(&self) -> usize
pub fn working_dim(&self) -> usize
Dimensions actually quantized (see VecqIndex::with_working_dim).
pub fn seed(&self) -> u64
Sourcepub fn add(&mut self, v: &[f32]) -> usize
pub fn add(&mut self, v: &[f32]) -> usize
Quantize and add one vector (any norm; normalized internally).
Returns the slot index holding the vector (stable until compaction).
Sourcepub fn add_keyed(&mut self, key: u64, v: &[f32]) -> usize
pub fn add_keyed(&mut self, key: u64, v: &[f32]) -> usize
Quantize and add one vector under a caller-chosen u64 key.
If key already exists, the vector replaces the key’s primary slot in
place (the slot index is preserved, matching usearch’s insert
semantics). Otherwise a new slot is appended. Returns the slot index
holding the vector.
Sourcepub fn add_keyed_multi(&mut self, key: u64, v: &[f32]) -> usize
pub fn add_keyed_multi(&mut self, key: u64, v: &[f32]) -> usize
Quantize and add one more vector under an existing (or new) key.
Unlike VecqIndex::add_keyed this never replaces: the key accumulates
vectors (usearch’s multi mode). Returns the new slot index.
VecqIndex::search_keyed reports each key once, scored by its best
slot; VecqIndex::remove_keyed removes all of a key’s slots, while
VecqIndex::remove_keyed_at removes one.
Sourcepub fn relabel(&mut self, old_key: u64, new_key: u64) -> bool
pub fn relabel(&mut self, old_key: u64, new_key: u64) -> bool
Rename old_key to new_key in place (slot indices untouched).
Returns false if old_key is unknown or new_key is already taken;
renaming a key onto itself is a successful no-op.
Sourcepub fn remove_keyed(&mut self, key: u64) -> bool
pub fn remove_keyed(&mut self, key: u64) -> bool
Remove a keyed vector. The slot becomes a tombstone: its storage is
kept (slot indices stay stable) but searches skip it until
VecqIndex::compact. For multi-slot keys every slot of the key is
removed. Returns false if the key is unknown.
Sourcepub fn remove_keyed_at(&mut self, key: u64, slot: usize) -> bool
pub fn remove_keyed_at(&mut self, key: u64, slot: usize) -> bool
Remove one slot of a (possibly multi-slot) key.
Returns false if the key is unknown or slot is not one of its live
slots. Removing a single-slot key’s slot removes the key entirely; a
multi-slot key survives while at least one slot remains.
Sourcepub fn key_of(&self, slot: usize) -> Option<u64>
pub fn key_of(&self, slot: usize) -> Option<u64>
Look up the key stored at slot (None for anonymous slots,
tombstones, or out-of-range indices).
Sourcepub fn contains_key(&self, key: u64) -> bool
pub fn contains_key(&self, key: u64) -> bool
Whether key currently identifies at least one live vector.
Sourcepub fn enable_cascade(&mut self)
pub fn enable_cascade(&mut self)
Derive the 2-bit signatures used by VecqIndex::search_cascade:
the two high bits of each stored nibble (nibble >> 2), i.e. a
coarse Lloyd re-quantization of the rotated dims. Costs
n * padded/4 bytes of memory. Adding, replacing or compacting
vectors drops the signatures — call this again to re-enable.
Sourcepub fn cascade_enabled(&self) -> bool
pub fn cascade_enabled(&self) -> bool
Whether cascade signatures are currently available.
Sourcepub fn search_cascade(&self, q: &[f32], k: usize, r: usize) -> Vec<(usize, f32)>
pub fn search_cascade(&self, q: &[f32], k: usize, r: usize) -> Vec<(usize, f32)>
Approximate top-k search: rank slots by L1 distance between the
query’s and each slot’s 2-bit signature codes (pure integer math),
keep the r closest, rescore those with the standard 4-bit path, and
return the top k. Slot indices are stable until compaction;
tombstoned slots are skipped.
Requires VecqIndex::enable_cascade (panics otherwise). r is
clamped to [k, live]; with r >= live the result is identical to
VecqIndex::search bit for bit. The cascade is deterministic: same
file + query -> same result bits on any platform.
Prefilter quality is data-dependent: the coarser the codes, the
larger r must be. Measure recall@k vs r on your data (the
synthetic clustered set in the tests needs r ~ 100 for ~0.9
recall@10 at n=1k; real embeddings need far less).
Sourcepub fn compact(&mut self)
pub fn compact(&mut self)
Rebuild the index in place, dropping tombstoned slots.
All remaining vectors keep their keys; slot indices shift to become dense (0..len). Search results are unchanged.
Sourcepub fn prepare_query(&self, q: &[f32]) -> PreparedQuery
pub fn prepare_query(&self, q: &[f32]) -> PreparedQuery
Prepare an f32 query in rotated space (call once per query).
Sourcepub fn score(&self, pq: &PreparedQuery, idx: usize) -> f32
pub fn score(&self, pq: &PreparedQuery, idx: usize) -> f32
Asymmetric score of vector idx against a prepared query.
Returns estimated cosine similarity in [-1, 1].
Dispatches to the explicit NEON path on aarch64, the explicit AVX2
path on x86_64 when the host supports it (runtime detection), and the
fixed 8-bucket scalar path otherwise. All use the identical
association order (per code byte: mul, mul, add, then add into bucket
j; final pairwise tree), so they produce the same f32 bits — guarded
by neon_matches_scalar_bitwise / avx2_matches_scalar_bitwise in
tests.
Sourcepub fn search(&self, q: &[f32], k: usize) -> Vec<(usize, f32)>
pub fn search(&self, q: &[f32], k: usize) -> Vec<(usize, f32)>
Brute-force top-k search. Returns (slot index, score) sorted by score desc. Tombstoned slots are skipped.
Uses a bounded min-heap of size k (no O(n log n) sort, no O(n) allocation per query): push while the heap is not full, then only push-and-pop when the candidate beats the current k-th score.
Sourcepub fn search_keyed(&self, q: &[f32], k: usize) -> Vec<(u64, f32)>
pub fn search_keyed(&self, q: &[f32], k: usize) -> Vec<(u64, f32)>
Keyed variant of VecqIndex::search: returns (key, score) sorted by
score desc, restricted to live keyed vectors. Multi-slot keys appear
once, scored by their best slot — so the result can hold fewer than
k entries when keys occupy several of the top slots.