turbovec exposes two index types and one serialization format per type.
TurboQuantIndex— positional index, O(1)swap_removedelete.IdMapIndex— stable externalu64ids on top ofTurboQuantIndex.- File formats —
.tvand.tvim.
All examples below are Python. The Rust API mirrors it — see each type's rustdoc for the exact signatures.
Positional index. Each vector is identified by its insertion slot (0..n). Fast and small, but external references to slots are invalidated by swap_remove. If you need stable ids, use IdMapIndex.
from turbovec import TurboQuantIndex
idx = TurboQuantIndex(dim=1536, bit_width=4)
idx.add(vectors) # np.ndarray of shape (n, dim), float32
scores, indices = idx.search(queries, k=10)
idx.swap_remove(5) # O(1); the previously-last vector moves into slot 5
idx.write("index.tv") # .tv format
loaded = TurboQuantIndex.load("index.tv")| Method | Notes |
|---|---|
TurboQuantIndex(dim, bit_width) |
bit_width ∈ {2, 4} |
add(vectors) |
vectors must be contiguous float32 (n, dim). |
search(queries, k) |
Returns (scores, indices), both shape (nq, k). Indices are int64 slot positions. |
swap_remove(idx) |
O(1). Moves the last vector into idx; returns the previous position of that moved vector (so external refs can be updated if needed). |
prepare() |
Optional. Eagerly builds the rotation matrix, Lloyd-Max centroids and SIMD-blocked layout so the first search call doesn't pay the one-time cost. |
write(path) / load(path) |
.tv format. |
len(idx) / idx.dim / idx.bit_width |
Introspection. |
swap_remove(i) is named to match Rust's Vec::swap_remove: the last element moves into slot i, and the vector is truncated by one. It is not a shift (FAISS's IndexPQ::remove_ids behaviour). Order is not preserved; slot indices of vectors you didn't delete may now point at different vectors than before.
Use IdMapIndex if external references have to stay stable across deletes.
Stable-id wrapper around TurboQuantIndex. Roughly equivalent to FAISS's IndexIDMap2 — hash-table backed, O(1) remove(id).
import numpy as np
from turbovec import IdMapIndex
idx = IdMapIndex(dim=1536, bit_width=4)
idx.add_with_ids(vectors, np.array([1001, 1002, 1003], dtype=np.uint64))
scores, ids = idx.search(queries, k=10) # ids are uint64 external ids
idx.remove(1002) # O(1) by id
assert 1003 in idx # __contains__ sugar
idx.write("index.tvim") # .tvim format
loaded = IdMapIndex.load("index.tvim")| Method | Notes |
|---|---|
IdMapIndex(dim, bit_width) |
|
add_with_ids(vectors, ids) |
ids is a uint64 array with length vectors.shape[0]. Rejects duplicate ids (raises). |
remove(id) -> bool |
True if the id was present and removed, False otherwise. O(1). |
search(queries, k) |
Returns (scores, ids) — ids are uint64 external ids. |
contains(id) / id in idx |
Membership. |
write(path) / load(path) |
.tvim format. |
len(idx) / idx.dim / idx.bit_width / prepare() |
Same as TurboQuantIndex. |
TurboQuantIndex— you never delete, or you're fine with positional ids.IdMapIndex— you need stable external ids (e.g. string-id → vector mapping maintained by the caller).
All the framework integrations (LangChain, LlamaIndex, Haystack) use IdMapIndex internally for exactly this reason.
┌──────────────────────────────────────┐
│ 9-byte header │
│ bit_width (u8) │
│ dim (u32 LE) │
│ n_vectors (u32 LE) │
├──────────────────────────────────────┤
│ packed codes │
│ (dim / 8) * bit_width * n_vectors │
├──────────────────────────────────────┤
│ norms (n_vectors × f32 LE) │
└──────────────────────────────────────┘
┌──────────────────────────────────────┐
│ magic "TVIM" (4 bytes) │
│ version u8 = 1 │
├──────────────────────────────────────┤
│ core payload (same as .tv) │
├──────────────────────────────────────┤
│ slot_to_id (n_vectors × u64 LE) │
└──────────────────────────────────────┘
On load, the reverse id → slot map is rebuilt in memory. Duplicate ids in the slot_to_id table are rejected as corrupt.
Both formats are stable across minor versions. Breaking changes bump the file-format version byte (.tvim) or the header length (.tv).