Table of Contents

API

This page documents Orleans.Lattice.Vector, which is unreleased, in the documentation for Orleans.Lattice 9.9.0 (release line 9.9), built 2026-10-04. It is also published as markdown, with every table and list, at api.md, and llms.txt lists every page.

The public surface of Orleans.Lattice.Vector, in the order you meet it.

Namespaces: Orleans.Lattice.Vector (the in-memory core, no Orleans dependency) and Orleans.Lattice.Vector.Persistence (the durable layer).

The in-memory core

VectorIndex

The index itself. Constructed from VectorIndexOptions.

Shape and state

Member Purpose
Dimensions, Metric, Seed The immutable configuration the index was built with.
Count, Capacity Live vectors, and reserved slots.
PartitionCount, Probes The partitioning, and how many partitions a query probes.
Version, PartitionVersion(int) Mutation stamps. The per-partition stamp advances whenever a vector enters or leaves that partition, so a durable layer can persist only what moved.
PartitionSize(int) How many vectors a partition currently holds.
State, IsReady, Status Empty, Building or Ready, and a VectorIndexStatus snapshot.
CentroidsComplete Whether a restore has finished streaming its centroids.

Mutation

Member Purpose
Add(long, ReadOnlySpan<float>) Insert a vector under a key that is not already present.
Upsert(long, ReadOnlySpan<float>) Insert or replace.
Remove(long) Retire a vector. Constant time in the corpus size (one backfill, whatever the corpus holds); never a tombstone.
Contains(long), TryGetVector(long, Span<float>) Presence and retrieval.
Clear() Drop everything.
EnsureCapacity(int) Reserve ahead of a bulk load. On an untrained index the whole reservation goes to its single cell, which makes the insert run allocation-free; on a trained one it is spread evenly over the cells, a hint rather than a guarantee.
Train() Partition the corpus. Synchronous and expensive; keep it off the request path. Returns false, and leaves the index unpartitioned and answering exhaustively, when the corpus is too small to partition usefully (below MinimumTrainingCount, or resolving to fewer than two partitions).

Query

Member Purpose
Search(query, Span<VectorSearchResult>) Top-k into a caller-owned span.
Search(query, results, out VectorSearchMode) The same, plus how the answer was produced. Prefer this overload: it is the per-response honesty signal.
SelectPartitions(query, Span<int>) Which partitions this query would probe, without scoring anything. The durable layer uses it to fetch only the chunks a query needs.

Persistence seam

Member Purpose
CreateSnapshot(int maxItemsPerChunk) Plan a chunked snapshot. Nothing is copied yet.
Restore(VectorIndexHeader, VectorIndexOptions) Static. Create an index shaped by a persisted header.
ApplyChunk(ReadOnlySpan<byte>) Apply one chunk. Order-independent and idempotent.

Value types and enums

Type Purpose
VectorSearchResult(long Key, float Score) One hit.
VectorIndexStatus A snapshot of state, counts, partitioning and BytesPerVector.
VectorIndexSnapshot A snapshot plan: Header, ChunkCount, Describe(i), MeasureChunk(i), WriteChunk(i, Span<byte>).
VectorIndexHeader The 56-byte durable header (Size). Write, Read, and TryRead (which returns false rather than throwing on bytes this build cannot read: too short, the wrong marker, an unsupported version, an out-of-range field, or more centroid chunks than chunks in total).
VectorIndexChunkDescriptor Kind, partition, sequence, item count and byte count for one chunk, without rendering it.
VectorDistanceMetric Cosine or DotProduct.
VectorIndexState Empty, Building, Ready.
VectorSearchMode Exhaustive or Approximate.
VectorIndexChunkKind Centroids or Vectors.
VectorIndexFormat Format constants - HeaderMagic, ChunkMagic, Version (1), HeaderSize (56) and ChunkHeaderSize (24) - and IsSupported.
VectorIndexFormatException Thrown when a persisted form cannot be believed.
VectorSimilarity Dot, Norm, Cosine, Scale, Normalize, vectorised.
VectorIndexMemory Per-slot and total byte accounting: SideBytesPerSlot (12 - one cached float norm and one long key per slot) and Bytes(capacity, dimensions, partitionCount), which counts the vector blocks, their side arrays and the centroid block, and deliberately excludes the key-to-location map and the per-cell array headers. VectorIndexStatus.BytesPerVector is that total divided by the live count.

The durable layer

DurableVectorIndex

Orchestrates persistence and incremental maintenance over a VectorIndex.

There is no public constructor. DurableVectorIndex.OpenAsync(store, source, options, loadMode, cancellationToken) is the normal entry point; loadMode selects a full load or a lazy partial one. A caller that must keep a partially-loaded instance across a fault uses DurableVectorIndex.CreateUnloaded(store, source, options, loadMode) followed by LoadOrResumeAsync(...), which continues the interrupted load rather than restarting it.

