vectorindex

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 4, 2026 License: Apache-2.0 Imports: 37 Imported by: 0

Documentation

Overview

Package vectorindex implements the record layer's vector indexes: Java's VECTOR index (VectorIndexMaintainer) with its HNSW and GuardiANN engines, and Go's SPFresh index (RFC-094).

Importing the package registers their index maintainer factories with package recordlayer, as Java's service loader finds VectorIndexMaintainerFactory. A binary that opens a store holding a vector index must link it:

import _ "fdb.dev/pkg/recordlayer/vectorindex"

Without it, using such an index fails with recordlayer.IndexMaintainerNotLinkedError.

Index

Constants

View Source
const (
	UnsplittableNoUsablePartition = "no usable partition under the rule"
	UnsplittablePeelNotAdmitted   = "a peel the rule does not admit"
)

Causes a ClusterUnsplittableError records.

Variables

View Source
var (
	CountVectorVectorReads                      = recordlayer.Event{Name: "vector_vector_reads", Title: "vector references read", Kind: recordlayer.KindCount}
	CountVectorTaskEnqueued                     = recordlayer.Event{Name: "vector_task_enqueued", Title: "vector maintenance tasks enqueued", Kind: recordlayer.KindCount}
	CountVectorTaskExecuted                     = recordlayer.Event{Name: "vector_task_executed", Title: "vector maintenance tasks executed", Kind: recordlayer.KindCount}
	CountVectorIndexDisabledOnNegativeTaskCount = recordlayer.Event{Name: "vector_index_disabled_on_negative_task_count", Title: "vector indexes disabled on negative task count", Kind: recordlayer.KindCount}
)

The vector counters Java 4.14 adds to FDBStoreTimer.Counts: references GuardiANN reads (OnRead.onVectorRead), deferred tasks it enqueues and executes (OnWrite), and indexes disabled because a task count went negative (VectorIndexMaintainer.disableIndexOnNegativeTaskCount).

View Source
var (
	CountVectorNodeReads       = recordlayer.Event{Name: "vector_node_reads", Title: "intermediate nodes read", Kind: recordlayer.KindCount}
	CountVectorNodeReadBytes   = recordlayer.Event{Name: "vector_node_read_bytes", Title: "intermediate node bytes read", Kind: recordlayer.KindSize}
	CountVectorNode0Reads      = recordlayer.Event{Name: "vector_node0_reads", Title: "intermediate nodes read", Kind: recordlayer.KindCount}
	CountVectorNode0ReadBytes  = recordlayer.Event{Name: "vector_node0_read_bytes", Title: "intermediate node bytes read", Kind: recordlayer.KindSize}
	CountVectorNodeWrites      = recordlayer.Event{Name: "vector_node_writes", Title: "intermediate nodes written", Kind: recordlayer.KindCount}
	CountVectorNodeWriteBytes  = recordlayer.Event{Name: "vector_node_write_bytes", Title: "intermediate node bytes written", Kind: recordlayer.KindSize}
	CountVectorNode0Writes     = recordlayer.Event{Name: "vector_node0_writes", Title: "intermediate nodes written", Kind: recordlayer.KindCount}
	CountVectorNode0WriteBytes = recordlayer.Event{Name: "vector_node0_write_bytes", Title: "intermediate node bytes written", Kind: recordlayer.KindSize}
)

The HNSW node counters of FDBStoreTimer.Counts, which Java's HnswVectorIndexEngine OnRead / OnWrite listeners record: nodes read and written, and their key-value bytes, split by layer 0 and the layers above.

