Rust · Inputs and Outputs

Inputs and Outputs

use usearch::DenseInsertConfig;use usearch::DenseSearchConfig;use usearch::DenseTraversalStats;use usearch::Arena;use usearch::ArrowArray;use usearch::ArrowColumn;use usearch::ArrowDeviceArray;use usearch::ArrowExport;use usearch::ArrowRows;use usearch::ArrowSchema;use usearch::Element;use usearch::Matches;use usearch::QueryMatches;use usearch::Scalar;use usearch::Traversal;use usearch::*; 3.0.0-rc.1 · 4c3e5023

§ Arenas

Every output whose length only the call knows, such as matches, rows, products, monoliths and cursor or stream batches, lands in an Arena you pass, and the verb answers a view over it without copying. Arena::new_in(&executor) places it where that executor computes, decided once, and Arena::new() makes host pages for the calling thread. Each call rewinds its arena, so the previous call’s views die and a steady loop allocates once. To keep a result past the next call, or to read one while making the next, pass that call another arena. An arena serves one call at a time, so each deferred call needs its own. Every call, blocking or deferred, borrows the arena as &mut Arena and its result borrows it in turn, so the borrow checker refuses a stale view at compile time and keeps a call’s inputs out of the arena it rewinds. An arena from a VRAM executor holds device memory, which host accessors refuse with Status::Device. It grows, fills and frees on that executor’s first stream, so it serves only calls on that executor, any other refused with Status::Device, and holds the executor until it drops.

let mut arena = Arena::new_in(&executor)?;                           // Arena::new() is host pages for the calling thread
for batch in &batches {
    let matches = index.on(&executor).search(batch, 10, &mut arena)?; // rewinds: the last batch's matches are gone
    total += matches.query(0)?.distances.iter().sum::<f64>();
}

let (mut found, mut fetched) = (Arena::new()?, Arena::new()?);       // two live results, two arenas
for &key in index.search(&query, 10, &mut found)?.query(0)?.keys {
    rerank(key, index.get::<f32>(key, &mut fetched)?.view()?);       // rewinds fetched, never found
}

let (mut executors, mut arenas) = (Vec::new(), Vec::new());          // one executor and one arena per submitted call
for _ in &shards {
    executors.push(Executor::for_ram(&[], None, 8)?);
}
for executor in &executors {
    arenas.push(Arena::new_in(executor)?);
}
let mut calls = Vec::new();
for ((shard, executor), arena) in shards.iter().zip(&executors).zip(&mut arenas) {
    calls.push(shard.deferred(executor).search(queries.clone().expect("room for a copy"), 10, arena)?);
}
for call in calls {
    consume(call.await?);                                            // or: call.wait()?
}

The library never frees an arena under a call: dropping one waits for the calls writing into it, and a call naming an arena that serves another call finishes with Status::Contended and rewinds nothing, so even a leaked Deferred leaves it safe. An arena’s describe() says where it lives, and on which device. Fixed-size reports, such as stats, descriptions and required, stay plain values, and so does state you carry between calls, like the attention carry.

§ Rows & Scalars

rows borrows a packed slab; a dataset whose rows are padded to a wider pitch passes MatrixView::from_raw_parts instead, which takes the byte stride directly. The scalar type of the input is independent of the stored quantization, so the f32 slab of the Quickstart lands in an i8 index with nothing converted beforehand. The scalar types are NumKong’s, so nothing needs re-wrapping to move between the two crates, f16 and bf16 included, emulated where the CPU has no native support.

Rust typeScalar
f64, f32F64, F32
bf16, f16BF16, F16
e5m2, e4m3, e3m2, e2m3E5M2, E4M3, E3M2, E2M3
i8, i4x2I8, I4x2
u8, u4x2, u1x8U8, U4x2, U1x8

Initializing an index with a packed quantization and inserting floating-point vectors quantizes on ingest; the u1x8 type also lets you pass packed bit-vectors directly.

use numkong::u1x8;

let options = DenseOptions {
    dimensions: 8,
    metric: DenseMetric::Hamming,
    quantization: Scalar::U1x8,
    ..DenseOptions::default()
};
let index = DenseIndex::init_ram(&options, &[])?;
let slab = [u1x8(0b0000_1111), u1x8(0b1111_0000)];
index.insert(&keys, &rows(&slab, 8)?)?;

An insert batch is all-or-nothing and becomes visible at once, and it overlaps safely with concurrent searches on the same handle.

