/** * SMI-4577: HNSW search backend for `EmbeddingService.findSimilar`. * * Lazily loads `hnswlib-node` (declared as `optionalDependencies` on * @skillsmith/core), builds or loads an on-disk index at * `~/.skillsmith/cache/hnsw-{modelName}.{bin,meta.json,labels.json}`, * and exposes incremental `addPoint`/`markDelete` semantics with a debounced * atomic-rename persist. * * Failure modes: * - `MODULE_NOT_FOUND` on import → permanently disable; brute-force fallback * in `EmbeddingService.findSimilar` covers the case. * - `readIndex` failure on a corrupt cache → delete + rebuild on next call * (treat as transient). * - Concurrent writers → atomic-rename (`writeIndex` to `.tmp`, * `fs.renameSync` to final). Loser-of-race acceptable; readers re-read * via the atomic pointer. * * @see ADR-009 (2026-05 amendment): brute-force fallback retained for * environments where the optional dep failed to install. */ import type { HierarchicalNSW } from './hnsw-store.types.js'; /** * Persisted metadata describing the on-disk HNSW index. Used to invalidate the * cached graph when the embedding count, model, or vector dimension drift. */ export interface HnswMeta { /** Schema version. Bump on incompatible meta changes. */ version: 1; /** Model identifier (e.g. `Xenova/all-MiniLM-L6-v2`). */ modelName: string; /** Vector dimensionality. */ dim: number; /** Number of points the cache was built from. */ count: number; /** ISO timestamp the cache was last persisted. */ builtAt: string; } /** * Wrapper exposing the live HNSW index plus the bookkeeping needed by * `EmbeddingService` to rewire incremental upserts/removes. */ export interface HnswHandle { /** The live HNSW index. */ index: HierarchicalNSW; /** label → skillId mapping (HNSW returns numeric labels). */ labelToId: Map; /** skillId → label mapping (for incremental updates / deletes). */ idToLabel: Map; /** Next label to assign for new points. */ nextLabel: number; /** Filesystem paths the index will read/write. Exposed for diagnostics/tests. */ paths: HnswCachePaths; /** Schedule a debounced persist (5s). Safe to call repeatedly. */ schedulePersist: () => void; /** Persist immediately (used at shutdown / for tests). */ persistNow: () => void; } export interface HnswCachePaths { bin: string; meta: string; labels: string; binTmp: string; metaTmp: string; labelsTmp: string; } /** * Status reported back to `EmbeddingService` so it can distinguish * "the optional dep is missing" (permanent) from "we hit a transient * write error" (try again next time). */ export type HnswStatus = { kind: 'ok'; handle: HnswHandle; } | { kind: 'permanently-unavailable'; reason: string; } | { kind: 'temporarily-unavailable'; reason: string; }; /** * Build (or load from cache) an HNSW index for the supplied embedding map. * * `embeddings` is the canonical source of truth (from `EmbeddingService`'s * SQLite cache). On a cold start with a populated cache file matching * meta.json, we `readIndex` and skip the rebuild. Otherwise we initialise * a fresh index and add every point — same I/O cost as a brute-force seed * but the resulting graph survives subsequent `findSimilar` calls. */ export declare function loadOrBuildHnsw(args: { embeddings: Map; modelName: string; dim: number; /** Capacity hint. Defaults to ~2x current size, clamped to 1024 minimum. */ maxElements?: number; /** Override hyperparams. Defaults match `DEFAULT_HNSW_CONFIG` in hnsw-store.types.ts. */ m?: number; efConstruction?: number; efSearch?: number; }): Promise; /** * Top-K nearest-neighbour search via the supplied handle. * * @param handle - HNSW handle returned by `loadOrBuildHnsw` * @param query - Query vector (must match `handle.dim`) * @param topK - Maximum neighbours to return * @returns Result rows in HNSW score order; `score` is `1 - cosineDistance`. */ export declare function findSimilarHnsw(handle: HnswHandle, query: Float32Array, topK: number): Array<{ skillId: string; score: number; }>; /** * Add or replace a point. Used by `EmbeddingService.storeEmbedding` to keep * the in-memory graph aligned with the SQLite cache. Marks the handle dirty; * persist happens via the debounced 5s timer (or `persistNow`). */ export declare function upsertPoint(handle: HnswHandle, skillId: string, vector: Float32Array): void; /** * Mark a point deleted. The point stays in the graph for traversal correctness * but `findSimilarHnsw` filters it out via the labelToId lookup. */ export declare function removePoint(handle: HnswHandle, skillId: string): boolean; /** * Test-only helper — clears the cached `hnswlib-node` constructor reference so * tests can simulate "module reinstalled" scenarios. Not part of the public * API; do not use in production code. * * @internal */ export declare function __resetCachedHnswCtorForTests(): void; //# sourceMappingURL=hnsw-search.d.ts.map