Swift · Metrics

Metrics

import USearch 3.0.0-rc.1 · 4c3e5023

§ Dense Indexes

A DenseIndex holds equidimensional vectors under keys, and its verbs below take every receiver above.

API

Types

DistanceA score between two entries, in the metric of the index that produced it.
Types.swift:141 ↗
typealias Distance = Double
StructDenseMetricA distance kind supported out of the box, with SIMD kernels dispatched by the machine's capabilities.8
DenseIndex.swift:25 ↗
struct DenseMetric

All of them derive from a single inner-product kernel with pre-computed magnitudes; hamming and jaccard are bit-level, over the packed sub-byte kinds.

Arbitrary metrics belong to the baseline tier, which forfeits the SIMD kernel chains and the magnitude cache both.

Conforms to

Hashable, Sendable, CustomStringConvertible, Equatable
unknownNo kind named - an unset field, refused by every constructor.
DenseIndex.swift:30 ↗
static let unknown: DenseMetric
innerThe inner product.
DenseIndex.swift:32 ↗
static let inner: DenseMetric
angularThe cosine distance.
DenseIndex.swift:34 ↗
static let angular: DenseMetric
euclideanThe L2 distance.
DenseIndex.swift:36 ↗
static let euclidean: DenseMetric
hammingThe count of differing bits.
DenseIndex.swift:38 ↗
static let hamming: DenseMetric
jaccardThe bit-level jaccard distance.
DenseIndex.swift:40 ↗
static let jaccard: DenseMetric
descriptionThe metric rendered as the library spells it.
DenseIndex.swift:43 ↗
var description: String { get }
initReads a spelling back, and nil for one no metric carries.
DenseIndex.swift:46 ↗
init?(name: String)
StructDenseOptionsThe construction options of a dense index, as initRam and initVram freeze them.8
DenseIndex.swift:58 ↗
struct DenseOptions

Stored vectors always round up to the SIMD step quantum, so they stay cache-line aligned and every kernel runs tail-free; the arena is slightly larger for it, and that is not a knob.

Conforms to

Sendable
dimensionsThe width of the indexed vectors; zero leaves the geometry unfrozen, so the index admits no entries.
DenseIndex.swift:61 ↗
var dimensions: Int
metricThe distance kind used between vectors.
DenseIndex.swift:63 ↗
var metric: DenseMetric
quantizationThe scalar type vectors are stored and quantized into.
DenseIndex.swift:65 ↗
var quantization: Scalar
connectivityThe bound on connections per node in the graph; zero derives.
DenseIndex.swift:67 ↗
var connectivity: Int
valuesPerKeyThe bound on entries sharing one key; zero is unbounded.
DenseIndex.swift:69 ↗
var valuesPerKey: Int
cpuClassThe class a host index dispatches as; nil derives the CPU's own.
DenseIndex.swift:71 ↗
var cpuClass: CPUClass?
gpuClassThe class a device index dispatches as; nil derives the devices' own.
DenseIndex.swift:73 ↗
var gpuClass: GPUClass?
initAn angular bf16 index, with every optional field derived.
DenseIndex.swift:76 ↗
init(dimensions: Int = 0, metric: DenseMetric = .angular, quantization: Scalar = .bf16, connectivity: Int = 0, valuesPerKey: Int = 0, cpuClass: CPUClass? = nil, gpuClass: GPUClass? = nil)
ClassDenseIndexA Vector Search Index over equidimensional dense representations.32
DenseIndex.swift:868 ↗
final class DenseIndex

A handle may be shared across threads, so no method here takes a lock. On a RAM handle, insertions and searches overlap freely: searches read only entries published behind the visibility watermark and never write to the graph, while insertions synchronize through per-node locks and publish in order. Serialization is the one exception, demanding a mutation-free window, though concurrent readers keep searching throughout.

Every verb called on the index runs on the calling thread and blocks until its outcome is final; on(_:) runs the same verbs on an executor, and deferred(_:) submits them there.

Conforms to

Sendable
initRamInitializes an empty, mutable index in host memory, one shard per memory domain; an empty list derives every node the process may allocate on.
DenseIndex.swift:887 ↗
static func initRam(
    _ options: DenseOptions,
    memoryDomains: [Int32] = []
) throws(USearchError) -> DenseIndex
initVramInitializes an empty, mutable index in device memory, sharded across the named devices and never replicated; an empty list derives one free device.
DenseIndex.swift:917 ↗
static func initVram(
    _ options: DenseOptions,
    devices: [Int32] = []
) throws(USearchError) -> DenseIndex
loadRamCopies a checkpoint into a new, mutable index in host memory.
DenseIndex.swift:946 ↗
static func loadRam(
    _ path: String,
    config: DenseLoadRamConfig = DenseLoadRamConfig()
) throws(USearchError) -> DenseIndex

The geometry and metric come from the checkpoint's own headers, and part p lands whole on shard p modulo the shard count, wherever it was written from.

A cancelled or failed load creates no index.