View Source
var (
	// Query path.
	EventSPFreshSearch = recordlayer.Event{Name: "spfresh_search", Title: "SPFresh Search", Kind: recordlayer.KindTimed}
	// Probed/pruned: the Eq.(3) pruning decomposition per search — probed
	// lists cost range reads, pruned ones were skipped.
	CountSPFreshPostingsProbed  = recordlayer.Event{Name: "spfresh_postings_probed", Title: "SPFresh Postings Probed", Kind: recordlayer.KindCount}
	CountSPFreshPostingsPruned  = recordlayer.Event{Name: "spfresh_postings_pruned", Title: "SPFresh Postings Pruned", Kind: recordlayer.KindCount}
	CountSPFreshEntriesScanned  = recordlayer.Event{Name: "spfresh_entries_scanned", Title: "SPFresh Entries Scanned", Kind: recordlayer.KindCount}
	CountSPFreshRerankReads     = recordlayer.Event{Name: "spfresh_rerank_reads", Title: "SPFresh Rerank Reads", Kind: recordlayer.KindCount}
	CountSPFreshStarvationWiden = recordlayer.Event{Name: "spfresh_starvation_widenings", Title: "SPFresh Starvation Widenings", Kind: recordlayer.KindCount}
	CountSPFreshForwardFollows  = recordlayer.Event{Name: "spfresh_forward_follows", Title: "SPFresh Forward Follows", Kind: recordlayer.KindCount}
	// Phase 2 reached: the VBASE relaxed-monotonicity termination (M_q^s > R_q)
	// latched during a one-shot search's traversal — the recently-traversed cells
	// no longer beat the running k-th best (RFC-156 Phase A). PURE TELEMETRY on
	// the one-shot wrapper path: nothing consults f.phase2 as a stop signal (it
	// does not truncate the exact horizon). The resumable Phase B/C streaming
	// cursor does NOT consult phase2 — it terminates on the relaxed-monotonicity
	// EMISSION BARRIER plus the budget/exhaustion caps (spfresh_stream.go), and
	// it never calls refreshPhase2, so this counter NEVER increments on a streaming
	// query (the SHARED searchInit probe still feeds observe(), but the streaming
	// WIDEN bursts gate it off — scoreCells, f.reranked==nil — since those queues
	// are dead weight once nothing will read phase2).
	CountSPFreshPhase2Reached = recordlayer.Event{Name: "spfresh_phase2_reached", Title: "SPFresh Phase 2 Reached", Kind: recordlayer.KindCount}
	// Capped posting reads: a search's posting fetch returned exactly the
	// 4×Lmax+1 cap — the posting is PAST the split-dispatch envelope and its
	// tail is invisible to queries. Nonzero means a split trigger was lost
	// (the read path re-files it; see CountSPFreshReadPathSplitFiles).
	CountSPFreshCappedPostingReads = recordlayer.Event{Name: "spfresh_capped_posting_reads", Title: "SPFresh Capped Posting Reads", Kind: recordlayer.KindCount}
	// Split tasks re-filed from the read path after a capped read found an
	// over-envelope posting with no pending split.
	CountSPFreshReadPathSplitFiles = recordlayer.Event{Name: "spfresh_readpath_split_files", Title: "SPFresh Read-Path Split Files", Kind: recordlayer.KindCount}
	// Stream widen batches: each demand-driven widening step of the RFC-156
	// Phase C ordered-stream cursor (a batch of ε-pruned/re-routed cells admitted
	// in d2 order because the consumer above drained the finalized prefix and
	// pulled for more). Batched, never one-cell-serial.
	CountSPFreshStreamWiden = recordlayer.Event{Name: "spfresh_stream_widenings", Title: "SPFresh Stream Widenings", Kind: recordlayer.KindCount}
	// Filtered truncation: the RFC-156 Phase C ordered-stream cursor hit its
	// budget cap (max cells probed / max candidates) BEFORE the consumer was
	// satisfied, and returned NoNextReason.ScanLimitReached + a positional
	// continuation rather than a silent < k. This is telemetry IN ADDITION to the
	// reason (RFC-156 §C) — the reason is the contract, this counts
	// how often the budget bound a filtered KNN.
	CountSPFreshFilteredTruncated = recordlayer.Event{Name: "spfresh_filtered_truncated", Title: "SPFresh Filtered Truncations", Kind: recordlayer.KindCount}

	// Write path.
	EventSPFreshInsert           = recordlayer.Event{Name: "spfresh_insert", Title: "SPFresh Insert", Kind: recordlayer.KindTimed}
	CountSPFreshInsertFenceReads = recordlayer.Event{Name: "spfresh_insert_fence_reads", Title: "SPFresh Insert Fence Reads", Kind: recordlayer.KindCount}
	CountSPFreshInsertReplicas   = recordlayer.Event{Name: "spfresh_insert_replicas", Title: "SPFresh Insert Replicas", Kind: recordlayer.KindCount}
	CountSPFreshStaleRouteRetry  = recordlayer.Event{Name: "spfresh_stale_route_retries", Title: "SPFresh Stale Route Retries", Kind: recordlayer.KindCount}

	// Maintenance (rebalancer / sweeper).
	CountSPFreshSplits       = recordlayer.Event{Name: "spfresh_splits", Title: "SPFresh Splits", Kind: recordlayer.KindCount}
	CountSPFreshMerges       = recordlayer.Event{Name: "spfresh_merges", Title: "SPFresh Merges", Kind: recordlayer.KindCount}
	CountSPFreshCSplits      = recordlayer.Event{Name: "spfresh_csplits", Title: "SPFresh Coarse Splits", Kind: recordlayer.KindCount}
	CountSPFreshNPAs         = recordlayer.Event{Name: "spfresh_npas", Title: "SPFresh NPA Reassignments", Kind: recordlayer.KindCount}
	CountSPFreshZombieCleans = recordlayer.Event{Name: "spfresh_zombie_cleans", Title: "SPFresh Zombie Cleanups", Kind: recordlayer.KindCount}
	CountSPFreshCSplitDefers = recordlayer.Event{Name: "spfresh_csplit_defers", Title: "SPFresh Coarse Split Deferrals", Kind: recordlayer.KindCount}
	CountSPFreshLeaseSkips   = recordlayer.Event{Name: "spfresh_lease_skips", Title: "SPFresh Lease Skips", Kind: recordlayer.KindCount}
	CountSPFreshSplitDefers  = recordlayer.Event{Name: "spfresh_split_defers", Title: "SPFresh Split Deferrals", Kind: recordlayer.KindCount}
	// Assignment refinement (RFC-104) fleet pass: vectors re-routed against the
	// converged topology, and tenants whose cursor wrapped a full cycle moving
	// nothing. Moves trending to zero while Converged rises = the fleet has
	// recovered ingest recall-drift and is quiescing; sustained nonzero Moves =
	// ongoing drift (e.g. a steady fast-ingest tenant) the refinement loop is
	// absorbing.
	CountSPFreshRefineMoves     = recordlayer.Event{Name: "spfresh_refine_moves", Title: "SPFresh Refinement Moves", Kind: recordlayer.KindCount}
	CountSPFreshRefineConverged = recordlayer.Event{Name: "spfresh_refine_converged", Title: "SPFresh Refinement Converged Tenants", Kind: recordlayer.KindCount}
	// Task handler errors in a rebalance pass: the pass SKIPS the failed
	// task and continues (a poisoned task at the deterministic queue head
	// must not starve everything behind it), then surfaces the joined
	// errors. Nonzero here with a stable queue depth = a poisoned task; the
	// runbook's "task queue growing" playbook keys off it.
	CountSPFreshTaskErrors = recordlayer.Event{Name: "spfresh_task_errors", Title: "SPFresh Task Errors", Kind: recordlayer.KindCount}
)

