One executor serves any number of indexes, and its kind must match the residency their handles were built for.
There is a constructor per C one, each taking a config of exactly what its C structure takes:
Zero threads takes 7/8 of the cores, since full occupancy starves the ordered publication path; forVram takes no thread count at all, driving one stream per device from the calling thread, either its own or one of the CUDA streams you lend. drain(mood) waits until no call holds the executor, then lets the threads go .parked or stay .spinning once idle.
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, refused while another call holds it, and index.deferred(executor, &call) submits it there into a usearch.Deferred you hold:
const here = try index.search(queries, 10, &arena, .{}); // the calling threadconst there = try index.on(executor).search(queries, 10, &other, .{}); // blocking, on the executorvar call: usearch.Deferred(usearch.DenseSearch) = undefined;try index.deferred(executor, &call).search(queries, 10, &spare, .{}); // submitted, into `call`defer call.deinit();
DenseIndex.On and DenseIndex.DeferredOn(Call) offer every verb the index does, with the same arguments, the *Arena included, because one generic writes each verb once for both; a deferred receiver compiles only the verb its storage holds.
Constructors address no index yet, so no receiver offers them; the ones the C interface lets fan out take an executor in their config, and run on the calling thread without one.
A Deferred owns what the library reports into until its call is final, the outcome and any fixed-size result, and borrows the rest, as a blocking call does.
The handles it names, such as the index, the executor, the arena, a cursor or a stream, need nothing more: freeing one waits for every deferred call naming it.
The plain memory it reads, such as the rows, the keys, the path, the carry and the handlers’ contexts, stays yours, and must outlive the call.
var call: usearch.Deferred(usearch.DenseGet) = undefined;try index.deferred(executor, &call).get(key, .f32, &arena);defer call.deinit();if (call.status() == .deferred) { // other work}const vectors = try call.wait(); // the view a blocking `get` answers, valid until the next call given `arena`
The storage is yours to place, so submitting allocates nothing: a stack slot, a field, or an array of them for a batch of submitted calls.
The library reports into the storage, so a submitted Deferred stays put until deinit; a heap slot comes from allocator.create and is destroyed after deinit.
Zig has no async keyword left to lean on, so a deferred call is waited on and nothing else: status reads the outcome with acquire ordering and never blocks, and wait blocks in the library until the call is final and answers its result or error, the same one however often it is asked. deinit waits the same way, and the slot can then hold another call.
A Deferred has one owner: wait and deinit never run on two threads at once, while status reads from any. message answers the static detail of the final status, empty until the call is final and on success.
One call holds an executor at a time, so a second call finding it held finishes at once with Error.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.
A shape only the binding can check, like a key run longer than its rows, is refused before anything is submitted, so the submitting call itself fails and the slot holds nothing to deinit.
API
Types
ExecutorForRamConfigOptional settings of Executor.forRam, mirroring usearch_executor_for_ram_t.
pub const ExecutorForRamConfig = struct
Fields
memory_domains
[]const i32
The memory domains of the indexes it will drive; empty derives every node the process may use.
threads
usize
Threads to spawn; zero takes 7/8 of the cores those domains carry.
deferred_limit
usize
Deferred calls it tracks at once, sizing its deferred table; zero takes a default.
StructExecutorThe host threads or device streams that advance a call, and the traversal scratch each of them walks with - everything a verb needs to run but an index does not own.6
One executor serves any number of index handles, and its kind is fixed by the constructor that
produced it: it must match the residency of every handle it drives, or the call reports
Error.Device. A verb called on the index itself takes no executor and runs on the calling thread.
Calls on one executor run in submission order; handles stay freely concurrent - it is the
executor that orders, never the index. An executor without threads of its own holds a deferred
call until its wait, its next drain or blocking call, or the free of a handle it names.
The residency of the constructor that produced it, which places the arenas it makes.
forRamSpawns host threads for RAM-resident indexes across the domains local to memory_domains, which should be the very slice the indexes were placed with.
Zero threads takes 7/8 of the cores those domains carry, because full occupancy starves
the ordered publication path. Calls usearch_executor_for_ram.
onPoolCreates a RAM executor over a borrowed ForkUnion fu_pool_t, so several libraries in one process share one set of threads; the pool must outlive the executor, and keeps its lender's mood.
forVramOpens one submission stream on every named device, or borrows streams, one CUDA stream per device in devices order, all driven by whichever host thread submits.
Reports only what befell the executor itself.
Calls usearch_executor_drain.
deinitDrains every call, releases the scratch and the streams it owns, and frees the executor, which is invalid afterwards; the host arenas it made keep their memory, while its GPU arenas must be freed first.
StructDeferredA submitted call, held in storage the caller places - on the stack, in a struct, or from an allocator - which owns its outcome until the call is final, while the arena, the handles and the slices it was handed stay borrowed until then.5
status polls without blocking, wait blocks until the call is final and answers its result or
error - the same one however often it is asked - and deinit waits the same way. A submitted
Deferred stays put until deinit, and has one owner: wait and deinit never run on two
threads at once, while status reads from any.