Rust · Metrics

Metrics

use usearch::DenseIndex;use usearch::DenseMetric;use usearch::DenseOptions;use usearch::*; 3.0.0-rc.1 · 4c3e5023

§ Dense Indexes

A DenseIndex holds equidimensional dense vectors, built from DenseOptions: dimensions, metric, the stored quantization, and the connectivity and values_per_key bounds, where zero derives. Every metric derives from a single inner-product kernel with pre-computed magnitudes; arbitrary metrics belong to the C++ baseline tier, forfeiting the SIMD kernel chains and the magnitude cache both.

  • DenseMetric::Inner - inner product.
  • DenseMetric::Angular - cosine distance, over pre-computed magnitudes.
  • DenseMetric::Euclidean - L2 distance.
  • DenseMetric::Hamming - bit-level, over u1x8 vectors alone.
  • DenseMetric::Jaccard - bit-level, over u1x8 vectors alone.

API

Types

DistanceDistance or similarity score in the metric of the index that produced it.
lib.rs:689 ↗
pub type Distance = f64;
StructDenseIndexVector Search Index over equidimensional dense representations, whose verbs come from DenseVerbs.33
dense_index.rs:1456 ↗
pub struct DenseIndex

DenseIndex::clear alone takes &mut self, since no other call may overlap it.

use usearch::*;

let options = DenseOptions {
    dimensions: 4,
    metric: DenseMetric::Angular,
    quantization: Scalar::I8,
    ..DenseOptions::default()
};
let index = DenseIndex::init_ram(&options, &[])?;
index.reserve(1_000)?;

let keys = [Key::from(0u64), Key::from(1u64)];
let slab: [f32; 8] = [0.2, 0.1, 0.2, 0.1, 0.1, 0.2, 0.1, 0.2];
index.insert(&keys, &rows(&slab, 4)?)?;

let query: [f32; 4] = [0.2, 0.1, 0.2, 0.1];
let executor = Executor::for_ram(&[], None, 0, 0)?;
let mut arena = Arena::new_in(&executor)?;
let matches = index.on(&executor).search(&rows(&query, 4)?, 10, &mut arena)?;
for query in matches.iter()? {
    println!("{} matches", query.keys.len());
}

Dropping it waits for every deferred call naming the index, then frees it.

Implements

Debug, Send, Sync, Drop, DenseVerbs<'index>
onThe receiver whose verbs block on executor, after the calls submitted there before them.
dense_index.rs:1480 ↗
pub fn on<'index>(&'index self, executor: &'index Executor) -> On<'index, DenseIndex>

Rust-only.

deferredThe receiver whose verbs submit their calls to executor, answering a Deferred from the same verbs given owned arguments.
dense_index.rs:1519 ↗
pub fn deferred<'index>(
    &'index self,
    executor: &'index Executor,
) -> On<'index, DenseIndex, Deferring>

The library reads a deferred call's inputs after the verb returns, so lending a borrowed query does not compile:

use usearch::*;

let index = DenseIndex::init_ram(&DenseOptions { dimensions: 4, ..DenseOptions::default() }, &[])?;
let executor = Executor::for_ram(&[], None, 0, 0)?;
let mut arena = Arena::new_in(&executor)?;
let query = [0.0f32; 4];
let call = index.deferred(&executor).search(&rows(&query, 4)?, 10, &mut arena)?;

The arena is borrowed as a blocking call borrows it, and so is the waited result:

use usearch::*;

let index = DenseIndex::init_ram(&DenseOptions { dimensions: 4, ..DenseOptions::default() }, &[])?;
let executor = Executor::for_ram(&[], None, 0, 0)?;
let mut arena = Arena::new_in(&executor)?;
let call = index.deferred(&executor).get::<f32>(Key::from(1u64), &mut arena)?;
let vectors = call.wait()?;

Rust-only.

