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
- Variables
- func BuildSPFreshIndex(ctx context.Context, db *recordlayer.FDBDatabase, ...) error
- func GuardiannVectorIDHashMapOrder(pks []tuple.Tuple, uuids []tuple.UUID) []int
- func NewHNSWGraph(storage *hnswStorage, config HNSWConfig) *hnswGraph
- func RebalanceSPFreshIndex(ctx context.Context, db *recordlayer.FDBDatabase, ...) (int, error)
- func RefineSPFreshIndex(ctx context.Context, db *recordlayer.FDBDatabase, ...) (int, bool, error)
- func RefineSPFreshIndexAll(ctx context.Context, db *recordlayer.FDBDatabase, ...) (int, error)
- func RunSPFreshMaintenance(ctx context.Context, db *recordlayer.FDBDatabase, ...) error
- func SPFreshAuditTrail(fineID int64) []string
- func SPFreshDebugIntegrity(rtx *recordlayer.FDBRecordContext, store *recordlayer.FDBRecordStore, ...) string
- func SPFreshDebugTopology(rtx *recordlayer.FDBRecordContext, store *recordlayer.FDBRecordStore, ...) string
- func SPFreshDisableAudit()
- func SPFreshEnableAudit()
- func SPFreshHasPendingMaintenance(ctx context.Context, db *recordlayer.FDBDatabase, ...) (bool, error)
- func SearchSPFreshIndex(store *recordlayer.FDBRecordStore, indexName string, queryVector []float64, ...) ([]recordlayer.VectorSearchResult, error)
- func SerializeVector(vec []float64) []byte
- func ValidateSPFreshConfig(c SPFreshConfig) error
- type ClusterUnsplittableError
- type GuardiannEngine
- func (e *GuardiannEngine) Delete(tx fdb.WritableTransaction, pk tuple.Tuple, vector []float64, ...) error
- func (e *GuardiannEngine) ExecuteDeferredTasks(tx fdb.WritableTransaction, numTasks int) (int, error)
- func (e *GuardiannEngine) Insert(tx fdb.WritableTransaction, pk tuple.Tuple, vector []float64, ...) error
- func (e *GuardiannEngine) Search(tx fdb.ReadTransaction, k int, vector []float64) ([]tuple.Tuple, error)
- type HNSWConfig
- type HNSWStats
- type NegativeTaskCountError
- type SPFreshConfig
- type SPFreshIntegrityReport
- type SPFreshIntegrityViolation
- type SPFreshMaintenanceOptions
- type SPFreshRecallReport
- type SPFreshRefineOptions
- type SPFreshRefineResult
- type SPFreshSweepOptions
- type SPFreshSweepResult
- type SPFreshTenant
- type VectorCapabilityError
- type VectorEngineKind
- type VectorIndexClusterTooLargeError
- type VectorMetric
- type VectorQuantizer
- type VectorScanBounds
Constants ¶
const ( UnsplittableNoUsablePartition = "no usable partition under the rule" UnsplittablePeelNotAdmitted = "a peel the rule does not admit" )
Causes a ClusterUnsplittableError records.
Variables ¶
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).
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
GetHNSWStats retrieves HNSWStats from a context, or nil if not tracking.
type NegativeTaskCountError ¶
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 ¶
VectorIndexClusterTooLargeError is Java's VectorIndexClusterTooLargeException: a deferred-maintenance insert found its cluster at the hard cap.
func (*VectorIndexClusterTooLargeError) Error ¶
func (e *VectorIndexClusterTooLargeError) Error() string
func (*VectorIndexClusterTooLargeError) JavaRecordCoreException ¶
func (*VectorIndexClusterTooLargeError) JavaRecordCoreException()
func (*VectorIndexClusterTooLargeError) Unwrap ¶
func (e *VectorIndexClusterTooLargeError) Unwrap() error
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).
Source Files
¶
- doc.go
- fht_kac_rotator.go
- guardiann_centroids.go
- guardiann_config.go
- guardiann_consumer_outcome.go
- guardiann_engine.go
- guardiann_harness.go
- guardiann_kmeans.go
- guardiann_ops.go
- guardiann_splitmerge.go
- guardiann_storage.go
- guardiann_tasks.go
- guardiann_training.go
- guardiann_types.go
- guardiann_unsplittable.go
- guardiann_vectors.go
- hnsw.go
- hnsw_distance_bytes.go
- hnsw_options.go
- hnsw_stats.go
- hnsw_vector.go
- spfresh_build.go
- spfresh_cache.go
- spfresh_codec.go
- spfresh_config.go
- spfresh_csplit.go
- spfresh_gc.go
- spfresh_index_maintainer.go
- spfresh_kmeans.go
- spfresh_layout.go
- spfresh_merge.go
- spfresh_metrics.go
- spfresh_npa.go
- spfresh_query.go
- spfresh_rebalancer.go
- spfresh_recall.go
- spfresh_refine.go
- spfresh_split.go
- spfresh_storage.go
- spfresh_stream.go
- spfresh_sweeper.go
- spfresh_util.go
- spfresh_verify_api.go
- spfresh_worker.go
- spfresh_write.go
- util.go
- vector_engine.go
- vector_index_maintainer.go
- vector_index_maintainer_factory.go
- vector_index_queue.go
- vector_index_validation.go