SPFresh instrumentation events, recorded into the context's StoreTimer — the same FDBStoreTimer idiom every other index uses (the TEXT index's InstrumentedBunchedMap is the precedent). StoreTimer methods are nil-receiver-safe, so an uninstrumented context costs one nil check per site and SPFresh internals thread the timer unconditionally.

Timed events record nanoseconds; counts record occurrences or sized quantities. These are the operator-facing signals the index needs in production: query cost decomposition (probed/pruned/scanned/reranked), write-path health (fence reads, replicas, stale-route retries), and maintenance progress (per-kind lifecycle actions, zombie cleanups, lease skips). Scrape via StoreTimer.Snapshot().

Functions

func BuildSPFreshIndex

func BuildSPFreshIndex(ctx context.Context, db *recordlayer.FDBDatabase, storeBuilder func(*recordlayer.FDBRecordContext) (*recordlayer.FDBRecordStore, error), indexName string, seed int64) error

BuildSPFreshIndex bulk-builds an SPFresh index over the store's existing records (RFC-094 §8) and flips it readable. The §8 step order is the foreground-interleaving contract: the COARSE table commits BEFORE the assignment scan, so every record save thereafter can route itself (UpdateWhileWriteOnly stages or goes live by cellfin state), and every record saved before it is covered by the assignment scan's later read versions. Double coverage (a save the scan also reads) is harmless — staging writes are idempotent Sets on the same key.

func GuardiannVectorIDHashMapOrder

func GuardiannVectorIDHashMapOrder(pks []tuple.Tuple, uuids []tuple.UUID) []int

GuardiannVectorIDHashMapOrder is the order a default HashMap<VectorId, ...> filled with these (primary key, UUID) ids in list order iterates them in (javaHashMapOrder), for cross-engine conformance.

func NewHNSWGraph

func NewHNSWGraph(storage *hnswStorage, config HNSWConfig) *hnswGraph

NewHNSWGraph creates a new HNSW graph.

func RebalanceSPFreshIndex

func RebalanceSPFreshIndex(ctx context.Context, db *recordlayer.FDBDatabase, storeBuilder func(*recordlayer.FDBRecordContext) (*recordlayer.FDBRecordStore, error), indexName string) (int, error)

RebalanceSPFreshIndex drains the index's task queue to quiescence: scan, act, repeat until a round acts on nothing (splits enqueue NPA follow-ups, so multiple rounds are normal). Bounded against pathological re-triggering. Returns the total number of lifecycle actions taken.

func RefineSPFreshIndex

func RefineSPFreshIndex(ctx context.Context, db *recordlayer.FDBDatabase, storeBuilder func(*recordlayer.FDBRecordContext) (*recordlayer.FDBRecordStore, error), indexName string, budget int) (int, bool, error)

RefineSPFreshIndex runs ONE budgeted refinement pass over the index (RFC-104): it advances the persistent round-robin cursor by up to `budget` vectors, re-routing each against the current topology and moving the stale ones to recover the closure replication fast ingest never fired. It does NOT drain to quiescence — the deployment runs it on its own cadence (a refinement loop beside the rebalancer loop). Returns (moves, cycleConverged): cycleConverged is true when this call wrapped the cursor AND the whole cycle since the last wrap moved nothing — the signal a caller uses to back off a converged tenant. It is NOT merely "this pass reached the end": a budgeted pass whose tail moves zero does not imply the cycle's earlier passes did.

func RefineSPFreshIndexAll

func RefineSPFreshIndexAll(ctx context.Context, db *recordlayer.FDBDatabase, storeBuilder func(*recordlayer.FDBRecordContext) (*recordlayer.FDBRecordStore, error), indexName string) (int, error)

RefineSPFreshIndexAll is the exported one-shot validation entry (refine every vector once). Production callers use the budgeted RefineSPFreshIndex.

func RunSPFreshMaintenance

func RunSPFreshMaintenance(ctx context.Context, db *recordlayer.FDBDatabase, opts SPFreshMaintenanceOptions) error

RunSPFreshMaintenance runs the maintenance loop until ctx is canceled, then returns nil. It does an immediate sweep on entry (so a freshly-started worker makes progress without waiting a full interval), then sweeps on SweepInterval and refines on RefineInterval. Per-pass results/errors flow to OnSweep / OnRefine; nothing here fails the loop. With no tenants it is a quiet no-op loop (each pass returns an empty result), so it is safe to start before the tenant list is populated.

func SPFreshAuditTrail

func SPFreshAuditTrail(fineID int64) []string

SPFreshAuditTrail returns the recorded trail for a fineID.

func SPFreshDebugIntegrity

func SPFreshDebugIntegrity(rtx *recordlayer.FDBRecordContext, store *recordlayer.FDBRecordStore, indexName string, sample int) string