clearErases all entries on the calling thread, keeping the reserved capacity and frozen geometry; &mut self keeps every other call on the handle from overlapping it.
dense_index.rs:1529 ↗
pub fn clear(&mut self) -> Result<()>
init_ramCreates an empty, mutable index in host memory, one shard per NUMA node memory_domains names; empty derives every node the process may use.
dense_index.rs:1544 ↗
pub fn init_ram(options: &DenseOptions, memory_domains: &[i32]) -> Result<Self>
init_vramCreates an empty, mutable index in device memory, sharded across devices and never replicated; empty derives one free device.
dense_index.rs:1564 ↗
pub fn init_vram(options: &DenseOptions, devices: &[i32]) -> Result<Self>
load_ramCopies a checkpoint into a new, mutable index in host memory; a bare path reads on the calling thread.
dense_index.rs:1584 ↗
pub fn load_ram<'config>(config: impl Into<DenseLoadRamConfig<'config>>) -> Result<Self>
load_vramUploads a checkpoint into a new, mutable index in device memory, placing its parts as DenseIndex::load_ram does.
dense_index.rs:1609 ↗
pub fn load_vram<'config>(config: impl Into<DenseLoadVramConfig<'config>>) -> Result<Self>
load_ram_bufferCopies a monolith from a buffer into a new, mutable index in host memory.
dense_index.rs:1632 ↗
pub fn load_ram_buffer<'config>(
    config: impl Into<DenseLoadRamBufferConfig<'config>>,
) -> Result<Self>
load_vram_bufferUploads a monolith from a buffer into a new, mutable index in device memory.
dense_index.rs:1656 ↗
pub fn load_vram_buffer<'config>(
    config: impl Into<DenseLoadVramBufferConfig<'config>>,
) -> Result<Self>
view_ramServes a checkpoint in place through a memory mapping, as a read-only index whose mutating verbs report Status::ReadOnly; the files must stay unmodified while it lives.
dense_index.rs:1680 ↗
pub fn view_ram(path: impl AsRef<Path>) -> Result<Self>
view_nvmeServes a checkpoint in place from NVMe drives, as a read-only index whose mutating verbs report Status::ReadOnly.
dense_index.rs:1698 ↗
pub fn view_nvme<'config>(config: impl Into<DenseViewNvmeConfig<'config>>) -> Result<Self>

Answers Status::Unsupported until NVMe checkpoints carry keys.

metadataReads a checkpoint's geometry and metric without loading it; a manifest answers from its summary and its first part's header, so the cost does not grow with the index.
dense_index.rs:1719 ↗
pub fn metadata(path: impl AsRef<Path>) -> Result<DenseMetadata>
metadata_bufferReads geometry and metric out of a serialized monolith buffer.
dense_index.rs:1737 ↗
pub fn metadata_buffer(buffer: &[u8]) -> Result<DenseMetadata>
handlesThe index this receiver reaches and the executor its calls run on.
dense_index.rs:2167 ↗

Inherited from DenseVerbs

fn handles(&self) -> Handles
reserveReserves capacity for a number of incoming vectors ahead of insertion, a hint, since insertions grow the index on demand.
dense_index.rs:1779 ↗

Inherited from DenseVerbs

fn reserve(
    self,
    capacity: usize,
) -> <<Self as DenseVerbs<'index>>::Completion as Completion>::Answer<'index, DenseReserve>
describeReports the frozen metadata and the current counters in one call.
dense_index.rs:1787 ↗

Inherited from DenseVerbs

fn describe(
    self,
) -> <<Self as DenseVerbs<'index>>::Completion as Completion>::Answer<'index, DenseDescribe>
countThe number of entries stored under key.
dense_index.rs:1795 ↗

Inherited from DenseVerbs

fn count(
    self,
    key: Key,
) -> <<Self as DenseVerbs<'index>>::Completion as Completion>::Answer<'index, DenseCount>
containsWhether the index holds at least one entry under key.
dense_index.rs:1806 ↗

Inherited from DenseVerbs

fn contains(self, key: Key) -> Result<bool>
where
    Self: DenseVerbs<'index, Completion = Blocking>,

Rust-only.

getRetrieves every vector stored under key into arena, which the result borrows; an absent key yields no rows.
dense_index.rs:1815 ↗

Inherited from DenseVerbs

