In-process vector search over the turbovec Rust crate (IdMapIndex).
{:ok, results} =
TurboVec.new!(dim: 1536, bit_width: 4)
|> TurboVec.add(vectors, ids)
|> TurboVec.search(query, k: 10, allowlist: tenant_ids)results is [{id, score}, ...], length <= k, best first. Vectors
and queries are a native-endian f32 binary, or an Nx.Tensor of type
{:f, 32}.
The index handle is a node-local NIF resource: it cannot cross nodes,
survive :erlang.term_to_binary/1, or live in ETS across restarts.
Move an index between nodes or lifetimes with write/2 and load/1.
In-memory vector binaries are native-endian f32; the on-disk .tvim
format is little-endian. Do not checksum one against the other.
After a NIF panic the recovered index may be half-mutated; a
subsequent sync persists that state.
Summary
Functions
Adds vectors with stable u64 ids. Returns the handle so it can be piped. Error atomicity: a rejected batch leaves the index exactly as it was — no cleanup needed.
Bits per coordinate (2, 3, or 4). Infallible.
Whether id is in the index. Bare boolean — the one deliberate break
from ok/error tuples. An integer outside u64 returns false: it cannot
be present. The first id lookup after load/1 builds the id map, O(n).
Number of vectors in the index. Infallible.
Vector dimensionality. Infallible; always committed.
Same as load/1, but returns the handle or raises TurboVec.Error.
Creates an index. dim is required (positive multiple of 8, ≤ 16384);
bit_width is 2, 3, or 4 (default 4).
Same as new/1, but returns the handle or raises TurboVec.Error.
Removes one id. Returns the handle. {:error, :not_found} if absent.
Top-k search. Returns up to k results — k is clamped to the index
(and allowlist) size, so length(results) <= k; an empty index returns
{:ok, []}. Scores are length-renormalized inner product, equal to
cosine only for L2-normalized vectors.
Durable incremental save. Returns the handle.
Full durable snapshot (atomic replace). Returns the handle.
Types
Functions
@spec add(index(), vector_input(), [non_neg_integer()]) :: index() | {:error, term()}
Adds vectors with stable u64 ids. Returns the handle so it can be piped. Error atomicity: a rejected batch leaves the index exactly as it was — no cleanup needed.
@spec bit_width(index()) :: 2 | 3 | 4
Bits per coordinate (2, 3, or 4). Infallible.
Whether id is in the index. Bare boolean — the one deliberate break
from ok/error tuples. An integer outside u64 returns false: it cannot
be present. The first id lookup after load/1 builds the id map, O(n).
@spec count(index()) :: non_neg_integer()
Number of vectors in the index. Infallible.
@spec dim(index()) :: pos_integer()
Vector dimensionality. Infallible; always committed.
Loads a write/2 snapshot or a sync/2 container. Corruption and
foreign-writer conflicts both surface as {:io_error, :invalid_data, msg}
— distinguishable only by message (upstream limitation).
Same as load/1, but returns the handle or raises TurboVec.Error.
Creates an index. dim is required (positive multiple of 8, ≤ 16384);
bit_width is 2, 3, or 4 (default 4).
Same as new/1, but returns the handle or raises TurboVec.Error.
@spec remove(index(), non_neg_integer()) :: index() | {:error, term()}
Removes one id. Returns the handle. {:error, :not_found} if absent.
@spec search(index(), vector_input(), keyword()) :: {:ok, [{non_neg_integer(), float()}]} | {:error, term()}
Top-k search. Returns up to k results — k is clamped to the index
(and allowlist) size, so length(results) <= k; an empty index returns
{:ok, []}. Scores are length-renormalized inner product, equal to
cosine only for L2-normalized vectors.
Durable incremental save. Returns the handle.
First call to a fresh path writes the whole file; the index stays bound to the path. One writer per path — a concurrent writer is detected at the next sync, not locked out. Holds the write lock for the disk IO: searches and mutations queue until it returns.
Full durable snapshot (atomic replace). Returns the handle.
Do not write/2 to a path another handle is incrementally syncing.
write then sync on the same handle rebuilds (the snapshot is
unclaimed, not foreign). Two handles incrementally syncing one path
is not supported — the lagging handle fails at its next sync.
Holding the read lock: mutations queue for the duration, and once one
queues, new searches may block behind it (no RwLock fairness is
guaranteed). Prefer sync/2 for routine durability; call write/2
in quiet periods.