SPFreshDebugIntegrity samples up to `sample` pks evenly from the index's OWN membership rows (no assumption about pk shape) and reports, for each, whether every membership target holds the posting entry and what state the target centroid is in. Diagnostics only: it streams the membership keyspace (O(index) reads) — never call it on a production write path.

func SPFreshDebugTopology

func SPFreshDebugTopology(rtx *recordlayer.FDBRecordContext, store *recordlayer.FDBRecordStore, indexName string) string

SPFreshDebugTopology dumps an index's coarse/fine topology summary (benchmark/operational diagnostics).

func SPFreshDisableAudit

func SPFreshDisableAudit()

SPFreshDisableAudit turns the audit log off and releases its memory. Flag first, then the map: a concurrent spfreshAudit that already passed the flag check still finds a (stale, discarded) map under the mutex.

func SPFreshEnableAudit

func SPFreshEnableAudit()

SPFreshEnableAudit turns on the centroid audit log (diagnostics).

func SPFreshHasPendingMaintenance

func SPFreshHasPendingMaintenance(ctx context.Context, db *recordlayer.FDBDatabase, storeBuilder func(*recordlayer.FDBRecordContext) (*recordlayer.FDBRecordStore, error), indexName string) (bool, error)

SPFreshHasPendingMaintenance reports whether the index has any pending maintenance task rows: one snapshot generation read + one limit-1 snapshot range read — the cheap probe that lets a sweeper skip idle tenants without paying a full task scan. Un-bootstrapped indexes have no work by definition.

func SearchSPFreshIndex

func SearchSPFreshIndex(store *recordlayer.FDBRecordStore, indexName string, queryVector []float64, k int) ([]recordlayer.VectorSearchResult, error)

SearchSPFreshIndex runs a kNN search against a readable SPFresh index, returning up to k nearest primary keys (with exact, sidecar-re-ranked distances, ascending) to queryVector using the index's default probe budget. It is the SPFresh counterpart to FDBRecordStore.SearchVectorIndex (vector_index_maintainer.go), which type-asserts the HNSW maintainer and rejects SPFresh. Callers needing per-query probe knobs (k, kc, w, c, ε) drive ScanByDistance through the executor instead.

Returns *IndexNotReadableError if the index is not scannable (matching the HNSW SearchVectorIndex contract). NOT purely read-only: like the SPFresh read path it shares (searchCurrentGeneration), a probe that reads a posting at the 4×Lmax+1 cap re-files that posting's split task (the read-path envelope repair) — a write the surrounding transaction commits. On a maintained index no posting is over the cap, so the common path performs no writes.

func SerializeVector

func SerializeVector(vec []float64) []byte

SerializeVector encodes a float64 vector into the on-disk byte format the HNSW vector index reads (RealVector.fromBytes). Exported so callers/tests can populate a record's vector column.

func ValidateSPFreshConfig

func ValidateSPFreshConfig(c SPFreshConfig) error

ValidateSPFreshConfig enforces the invariants the RFC-094 lifecycle and sizing arguments depend on. Called by the maintainer at construction; a violation is a config error (a MetaDataError, as an HNSW index's refused configuration is Java's MetaDataException or IllegalArgumentException), never a silently degraded index.

Types

type ClusterUnsplittableError

type ClusterUnsplittableError struct {
	IndexName string
	Prefix    tuple.Tuple
	Cluster   tuple.UUID
	Count     int
	Limit     int
	Cause     string
}

ClusterUnsplittableError is a GuardiANN split that found no usable partition and whose cluster, after its stale references are removed, still holds more primaries than primaryClusterHardMax. It is Go-only (Java throws NoSuchElementException or IllegalArgumentException there), poisons the transaction like any task error, and is deliberately not the insert cap's capacity error: its remedy is deleting vectors, not draining a merge.

func (*ClusterUnsplittableError) Error

func (e *ClusterUnsplittableError) Error() string

type GuardiannEngine

type GuardiannEngine struct {
	// contains filtered or unexported fields
}

GuardiannEngine is one GuardiANN structure over a raw subspace, Java's com.apple.foundationdb.async.guardiann.Guardiann, for cross-engine conformance: it drives the engine the record layer's vector maintainer drives, with no record store around it. options are the GuardiANN index options (the canonical index option names), so the configuration is read exactly as a GUARDIANN index's is.

func NewGuardiannEngine

func NewGuardiannEngine(ss subspace.Subspace, options map[string]string) (*GuardiannEngine, error)

NewGuardiannEngine builds the engine over ss from index options.

func (*GuardiannEngine) Delete

func (e *GuardiannEngine) Delete(tx fdb.WritableTransaction, pk tuple.Tuple, vector []float64, maintainInTransaction bool) error

Delete is Guardiann.delete(transaction, primaryKey, DoubleRealVector, maintainInTransaction).

func (*GuardiannEngine) ExecuteDeferredTasks

func (e *GuardiannEngine) ExecuteDeferredTasks(tx fdb.WritableTransaction, numTasks int) (int, error)

ExecuteDeferredTasks is Guardiann.executeDeferredTasks(transaction, numTasks, Long.MAX_VALUE).

func (*GuardiannEngine) Insert

func (e *GuardiannEngine) Insert(tx fdb.WritableTransaction, pk tuple.Tuple, vector []float64, maintainInTransaction bool) error

