import type { WorkerPool } from './WorkerPool.js'; export type DistanceMetric = 'cosine' | 'euclidean' | 'dot_product'; export interface HNSWOptions { dimensions: number; metric?: DistanceMetric; m?: number; efConstruction?: number; random?: () => number; } export interface HNSWRecord { id: number; vector: number[] | Float32Array; } export interface QuantizationOptions { enabled?: boolean; candidateMultiplier?: number; } export interface HNSWSearchOptions extends QuantizationOptions { vector: number[] | Float32Array; limit: number; efSearch?: number; } export interface Node { id: number; level: number; vector: Float32Array; neighbors: number[][]; } export declare class HNSWIndex { private static readonly MISSING_ID_SENTINEL; private static readonly MAX_NODE_ID; private static readonly MAX_ARRAY_BUFFER_BYTES; private static readonly MAX_NODE_SLOTS; private static readonly MAX_SERIALIZED_LEVEL; private static readonly INITIAL_VECTOR_STORAGE_BYTES; private static readonly MIN_INITIAL_CAPACITY; private static readonly MAX_INITIAL_CAPACITY; private static readonly MAX_INITIAL_HEAP_CAPACITY; /** * Allocate a Float32Array backed by SharedArrayBuffer when available. * This allows workers to read vector data without copying. */ private static allocateFloat32; /** * Allocate an Int8Array, optionally backed by SharedArrayBuffer. */ private static allocateInt8; /** Whether flatVectors/flatInt8Vectors use SharedArrayBuffer */ private useSharedMemory; private sharedGraphIndex; private sharedGraphNeighborData; private sharedGraphMaxLayerSlots; private sharedGraphWriteOffset; private sharedMetadata; /** Monotonic publication token for worker-visible graph/vector state. */ private sharedGraphGeneration; private sharedGraphUpdateInProgress; private workerPoolLeaseActive; private M; private M0; private efConstruction; private levelMult; private maxLevel; private entryPointId; private nodes; private nodeCount; private nextAutoId; private dimension; private metric; private maxLayers; private flatVectors; private flatVectorsCapacity; private visitedArray; private visitedArraySize; private visitedGeneration; private candidatesHeap; private resultsHeap; private selectionHeap; private heapCapacity; private vectorsAreNormalized; private distanceFn; private readonly random; private scalarQuantizer; private quantizationEnabled; private flatInt8Vectors; private flatInt8VectorsCapacity; private queryInt8Buffer; private lazyLoadEnabled; private vectorOffsets; private vectorBuffer; private vectorsLoaded; private queryNormBuffer; private neighborSets; private useInt8Construction; private constructionMode; private calibrationStats; private lastSearchVisitedNodes; constructor(options: HNSWOptions); /** * Ensure node array and flat vector storage have enough capacity */ private ensureCapacity; /** * Set vector in flat storage */ private setFlatVector; /** * Set node by ID */ private setNode; private batchNeighborIds; private batchDistances; /** * OPTIMIZATION: Batch distance calculation for better cache locality * Computes distances from query to multiple neighbors at once * Uses flat vector storage for contiguous memory access */ private calculateDistancesBatch; /** * OPTIMIZATION: Batch Int8 distance calculation for cache-friendly quantized search. * Mirrors calculateDistancesBatch but operates on contiguous flatInt8Vectors. * * For cosine (pre-normalized) and dot_product: uses -dotProductInt8 (1 sum, 8-wide) * For euclidean: uses l2Squared on int8 (8-wide unrolled) */ private calculateDistancesBatchInt8; /** * Check if a node has been visited in the current search. * Uses generation counting to avoid clearing the array. */ private isVisited; /** * Mark a node as visited in the current search. * Grows the array if needed. */ private markVisited; /** * Clear all visited markers by incrementing the generation. * Much faster than filling the array with zeros. */ private clearVisited; private normalizeVector; private randomFloat; private countLiveNodes; private selectLevel; /** * Calculate distance between two vectors using the configured metric. * Uses cached function pointer to avoid switch overhead. */ calculateDistance(a: Float32Array, b: Float32Array): number; private validateQueryVector; /** * Validate caller-owned numeric input before converting it to Float32. * JavaScript numbers can silently underflow to zero (or overflow to * infinity) during typed-array conversion, changing the vector while still * passing a post-conversion finite-value check. */ private validateFloat32Input; private validateSearchParams; private validateNodeId; /** * Get a node's vector, loading it if necessary (for lazy loading support) */ private getNodeVector; /** * Mutations require every existing vector to be detached from the serialized * backing buffer. Otherwise graph construction can compare against lazy * placeholder zero vectors, and a subsequent serialize cannot distinguish a * newly inserted node from an unloaded serialized node. */ private materializeAndDetachLazyState; private getLayerMaxConnections; private selectNeighbors; private static isBetterResult; private static isWorseResult; private addBidirectionalConnection; /** * Prune a node's neighbor list to maxConnections by removing the most distant neighbor. */ private pruneConnections; /** * Ensure heap capacity is sufficient for the given ef value. * Resizes heaps if needed. */ private ensureHeapCapacity; /** * Search a layer using the standard two-heap HNSW algorithm. * * Uses two heaps: * - candidatesHeap (min-heap): Tracks nodes to explore, prioritizing closest * - resultsHeap (max-heap): Tracks top-ef results, allowing O(log n) eviction of furthest * * Termination: Stops when closest unvisited candidate is farther than furthest result. */ private searchLayer; private greedySearch; /** * Add a point to the index (async wrapper for API compatibility) * For bulk operations, use addPointsBulk() which uses the faster sync version internally */ addPoint(id: number, vector: number[] | Float32Array, options?: { skipNormalization?: boolean; }): Promise; /** * Synchronous version of addPoint - avoids async/await microtask overhead. * Measure any bulk-workload speed difference on the target runtime. * @param skipNormalization - Set true if vectors are already unit-normalized (e.g., Cohere embeddings) */ /** Record-oriented low-level insertion for the explicit hnsw subpath. */ insert(records: readonly HNSWRecord[]): void; /** Record-oriented low-level search for the explicit hnsw subpath. */ search(options: HNSWSearchOptions): Array<{ id: number; distance: number; }>; addPointSync(id: number, vector: number[] | Float32Array, options?: { skipNormalization?: boolean; }): void; searchKNN(query: Float32Array, k: number, efSearch?: number): Array<{ id: number; distance: number; }>; /** Number of layer-0 candidates visited by the most recent search. */ getLastSearchVisitedNodes(): number; /** * Batch search for multiple query vectors. * More efficient than calling searchKNN multiple times as it reuses internal buffers. * * @param queries Array of query vectors * @param k Number of nearest neighbors to return per query * @param efSearch Search effort parameter (higher = better recall, slower) * @returns Array of results, one per query */ searchKNNBatch(queries: Float32Array[], k: number, efSearch?: number): Array>; /** * Optimized batch search that returns results in a flat structure for better performance. * Useful when you need to process many queries quickly. * * @param queries Flat Float32Array containing all queries concatenated * @param numQueries Number of queries in the array * @param k Number of nearest neighbors to return per query * @param efSearch Search effort parameter * @returns Object with flat arrays for ids and distances */ searchKNNBatchFlat(queries: Float32Array, numQueries: number, k: number, efSearch?: number): { ids: Uint32Array; distances: Float32Array; }; /** * Add a single vector with auto-generated ID. * Returns the assigned ID. * * @param vector Vector to add * @returns The auto-generated ID * * @example * ```typescript * const id = await index.add([0.1, 0.2, 0.3]); * console.log(`Added vector with ID: ${id}`); * ``` */ add(vector: number[] | Float32Array): Promise; /** * Simple query interface - find k nearest neighbors. * * @param vector Query vector * @param k Number of results (default: 10) * @returns Array of {id, distance} results * * @example * ```typescript * const results = index.query([0.1, 0.2, 0.3], 5); * results.forEach(r => console.log(`ID: ${r.id}, Distance: ${r.distance}`)); * ``` */ query(vector: number[] | Float32Array, k?: number): Array<{ id: number; distance: number; }>; /** * Add multiple vectors with auto-generated IDs. * Returns the assigned IDs. * * @param vectors Array of vectors to add * @returns Array of auto-generated IDs * * @example * ```typescript * const ids = await index.addAll([[0.1, 0.2], [0.3, 0.4]]); * ``` */ addAll(vectors: Array): Promise; /** * Bulk add multiple points with optimized O(1) neighbor lookups. * Significantly faster than sequential addPoint() calls for large batches. * Uses Set-based membership testing during construction, then releases memory. * * @param points Array of {id, vector} to add * @example * ```typescript * await index.addPointsBulk([ * { id: 0, vector: new Float32Array([0.1, 0.2, ...]) }, * { id: 1, vector: new Float32Array([0.3, 0.4, ...]) }, * ]); * ``` */ addPointsBulk(points: Array<{ id: number; vector: Float32Array; }>, options?: { skipNormalization?: boolean; useInt8Construction?: boolean; diverseSeedInsertion?: boolean; }): Promise; /** * Reorder points for diversity — inserts "diverse seeds" first to create a better graph backbone. * Uses farthest-point sampling on a random subset to select diverse initial points. */ private reorderForDiversity; /** * Synchronous bulk insertion without async microtask overhead. * Uses addPointSync() internally; benchmark the workload-specific gain. * @param options.skipNormalization - Set true if vectors are already unit-normalized * @param options.useInt8Construction - Use int8 quantized search at layer 0 during construction. * First 1000 vectors are inserted with float32 (to train quantizer), then remaining use int8. * Benchmark build time and recall together; no universal speedup or recall * guarantee is implied by this option. * @param options.diverseSeedInsertion - Reorder points to insert diverse seeds first for better graph backbone. * Uses farthest-point sampling on a random subset to select diverse initial points. * Only applies to batches > 1000 vectors. */ addPointsBulkSync(points: Array<{ id: number; vector: Float32Array; }>, options?: { skipNormalization?: boolean; useInt8Construction?: boolean; diverseSeedInsertion?: boolean; }): void; /** * Parallel bulk insertion using worker threads for search phase. * Workers perform HNSW search (the expensive part), main thread adds connections. * Uses SharedArrayBuffer for zero-copy vector sharing with workers. * * @param points Array of {id, vector} to insert * @param pool WorkerPool instance (caller manages lifecycle) * @param options.seedFraction Fraction of points to insert sequentially as seed (default: 0.1) * @param options.batchSize Points per parallel batch (default: 200). Larger = faster but lower recall. * Larger batches trade graph freshness for throughput. Measure the chosen * batch size and resync interval on the target dataset. * @param options.resyncInterval Full re-sync worker graph every N batches (default: 100) * @param options.repair Run 1-hop neighbor expansion after build to improve recall (default: false) * @param options.skipNormalization Skip vector normalization */ addPointsBulkParallel(points: Array<{ id: number; vector: Float32Array; }>, pool: WorkerPool, options?: { seedFraction?: number; batchSize?: number; resyncInterval?: number; repair?: boolean; skipNormalization?: boolean; }): Promise; /** * Repair graph connections via 1-hop neighbor expansion. * For each node, check if any neighbor-of-neighbor is closer than current neighbors. * This is O(N * M0^2) distance computations — much cheaper than re-searching. * Two passes catches transitive improvements. */ private repairGraphConnections; /** * Clear construction-time data structures to free memory. * Called automatically after addPointsBulk(), but can be called * manually if needed. */ clearConstructionCache(): void; private static readonly MAGIC; private static readonly FORMAT_VERSION; private static readonly HEADER_SIZE; private static ensureReadable; /** * Read a dimension-length float32 vector from a serialized buffer at the given * absolute byte offset. Callers must have already validated that * [absoluteOffset, absoluteOffset + dimension*4) fits within the buffer. * * Fast path: when the offset is 4-byte aligned we construct a Float32Array view * directly over the buffer and bulk-copy. Otherwise we fall back to a DataView * loop. Either way every component is validated for finiteness in a single pass. */ private static readVectorBytes; private static wrapDeserializeError; /** * Get all nodes as an array (filters out undefined slots) */ private getNodesArray; /** * Get shared search data for worker pool parallelism. * Returns a copy of flat vectors and serialized graph structure. */ /** * Convert flat vector storage to SharedArrayBuffer for zero-copy worker sharing. * Called automatically by addPointsBulkParallel. Can also be called before WorkerPool.init() * to enable zero-copy search dispatch. */ enableSharedMemory(): void; getSharedSearchData(): { flatVectors: Float32Array; flatInt8Vectors: Int8Array | null; dimension: number; nodeCount: number; metric: DistanceMetric; entryPointId: number; maxLevel: number; M: number; M0: number; nodeLevels: Uint8Array; quantizationEnabled: boolean; quantizationParams: { min: Float32Array; max: Float32Array; scale: Float32Array; offset: Float32Array; } | null; graphData?: ArrayBuffer; graphNeighborData?: Uint32Array; graphIndex?: Uint32Array; maxLayerSlots?: number; sharedMetadata?: Uint32Array; /** Worker-visible graph generation for legacy (copied graph) workers. */ graphGeneration?: number; } | null; /** * Serialize graph into SAB-backed flat typed arrays for zero-copy worker sharing. * Layout: * graphIndex[(nodeId * maxLayerSlots + layer) * 2] = offset into graphNeighborData * graphIndex[(nodeId * maxLayerSlots + layer) * 2 + 1] = neighbor count * graphNeighborData[offset..offset+count] = neighbor IDs * * Pre-allocates extra capacity for growth during parallel build. */ private serializeGraphToSharedBuffers; private nextSharedGraphGeneration; private beginSharedGraphUpdate; private completeSharedGraphUpdate; private publishSharedMetadataGeneration; /** * Update shared graph SABs for specific nodes. * Called during parallel build after each batch to sync graph changes. * Workers see updates immediately via shared memory — no postMessage needed. */ updateSharedGraphNodes(nodeIds: Iterable): void; /** * Update shared metadata SAB with current index state. * Workers read these values during search. */ updateSharedMetadata(): void; /** * Check if shared graph SABs are active (for parallel build optimization). */ hasSharedGraph(): boolean; /** Acquire a read-only shared-memory lease for WorkerPool. */ acquireWorkerPoolLease(): void; releaseWorkerPoolLease(): void; isWorkerPoolLeaseActive(): boolean; isConstructionMode(): boolean; private assertWorkerPoolMutationAllowed; /** * Clear shared graph references (called when pool is destroyed/re-initialized). */ clearSharedGraph(): void; /** * Serialize graph structure (neighbor lists) into a compact ArrayBuffer. * Legacy format for non-SAB fallback. * Format per node: [numLayers:uint8] [numNeighbors:uint16, neighborId:uint32...] per layer */ private serializeGraphStructure; /** * Reorder nodes for BFS cache locality. Nodes are renumbered so that * neighbors in the graph are stored contiguously in memory, reducing * cache misses during search. * * @returns Map from old node ID to new node ID */ reorderForLocality(): Map; serialize(): ArrayBuffer; /** * Deserialize an HNSW index from a buffer. * * @param buffer The serialized index buffer * @param options Optional loading options * - lazyLoadVectors: If true, don't load vectors immediately (v3+ only) */ static deserialize(buffer: ArrayBuffer, options?: { lazyLoadVectors?: boolean; }): HNSWIndex; /** * Load a specific vector on demand (for lazy-loaded indices). * Returns the vector if lazy loading is enabled, otherwise returns the already-loaded vector. */ loadVector(nodeId: number): Float32Array | null; /** * Preload vectors for specific node IDs. * Useful for warming up cache before searches. */ preloadVectors(nodeIds: number[]): void; /** * Check if lazy loading is enabled */ isLazyLoadEnabled(): boolean; /** * Get lazy loading statistics */ getLazyLoadStats(): { enabled: boolean; totalNodes: number; loadedVectors: number; /** Percentage of vector materialization deferred, not whole-index RSS. */ memoryReduction: string; totalVectorBytes: number; loadedVectorBytes: number; retainedSerializedBytes: number; residentIndexBytes: number; }; /** * Save to binary file using Bun APIs. * @deprecated Use serialize() with a StorageBackend instead for cross-platform support. */ saveToFile(filePath: string): Promise; /** * Load from binary file using Bun APIs. * @deprecated Use StorageBackend.read() + deserialize() instead for cross-platform support. */ static loadFromFile(filePath: string): Promise; destroy(): void; /** * Get memory usage statistics */ getMemoryUsage(): number; getDimension(): number; getMetric(): DistanceMetric; getM(): number; getEfConstruction(): number; /** * Get all vectors for brute-force search */ getAllVectors(): Map; /** * Get a single vector by its numeric node id, or null if absent. * Returns a defensive copy so callers cannot mutate internal storage. * Keyed consistently with getAllVectors() and Collection's numeric ids. */ getVectorById(id: number): Float32Array | null; /** * Enable Int8 traversal quantization with automatic float32 rescoring. * Trains the quantizer on existing vectors and quantizes them. * * The traversal representation is four times narrower per component. The * float32 index remains resident, so enabling this adds memory rather than * reducing total resident index memory. Measure speed and recall on the * target dataset and search configuration. * * @example * ```typescript * // After adding vectors * index.enableQuantization(); * * // Now use quantized search (automatically rescores for high recall) * const results = index.searchKNNQuantized(query, 10); * ``` */ enableQuantization(): void; /** * Build quantized state from an already-trained ScalarQuantizer. * * Shared by enableQuantization() (which trains the quantizer first) and * deserialize() (which restores the quantizer from a serialized blob). Mirrors * enableQuantization() exactly EXCEPT training: allocates the contiguous * flatInt8Vectors buffer, quantizes every live node's float vector into it, * allocates the reusable queryInt8Buffer, and flips quantizationEnabled on. */ private installQuantizer; /** * Check if quantization is enabled */ isQuantizationEnabled(): boolean; /** Return the trained traversal quantizer so a rebuild can preserve its * calibration instead of silently retraining on a different sample. */ getQuantizationState(): ArrayBuffer | null; /** Install previously trained quantization parameters on this index. */ restoreQuantizationState(serialized: ArrayBuffer): void; getCalibrationStats(): { meanEntryDist: number; stdEntryDist: number; } | null; restoreCalibrationStats(stats: { meanEntryDist: number; stdEntryDist: number; }): void; /** * Quantized traversal search with automatic float32 rescoring. * * Uses Int8 quantized vectors for initial candidate retrieval, * then rescores top candidates with float32 for accurate ranking. * * @param query Query vector * @param k Number of results to return * @param candidateMultiplier How many extra candidates to retrieve for rescoring (default: 3) * @param efSearch Search effort parameter * @returns Array of {id, distance} results (same format as searchKNN) * * Int8 traversal uses one byte per component instead of four. End-to-end * speed and recall remain dataset/configuration dependent, and float32 * vectors stay resident for final rescoring. */ searchKNNQuantized(query: Float32Array, k: number, candidateMultiplier?: number, efSearch?: number): Array<{ id: number; distance: number; }>; /** * Calibrate the index for adaptive efSearch. Runs greedy descent for sample * queries and records statistics about entry distances. * * @param sampleQueries Array of query vectors to calibrate with */ calibrate(sampleQueries: Float32Array[]): void; /** * Check if the index has been calibrated for adaptive search. */ isCalibrated(): boolean; /** * Adaptive efSearch — scales ef based on query difficulty. * Easy queries (close to graph) use lower ef, hard queries (far from graph) use higher ef. * Falls back to baseEfSearch if not calibrated. * * @param query Query vector * @param k Number of results to return * @param baseEfSearch Base ef parameter (will be scaled up/down) * @returns Same format as searchKNN */ searchKNNAdaptive(query: Float32Array, k: number, baseEfSearch?: number): Array<{ id: number; distance: number; }>; /** * Search layer using Int8 quantized distances for speed. * OPTIMIZED: Uses batch distance calculation on contiguous flatInt8Vectors. * Falls back to one-by-one float32 distance if contiguous int8 storage is unavailable. */ private searchLayerQuantized; /** * Get quantization statistics */ getQuantizationStats(): { enabled: boolean; vectorCount: number; traversalBytesPerComponent: 1 | 4; float32TraversalBytes: number; quantizedTraversalBytes: number; additionalResidentBytes: number; totalResidentIndexBytes: number; memoryReduction: string; expectedSpeedup: string; }; } //# sourceMappingURL=HNSWIndex.d.ts.map