Each configurable verb’s config converts into its Dense*Config struct, whose other fields are named and default to what the C interface derives. So the bare value is the whole call, as in search(&queries, 10, &mut arena), insert(&keys, &vectors) or save(path), and ..DenseSearchConfig::from(10) reaches every knob.

let mut arena = Arena::new_in(&executor)?;
let mut stats = DenseTraversalStats::default();
let matches = index.on(&executor).search(&rows(&queries, 768)?, DenseSearchConfig {
    expansion_factor: 128,
    stats: Some(&mut stats),
    ..DenseSearchConfig::from(10)
}, &mut arena)?;
println!("{} distances computed", stats.computed_distances);

query pairs one query’s keys with its distances, nearest first and trimmed to the cells the call wrote, and iter walks every query in order.

for query in matches.iter()? {
    for (key, distance) in query.keys.iter().zip(query.distances) {
        println!("{key} at {distance}");
}
}

The breadth of the candidates rides the call rather than the handle, as expansion_factor on every config that walks the graph, so a recall sweep is a loop over ef values against one immutable index, with no mutation and no synchronization between points. traversal: Traversal::Scan scores every entry instead of walking, on RAM and NVMe handles, which gives that sweep its exact ground truth; a VRAM handle refuses it with Status::Unsupported. DenseTraversalStats reports the members visited, the distances computed and the starvation restarts, a graph-connectivity signal rather than a cost, overwritten by every call it is passed to.

API

StructDenseInsertConfigThe rows of an insertion and the knobs of its walk; see DenseVerbs::insert.
dense_index.rs:938 ↗
pub struct DenseInsertConfig<'config, Rows>

Fields

vectorsRowsOne row per key, borrowed or owned.
expansion_factorusizeThe breadth of the candidates the walk explores; zero derives it from the geometry.
progressOption<&'config mut ProgressCallback<'config>>Reports the entries inserted; every entry is claimed before the first report, so a Break is ignored.
progress_stepsusizeReports the call makes at most, the last at completion; zero derives to ten.
statsOption<&'config mut DenseTraversalStats>Where the traversal telemetry lands, overwritten by the call.

Implements

From<Rows>
StructDenseSearchConfigThe matches a search keeps and the knobs of its walk; see DenseVerbs::search.
dense_index.rs:978 ↗
pub struct DenseSearchConfig<'config>

Fields

wantedusizeMatches kept per query, the "k" in "kANN".
expansion_factorusizeThe breadth of the candidates the walk explores; zero derives it from the geometry.
traversalTraversalWalks the graph by default; Traversal::Scan scores every entry exactly, on RAM and NVMe handles, and a VRAM handle refuses it with Status::Unsupported.
predicateOption<&'config Predicate<'config>>Admits only the keys it approves; the rest still route the walk, so recall does not collapse at low selectivity. A VRAM handle refuses one with Status::Device.
progressOption<&'config mut ProgressCallback<'config>>Reports the queries resolved and stops wherever it says to; a stopped search reports Status::Cancelled and leaves no matches.
progress_stepsusizeReports the call makes at most, the last at completion; zero derives to ten.
statsOption<&'config mut DenseTraversalStats>Where the traversal telemetry lands, overwritten by the call.

Implements

From<usize>
StructDenseTraversalStatsTraversal telemetry, fully overwritten by the call it is passed to.
dense_index.rs:708 ↗
pub struct DenseTraversalStats

Fields

visited_membersusizeGraph members visited by the call.
computed_distancesusizeDistances computed by the call, diversity re-selections included.
restartsusizeStarvation restarts - a graph-connectivity health signal, not a cost counter.
missing_hintsusizeHint keys no shard held an entry under; zero unless a search was handed hints.

Implements

Clone, Copy, StructuralPartialEq, PartialEq, Eq, Debug, Default
StructArenaMemory the library writes every output whose length scales with a call into, reused across calls.3
types.rs:1943 ↗
pub struct Arena

An executor places it where it computes, host pages or its GPU's memory, decided once, and nothing is allocated until a call writes into it. Every call rewinds and refills it, invalidating the previous call's outputs, so a steady loop allocates once. Every verb, blocking or deferred, borrows it as &mut Arena and answers outputs that borrow it in turn. A Residency::Vram arena holds device memory, so the host accessors of its outputs refuse with Status::Device. It grows, fills and frees on the first stream of the executor that made it, so it serves only calls on that executor, any other refused with Status::Device, and holds the executor until it drops.

Dropping it waits for the calls still writing into it.

Implements