Insert is Guardiann.insert(transaction, primaryKey, DoubleRealVector, null, maintainInTransaction).

func (*GuardiannEngine) Search

func (e *GuardiannEngine) Search(tx fdb.ReadTransaction, k int, vector []float64) ([]tuple.Tuple, error)

Search is Guardiann.kNearestNeighborsSearch(transaction, k, the default SearchConfig, false, DoubleRealVector): the primary keys found, nearest first.

type HNSWConfig

type HNSWConfig struct {
	NumDimensions         int
	M                     int             // Connectivity factor (default 16)
	MMax                  int             // Max connections for non-zero layers (default 16 — Java DEFAULT_M_MAX, a constant, NOT derived from M; setting M > 16 requires setting MMax too, like Java)
	MMax0                 int             // Max connections for layer 0 (default 32 — Java DEFAULT_M_MAX_0, a constant, NOT derived from M)
	EfConstruction        int             // Insertion search factor (default 200)
	Metric                VectorMetric    // Distance metric
	ExtendCandidates      bool            // Extend candidate set with 2nd-degree neighbors (default false)
	KeepPrunedConnections bool            // Retain pruned candidates to fill up to M (default false)
	EfRepair              int             // Delete-repair beam width; Java requires [m, 400] (default 64; 0 means "use default")
	UseInlining           bool            // Use inlining storage for layers > 0 (default false)
	Quantizer             VectorQuantizer // Optional quantizer (nil = raw float64 storage)

	// RaBitQ centroid-bootstrap stats (Java Config). When RaBitQ is enabled and no centroid
	// is yet established, inserts sample vectors into the SAMPLES subspace, periodically roll
	// them up, and once StatsThreshold vectors have accumulated, establish the rotated centroid.
	SampleVectorStatsProbability float64 // prob of sampling each inserted vector (default 0.5)
	MaintainStatsProbability     float64 // prob of rolling up samples on an insert (default 0.05)
	StatsThreshold               int     // accumulated count needed to establish the centroid (default 1000)

	// Concurrency limits — Java uses these to limit async pipeline parallelism.
	// Go's synchronous FDB model doesn't use these for concurrency control, but
	// we store and round-trip them so Java-written configs are preserved.
	// Matches Java's Config.maxNumConcurrentNodeFetches etc.
	MaxNumConcurrentNodeFetches         int // (0, 64], default 16 — node fetch parallelism in Java
	MaxNumConcurrentNeighborhoodFetches int // (0, 20], default 10 — neighborhood fetch parallelism in Java
	MaxNumConcurrentDeleteFromLayer     int // (0, 10], default 2  — layer deletion parallelism in Java
}

HNSWConfig configures an HNSW graph. Matches Java's com.apple.foundationdb.async.hnsw.Config.

func DefaultHNSWConfig

func DefaultHNSWConfig(numDimensions int) HNSWConfig

DefaultHNSWConfig returns a default HNSW configuration.

func HNSWConfigOf

func HNSWConfigOf(index *recordlayer.Index) (HNSWConfig, error)

HNSWConfigOf is the HNSW configuration a VECTOR index's options declare, as the maintainer reads it (parseHNSWConfig): Java's HnswVectorIndexEngine.parseConfig.

type HNSWStats

type HNSWStats struct {
	FDBGets       atomic.Int64 // individual Get() calls (point reads)
	FDBBatchGets  atomic.Int64 // batched Get() calls (pipelined, count as 1 RT)
	FDBRangeReads atomic.Int64 // GetRange() calls
	CacheHits     atomic.Int64 // reads served from in-tx cache
}

HNSWStats tracks I/O counters for a single HNSW operation. Attach to a context via WithHNSWStats and retrieve via GetHNSWStats. Zero cost when not attached — all tracking is behind a nil check.

func GetHNSWStats

func GetHNSWStats(ctx context.Context) *HNSWStats

GetHNSWStats retrieves HNSWStats from a context, or nil if not tracking.

func WithHNSWStats

func WithHNSWStats(ctx context.Context) (context.Context, *HNSWStats)

WithHNSWStats attaches an HNSWStats to the context for I/O tracking.

type NegativeTaskCountError

type NegativeTaskCountError struct {
	Prefix tuple.Tuple
	Count  int64
}

NegativeTaskCountError is VectorIndexTaskCounts.NegativeTaskCountException.

func (*NegativeTaskCountError) Error

func (e *NegativeTaskCountError) Error() string

func (*NegativeTaskCountError) JavaRecordCoreException

func (*NegativeTaskCountError) JavaRecordCoreException()

type SPFreshConfig

type SPFreshConfig struct {
	NumDimensions int
	Metric        VectorMetric
	Lmax          int     // posting split threshold (entries)
	LminRatio     int     // Lmin = Lmax / LminRatio (merge threshold)
	CellTarget    int     // fine centroids per cell, build target
	CellMax       int     // coarse-split threshold (fine centroids)
	Replication   int     // closure replication cap r
	Alpha         float64 // RNG closure threshold (> 1.0)
	Kn            int     // NPA reassignment neighborhood
	CooldownSec   int     // post-split merge cooldown
	NumExBits     int     // RaBitQ extended bits for posting residual codes
	Sidecar       bool    // fp16 sidecar for re-rank
	// BuildAssignCells is the bulk-build wave-B assignment width w_b (RFC-099):
	// nearest coarse cells whose fines are candidates per imported vector.
	BuildAssignCells int
}

