import * as usearch from 'usearch';import { DenseTraversalStats } from 'usearch';import { RowsView } from 'usearch';import { Arena } from 'usearch';import { ArenaView } from 'usearch';import { QueryMatches } from 'usearch';import { Matches } from 'usearch';3.0.0-rc.1 · 4c3e5023
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. new Arena(executor) places it where that executor computes, decided once, and new Arena() 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.
A deferred call borrows its arena as a blocking one does: another call naming it before the first is final finishes with Status.Contended, and free() waits for it.
A rewind or a free() detaches every buffer over the previous outputs, so a stale view throws Status.Uninitialized and typed arrays taken from it read as empty.
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 and keeps it alive until freed, and the executor’s free() refuses with Status.Contended while such an arena lives; a host arena may outlive its executor.
const { Arena, keyAt } = usearch;using arena = new Arena(executor); // new Arena() is host pages for the calling threadfor (const batch of batches) { const matches = index.on(executor).search(batch, 10, arena); // rewinds: last batch's matches are gone total += matches.scores.reduce((sum, score) => sum + score, 0);}using found = new Arena(), fetched = new Arena(); // two live results, two arenasconst nearest = index.search(query, 10, found).get(0);for (let position = 0; position < nearest.count; position++) rerank(keyAt(nearest.keys, position), index.get(keyAt(nearest.keys, position), fetched));// One executor and one arena per submitted callconst calls = shards.map((shard, at) => shard.deferred(executors[at]).search(queries, 10, arenas[at]));const results = [];for (const call of calls) results.push(await call); // or: call.wait()
Only fixed-size reports, like describe and the telemetry readings, come back as plain objects.
A bare TypedArray is one row, and a RowsView is a run of them borrowed in place.
It names the array, the row count, the columns, the bytes between rows, and the scalar kind, so a memory-mapped dataset reaches the index with no reshape and no copy. RowsView.of reads a packed array as rows of the width it is given:
const keys = new BigUint64Array([15n, 16n]);const vectors = usearch.RowsView.of(new Float32Array([10, 20, 10, 25]), 2); // two rows of twoindex.insert(keys, vectors);const padded = new usearch.RowsView(mapped, rows, 768, 1024 * 4, usearch.Scalar.F32); // a pitch of 1024 floatsconst matches = index.search(vectors, 2, arena);
The engine converts rows on ingest, so what a caller holds is independent of what the index stores.
A Float64Array, Float32Array, Float16Array, Int8Array or Uint8Array names its own scalar kind; every other kind travels as bytes in a RowsView naming what they mean:
search lands its matches in the arena as Matches, three views: the keys, the scores, and how many matches every query actually found.
const matches = index.search(queries, 10, arena);matches.queries; // Queries in the runmatches.columns; // Columns per query the call wrote, at most its 'k'matches.counts; // BigUint64Array, the matches every query foundmatches.keys; // Uint8Array, sixteen bytes per cell, queries laid end to endmatches.scores; // Float64Array, one distance per cellconst found = matches.get(0); // A QueryMatches, borrowed out of the run rather than copiedfound.count; // Matches this query found, never above `wanted`found.keys; // Sixteen bytes per match foundfound.scores; // One distance per match found
Every view reads the arena’s own memory rather than a copy, so reading a query costs nothing.
Scores come back as a Float64Array, which is the width the ABI writes.
The breadth of the candidates rides every call rather than the construction, so a recall sweep runs against one immutable index with no mutation between points:
insert, refine, search, maxsim, multiply and attend each report how much graph they walked, into a DenseTraversalStats reading passed through the config:
const stats = new usearch.DenseTraversalStats();index.on(executor).search(queries, 10, arena, { stats });stats.visitedMembers; // entries whose neighborhoods the walk expandedstats.computedDistances; // distance evaluations the walk paid forstats.restarts; // times the walk fell back to a fresh entry pointconst total = new usearch.DenseTraversalStats(); // sweeping, and reading the whole sweep's costfor (const expansionFactor of [16, 32, 64, 128]) { index.on(executor).search(queries, 10, arena, { expansionFactor, stats }); total.accumulate(stats);}
Telemetry travels through the reading rather than the return value, so refine still answers with its report and a ranked verb with its matches, and passing none costs nothing.
A reading is a pure output that each call fully overwrites, so one DenseTraversalStats handed to several calls holds the last of them, and a deferred call writes it once it settles.
Use accumulate to fold readings together: expansionFactor buys recall in distance computations, and only the counters say how many, so a sweep reusing one arena allocates once and reads its cost between the points.
API
Types
DenseInsertConfigOptional per-call settings of an insertion.
The engine converts on ingest, so this is independent of the index's own quantization: an
Int8Array reaches an f16 index unchanged. The kinds no TypedArray spells on its own -
bf16, the mini-floats, the packed nibbles and bits - travel as bytes in a Uint8Array or
Uint16Array, inside a RowsView naming their Scalar.
ClassDenseTraversalStatsTraversal telemetry from one call, a pure output the call fully overwrites.4
One reading handed to several calls holds the last of them, so a sweep that wants totals folds
them with accumulate rather than expecting the calls to sum themselves.
accumulateFolds another reading into this one, which is how a sweep gathers totals.
Every verb with such an output takes one and answers a view into it rather than a copy, so a
steady loop allocates once. Each call rewinds the arena it is handed and detaches every buffer
over the previous call's outputs: a view refuses its reads with Status.Uninitialized from
then on, and every array already taken out of one reads empty rather than another call's memory.
To keep one result past the next call, give that call another arena. A deferred call borrows it
as a blocking one does: another call naming it before the first is final finishes
Status.Contended and leaves the first call's outputs alone, and free waits for it. The
collector frees the arena once nothing views it; free releases it now.
A Vram arena grows, fills and frees on its executor's first stream, so it serves only calls on
that executor and keeps it alive until freed; host pages may outlive their executor.
ArenaMakes an arena where executor computes, or host pages for calls on the calling thread when absent.
The rewind detaches the buffer under every array, so each accessor refuses once the arena moved
on, with Status.Uninitialized, and refuses device memory with Status.Device, so a
view never reads memory its call did not write.
ClassQueryMatchesThe matches of one query, borrowed out of a run rather than copied.2