enum Mood Values
Parked | "parked" | Idle threads sleep; the next call pays a wake-up. |
|---|---|---|
Spinning | "spinning" | Idle threads stay hot, trading power for the next call's latency. |
A call that names no executor runs on the calling thread, single-threaded on RAM and on the handle’s device and default stream on VRAM, and spawns nothing behind the caller’s back.
An Executor owns the workers instead, and one can be shared by several indexes.
Each residency has its own factory:
const hostPool = usearch.Executor.forRam({ threads: 2, memoryDomains: [0, 1] });
const devicePool = usearch.Executor.forVram({ devices: [0, 1], scratchLimit: 1 << 30 });
const diskPool = usearch.Executor.forNvme({ threads: 4, paths: ['/mnt/nvme0', '/mnt/nvme1'] });forRam creates an executor of host threads and spreads them over the named NUMA nodes, absent naming none and spreading them over every node. onPool borrows an existing ForkUnion pool by its BigInt address instead of spawning one. forVram opens one CUDA stream per named ordinal, absent taking one free device, or borrows the streams given as BigInt addresses, one per device.
It takes no thread count because one host thread drives one stream per device, so the device list is what sizes the work rather than a number of workers. forNvme opens one submission ring per worker over the named device or directory paths, absent deriving them.
Each factory’s config holds only what its own C structure takes: memoryDomains for RAM, devices for VRAM, and paths for NVMe.
drain(mood) waits until no call holds the executor, then idles the workers in a usearch.Mood: Parked threads sleep, and Spinning ones stay hot for the next call’s latency.
The collector frees an executor eventually, and free() waits out the call holding it and joins its workers now; a freed executor is refused by every later call.
The receiver chooses where a verb runs and how it completes, and the verb stays the same.
The index runs it on the calling thread, index.on(executor) blocks on an executor, and index.deferred(executor) submits it there and answers a usearch.Deferred:
const matches = index.search(queries, 10, arena); // the calling thread
const onPool = index.on(executor).search(queries, 10, arena); // blocking, holding the executor until it returns
const call = index.deferred(executor).search(queries, 10, arena); // Deferred<Matches>DenseIndexOn and DenseIndexDeferredOn offer the same verbs with the same arguments.
Only the blocking receivers add contains, cursor and searchStream, whose every pull blocks, and only the index itself adds attend.
Constructors address no index yet, so no receiver offers them; the ones the C interface lets fan out take an executor in their config.
A Deferred keeps everything the library reads or writes alive until its call is final: the outcome, the inputs, the outputs and every handle it names.
It is awaitable, implementing the thenable protocol, so await reaches it like any other asynchronous value:
const call = index.deferred(executor).search(queries, 10, arena);
if (call.status === usearch.Status.Deferred) {
// other work
}
const matches = await call; // or: call.wait()await waits on the libuv pool, so the event loop keeps turning, while wait() blocks the calling thread.
Both answer the same cached result, or throw the same failure, however often they are asked, and status reads the outcome without blocking. using call = ... waits for the call before its block ends, and one dropped unfinished is handed back to the collector until it is final, so nothing it borrowed is freed under the library.
Refusals this binding catches before the call crosses, like keys that do not match the rows, throw at submission.
One call holds an executor at a time, so a second call finding it held finishes at once with Status.Contended, and nothing queues.
A deferred call on a caller-exclusive pool runs on the pool’s thread 0, so polling status alone finishes it, and its progress handler and predicate run on the pool’s threads; a single thread, WebAssembly or a VRAM executor runs it at submission.
Calls depending on each other are ordered by waiting on the first before submitting the second.
The arrays a deferred verb is handed must not change until it is final, since the library reads them until then.
MoodWhat an executor does with its threads or streams once it goes idle.enum Mood Parked | "parked" | Idle threads sleep; the next call pays a wake-up. |
|---|---|---|
Spinning | "spinning" | Idle threads stay hot, trading power for the next call's latency. |
ExecutorForRamConfigWhere the host threads of a RAM executor are placed, how many there are, and how many deferred calls it tracks.interface ExecutorForRamConfig memoryDomains? | number[] | NUMA node IDs the workers are spread over; absent spreads them over every node. |
|---|---|---|
threads? | number | Workers to spawn; absent takes 7/8 of the cores the domains carry. |
deferredLimit? | number | Deferred calls tracked at once; absent takes a default. |
cpuClass? | string | The CPU class to dispatch as by its canonical name; absent dispatches as the machine. |
ExecutorOnPoolConfigWhere the borrowed pool of a RAM executor is placed, and how many deferred calls it tracks.interface ExecutorOnPoolConfig memoryDomains? | number[] | NUMA node IDs of the indexes it will drive; absent derives every node. |
|---|---|---|
deferredLimit? | number | Deferred calls tracked at once; absent takes a default. |
cpuClass? | string | The CPU class to dispatch as by its canonical name; absent dispatches as the machine. |
ExecutorForVramConfigWhich devices a VRAM executor drives, and on which streams.interface ExecutorForVramConfig devices? | number[] | CUDA ordinals to drive; absent takes one free device. |
|---|---|---|
streams? | bigint[] | Borrowed CUDA stream addresses, one per device in devices order; absent creates its own. |
scratchLimit? | number | Ceiling on scratch bytes per device; absent lets scratch grow as geometry asks. |
deferredLimit? | number | Deferred calls tracked at once; absent takes a default. |
gpuClass? | string | The GPU class to dispatch as by its canonical name; absent dispatches as the devices. |
ExecutorForNvmeConfigWhere an NVMe executor submits its reads.interface ExecutorForNvmeConfig paths? | string[] | Device or directory paths; absent derives them. |
|---|---|---|
threads? | number | Workers to spawn, one submission ring apiece; absent derives from the devices' queue capabilities. |
deferredLimit? | number | Deferred calls tracked at once; absent takes a default. |
cpuClass? | string | The CPU class to dispatch as by its canonical name; absent dispatches as the machine. |
DenseVerbsThe verbs every receiver of a dense index shares, written once.17abstract class DenseVerbs<Receiving extends "blocking" | "deferred"> The receiver chooses where a verb runs and how it completes, and the verb stays the same: a
DenseIndex runs it on the calling thread, index.on(executor) blocks on an executor, and
index.deferred(executor) submits it there and answers a Deferred.
*× 3Claims room for a total entry count ahead of insertion, a hint, since insertions grow the index on demand.reserve(capacity: number): Answer<Receiving, void> capacitynumber | The desired total capacity, the current size included. |
|---|
*× 3Erases every entry, keeping the reserved capacity and the frozen geometry.clear(): Answer<Receiving, void> *× 3Reports the frozen metadata and the current counters in one call.describe(): Answer<Receiving, DenseIndexDescription> *× 3Audits how much of the graph a search reaches from its landmarks.health(): Answer<Receiving, DenseReachability> *× 3Counts the entries stored under one key.count(key: KeyLike): Answer<Receiving, number> keyKeyLike | The identifier to be enumerated. |
|---|
Will throw an error if the key is spelled in none of the accepted forms.
*× 3Add vectors to the index.insert(
keys: KeysLike,
vectors: RowsView | Vector,
config: DenseInsertConfig = {},
): Answer<Receiving, void> A typed array is one row, and a RowsView is a batch borrowed in place. A single key
broadcasts across every row. A batch is all-or-nothing: a rejected one leaves the index
untouched.
Rows keep whatever type they arrived in and the engine converts them into the index's own
quantization, so the two are independent - a packed u1x8 or a bf16 row travels as
bytes with its RowsView naming what they mean.
keysKeysLike | Input identifiers for every vector. | |
|---|---|---|
vectorsRowsView | Vector | One row, or a run of rows of the index width. | |
configDenseInsertConfig | Per-call breadth and telemetry sink. |
Will throw an error if the length of keys doesn't match the number of vectors or if it's not a single key.
*× 3Re-selects the neighbors of every stored entry against the finished graph.refine(
config: DenseRefineConfig = {},
): Answer<Receiving, DenseRefineReport> A cheap insertion pass followed by one refinement reaches a better graph, in less total time, than a single expensive insertion pass - so a wide breadth belongs here.
configDenseRefineConfig | Per-call breadth and telemetry sink. |
|---|
changed means
settled.*× 3Searches the index for the nearest neighbours of every query row.search(
queries: RowsView | Vector,
wanted: number,
arena: Arena,
config: DenseSearchConfig = {},
): Answer<Receiving, Matches> The matches land in arena and come back as a view into it, so a recall sweep reusing one
arena allocates once.
queriesRowsView | Vector | One query row, or a run of them, at the index's own width. | |
|---|---|---|
wantednumber | Matches per query, the "k" in "kANN". | |
arenaArena | The memory the matches land in, rewound by the call. | |
configDenseSearchConfig | Per-call breadth and telemetry sink. |
arena.Will throw when the queries do not split into whole rows of the index geometry.
*× 3Late-interaction scoring of multi-row queries: every query row takes its best match within a key, and those maxima sum to that key's score.maxsim(
queries: RowsView | Vector,
rowsPerQuery: number,
wanted: number,
arena: Arena,
config: DenseMaxsimConfig = {},
): Answer<Receiving, DenseProducts> The run is over keys rather than rows, which is what an index built with valuesPerKey
above one exists for. Runs on the inner-product kinds alone.
queriesRowsView | Vector | The token rows of every query, end to end. | |
|---|---|---|
rowsPerQuerynumber | Rows in every query; uniform, so padded queries apply as is. | |
wantednumber | Documents per query, not per token row; zero keeps every document a query scored. | |
arenaArena | The memory the scored keys land in, rewound by the call. | |
configDenseMaxsimConfig | Per-call breadth and telemetry sink. |
arena.Will throw when the token rows do not split into whole queries.
*× 3Retrieves the vectors stored under one key, one row per entry, in insertion order.get(
key: KeyLike,
arena: Arena,
config: DenseGetConfig = {},
): Answer<Receiving, DenseVectors> Rows land in arena and come back as a view into it, converted into scalar, shaped by the
width and stride the call reports.
keyKeyLike | The identifier whose entries are exported. | |
|---|---|---|
arenaArena | The memory the rows land in, rewound by the call. | |
configDenseGetConfig | The kind to export as. |
*× 3Multiplies a matrix against the index, sparsely - every pair the walk scored, not only the nearest.multiply(
left: RowsView | Vector,
arena: Arena,
config: DenseMultiplyConfig = {},
): Answer<Receiving, DenseProducts> How many pairs a walk scores is known only once it ends, so the products land in arena and
come back as views into it. The scores are unnormalized logits, so the handle must hold the
inner-product kind.
leftRowsView | Vector | One row, or a run of rows, at the index's own width. | |
|---|---|---|
arenaArena | The memory the pairs and their offsets land in, rewound by the call. | |
configDenseMultiplyConfig | Per-call pair bound, breadth and telemetry sink. |
*× 3Absorbs every donor into this index, one after another, keeping their graphs rather than re-inserting their vectors and linking each in across the appended part.absorb(
donors: DenseIndex[],
): Answer<Receiving, DenseRefineReport> A donor the library consumed refuses every later verb with Status.Uninitialized; one it
did not absorb, like a donor of another scalar refused with Status.ConfigMismatch, stays
a live index the caller still owns, though its entries may already have moved here. Until a
deferred absorb is final, every donor it names refuses every verb, and no other absorb can name it.
donorsDenseIndex[] | The indexes to absorb. |
|---|
*× 3Splits this index into the receivers, the inverse of absorb: graph-local parts keeping their edges, one per receiver, leaving this index empty but live.split(
receivers: DenseIndex[],
config: DenseSplitConfig = {},
): Answer<Receiving, DenseRefineReport> Receivers must be empty and share the index's residency, scalar, class, metric, geometry and
shard count, or the call refuses with Status.ConfigMismatch, moving nothing.
receiversDenseIndex[] | Two or more empty indexes, each receiving one part. | |
|---|---|---|
configDenseSplitConfig | The seed and breadth. |
*× 3Draws keys around config.focus, or across the whole index, each hanging from a shown or earlier draw and placed in three dimensions beside the shown ones.sample(
wanted: number,
arena: Arena,
config: DenseSampleConfig = {},
): Answer<Receiving, DenseDraws> A larger wanted draws a superset, so a view grows one connected sample as it zooms. Only RAM
samples yet.
wantednumber | The upper bound on draws. | |
|---|---|---|
arenaArena | The memory the draws land in, rewound by the call. | |
configDenseSampleConfig | The shown keys and their coordinates, the focus, the seed and the breadth. |
arena.*× 3Saves the index in the layout the path extension picks - one .safegraphs monolith, or a .safegraphs.json manifest beside its shards.save(path: string): Answer<Receiving, void> pathstring | File path to write. |
|---|
Will throw an error if path is not a string.
save*× 3Writes the index into memory as one monolith.saveBuffer(arena: Arena): Answer<Receiving, Monolith> The bytes are what a checkpoint holds, so a service that keeps its indexes in an object
store never touches the filesystem. They land in arena and come back as a view into it.
arenaArena | The memory the monolith lands in, rewound by the call. |
|---|
executorThe executor the verbs run on; absent runs them on the calling thread.readonly executor: Executor | undefined DenseIndexOnThe verbs of one index, blocking on one executor: index.on(executor).5class DenseIndexOn extends DenseVerbs<"blocking"> Every call starts after every earlier submission to that executor, and returns with its outputs written.
DenseIndexOnMade by index.on(executor), never directly.new DenseIndexOn(
compiled: CompiledIndex,
executor: Executor | undefined,
) containsVerifies the presence of one key in the index.contains(key: KeyLike): boolean Binding-only sugar over count.
keyKeyLike | The identifier to be checked for presence in the index. |
|---|
cursorOpens a walk over the current contents, every pull on this receiver's executor.cursor(starts?: KeysLike): DenseCursor Without starts it yields every entry as stored; with them, the start keys' own entries first in order, then the entries those link to breadth-first, each once.
startsKeysLike | The keys to walk from; absent walks every entry. |
|---|
searchStreamOpens a resumable nearest-neighbor walk over one query row, every pull on this receiver's executor.searchStream(
query: RowsView | Vector,
config: DenseSearchStreamInitConfig = {},
): DenseSearchStream queryRowsView | Vector | A single query row of the index geometry. | |
|---|---|---|
configDenseSearchStreamInitConfig | The breadth. |
executorThe executor the verbs run on; absent runs them on the calling thread.DenseIndexDeferredOnThe verbs of one index, submitted to one executor and answered as Deferred calls: index.deferred(executor).2class DenseIndexDeferredOn extends DenseVerbs<"deferred"> Cursors, streams and contains stay blocking, so this receiver offers none of them.
A verb with outputs borrows its arena as the blocking verb does, and waits into the same view.
DenseIndexDeferredOnMade by index.deferred(executor), never directly.new DenseIndexDeferredOn(
compiled: CompiledIndex,
executor: Executor,
) executorThe executor the verbs run on; absent runs them on the calling thread.ExecutorThe workers a fanned-out verb runs on, shared across indexes.6interface Executor extends Disposable An executor owns host threads or device streams; an index owns entries. Keeping them apart is
what lets one pool serve several indexes, and what makes the cost of spawning visible at the
line that spawns it. The collector frees it eventually; free joins the workers now.
forRamSpawns host worker threads for a RAM-resident index.static forRam(config?: ExecutorForRamConfig): Executor configExecutorForRamConfig | The NUMA nodes, the thread count and the deferred limit. |
|---|
onPoolBorrows a ForkUnion fu_pool_t for a RAM-resident index, keeping its lender's mood.static onPool(
pool: bigint,
config?: ExecutorOnPoolConfig,
): Executor poolbigint | The pool's address; it must outlive the executor. | |
|---|---|---|
configExecutorOnPoolConfig | The NUMA nodes and the deferred limit. |
forVramOpens one stream per device for a VRAM-resident index, driven from a single host thread.static forVram(config?: ExecutorForVramConfig): Executor configExecutorForVramConfig | The devices, borrowed streams, scratch ceiling and deferred limit. |
|---|
forNvmeOpens one submission ring per worker for an NVMe-resident index.static forNvme(config?: ExecutorForNvmeConfig): Executor configExecutorForNvmeConfig | The device or directory paths, the thread count and the deferred limit. |
|---|
drainWaits for every call submitted to this executor so far, then idles in mood.drain(mood?: Mood): void moodMood | What the threads or streams do once they go idle. |
|---|
freeRuns every deferred call and joins the workers now; a freed executor refuses every later call, and a second free does nothing.DeferredA call submitted by a deferred receiver, final once its executor finishes it.4class Deferred<T> implements PromiseLike<T>, Disposable It keeps everything the library reads or writes alive until then - the outcome, the inputs,
the outputs and every handle it names - so none of them is collected under the call, and it
borrows its arena as a blocking call does. A result read after the arena was rewound or freed
throws Status.Uninitialized. status polls without
blocking, wait blocks and answers, and await runs that wait on the libuv pool; both answer
the same cached result, or throw the same failure, however often they are asked.
const matches = await call; // or: call.wait()
DeferredMade by a verb of a deferred receiver, never directly.new Deferred(call: CompiledDeferred, answer: (raw: never) => T) waitBlocks the calling thread until the call is final, then answers its result or throws its failure.wait(): T thenWaits on the libuv pool rather than the calling thread, which is what await reaches for.then<Fulfilled = T, Rejected = never>(
onFulfilled?: ((value: T) => Fulfilled | PromiseLike<Fulfilled>) | null,
onRejected?: ((reason: unknown) => Rejected | PromiseLike<Rejected>) | null,
): Promise<Fulfilled | Rejected> statusStatus.Deferred until the call is final, then Status.Success or its failure.