SPFreshConfig is the structural configuration of an SPFresh index. Every field here is immutable for an existing index (RFC-094 §10).

func DefaultSPFreshConfig

func DefaultSPFreshConfig(numDimensions int) SPFreshConfig

DefaultSPFreshConfig returns the RFC-094 defaults for the given dimensionality.

func (SPFreshConfig) Lmin

func (c SPFreshConfig) Lmin() int

Lmin returns the merge threshold in entries.

type SPFreshIntegrityReport

type SPFreshIntegrityReport struct {
	Members                int            // total membership rows in the index
	Sampled                int            // pks actually checked
	OK                     int            // sampled pks whose every membership target holds the posting entry
	MembershipWithoutEntry int            // sampled pks with >=1 membership target missing its posting entry
	TargetStates           map[string]int // membership-target references by centroid state: active/sealed/forward/dead/absent
	BadTargets             int            // target references in forward/dead/absent state (unambiguously bad post-drain)

	// Posting-size accounting (SPANN §3.2.1 balanced-postings / SPFresh LIRE
	// split-merge bounds). The papers' defining property is length-limited
	// postings; LIRE's split exists to hold the ceiling. Post-drain the normal
	// envelope is mostly <=Lmax with some <=4*Lmax (just-split children,
	// unsampled Lmax..2Lmax postings, closure overcount), and NEVER >4*Lmax —
	// the chunked-drain hard guarantee. A split that fails to drain grows a
	// posting unboundedly past the envelope, which OversizedHard catches.
	ActiveFines   int // ACTIVE fine centroids scanned
	Oversized     int // ACTIVE postings with > Lmax entries (soft: includes the legitimate <=4Lmax band)
	OversizedHard int // ACTIVE postings with > 4*Lmax entries (the LIRE hard-envelope violation)
	MaxPostingLen int // largest ACTIVE posting observed

	Violations []SPFreshIntegrityViolation
}

SPFreshIntegrityReport is the structured result of SPFreshCheckIntegrity — the same membership⊆postings + target-state invariants SPFreshDebugIntegrity formats as a string, returned as data for programmatic assertions. The report is descriptive; the caller sets policy. After the maintenance queue is drained to quiescence the strict invariant is MembershipWithoutEntry == 0, BadTargets == 0, and OversizedHard == 0 (every membership target ACTIVE-or- SEALED with its posting entry present, no posting over the 4×Lmax envelope). SEALED targets are valid — sealed postings are still searched mid-split; only FORWARD/DEAD/absent count as BadTargets.

func SPFreshCheckIntegrity

func SPFreshCheckIntegrity(rtx *recordlayer.FDBRecordContext, store *recordlayer.FDBRecordStore, indexName string, sample int) (SPFreshIntegrityReport, error)

SPFreshCheckIntegrity samples up to `sample` pks evenly from the index's own membership rows and verifies, for each, that every membership target holds the pk's posting entry (membership ⊆ postings) and classifies the target centroid's lifecycle state. It is the structured, programmatic form of SPFreshDebugIntegrity. O(index) reads (it streams the membership keyspace) — never call it on a serving path. `sample` <= 0 checks a single pk.

Violations beyond a small cap are counted but not individually recorded (BadTargets / MembershipWithoutEntry stay exact; Violations is truncated).

type SPFreshIntegrityViolation

type SPFreshIntegrityViolation struct {
	PrimaryKey tuple.Tuple
	FineID     int64
	// Kind: "missing_posting" (membership target lacks this pk's posting
	// entry), "absent_target" (target fineID is in no cell), "forward_target"
	// (target centroid split away), "dead_target" (target centroid GC'd).
	Kind string
}

SPFreshIntegrityViolation is one structural defect found by SPFreshCheckIntegrity: a membership row referencing a posting/centroid that the LIRE invariants forbid.

func (SPFreshIntegrityViolation) String

func (v SPFreshIntegrityViolation) String() string

type SPFreshMaintenanceOptions

type SPFreshMaintenanceOptions struct {
	// Tenants is the set of (store, index) pairs to maintain. The caller owns
	// discovery — store layout is application keyspace, not enumerable from
	// inside the record layer. Sharding the list across workers is safe:
	// SweepSPFreshIndexes is concurrent-safe by construction (unique lease
	// owners, task-level exclusion).
	Tenants []SPFreshTenant

	// SweepInterval is the rebalance cadence. <= 0 uses the default (10s).
	SweepInterval time.Duration
	// RefineInterval is the refinement cadence. <= 0 uses the default (5m).
	// Ignored when DisableRefine is set.
	RefineInterval time.Duration
	// DisableRefine turns off the RFC-104 refinement loop entirely (sweep
	// only) — e.g. for a read-mostly fleet where ingest drift never builds up.
	DisableRefine bool

	// Sweep / Refine carry the per-pass budgets and the StoreTimer that
	// accumulates instrumentation (per-kind action counts, lease skips,
	// refine moves/converged — see spfresh_metrics.go). Their zero values are
	// the documented defaults; set Timer to scrape metrics.
	Sweep  SPFreshSweepOptions
	Refine SPFreshRefineOptions

	// OnSweep / OnRefine, when set, are called after each pass with its result
	// and joined per-tenant error — the hook for logging / exporting metrics /
	// alerting on backlog. A pass error is never fatal to the loop (per-tenant
	// failures are already isolated inside Sweep/Refine); the loop runs until
	// ctx is canceled.
	OnSweep  func(SPFreshSweepResult, error)
	OnRefine func(SPFreshRefineResult, error)
}