Member Purpose
OpenAsync Static. Opens (and, for a full load, restores) an index over a store and a source.
CreateUnloaded Static. Creates an instance without reading durable state, so a caller can retry LoadOrResumeAsync on the same instance after a fault. Not usable until a load completes.
LoadOrResumeAsync Runs the durable load, resuming a previous attempt that faulted partway; a no-op once loaded. The two-token overload bounds only the resumable key-map walk with the first token and the whole load with the second. A full load throws VectorIndexRecordUnavailableException instead of discarding when a record the committed state names was missing from one read of the store but returned by another: the durable index is kept and the same instance is retried after a backoff.
LoadDiscardReason VectorIndexLoadDiscardReason: None unless a full load completed a discard, otherwise UnloadableRecord, CountMismatch, or EmbeddingSpaceChange. Retained on that instance; an explicit rebuild is not a load discard. Lazy readers refuse invalid state without deleting it and leave this value None. Generation differences that normal recovery can adopt are not discards.
LoadDiscardedManifest The committed VectorIndexManifest of the state a full load discarded - generation, vector count, and header (partition count) - or null when nothing was discarded or the discarded state had no decodable manifest. Use it to report what a discard cost, since a discarded index otherwise reads the same as a first-ever build.
LoadedKeyCount, IsLoaded, HasBankedLoadProgress Load observability: identifier mappings loaded so far, whether a load has completed, and whether an interrupted load banked progress.
KeyPrefix, Generation, LoadMode Where the index lives, which partitioning is live, and how it was opened.
Status, Count, UpdatesSinceTraining The core's status, the live vector count, and the drift signal that tells you when to retrain.
Progress A VectorIndexBuildProgress: phase, generation, vectors indexed and expected, partitions persisted and total, whether the state was restored rather than recomputed, the lifetime counts of ingest slices stopped by their wall-clock budget (SlicesDeadlined) and of those that banked nothing (SlicesDeadlinedWithoutProgress), plus EmptyDeadlinesSinceLastAdvance, IsStarvedBySource, IsReady, and IngestedFraction. IsStarvedBySource is the present-tense stall signal: true while at least one slice has been deadlined empty-handed since the build last banked anything, cleared the moment a slice banks an item. IsReady is true only when the build has finished and produced a partitioning. IngestedFraction reports 1 when the build has finished or when the expected count is unknown, deliberately, so a caller never renders a progress bar implying knowledge the index does not have. While the expected count is unknown (VectorsExpected is 0, which a failed source count also leaves), each ingest slice retries the count until it succeeds.
BuildStepAsync Does one bounded slice of build work and returns progress.
RunBuildAsync Loops BuildStepAsync to completion.
UpsertAsync, RemoveAsync Incremental maintenance.
TryGetId, TryGetKey Resolve between an external string identifier and the index's long key.
FlushAsync Persist the partitions whose stamps moved, rewriting only the chunks whose content changed. While a trained generation is part-way through committing, a flush completes that commit instead.
Search Synchronous and allocation-free, into a caller-owned span. Returns the number of hits written and reports the path through an out parameter. Under a lazy load it answers from whatever cells are already resident.
SearchAsync Query, returning a VectorSearchOutcome. Under a lazy load it fetches any cell the query would probe.
ReconcileAsync Bounded sweep against the store of record, always settling in the source's favour.
RebuildAsync, RetrainAsync RebuildAsync discards every durable trace of the index and resets the build to NotStarted, to be driven again from the store of record. RetrainAsync re-partitions the resident corpus after distribution drift - re-reading nothing - and commits the result as a fresh generation, deleting the one it supersedes; calling it again after a failed attempt resumes that commit instead of training again.

The build is a caller-driven pump, not a thread. BuildStepAsync does one bounded slice and returns; RunBuildAsync loops it. This honours the core's single-writer constraint without fencing, lets the host decide when it can afford the work, and makes resumability deterministic.

Two readiness signals, deliberately distinct

Do not conflate these:

  • Progress.Phase == Ready - the build has finished.
  • Status.State == Ready - a usable partitioning exists.

A corpus below the training minimum legitimately finishes its build with no partitioning and answers exactly by exhaustive scan. Reporting that as not-ready would be wrong; reporting it as approximate would also be wrong. Progress.IsReady is the conjunction of the two - the build has finished and a partitioning exists - so it stays false for that small corpus even though Progress.Phase is Ready.

Supporting types

