JavaScript · Executors

Executors

import * as usearch from 'usearch';import { DenseVerbs } from 'usearch';import { DenseIndexOn } from 'usearch';import { DenseIndexDeferredOn } from 'usearch';import { Executor } from 'usearch';import { Deferred } from 'usearch'; 3.0.0-rc.1 · 4c3e5023

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.

§ Receivers & Deferred Calls

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.

API

Types

MoodWhat an executor does with its threads or streams once it goes idle.
usearch.ts:73 ↗
enum Mood

Values

Parked&quot;parked&quot;Idle threads sleep; the next call pays a wake-up.
Spinning&quot;spinning&quot;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.
usearch.ts:370 ↗
interface ExecutorForRamConfig

Fields

memoryDomains?number[]NUMA node IDs the workers are spread over; absent spreads them over every node.
threads?numberWorkers to spawn; absent takes 7/8 of the cores the domains carry.
deferredLimit?numberDeferred calls tracked at once; absent takes a default.
cpuClass?stringThe 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.
usearch.ts:382 ↗
interface ExecutorOnPoolConfig

Fields

memoryDomains?number[]NUMA node IDs of the indexes it will drive; absent derives every node.
deferredLimit?numberDeferred calls tracked at once; absent takes a default.
cpuClass?stringThe CPU class to dispatch as by its canonical name; absent dispatches as the machine.
ExecutorForVramConfigWhich devices a VRAM executor drives, and on which streams.
usearch.ts:392 ↗
interface ExecutorForVramConfig

Fields

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?numberCeiling on scratch bytes per device; absent lets scratch grow as geometry asks.
deferredLimit?numberDeferred calls tracked at once; absent takes a default.
gpuClass?stringThe GPU class to dispatch as by its canonical name; absent dispatches as the devices.
ExecutorForNvmeConfigWhere an NVMe executor submits its reads.
usearch.ts:406 ↗
interface ExecutorForNvmeConfig

Fields

paths?string[]Device or directory paths; absent derives them.
threads?numberWorkers to spawn, one submission ring apiece; absent derives from the devices' queue capabilities.
deferredLimit?numberDeferred calls tracked at once; absent takes a default.
cpuClass?stringThe CPU class to dispatch as by its canonical name; absent dispatches as the machine.
ClassDenseVerbsThe verbs every receiver of a dense index shares, written once.17
dense_index.ts:633 ↗
abstract 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.
dense_index.ts:655 ↗
reserve(capacity: number): Answer<Receiving, void>

Parameters

capacitynumberThe desired total capacity, the current size included.
*× 3Erases every entry, keeping the reserved capacity and the frozen geometry.
dense_index.ts:663 ↗
clear(): Answer<Receiving, void>
*× 3Reports the frozen metadata and the current counters in one call.
dense_index.ts:674 ↗
describe(): Answer<Receiving, DenseIndexDescription>

Returns

The geometry, the size, the capacity, the memory and the kernels in use.
*× 3Audits how much of the graph a search reaches from its landmarks.
dense_index.ts:688 ↗
health(): Answer<Receiving, DenseReachability>

Returns

The population audited, and how much of it is reachable.
*× 3Counts the entries stored under one key.
dense_index.ts:702 ↗
count(key: KeyLike): Answer<Receiving, number>

Parameters

keyKeyLikeThe identifier to be enumerated.

Returns

How many entries the key holds.

Throws

Will throw an error if the key is spelled in none of the accepted forms.

*× 3Add vectors to the index.
dense_index.ts:727 ↗
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.

Parameters

keysKeysLikeInput identifiers for every vector.
vectorsRowsView | VectorOne row, or a run of rows of the index width.
configDenseInsertConfigPer-call breadth and telemetry sink.

Throws

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.
dense_index.ts:747 ↗
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.

Parameters

configDenseRefineConfigPer-call breadth and telemetry sink.

Returns

The entries re-selected and changed, and the islands healed; zero changed means settled.
*× 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.
dense_index.ts:805 ↗
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.

Parameters

queriesRowsView | VectorThe token rows of every query, end to end.
rowsPerQuerynumberRows in every query; uniform, so padded queries apply as is.
wantednumberDocuments per query, not per token row; zero keeps every document a query scored.
arenaArenaThe memory the scored keys land in, rewound by the call.
configDenseMaxsimConfigPer-call breadth and telemetry sink.

Returns

The scored keys, one run per query, largest first, viewed inside arena.

Throws

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.
dense_index.ts:842 ↗
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.

Parameters

keyKeyLikeThe identifier whose entries are exported.
arenaArenaThe memory the rows land in, rewound by the call.
configDenseGetConfigThe kind to export as.

Returns

The rows, laid end to end; empty when the key is absent.
*× 3Multiplies a matrix against the index, sparsely - every pair the walk scored, not only the nearest.
dense_index.ts:864 ↗
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.

Parameters

leftRowsView | VectorOne row, or a run of rows, at the index's own width.
arenaArenaThe memory the pairs and their offsets land in, rewound by the call.
configDenseMultiplyConfigPer-call pair bound, breadth and telemetry sink.

Returns

The pairs every left row's walk scored.
*× 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.
dense_index.ts:893 ↗
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.

Parameters

