Skip to main content

VecqIndex

Struct VecqIndex 

Source
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

Source

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.

Source

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

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn bits(&self) -> u8

Configured Lloyd-Max code width in bits (4, 5, or 6).

Source

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).

Source

pub fn is_residual(&self) -> bool

Whether this index carries second-pass residual codes.

Source

pub fn len(&self) -> usize

Number of live (searchable) vectors.

Source

pub fn is_empty(&self) -> bool

Source

pub fn slots(&self) -> usize

Total slots in use, including tombstoned ones (slots() == len() + tombstones()).

Source

pub fn tombstones(&self) -> usize

Number of tombstoned slots awaiting VecqIndex::compact.

Source

pub fn dim(&self) -> usize

Source

pub fn working_dim(&self) -> usize

Dimensions actually quantized (see VecqIndex::with_working_dim).

Source

pub fn seed(&self) -> u64

Source

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).

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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).

Source

pub fn contains_key(&self, key: u64) -> bool

Whether key currently identifies at least one live vector.

Source

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.

Source

pub fn cascade_enabled(&self) -> bool

Whether cascade signatures are currently available.

Source

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).

Source

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.

Source

pub fn prepare_query(&self, q: &[f32]) -> PreparedQuery

Prepare an f32 query in rotated space (call once per query).

Source

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.

Source

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.

Source

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.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.