loadVramCopies a checkpoint into a new, mutable index in device memory, with the placement rules of loadRam.
DenseIndex.swift:980 ↗
static func loadVram(
    _ path: String,
    config: DenseLoadVramConfig = DenseLoadVramConfig()
) throws(USearchError) -> DenseIndex
loadRamBufferCopies a monolith from a buffer into a new index in host memory, as loadRam does a path.
DenseIndex.swift:1011 ↗
static func loadRamBuffer(
    _ buffer: RawSpan,
    config: DenseLoadRamBufferConfig = DenseLoadRamBufferConfig()
) throws(USearchError) -> DenseIndex
loadVramBufferCopies a monolith from a buffer into a new index in device memory, as loadVram does a path.
DenseIndex.swift:1045 ↗
static func loadVramBuffer(
    _ buffer: RawSpan,
    config: DenseLoadVramBufferConfig = DenseLoadVramBufferConfig()
) throws(USearchError) -> DenseIndex
viewRamServes a checkpoint in place through a memory mapping, as a read-only index whose mutating verbs report readOnly; the files must stay unmodified while it lives.
DenseIndex.swift:1077 ↗
static func viewRam(_ path: String) throws(USearchError) -> DenseIndex
viewNvmeServes a checkpoint in place from NVMe drives, read through the executor's submission rings, as a read-only index.
DenseIndex.swift:1089 ↗
static func viewNvme(
    _ path: String,
    config: DenseViewNvmeConfig = DenseViewNvmeConfig()
) throws(USearchError) -> DenseIndex

Until NVMe checkpoints carry keys, it reports unsupported.

metadataReadReads a checkpoint's geometry and metric without loading it - from the manifest's summary when the path names one, so no shard is opened.
DenseIndex.swift:1114 ↗
static func metadataRead(
    _ path: String,
    executor: Executor? = nil
) throws(USearchError) -> DenseMetadata
metadataReadBufferReads geometry and metric metadata from a serialized monolith buffer.
DenseIndex.swift:1131 ↗
static func metadataReadBuffer(
    _ buffer: RawSpan,
    executor: Executor? = nil
) throws(USearchError) -> DenseMetadata
onThe same verbs, blocking on executor after every call submitted there before them.
DenseIndex.swift:1161 ↗
func on(_ executor: Executor) -> DenseIndexOn

Binding-only: names the executor every call passes.

deferredThe same verbs, submitted to executor and answered at once as a Deferred.
DenseIndex.swift:1165 ↗
func deferred(_ executor: Executor) -> DenseIndexDeferredOn

Binding-only: names the executor and the deferred completion every call passes.

reserveReserves capacity for a total number of entries, the current size included.
DenseIndex.swift:1173 ↗
func reserve(_ capacity: Int) throws(USearchError)
clearErases all entries, keeping the reserved capacity and the frozen geometry.
DenseIndex.swift:1176 ↗
func clear() throws(USearchError)
describeReports the frozen metadata and the current counters in one call.
DenseIndex.swift:1179 ↗
func describe() throws(USearchError) -> DenseIndexDescription
countThe number of entries stored under a key; zero means the index holds none.
DenseIndex.swift:1182 ↗
func count(key: Key) throws(USearchError) -> Int
containsWhether the index holds at least one entry under a key.
DenseIndex.swift:1185 ↗
func contains(key: Key) throws(USearchError) -> Bool

Binding-only: calls count.

getRetrieves every vector stored under a key, one row per entry sharing the key, in insertion order, converted to Value; an absent key yields no rows.
DenseIndex.swift:1190 ↗
func get<Value>(
    key: Key,
    as value: Value.Type,
    arena: Arena
) throws(USearchError) -> DenseVectors<Value> where Value : ScalarValue

The rows land in arena, read in place by the result until the arena's next call.

insertInserts rows of vectors under their keys - the native batch operation, where a single vector is a one-row matrix.
DenseIndex.swift:1206 ↗
func insert(
    keys: [Key],
    vectors: RowsView,
    config: DenseInsertConfig = DenseInsertConfig()
) throws(USearchError)

Overlaps safely with concurrent searches on the same handle.

The input scalar type is independent of the stored quantization; the library converts on ingest. A batch is all-or-nothing, and its entries become visible at once: every fallible step runs before the first entry identity is claimed, so a thrown failure leaves the index untouched. Every entry is claimed before the handler's first report, so it reports but cannot cancel.

refineRe-selects the neighbors of every stored entry against the finished graph, reporting the entries re-selected and changed and the islands healed; zero changed means settled.
DenseIndex.swift:1221 ↗
@discardableResult func refine(
    config: DenseRefineConfig = DenseRefineConfig()
) throws(USearchError) -> DenseRefineReport

A build step rather than a repair: a cheap insertion pass followed by one refinement reaches a better graph, in less total time, than one expensive insertion pass - so a wide expansion factor belongs here rather than in insert. Searches may run throughout.

healthAudits how much of the graph a search can reach from its landmarks; entries it misses stay unfound until a refinement re-links them.
DenseIndex.swift:1227 ↗
func health() throws(USearchError) -> DenseReachability
maxsimLate-interaction scoring of multi-row queries against multi-vector documents: every query row takes its best match within a key, and those maxima sum to that key's score.
DenseIndex.swift:1254 ↗
func maxsim(
    _ queries: RowsView,
    rowsPerQuery: Int,
    wanted: Int,
    arena: Arena,
    config: DenseMaxsimConfig = DenseMaxsimConfig()
) throws(USearchError) -> DenseProducts