Type Purpose
IVectorIndexStore The narrow async store seam: ReadAsync (one record, null when absent), ReadManyAsync (absent keys are simply missing from the result), WriteAsync, DeleteAsync (an absent key is a no-op), ScanAsync by prefix in ascending ordinal key order (an overload resumes strictly after a key already consumed, with a correct but unoptimised default implementation), and DeletePrefixAsync.
LatticeVectorIndexStore The ILattice adapter. The only type in the package that binds to Orleans. Its prefix scans push a resume point down into the tree scan, and resume a page walk abandoned by a bare Orleans response TimeoutException up to DefaultScanTimeoutResumeAttempts (2) consecutive times without banking a record.
IVectorSource, VectorSourceEntry The store-of-record seam the background build streams from.
VectorKeyDictionary The durable string-to-long identifier mapping. A monotonic allocator, never a hash. LoadAsync adopts the persisted mapping, replacing what is held in memory; a load that faulted partway continues from where it stopped on the next call (HasBankedLoadProgress reports that it will), while a fresh load also discards any buffered records. GetOrAddAsync writes a new identifier's mapping record at once; GetOrAddBufferedAsync assigns the key the same way but buffers the record until FlushPendingAsync writes every buffered record in one store write (PendingWriteCount reports how many are waiting). TryGetKey and TryGetId resolve either direction in memory without touching the store. RemoveAsync drops one identifier's mapping and its record, and ClearAsync forgets every mapping; both also discard any matching record still buffered, so a later FlushPendingAsync cannot write a dropped mapping back, and neither rewinds the counter (NextKey). Count and Ids report the current mapping.
IVectorIndexBuildObserver, VectorIndexBuildSliceTimings The build-timing seam, bound through DurableVectorIndexOptions.BuildObserver. OnSliceCompleted receives a VectorIndexBuildSliceTimings - SourceWait, KeyAssign, IndexUpsert, KeyFlush, and Consumed - so a host can publish them on its own meter; the package itself declares no instruments. It is called once for each ingest slice that returns normally, after that slice has checkpointed and written its build state, including a slice that consumed nothing; a slice that throws, including one whose checkpoint or build-state write fails, reports nothing. SourceWait, KeyAssign and IndexUpsert accumulate across the slice's items, and KeyFlush times its one batched key-map write. The four do not cover the whole slice: the source count taken while the expected count is still unknown, releasing the source enumerator, the ingest checkpoint that then persists the slice's vector chunks and build state, and the loop's own bookkeeping belong to no stage, so the four sum to less than the slice's elapsed time. An implementation must not throw, block, or retain the timings.
VectorIndexBuildPhase NotStarted, Ingesting, Training, Persisting, Ready, in that order during a build. It is not monotonic over the index's life: RebuildAsync (or a failed verification) returns it to NotStarted, and a writer that reopens an index whose build committed its trained generation but had not yet deleted the one it superseded resumes at Persisting, finishing that deletion on its next build step.
VectorIndexBuildState, VectorIndexManifest, VectorIndexPartitionState The durable build checkpoint, the commit record, and per-partition commit state. Each renders itself as a complete checksummed record with ToRecord and decodes one with TryReadRecord, which returns false rather than throwing on a record it cannot verify or act on; the manifest and partition state also report their fixed payload Size and Write that payload into a caller's span. VectorIndexPartitionState.TryReadRecord accepts only the compact form, in which every chunk lives under one epoch; a partition that a flush rewrote in part is stored in an extended form that only the durable index decodes.
VectorIndexStorageKeys, VectorIndexPersistenceFormat, VectorIndexRecord The key layout, the framing constants, and the checksummed record envelope. VectorIndexStorageKeys builds every key and prefix beneath the index's root prefix - the manifest, the build checkpoint, the identifier watermark and mapping, the retirement journal, and the per-generation centroid, partition-state and vector-chunk keys - padding generation and epoch components to CounterWidth (19), partition identifiers to PartitionWidth (5) and chunk sequences to SequenceWidth (8) so ordinal key order is numeric order; TryReadKeyMapId and TryReadRetirementKey parse a mapping or retirement key back. VectorIndexPersistenceFormat carries RecordMagic, Version (1, checked by IsSupported), RecordHeaderSize (24), ManifestPayloadSize and PartitionStatePayloadSize. VectorIndexRecord.Wrap frames a payload (Measure and Seal stamp the envelope over a payload already rendered into its final buffer), and TryUnwrap returns false for a record that is truncated, lacks the marker, declares an unsupported layout version or a length that does not match, or fails its checksum.
VectorIndexLoadMode Full load versus lazy partial load.
VectorIndexRecordUnavailableException Thrown by a full load when a record the committed state names was missing from one read of the store but returned by another. Nothing is discarded; retry LoadOrResumeAsync on the same instance after a backoff.
VectorSearchOutcome How many hits a search wrote into the caller's buffer, plus the mode that produced them.
DurableVectorIndexOptions Durable-layer configuration; see Configuration.

Identifier mapping

Index keys are long; most consumers have string identifiers. VectorKeyDictionary owns that mapping and is not a hash at any width, because a collision returns the wrong record silently and undiagnosably. It is a durable monotonic allocator: the watermark is made durable before any identifier in a block is handed out, so a crash burns the remainder of a block rather than reissuing. Keys are never recycled, and a rebuild does not rewind the counter.

The background build does not issue a store write per identifier. It assigns keys through the buffered path and writes each slice's mapping records in one batch before that slice's checkpoint, so the mapping may run ahead of the committed cells but never behind them: a committed cell whose key had no durable identifier would be unresolvable on the next load. A failed batch keeps its records for the next attempt, and because keys are never recycled a retry rewrites the same identifier to the same key. Only the per-identifier records are batched; the watermark is still written before any identifier in a new block is handed out. UpsertAsync of an identifier the index has not seen writes its mapping record immediately.

Only the forward direction is persisted; the reverse map is rebuilt in memory from the same scan, so resolving a result costs no round trip and no allocation.