fn get<'arena, Value: Element>(
    self,
    key: Key,
    arena: &'arena mut Arena,
) -> <<Self as DenseVerbs<'index>>::Completion as Completion>::Answer<'index, DenseGet<'arena, Value>>
insertAdds a matrix of vectors under their keys - the native batch operation.
dense_index.rs:1836 ↗

Inherited from DenseVerbs

fn insert<'config, Keys: AsRef<[Key]>, Rows: RowsView>(
    self,
    keys: Keys,
    config: impl Into<DenseInsertConfig<'config, Rows>>,
) -> <<Self as DenseVerbs<'index>>::Completion as Completion>::Answer<'index, DenseInsert<'config, Keys, Rows>>
where
    Self::Completion: Accepts<Keys> + Accepts<DenseInsertConfig<'config, Rows>>,

A batch is all-or-nothing and becomes visible at once. Every entry is claimed before the first progress report, so a Break is ignored. Overlaps safely with concurrent searches on the same handle, and the input scalar type is independent of the stored quantization - the library converts on ingest.

refineRe-selects the neighbors of every stored entry against the finished graph, answering a RefineReport of the nodes re-selected and changed and the islands it re-linked.
dense_index.rs:1859 ↗

Inherited from DenseVerbs

fn refine<'config>(
    self,
    config: DenseRefineConfig<'config>,
) -> <<Self as DenseVerbs<'index>>::Completion as Completion>::Answer<'index, DenseRefine<'config>>
where
    Self::Completion: Accepts<DenseRefineConfig<'config>>,

A cheap insertion pass followed by refinement beats one expensive pass, so a wide expansion factor belongs here rather than in DenseVerbs::insert. On a RAM handle searches may run throughout, on a VRAM one they may not, and insertions never may. A stop is a time budget: the call still re-links its islands, leaving the graph correct, and answers Status::Cancelled.

healthAudits how much of the graph a search reaches: forward reachability from as many landmarks as one search draws, swept level by level.
dense_index.rs:1874 ↗

Inherited from DenseVerbs

fn health(
    self,
) -> <<Self as DenseVerbs<'index>>::Completion as Completion>::Answer<'index, DenseHealth>

Read-only, so concurrent insertions merely leave fresh entries unaudited.

maxsimLate-interaction scoring of multi-row queries against multi-row entries, writing up to wanted keys per query, largest score first, in arena as one ragged run; zero keeps every key a query scored.
dense_index.rs:1914 ↗

Inherited from DenseVerbs

fn maxsim<'config, 'arena, Queries: RowsView>(
    self,
    queries: Queries,
    rows_per_query: usize,
    config: impl Into<DenseMaxsimConfig<'config>>,
    arena: &'arena mut Arena,
) -> <<Self as DenseVerbs<'index>>::Completion as Completion>::Answer<'index, DenseMaxsim<'config, 'arena, Queries>>
where
    Self::Completion: Accepts<Queries> + Accepts<DenseMaxsimConfig<'config>>,

Every query row takes its best match within a key, and those maxima sum to that key's score - so rows_per_query is the token count each query occupies, queries holds them end to end, and wanted counts documents rather than rows. This is what values_per_key above one exists for. Runs on an inner-product index alone.

See https://arxiv.org/abs/2004.12832 - ColBERT, which introduced this scoring.

multiplyApproximate sparse product of a matrix against the indexed vectors, writing its keys, products and offsets in arena as one ragged run.
dense_index.rs:1942 ↗

Inherited from DenseVerbs

fn multiply<'config, 'arena, Left: RowsView>(
    self,
    left: Left,
    config: impl Into<DenseMultiplyConfig<'config>>,
    arena: &'arena mut Arena,
) -> <<Self as DenseVerbs<'index>>::Completion as Completion>::Answer<'index, DenseMultiply<'config, 'arena, Left>>
where
    Self::Completion: Accepts<Left> + Accepts<DenseMultiplyConfig<'config>>,

The linear-algebra sibling of DenseVerbs::search, reporting every pair the traversal scored rather than only the nearest. Pairs arrive in traversal order rather than sorted. Runs on an inner-product index alone, since a product is a logit.