A document is one key holding its token rows, which is what a valuesPerKey above one exists for. rowsPerQuery is the token count each query occupies, queries holds them end to end, and up to wanted documents land in arena as one ragged run, one run per whole query rather than per token, largest score first; a zero wanted keeps every document the query scored. Approximate downward only: a token whose neighborhood the walk never entered contributes no maximum, so a key can score below its exact value but never above it. Runs on an inner-product index alone.

See https://arxiv.org/abs/2004.12832 - ColBERT, which introduced this scoring.

multiplyThe approximate sparse product of rows against the indexed vectors - the linear-algebra sibling of search, reporting every pair the traversal scored rather than only the nearest of them.
DenseIndex.swift:1272 ↗
func multiply(
    _ left: RowsView,
    arena: Arena,
    config: DenseMultiplyConfig = DenseMultiplyConfig()
) throws(USearchError) -> DenseProducts

Pairs arrive in traversal order rather than sorted, and carry no duplicates. A non-zero wanted keeps the first pairs each row's walk scored, and required says how many there were. The keys, products and row offsets land in arena as one ragged run. Runs on an inner-product index alone, since a product needs logits rather than distances.

attendSoftmax attention of a run of query heads over every indexed key their graph walks score, folded as Flash Attention folds key tiles, with no top-k cut.
DenseIndex.swift:1294 ↗
func attend<Value>(
    _ queries: RowsView,
    lookup: ValueLookup,
    as value: Value.Type,
    arena: Arena,
    config: DenseAttendConfig = DenseAttendConfig()
) throws(USearchError) -> DenseVectors<Value> where Value : ScalarValue

Row t · headCount + h of queries, outputs and carry is head h of token t: the headCount query heads of grouped-query attention share the index's one key-value head and one walk per token. The index stores no values, so lookup resolves the keys of every scored batch to rows of its values.

The outputs land in arena as Value rows, and come back divided by the partition. Passing a carry asks for the softmax state, one entry per query row, which folds this branch into another over the same queries by partial-softmax rescaling. Runs on an inner-product index alone, and refuses a VRAM one with device, whose walk needs a device function as its lookup.

See https://arxiv.org/abs/2205.14135 - Flash Attention, whose recurrence this reuses.

absorbAbsorbs every donor into this index, keeping their graphs rather than re-inserting their vectors.
DenseIndex.swift:1310 ↗
@discardableResult func absorb(
    _ donors: [DenseIndex],
    config: DenseAbsorbConfig = DenseAbsorbConfig()
) throws(USearchError) -> DenseRefineReport

Every donor absorbed is consumed, and every later verb on one reports uninitialized; an absorb stopped early hands the rest back to their indexes once final. A donor named twice refuses with invalidArgument, and the receiver itself with configMismatch, as the library does.

splitSplits this index into receivers, the inverse of absorb: graph-local parts keeping their edges, one per receiver, leaving this index empty but live.
DenseIndex.swift:1322 ↗
@discardableResult func split(
    into receivers: [DenseIndex],
    config: DenseSplitConfig = DenseSplitConfig()
) throws(USearchError) -> DenseRefineReport

Receivers must be empty and share the index's residency, scalar, class, metric, geometry and shard count, or the call answers configMismatch, and a refusal moves nothing.

sampleDraws up to wanted 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, written in arena; a larger wanted draws a superset.
DenseIndex.swift:1332 ↗
func sample(
    wanted: Int,
    arena: Arena,
    config: DenseSampleConfig = DenseSampleConfig()
) throws(USearchError) -> DenseDraws

Only RAM samples yet.

saveWrites the index under a path, in the layout its extension picks - one .safegraphs monolith, or a .safegraphs.json manifest beside its shards.
DenseIndex.swift:1343 ↗
func save(path: String, config: DenseSaveConfig = DenseSaveConfig()) throws(USearchError)

Demands a mutation-free window; concurrent readers keep searching throughout.

saveBufferWrites the index as one monolith, the same bytes save(path:) writes to a .safegraphs path, into arena.
DenseIndex.swift:1349 ↗
func saveBuffer(
    arena: Arena,
    config: DenseSaveBufferConfig = DenseSaveBufferConfig()
) throws(USearchError) -> Monolith
cursorOpens a walk over the current contents, for export or migration.
DenseIndex.swift:1360 ↗
func cursor(starts: [Key] = []) throws(USearchError) -> DenseCursor

Each shard's publication watermark is read as the walk enters it, so an entry once passed is never revisited. 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.

searchStreamOpens a resumable nearest-neighbor stream over one query row, so a caller that post-filters aggressively pulls until satisfied rather than guessing a wanted count upfront.
DenseIndex.swift:1364 ↗
func searchStream(
    _ query: RowsView,
    config: DenseSearchStreamInitConfig = DenseSearchStreamInitConfig()
) throws(USearchError) -> DenseSearchStream