From<&'arena mut Arena>, Debug, Send, Sync, Drop
newMakes an arena of host pages for calls on the calling thread.
types.rs:1958 ↗
pub fn new() -> Result<Self>
new_inMakes an arena where executor computes, serving the calls of its residency, or only those on executor itself for a GPU arena.
types.rs:1962 ↗
pub fn new_in(executor: &Executor) -> Result<Self>
residencyThe residency of the executor that made it, whose memory it keeps.
types.rs:1983 ↗
pub fn residency(&self) -> Residency
StructArrowArrayA column's buffers in Apache Arrow's C data interface, laid out as arrow::ffi::FFI_ArrowArray and released when dropped.1
types.rs:1141 ↗
pub struct ArrowArray

Implements

Default, Drop, Send
lengthRows the column holds.
types.rs:1223 ↗
pub fn length(&self) -> i64
TraitArrowColumnOne Arrow column a producer handed over through the C data interface, which arrow::ffi pairs as FFI_ArrowSchema and FFI_ArrowArray.2
types.rs:1259 ↗
pub unsafe trait ArrowColumn

Safety

Both pointers must address live structures laid out as ArrowSchema and ArrowArray and following Arrow's C data interface, whose host buffers stay valid and unwritten for as long as self stays borrowed.

*× 2The column's type.
types.rs:1261 ↗
fn schema(&self) -> *const ArrowSchema
arrayThe column's buffers.
types.rs:1263 ↗
fn array(&self) -> *const ArrowArray
StructArrowDeviceArrayAn ArrowArray and the device holding its buffers, as Arrow's C device data interface lays them out.1
types.rs:1157 ↗
pub struct ArrowDeviceArray

Implements

Default, Send
arrayThe column itself.
types.rs:1228 ↗
pub fn array(&self) -> &ArrowArray
StructArrowExportMatches exported as Arrow in place, pinning the arena they sit in until released.2
types.rs:1316 ↗
pub struct ArrowExport<'arena>

Rust-only.

arrayThe column's buffers and the device holding them.
types.rs:1327 ↗
pub fn array(&self) -> &ArrowDeviceArray
into_partsHands the column over past the arena's borrow, for a consumer that takes the structures by value, refused with Status::Device over device memory, which must be released before its executor drops.
types.rs:1333 ↗
pub fn into_parts(self) -> Result<(ArrowSchema, ArrowArray)>

The arena stays pinned, so a verb naming it fails with Status::Contended until the last release, and a dropped arena waits for that release.

StructArrowRowsThe rows of one Arrow fixed-size list column, read in place through its buffers, which a verb takes wherever it takes a matrix.1
types.rs:1275 ↗
pub struct ArrowRows<'column, Value>

Rust-only.