attendSoftmax attention of every query row over every indexed key its walk scores, folded as Flash Attention folds key tiles, with no top-k cut, writing into arena as rows of the queries' own scalar type.
dense_index.rs:1976 ↗

Inherited from DenseVerbs

fn attend<'config, 'lookup, 'arena, Queries: RowsView, Value: Element>(
    self,
    queries: Queries,
    value_lookup: &'lookup ValueLookup<'lookup, Value>,
    config: impl Into<DenseAttendConfig<'config>>,
    arena: &'arena mut Arena,
) -> <<Self as DenseVerbs<'index>>::Completion as Completion>::Answer<'index, DenseAttend<'config, 'lookup, 'arena, Queries, Value>>
where
    Self::Completion: Accepts<Queries> + Accepts<&'lookup ValueLookup<'lookup, Value>> + Accepts<DenseAttendConfig<'config>>,

The index holds one layer's keys for one key-value head and no values: value_lookup resolves every scored key to its value row, as wide as the keys and stored in the index's own quantization. A key left without a row, or given one of another type or width, refuses the call with Status::ShapeMismatch, since a missing row is a wrong softmax rather than a smaller one. A VRAM walk resolves values on the device, where no Rust closure runs, so an arena from a VRAM executor is refused with Status::Device. The outputs come back already divided by the partition. Runs on an inner-product index alone, since a softmax needs logits.

See https://arxiv.org/abs/2205.14135 - Flash Attention, whose recurrence this reuses.

absorbAbsorbs every donor into this index, one after another, keeping their graphs rather than re-inserting their vectors and linking each in by walks across the appended part.
dense_index.rs:2006 ↗

Inherited from DenseVerbs

fn absorb<'config, Donors: IntoIterator<Item = DenseIndex>>(
    self,
    config: impl Into<DenseAbsorbConfig<'config, Donors>>,
) -> <<Self as DenseVerbs<'index>>::Completion as Completion>::Answer<'index, DenseAbsorb<'config>>
where
    Self::Completion: Accepts<DenseAbsorbConfig<'config, Donors>>,

Readers of a RAM index may keep searching throughout, while a VRAM one admits no overlapping search. The donors are consumed whether or not the absorb succeeds: a stop cuts only the closing refinement, every donor already moved, and answers Status::Cancelled, while a failure frees the donors it never reached and may leave the failing donor's entries behind.

splitSplits this index into the empty receivers, the inverse of [DenseVerbs::absorb]: the entries are partitioned along the graph, each part lands in one receiver with its edges kept, and a closing refinement re-links every receiver, leaving this index empty and live.
dense_index.rs:2026 ↗

Inherited from DenseVerbs

fn split<'config>(
    self,
    config: impl Into<DenseSplitConfig<'config>>,
) -> <<Self as DenseVerbs<'index>>::Completion as Completion>::Answer<'index, DenseSplit<'config>>
where
    Self::Completion: Accepts<DenseSplitConfig<'config>>,
sampleDraws keys around the focus, or across the whole index, each hanging from a shown or earlier draw and placed in three dimensions beside the shown ones, writing up to wanted of them into arena as a Sample.
dense_index.rs:2042 ↗

Inherited from DenseVerbs

fn sample<'config, 'arena>(
    self,
    wanted: usize,
    arena: &'arena mut Arena,
    config: DenseSampleConfig<'config>,
) -> <<Self as DenseVerbs<'index>>::Completion as Completion>::Answer<'index, DenseSample<'config, 'arena>>
where
    Self::Completion: Accepts<DenseSampleConfig<'config>>,

A larger wanted under the same seed draws a superset; only a RAM index samples yet.

saveSaves the index in the layout the path extension picks - one .safegraphs monolith, or a .safegraphs.json manifest beside its shards.
dense_index.rs:2069 ↗

Inherited from DenseVerbs

fn save<'config, Location: AsRef<Path>>(
    self,
    config: impl Into<DenseSaveConfig<'config, Location>>,
) -> <<Self as DenseVerbs<'index>>::Completion as Completion>::Answer<'index, DenseSave<'config, Location>>
where
    Self::Completion: Accepts<DenseSaveConfig<'config, Location>>,

