C# · Inputs and Outputs

Inputs and Outputs

using Cloud.Unum.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. 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, and a call naming an arena that serves another call finishes with Status.Contended and rewinds nothing, so the holder’s views stay readable. A stale view throws USearchException with Status.Uninitialized; a ReadOnlySpan an accessor already returned cannot outlive its stack frame, so read it before the next call. An arena’s Describe() says where it lives, and one 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 holds it open until disposed; a host arena may outlive its executor.

using Arena arena = new(pool);                                     // new Arena() is host pages for the calling thread
foreach (RowsView batch in batches)
{
    Matches matches = index.On(pool).Search(batch, 10, arena);   // Rewinds: the last batch's matches are gone
    foreach (double distance in matches[0].Distances) total += distance;
}

using Arena found = new(), fetched = new();                        // Two live results, two arenas
foreach (Key key in index.Search(query, 10, found)[0].Keys)
{
    Rerank(key, index.Get(key, fetched));
}

Executor[] pools = [.. shards.Select(_ => Executor.ForRam(threads: 8))];  // One executor and one arena per submitted call
Arena[] arenas = [.. pools.Select(own => new Arena(own))];
Deferred<Matches>[] calls = [.. shards.Select((shard, at) => shard.Deferred(pools[at]).Search(queries, 10, arenas[at]))];
foreach (Deferred<Matches> call in calls) Consume(await call);   // Or: call.Wait()

§ Rows & Scalars

Every batch argument is a RowsView: a caller-owned slab viewed as rows of equal width, carrying its row count, its logical width, its byte pitch, and the scalar kind its values hold. A single vector is a one-row matrix, and the input scalar type is independent of the stored quantization, since the library converts on ingest.

float[] rows = [0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f];
RowsView packed = RowsView.RowsOf<float>(rows, dimensions: 3);              // Three rows, pitch 12 bytes

byte[] bits = new byte[16];
RowsView bitsets = RowsView.RowsOf<byte>(bits, dimensions: 64, Scalar.U1x8);   // Two rows, pitch 8 bytes

float[] padded = new float[2 * 8];
RowsView strided = RowsView.StridedRowsOf<float>(                          // The layout a mapped dataset arrives in
    padded, rows: 2, dimensions: 3, strideBytes: 8 * sizeof(float), Scalar.F32);

The kind travels with the slab rather than being derived from its element type, which is what makes the packed sub-byte kinds reachable: sixty-four Scalar.U1x8 dimensions occupy eight bytes, not sixty-four.

A RowsView borrows and never owns: it holds the Memory<T> it was built from, so the slab stays reachable for as long as the view is, and a deferred call pins it until final.

Key[] keys = [1UL, 2UL, 3UL];
DenseTraversalStats stats = new();
index.On(pool).Insert(keys, packed, expansionFactor: 128, stats: stats);

Matches found = index.On(pool).Search(packed, wanted: 4, arena, expansionFactor: 64);
for (int query = 0; query < found.Queries; query++)
{
    ReadOnlySpan<Key> matched = found[query].Keys;              // Already trimmed to the true count
    ReadOnlySpan<double> distances = found[query].Distances;
}

The breadth of the candidates rides each call rather than the handle, so a recall sweep runs against one immutable index with no mutation 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 a scan with Status.Unsupported. DenseTraversalStats is a pure output, overwritten by every call it is passed to, and Accumulate folds one call’s counters into another’s to total a run of batches.

A sweep reuses one arena across every point, which keeps its high-water mark, so nothing is allocated between them:

foreach (int breadth in new[] { 16, 32, 64, 128 })
{
    Matches point = index.On(pool).Search(packed, wanted: 10, arena, expansionFactor: (ulong)breadth);
    // Read `point` here: the next point's call rewinds the arena
}

API

StructRowsViewA caller-owned slab viewed as rows of equal width: a base, a row count, a logical width, and a byte pitch between consecutive row starts.2
Types.cs:603 ↗
struct RowsView

It is the shape every batch argument takes, as DenseIndex.Insert and DenseIndex.Search read it. Mirrors usearch_rows_view_t.

A view borrows and never owns: it holds the Memory<T> it was built from, so the slab stays reachable for as long as the view is, and a deferred call pins it until final.

The scalar kind is carried rather than derived, so a slab of bytes reaches the index as packed Scalar.U1x8 bit sets or as plain Scalar.U8 integers, the caller having said which.

RowsOf< TScalar >Views a packed slab as rows of the given width, naming the kind its element type implies.
Types.cs:634 ↗
RowsView RowsOf< TScalar >(Memory<TScalar> data, int dimensions);
unsafe RowsView RowsOf< TScalar >(
    Memory<TScalar> data,
    int dimensions,
    Scalar scalar);

Views a packed slab as rows of the given width, under a kind the caller names.

Parameters

dataThe slab, which divides evenly into rows.
dimensionsThe logical width of one row.
scalarThe scalar type the values hold.

Returns

The view over that slab.

StridedRowsOf< TScalar >Views a slab whose pitch exceeds its row width - the padded layout a memory-mapped dataset arrives in.
Types.cs:668 ↗
unsafe RowsView StridedRowsOf< TScalar >(
    Memory<TScalar> data,
    int rows,
    int dimensions,
    int strideBytes,
    Scalar scalar);

Parameters

dataThe slab the rows are cut from.
rowsHow many rows the view spans.
dimensionsThe logical width of one row.
strideBytesThe byte pitch between consecutive row starts.
scalarThe scalar type the values hold.

Returns

The view over that slab.