newViews column's rows, refused with Status::UnsupportedLayout for another Arrow type, Status::InvalidArgument for a null, and Status::UnknownKind for rows of another scalar than Value.
types.rs:1284 ↗
pub fn new<Column: ArrowColumn + ?Sized>(column: &'column Column) -> Result<Self>
StructArrowSchemaA column's type in Apache Arrow's C data interface, laid out as arrow::ffi::FFI_ArrowSchema and released when dropped.1
types.rs:1126 ↗
pub struct ArrowSchema

Implements

Default, Drop, Send
formatThe column's format string, or none once released.
types.rs:1215 ↗
pub fn format(&self) -> Option<&CStr>
TraitElementTies a NumKong storage type to the Scalar the library tags its buffers with.1
types.rs:870 ↗
pub unsafe trait Element: StorageElement + 'static

The stored quantization is chosen separately in the index options, so an f32 query may search an i8 index - the library converts on the way in.

Rust-only.

Safety

The associated scalar must describe the type's in-memory encoding exactly; the library reads raw buffers under that tag.

Implementors

SCALARThe tag the library knows this type by.
types.rs:872 ↗
const SCALAR: Scalar;
StructMatchesThe ranked matches a search or a stream pull wrote into the arena it borrows; see DenseVerbs::search.4
types.rs:1056 ↗
pub struct Matches<'arena>
queriesQueries the call resolved.
types.rs:1064 ↗
pub fn queries(&self) -> usize
queryMatches written for one query, refused with Status::InvalidArgument when query is not below Matches::queries and with Status::Device over device memory.
types.rs:1068 ↗
pub fn query(&self, query: usize) -> Result<QueryMatches<'arena>>
iterEvery query's matches, in query order, refused with Status::Device over device memory.
types.rs:1078 ↗
pub fn iter(&self) -> Result<impl Iterator<Item = QueryMatches<'arena>>>
to_arrowExports every query's matches as one Arrow LargeListView of Struct<key, distance> in place, refused with Status::Contended while another call fills the arena.
types.rs:1104 ↗
pub fn to_arrow(&self) -> Result<ArrowExport<'arena>>

The export borrows the arena, so no verb reaches it while the export lives:

use usearch::*;
let options = DenseOptions { dimensions: 2, ..DenseOptions::default() };
let index = DenseIndex::init_ram(&options, &[])?;
let slab = [1.0f32, 0.0];
let mut arena = Arena::new()?;
let matches = index.search(&rows(&slab, 2)?, 1, &mut arena)?;
let export = matches.to_arrow()?;
index.search(&rows(&slab, 2)?, 1, &mut arena)?;
drop(export);
StructQueryMatchesOne query's matches, or one left row's pairs, trimmed to the cells the call wrote.
types.rs:1045 ↗
pub struct QueryMatches<'matches>

Rust-only.

Fields

keys&'matches [Key]The keys written for this query: nearest first for a search, largest score first for a maxsim, and in traversal order for a multiply.
distances&'matches [Distance]Positionally paired with the keys: distances for a search, summed similarities for a maxsim, and similarities for a multiply.

Implements

Clone, Copy, StructuralPartialEq, PartialEq, Debug
EnumScalarScalar types the indexes store and exchange, named by width and encoding.
types.rs:783 ↗
pub enum Scalar

Sub-byte kinds pack several dimensions per byte, as their xN suffixes spell. The library reserves the numbering gaps for kinds not yet implemented, so this enum is open.

Values

F641IEEE-754 binary64: ±1.80e308, epsilon 2.22e-16, subnormals to 4.94e-324, with infinities and NaNs.
F322IEEE-754 binary32: ±3.40e38, epsilon 1.19e-7, subnormals to 1.40e-45, with infinities and NaNs.
BF163Truncated f32 - 8-bit exponent, 7-bit mantissa: f32's range at ±3.39e38 and epsilon 7.81e-3, with infinities and NaNs.
F164IEEE-754 binary16: ±65504, epsilon 9.77e-4, subnormals to 5.96e-8, with infinities and NaNs.
E5M25FP8 with a 5-bit exponent: ±57344, epsilon 0.25, with infinities and NaNs - f16's dynamic range carried on two mantissa bits.
E4M36FP8 with a 4-bit exponent: ±448, epsilon 0.125, no infinities, and two NaN codes at 0x7F and 0xFF.
E3M27FP6 with a 3-bit exponent, byte-aligned with two padding bits: ±28, no infinities or NaNs, so every one of its 64 codes is a number.
E2M38FP6 with a 2-bit exponent, byte-aligned with two padding bits: ±7.5, no infinities or NaNs, and 72% of its codes outside [-1, 1].
I8108-bit signed integer spanning -128 to 127, every code a number.
I4x211Two signed nibbles per byte spanning -8 to 7, even dimensions in the low nibble and odd ones in the high.
I321232-bit signed integer spanning ±2.15e9, every code a number.
U8208-bit unsigned integer spanning 0 to 255, every code a number.
U4x221Two unsigned nibbles per byte spanning 0 to 15, even dimensions in the low nibble and odd ones in the high.
U1x822Eight single-bit dimensions per byte - the form the Hamming & Jaccard metrics score, where only the set-bit count of a XOR or AND carries meaning.

Implements

Clone, Copy, StructuralPartialEq, PartialEq, Eq, Debug, Hash, Display, FromStr
EnumTraversalHow a search reaches the indexed entries it scores.
types.rs:1698 ↗
pub enum Traversal

Values

Walk1Walks the graph, scoring the entries the expansion reaches - approximate.
Scan2Scores every entry published when the query's scan starts - exact, on RAM and NVMe handles.

Implements

Clone, Copy, StructuralPartialEq, PartialEq, Eq, Debug, Default, Hash, Display, FromStr

Functions

rowsBorrows a packed slab as a row-major matrix of dimensions-wide rows.
lib.rs:958 ↗
pub fn rows<Value: StorageElement>(
    data: &[Value],
    dimensions: usize,
) -> Result<MatrixView<'_, Value>>

Slabs whose rows are padded to a wider pitch - memory-mapped datasets, cache-line-aligned arenas - go through MatrixView::from_raw_parts instead, which takes the byte stride.

Rust-only.

keys_from_arrowViews the keys of one Arrow fixed_size_binary(16) column in place, tagged arrow.uuid or not, refused as ArrowRows::new refuses.
lib.rs:1305 ↗
pub fn keys_from_arrow<Column: ArrowColumn + ?Sized>(column: &Column) -> Result<&[Key]>