@claude-flow/embeddings
Version:
V3 Embedding Service - OpenAI, Transformers.js, Agentic-Flow (ONNX), Mock providers with hyperbolic embeddings, normalization, and chunking
146 lines • 6.17 kB
TypeScript
/**
* SearchableEmbeddingCache — ADR-121 Phase 2.
*
* The legacy `PersistentEmbeddingCache` and the in-process `LRUCache`
* only support exact-key lookup. That's fine for "I've seen this
* exact text before" but useless for "I haven't computed this exact
* text but I have something within ε" — which is the whole point of
* a vector cache for embedding-heavy workloads.
*
* This module adds an HNSW-backed cache implementation that:
* - Implements the same key-value contract as the legacy cache
* (back-compat: existing callers don't change)
* - Adds `search(query, k)` and `searchByText(text, k, embed)` for
* ANN retrieval over the cached vectors
* - Lazy-loads `@ruvector/core` as an optional peer dep; falls back
* to in-memory exact-only when the dep isn't installed
*
* Phase 2 acceptance:
* - ≥0.95 near-duplicate recall (where exact key match would be 0)
* - ≥10k inserts/sec on the standard test machine
* - Legacy PersistentEmbeddingCache unchanged
*/
import type { EmbeddingResult } from './types.js';
/**
* Search result row.
*
* `score` is a **similarity** score: higher = more similar, identical
* vectors → 1.0. The cache normalizes the raw distance metric returned
* by `@ruvector/core` (which returns cosine *distance* and Euclidean
* *distance*) into the similarity convention used elsewhere in this
* package (e.g. `cosineSimilarity()`).
*
* Conversion table:
* cosine : similarity = 1 - distance
* dot-product: similarity = -distance (already in dot-product space)
* euclidean : similarity = 1 / (1 + distance) (monotonic, bounded [0,1])
* manhattan : similarity = 1 / (1 + distance)
*/
export interface CacheSearchHit {
/** Stable opaque id assigned to the cached entry. */
readonly id: string;
/** The cached embedding vector. */
readonly vector: Float32Array;
/** The original text the vector embeds, when available. */
readonly text?: string;
/** Similarity score in [0, 1] (higher = more similar). */
readonly score: number;
}
export interface SearchableCacheOptions {
/** Embedding dimension. Required — must match the producer service. */
readonly dimension: number;
/** Distance metric (default 'cosine'). */
readonly metric?: 'cosine' | 'euclidean' | 'dot-product' | 'manhattan';
/** Max in-memory entries (default 10,000). */
readonly maxSize?: number;
/**
* Quantization mode for cached vectors (ADR-121 Phase 3, alpha.21):
* 'none' (default) — full float32 in memory
* 'int8' — per-vector int8 + scale, 4× memory reduction,
* recall ≥0.98 on unit-normalized vectors
* RaBitQ (1-bit, 32× reduction) is a separate batch API; see
* `RabitqSnapshot` for that path — it doesn't fit the streaming
* insert/get/delete contract this cache exposes.
*/
readonly quantize?: 'none' | 'int8';
}
export declare class SearchableEmbeddingCache {
private backing;
private degraded;
private readonly options;
/** Map from cache key (text or content hash) to the assigned vector id. */
private readonly keyToId;
/** Reverse map from vector id back to the original text key. */
private readonly idToKey;
/**
* Per-id int8 store. Only populated when `quantize === 'int8'`. The
* cache continues to hand back float32 vectors to callers (dequantized
* on demand) — quantization is a memory-layout concern transparent
* to the IEmbeddingService contract.
*/
private readonly int8Store;
private nextId;
private hits;
private misses;
constructor(options: SearchableCacheOptions);
/** Lazy-initialize the HNSW backing. Idempotent. */
initialize(): Promise<void>;
/** Cache the embedding under the given text key. */
set(key: string, vector: Float32Array, opts?: {
id?: string;
}): Promise<void>;
/** Exact-key lookup. Returns the cached vector or undefined. */
get(key: string): Promise<Float32Array | undefined>;
/** Whether the exact key is cached. */
has(key: string): Promise<boolean>;
/** Remove a cached entry by key. */
delete(key: string): Promise<boolean>;
/**
* ANN search over the cached embeddings. Returns top-k neighbors
* by similarity, sorted descending by score (higher = more similar).
*/
search(query: Float32Array, k?: number): Promise<CacheSearchHit[]>;
/**
* Normalize whatever the backing returns (Float32Array, number[],
* or an object-as-array from a NAPI bridge) into a Float32Array.
*/
private toFloat32Array;
/**
* Embed a text query (via the supplied embedder), then ANN search
* for nearest cached neighbors. Convenience helper so callers
* don't have to wire embed → search manually.
*/
searchByText(text: string, k: number, embed: (text: string) => Promise<EmbeddingResult>): Promise<CacheSearchHit[]>;
/**
* Snapshot of cache stats — size, hit rate, backing status.
*
* Size is read from the local id-map (synchronous) instead of the
* backing's `len()` because the real `@ruvector/core` returns a
* Promise for that. The id-map stays in lockstep with the backing
* through `set`/`delete`/`clear`/`maybeEvict`, so this is exact.
*/
stats(): {
size: number;
hits: number;
misses: number;
hitRate: number;
degraded: boolean;
metric: string;
dimension: number;
quantize: 'none' | 'int8';
estimatedBytes: number;
};
/** Clear all cached entries. */
clear(): Promise<void>;
/**
* FIFO eviction when over maxSize. Async because the backing's
* `delete` may be async (the real NAPI binding returns a Promise).
*/
private maybeEvict;
}
/**
* Factory — keeps the constructor a one-liner and matches the
* createXxxService convention used elsewhere in the package.
*/
export declare function createSearchableEmbeddingCache(options: SearchableCacheOptions): SearchableEmbeddingCache;
//# sourceMappingURL=searchable-embedding-cache.d.ts.map