@claude-flow/embeddings
Version:
V3 Embedding Service - OpenAI, Transformers.js, Agentic-Flow (ONNX), Mock providers with hyperbolic embeddings, normalization, and chunking
142 lines • 5.79 kB
TypeScript
/**
* ADR-121 Phase 8 — `AnnRouter` composition layer.
*
* The package now ships three ANN backings, each with a different
* sweet spot:
*
* `SearchableEmbeddingCache` (HNSW + `@ruvector/core`)
* - in-memory, streaming insert + delete, ANN search
* - sweet spot: <100k vectors, frequently mutated, low-latency
*
* `RabitqSnapshot` (1-bit quantization + `@ruvector/rabitq-wasm`)
* - in-memory, batch-build only, 32× memory reduction with
* exact-L2² rerank
* - sweet spot: 100k-1M vectors, mostly read, memory-tight
*
* `DiskannSnapshot` (DiskANN/Vamana + `@ruvector/diskann`)
* - on-disk via `storagePath`, batch-build (with streaming insert
* before build), survives process restarts
* - sweet spot: >1M vectors, persistent, agent-fleet-scale
*
* Choosing between them requires understanding the workload —
* something a downstream caller often doesn't want to think about.
* `AnnRouter` takes a workload descriptor (`{ corpusSize, persistent,
* mutable }`) and routes to the right backing automatically, behind a
* single unified `search(query, k)` interface.
*
* Routing rules (documented + enumerable, see `decideBacking()`):
*
* persistent=true → DiskANN (only on-disk option)
* corpusSize ≥ 1_000_000 → DiskANN (memory pressure)
* corpusSize ≥ 100_000 + !mutable → RaBitQ (batch, compressed)
* default (small, mutable, in-RAM)→ HNSW cache
*
* When the preferred backing's peer dep isn't installed, the router
* falls back to the next-best available one and reports `degraded:
* true` + a `degradationReason`. Operators always get *something*
* that works (even if it's the linear-scan fallback inside the HNSW
* cache) rather than a hard failure.
*/
export type AnnBacking = 'hnsw' | 'rabitq' | 'diskann';
export interface AnnRouterWorkload {
/** Approximate corpus size (vectors). Used for the routing decision. */
readonly corpusSize: number;
/** Embedding dimension. Required. */
readonly dimension: number;
/**
* Will the corpus need to survive process restarts? When true,
* forces DiskANN regardless of size.
*/
readonly persistent?: boolean;
/**
* Will the corpus be mutated after the initial batch (streaming
* inserts, deletes)? When true, prefers HNSW over RaBitQ/DiskANN
* (those are batch-build).
*/
readonly mutable?: boolean;
/**
* On-disk storage path. Required when `persistent: true`. The
* snapshot lives at this path and can be reopened via
* `AnnRouter.load(workload)`.
*/
readonly storagePath?: string;
}
export interface AnnRouterHit {
readonly id: string;
/** Similarity-style score in [0, 1] for HNSW, raw distance for the snapshot backings. Lower-better unless via HNSW. */
readonly score: number;
/** Original vector when the backing returns it (HNSW does, snapshot backings don't always). */
readonly vector?: Float32Array;
}
export interface AnnRouterDecision {
/** Backing selected by `decideBacking`. */
readonly backing: AnnBacking;
/** Free-form rationale — useful for logs + the routing smoke. */
readonly reason: string;
/** Was a fallback used because the preferred backing wasn't available? */
readonly degraded: boolean;
/** When `degraded`, why the preferred backing was unavailable. */
readonly degradationReason?: string;
}
/**
* Pure function exposing the routing decision. Doesn't actually
* instantiate anything — useful for previewing what `AnnRouter.create`
* would do, and for tests that assert routing without booting NAPI.
*/
export declare function decideBacking(workload: AnnRouterWorkload): {
backing: AnnBacking;
reason: string;
};
/**
* Unified ANN-router handle. Always exposes the same `add`/`build`/
* `search` surface regardless of which backing is live. Callers can
* inspect `decision` to see what was picked + why.
*/
export declare class AnnRouter {
readonly decision: AnnRouterDecision;
private readonly workload;
private hnsw;
private rabitq;
private diskann;
private pending;
private constructor();
/**
* Pick a backing, instantiate it, return the handle. The router
* never throws on missing peer deps — it falls back to a less-
* preferred backing and reports `degraded: true`.
*/
static create(workload: AnnRouterWorkload): Promise<AnnRouter>;
/**
* Insert vectors. For HNSW this is immediate. For RaBitQ this
* buffers until `build()`. For DiskANN this calls `add()` on the
* underlying snapshot (also requires `build()` before search).
*/
add(entries: ReadonlyArray<{
id: string;
vector: Float32Array;
}>): Promise<void>;
/**
* Build the index. No-op for HNSW (streaming); required for
* RaBitQ + DiskANN before the first `search`.
*/
build(): Promise<void>;
/**
* Search the active backing. Always returns `AnnRouterHit[]` sorted
* such that the more-relevant result comes first — HNSW returns
* similarity-descending; RaBitQ + DiskANN return distance-ascending
* (the lower the score, the better the match). Consumers reading
* `score` should interpret it relative to `decision.backing`.
*/
search(query: Float32Array, k: number): Promise<AnnRouterHit[]>;
/**
* Current vector count. Differs slightly by backing (HNSW returns
* cache size; RaBitQ returns batch size; DiskANN includes tombstones).
*/
count(): number;
/**
* Persist the snapshot. Only meaningful for DiskANN (the other
* backings are in-memory). No-op + returns false otherwise.
*/
save(): boolean;
}
//# sourceMappingURL=ann-router.d.ts.map