SPFreshMaintenanceOptions configures the reference maintenance loop.

type SPFreshRecallReport

type SPFreshRecallReport struct {
	K               int     // neighbors per query
	CorpusSize      int     // indexed records scanned for ground truth
	QueriesRun      int     // query samples evaluated
	MeanRecall      float64 // average recall@k across queries (the headline signal)
	MinRecall       float64 // worst single-query recall@k
	PerfectFraction float64 // fraction of queries with recall@k == 1.0
}

SPFreshRecallReport summarizes a recall measurement.

func MeasureSPFreshRecall

func MeasureSPFreshRecall(ctx context.Context, store *recordlayer.FDBRecordStore, indexName string, k, querySamples int, seed int64) (SPFreshRecallReport, error)

MeasureSPFreshRecall measures recall@k of a readable SPFresh index against brute-force ground truth over `querySamples` query vectors drawn from the index's own records (deterministic by seed). Returns a zero-query report when the index is empty. The store must be bound to a transaction (call inside db.Run); the whole scan+queries run in that one tx, so keep the corpus under spfreshMaxRecallCorpus (see its doc for the scale limit). Mostly read-only, but it can inherit SearchSPFreshIndex's read-path split-repair write for any over-envelope posting (none on a maintained index). Distances use the index's configured metric, so the ground-truth ranking matches what the index optimizes, and recall is scored with the SPANN §4.2.1 tie correction (a result within the k-th true distance counts, so equidistant/duplicate vectors don't under-report).

Queries are drawn from the corpus itself (not a held-out set as in the SPANN SIFT benchmark), so absolute numbers run slightly optimistic — the self-match is a guaranteed hit. That is fine for the intended use as a relative drift signal: alert on MeanRecall falling below YOUR measured baseline, not on an absolute target.

type SPFreshRefineOptions

type SPFreshRefineOptions struct {
	// BudgetPerTenant bounds the vectors re-evaluated per tenant per pass.
	// 0 means the default (spfreshRefineFleetBudget).
	BudgetPerTenant int

	// Timer, when non-nil, accumulates refinement instrumentation across the pass
	// (CountSPFreshRefineMoves, CountSPFreshRefineConverged) — the observability
	// the rebalance sweeper gets via its own Timer. Nil disables recording.
	Timer *recordlayer.StoreTimer
}

SPFreshRefineOptions tunes one fleet refinement pass.

type SPFreshRefineResult

type SPFreshRefineResult struct {
	Moves     int // vectors moved (re-routed) across all tenants this pass
	Refined   int // tenants that completed a refine pass without error
	Converged int // tenants whose cursor wrapped a full cycle moving nothing
}

SPFreshRefineResult summarizes one fleet refinement pass.

func RefineSPFreshIndexes

func RefineSPFreshIndexes(ctx context.Context, db *recordlayer.FDBDatabase, tenants []SPFreshTenant, opts SPFreshRefineOptions) (SPFreshRefineResult, error)

RefineSPFreshIndexes runs ONE budgeted refinement pass over each tenant — the "refinement loop beside the rebalancer loop" (RFC-104). Unlike SweepSPFreshIndexes (task-driven: tenants with no pending lifecycle task rows are skipped), refinement is CURSOR-driven and unconditional — every tenant's persistent round-robin cursor advances by up to BudgetPerTenant vectors, re-routing each against the current (converged) topology and moving the stale ones, recovering the closure replication fast ingest never fired.

Run it on a SLOWER cadence than the rebalance sweep: refinement is recall-recovery, not correctness, and a fully converged tenant re-scans its cursor for zero moves each pass. The result's Converged count (cursor wrapped a full cycle with zero moves) lets a caller back off quiescent tenants. As in SweepSPFreshIndexes, per-tenant failures are isolated (joined, not fatal — one corrupt tenant must not halt fleet refinement) and ctx cancellation ends the pass between tenants.

type SPFreshSweepOptions

type SPFreshSweepOptions struct {
	// MaxRoundsPerTenant bounds the scan-and-act rounds per tenant per
	// pass. 0 means the default (8).
	MaxRoundsPerTenant int
	// MaxActionsPerTenant is the per-tenant fairness budget: at most this
	// many lifecycle actions per pass, ENFORCED WITHIN a round too — a
	// whale tenant whose single scan finds thousands of independent task
	// rows must not monopolize the pass. Undrained tenants
	// are reported, not errored — the next pass continues them. 0 means
	// the default (64).
	MaxActionsPerTenant int

	// Timer collects SPFresh maintenance instrumentation (per-kind action
	// counts, zombie cleanups, lease skips — see spfresh_metrics.go) across
	// the pass. Nil disables recording.
	Timer *recordlayer.StoreTimer
}

SPFreshSweepOptions tunes one sweep pass.

type SPFreshSweepResult

type SPFreshSweepResult struct {
	Actions   int // lifecycle actions executed across all tenants
	Worked    int // tenants that had pending maintenance
	Undrained int // tenants whose budget ran out with work remaining
}

SPFreshSweepResult summarizes one sweep pass.

func SweepSPFreshIndexes