donorsDenseIndex[]The indexes to absorb.

Returns

What the closing refine did to the graph.
*× 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.
dense_index.ts:912 ↗
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.

Parameters

receiversDenseIndex[]Two or more empty indexes, each receiving one part.
configDenseSplitConfigThe seed and breadth.

Returns

What the receivers' closing refines did to their graphs.
*× 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.
dense_index.ts:932 ↗
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.

Parameters

wantednumberThe upper bound on draws.
arenaArenaThe memory the draws land in, rewound by the call.
configDenseSampleConfigThe shown keys and their coordinates, the focus, the seed and the breadth.

Returns

The draws, viewed inside arena.
*× 3Saves the index in the layout the path extension picks - one .safegraphs monolith, or a .safegraphs.json manifest beside its shards.
dense_index.ts:963 ↗
save(path: string): Answer<Receiving, void>

Parameters

pathstringFile path to write.

Throws

Will throw an error if path is not a string.

save*× 3Writes the index into memory as one monolith.
dense_index.ts:979 ↗
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.

Parameters

arenaArenaThe memory the monolith lands in, rewound by the call.

Returns

One serialized monolith.
executorThe executor the verbs run on; absent runs them on the calling thread.
dense_index.ts:637 ↗
readonly executor: Executor | undefined
ClassDenseIndexOnThe verbs of one index, blocking on one executor: index.on(executor).5
dense_index.ts:991 ↗
class 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.
dense_index.ts:993 ↗
new DenseIndexOn(
    compiled: CompiledIndex,
    executor: Executor | undefined,
)
containsVerifies the presence of one key in the index.
dense_index.ts:1007 ↗
contains(key: KeyLike): boolean

Binding-only sugar over count.

Parameters

keyKeyLikeThe identifier to be checked for presence in the index.

Returns

Whether the key is present.
cursorOpens a walk over the current contents, every pull on this receiver's executor.
dense_index.ts:1018 ↗
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.

Parameters

startsKeysLikeThe keys to walk from; absent walks every entry.

Returns

The walk, which should be freed once pulling stops.
searchStreamOpens a resumable nearest-neighbor walk over one query row, every pull on this receiver's executor.
dense_index.ts:1032 ↗
searchStream(
    query: RowsView | Vector,
    config: DenseSearchStreamInitConfig = {},
): DenseSearchStream

Parameters

queryRowsView | VectorA single query row of the index geometry.
configDenseSearchStreamInitConfigThe breadth.

Returns

The stream, whose walk state lives until it is freed.
executorThe executor the verbs run on; absent runs them on the calling thread.
dense_index.ts:637 ↗

Inherited from DenseVerbs

readonly executor: Executor | undefined
ClassDenseIndexDeferredOnThe verbs of one index, submitted to one executor and answered as Deferred calls: index.deferred(executor).2
dense_index.ts:1044 ↗
class 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.
dense_index.ts:1046 ↗
new DenseIndexDeferredOn(
    compiled: CompiledIndex,
    executor: Executor,
)
executorThe executor the verbs run on; absent runs them on the calling thread.
dense_index.ts:637 ↗

Inherited from DenseVerbs

readonly executor: Executor | undefined
InterfaceExecutorThe workers a fanned-out verb runs on, shared across indexes.6
types.ts:424 ↗
interface 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.
types.ts:448 ↗
static forRam(config?: ExecutorForRamConfig): Executor

Parameters

configExecutorForRamConfigThe NUMA nodes, the thread count and the deferred limit.

Returns

The pool, which must outlive every call it runs.
onPoolBorrows a ForkUnion fu_pool_t for a RAM-resident index, keeping its lender's mood.
types.ts:457 ↗
static onPool(
    pool: bigint,
    config?: ExecutorOnPoolConfig,
): Executor

Parameters

poolbigintThe pool's address; it must outlive the executor.
configExecutorOnPoolConfigThe NUMA nodes and the deferred limit.

Returns

The executor over the borrowed pool.
forVramOpens one stream per device for a VRAM-resident index, driven from a single host thread.
types.ts:465 ↗
static forVram(config?: ExecutorForVramConfig): Executor

Parameters

configExecutorForVramConfigThe devices, borrowed streams, scratch ceiling and deferred limit.

Returns

The streams, which must outlive every call they run.
forNvmeOpens one submission ring per worker for an NVMe-resident index.
types.ts:473 ↗
static forNvme(config?: ExecutorForNvmeConfig): Executor

Parameters

configExecutorForNvmeConfigThe device or directory paths, the thread count and the deferred limit.

Returns

The pool, which must outlive every call it runs.
drainWaits for every call submitted to this executor so far, then idles in mood.
types.ts:430 ↗
drain(mood?: Mood): void

Parameters

moodMoodWhat 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.
types.ts:437 ↗
free(): void

Refuses with Status.Contended while a GPU arena made on it is live.

ClassDeferredA call submitted by a deferred receiver, final once its executor finishes it.4
types.ts:645 ↗
class 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.
types.ts:652 ↗
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.
types.ts:667 ↗
wait(): T
thenWaits on the libuv pool rather than the calling thread, which is what await reaches for.
types.ts:676 ↗
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.
types.ts:659 ↗
readonly status: Status

Reads the outcome without blocking.