Demands a mutation-free window; concurrent readers keep searching throughout. A stopped save leaves leftovers every reader rejects and the caller is left to remove.

save_bufferSaves the index as one monolith into the arena its config carries.
dense_index.rs:2083 ↗

Inherited from DenseVerbs

fn save_buffer<'config, 'arena>(
    self,
    config: impl Into<DenseSaveBufferConfig<'config, 'arena>>,
) -> <<Self as DenseVerbs<'index>>::Completion as Completion>::Answer<'index, DenseSaveBuffer<'config, 'arena>>
where
    Self::Completion: Accepts<DenseSaveBufferConfig<'config, 'arena>>,
cursorOpens a walk over the current contents, pulled blocking on this receiver's executor; each shard's publication watermark is read as the walk enters it, so an entry once passed is never revisited.
dense_index.rs:2103 ↗

Inherited from DenseVerbs

fn cursor(self, starts: &[Key]) -> Result<DenseCursor<'index>>
where
    Self: DenseVerbs<'index, Completion = Blocking>,

Without starts the walk yields every entry as stored. With them it yields the start keys' own entries first, in order, then the entries those link to breadth-first, each entry once.

search_streamOpens a resumable nearest-neighbor stream for a one-row query, pulled blocking on this receiver's executor.
dense_index.rs:2133 ↗

Inherited from DenseVerbs

fn search_stream<Query: RowsView>(
    self,
    config: impl Into<DenseSearchStreamInitConfig<Query>>,
) -> Result<DenseSearchStream<'index>>
where
    Self: DenseVerbs<'index, Completion = Blocking>,

Best-first traversal is naturally pausable, so callers that post-filter aggressively pull until satisfied instead of guessing "k" upfront. The stream keeps walk state rather than a thread.

No residency implements the stream yet, so the engine answers Status::Unsupported.

EnumDenseMetricThe dense metric kinds supported out of the box, all derived from a single inner-product kernel with pre-computed magnitudes.
dense_index.rs:558 ↗
pub enum DenseMetric

Hamming and Jaccard are bit-level over Scalar::U1x8 alone, a kind the other three refuse.

Arbitrary metrics belong to the C++ baseline tier, which forfeits the SIMD kernel chains and the magnitude cache both.

Values

Inner1Sum of elementwise products, unnormalized and scale-sensitive; the only kind the linear-algebra verbs accept, since a product and a softmax both need logits rather than distances.
Angular2Cosine distance in [0, 2], zero for identical directions and blind to magnitude, which is precomputed once per vector at insertion rather than per comparison.
Euclidean3L2 distance, non-negative and zero only for identical vectors - the scale-sensitive counterpart to angular, where a doubled vector is far from its original rather than identical to it.
Hamming4Count of differing bits, so its range is the dimensionality and every bit weighs the same.
Jaccard5Differing bits over union bits in [0, 1], ignoring the zeros both vectors share - the kind for sparse sets, where Hamming would call two nearly-empty vectors identical.

Implements

Clone, Copy, StructuralPartialEq, PartialEq, Eq, Debug, Default, Hash, Display, FromStr
StructDenseOptionsConstruction options of a dense index, whose stored vectors round up to the SIMD step quantum so every kernel runs tail-free.
dense_index.rs:616 ↗
pub struct DenseOptions

Fields

dimensionsusizeThe number of indexed dimensions, which init freezes; zero is refused with Status::ConfigMismatch.
metricDenseMetricThe metric kind used for distance calculations.
quantizationScalarThe scalar type vectors are stored and quantized into.
connectivityusizeThe connections-per-node bound of the graph; zero derives.
values_per_keyusizeUpper bound on entries sharing one key; zero is unbounded.
cpu_classOption<CPUClass>The class a host index dispatches as; None dispatches as the machine.
gpu_classOption<GPUClass>The class a device index dispatches as; None dispatches as the devices.

Implements

Clone, StructuralPartialEq, PartialEq, Eq, Debug, Hash, Default