func SweepSPFreshIndexes(ctx context.Context, db *recordlayer.FDBDatabase, tenants []SPFreshTenant, opts SPFreshSweepOptions) (SPFreshSweepResult, error)

SweepSPFreshIndexes makes ONE bounded pass over the tenants: a cheap pending-work probe per tenant (two snapshot reads), then a round-budgeted rebalance for the tenants that need it. Per-tenant failures are collected, not fatal — one corrupt tenant must not halt fleet maintenance; the pass continues and the joined error reports every failure. Callers loop passes on their own cadence and stop idling tenants however they like (the result says whether anything happened).

type SPFreshTenant

type SPFreshTenant struct {
	StoreBuilder func(*recordlayer.FDBRecordContext) (*recordlayer.FDBRecordStore, error)
	IndexName    string
}

SPFreshTenant names one index a sweeper drives: the store (the caller owns tenant discovery — store layout is application keyspace, not enumerable from inside the record layer) and the index within it.

type VectorCapabilityError

type VectorCapabilityError struct {
	IndexName string
	Engine    string
	Operation string
	Option    string
	Value     int
	Reason    string
}

VectorCapabilityError is a vector index operation Go refuses where Java would accept a write no query can use (RFC-257 WS-D section 1, "capability errors"). It is Go-only and terminal: no retry owner retries it.

func (*VectorCapabilityError) Error

func (e *VectorCapabilityError) Error() string

type VectorEngineKind

type VectorEngineKind int

VectorEngineKind is Java's VectorIndexEngineKind.

const (
	VectorEngineHNSW VectorEngineKind = iota
	VectorEngineGuardiann
)

func VectorEngineOf

func VectorEngineOf(index *recordlayer.Index) (VectorEngineKind, error)

VectorEngineOf is VectorIndexEngineKind.fromOptionValue over the index's vectorEngine option: case-insensitive, unknown values refused.

func (VectorEngineKind) String

func (k VectorEngineKind) String() string

type VectorIndexClusterTooLargeError

type VectorIndexClusterTooLargeError struct {
	Message string
	Cause   error
}

VectorIndexClusterTooLargeError is Java's VectorIndexClusterTooLargeException: a deferred-maintenance insert found its cluster at the hard cap.

func (*VectorIndexClusterTooLargeError) Error

func (*VectorIndexClusterTooLargeError) JavaRecordCoreException

func (*VectorIndexClusterTooLargeError) JavaRecordCoreException()

func (*VectorIndexClusterTooLargeError) Unwrap

type VectorMetric

type VectorMetric int

VectorMetric identifies a distance metric for HNSW vector search.

const (
	// VectorMetricEuclidean is true L2: sqrt(Σ(a_i−b_i)²). Matches Java's
	// EUCLIDEAN_METRIC (MetricDefinition.EuclideanMetric.distance = Math.sqrt(...)).
	VectorMetricEuclidean VectorMetric = iota
	VectorMetricCosine
	VectorMetricInnerProduct
	// VectorMetricEuclideanSquare is squared L2: Σ(a_i−b_i)² (no sqrt). Matches
	// Java's EUCLIDEAN_SQUARE_METRIC — same KNN ordering as L2, cheaper, but it is
	// NOT a true metric (does not satisfy the triangle inequality). Added at the end
	// so the existing iota values are unchanged (the int is never persisted; configs
	// round-trip via the option string, e.g. "EUCLIDEAN_SQUARE_METRIC").
	VectorMetricEuclideanSquare
)

func VectorIndexMetric

func VectorIndexMetric(index *recordlayer.Index) (VectorMetric, error)

VectorIndexMetric is the metric a vector index's maintainer builds it with, read by the maintainer's own metric reader: hnswMetric for an HNSW VECTOR index, and spfreshMetric for an SPFresh index. A value the maintainer refuses is an error. The planner reads a vector index's metric through it (Java's VectorIndexExpansionVisitor reads the engine's parsed Metric), so a candidate's metric is always the one the index is maintained with.

type VectorQuantizer

type VectorQuantizer interface {
	// Encode quantizes a float64 vector into compact bytes for storage.
	Encode(vector []float64) []byte

	// Distance estimates the distance between a raw query vector and stored
	// quantized bytes. Returns the estimated distance.
	Distance(query []float64, storedBytes []byte, numDimensions int) (float64, error)

	// Decode reconstructs an approximate float64 vector from stored quantized bytes.
	// Used for pairwise distance in the neighbor selection heuristic.
	Decode(storedBytes []byte, numDimensions int) ([]float64, error)

	// GetTypeByte returns the type ordinal byte used as the first byte of encoded data.
	// This is used to dispatch between raw and quantized storage.
	GetTypeByte() byte
}

VectorQuantizer is an optional quantization strategy for HNSW vector storage. When set on HNSWConfig, HNSW uses it for encoding, distance estimation, and vector reconstruction instead of raw float64 storage.

This interface allows quantization implementations (like RaBitQ) to live in separate packages, matching Java's architecture where RaBitQ is in fdb-extensions.

type VectorScanBounds

type VectorScanBounds struct {
	QueryVector []float64 // The query vector for similarity search.
	K           int       // Number of nearest neighbors to return.
	EfSearch    int       // Search exploration factor (0 = auto from K).
}

VectorScanBounds carries the parameters for a BY_DISTANCE kNN scan. Matches Java's VectorIndexScanBounds (query vector, k limit, efSearch, options).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL