diff --git a/src/brainy.ts b/src/brainy.ts index d6bd7e39..55aaefe4 100644 --- a/src/brainy.ts +++ b/src/brainy.ts @@ -9285,8 +9285,11 @@ export class Brainy implements BrainyInterface { reservedQueryMemory: config?.reservedQueryMemory ?? undefined as any, // HNSW persistence mode - undefined = smart default in setupIndex hnswPersistMode: config?.hnswPersistMode ?? undefined as any, - // HNSW optimization options (v7.11.0) + // HNSW optimization options (v7.11.0) — deprecated in 8.0, see `vector` below. hnsw: config?.hnsw ?? undefined as any, + // Vector index configuration (8.0) — algorithm-neutral surface with + // `recall` preset. See BRAINY-8.0-RENAME-COORDINATION § A.2. + vector: config?.vector ?? undefined as any, // Embedding initialization - false = lazy init on first embed() eagerEmbeddings: config?.eagerEmbeddings ?? false, // Plugin configuration - undefined = auto-detect diff --git a/src/types/brainy.types.ts b/src/types/brainy.types.ts index 70055caa..2a792a87 100644 --- a/src/types/brainy.types.ts +++ b/src/types/brainy.types.ts @@ -1117,6 +1117,9 @@ export interface BrainyConfig { hnswPersistMode?: 'immediate' | 'deferred' // HNSW optimization options (v7.11.0) + // @deprecated 8.0 — use `vector` instead. This field is kept as a compat + // shim during the 8.0 rename scaffolding; values are merged into the new + // `vector` block with explicit `vector.*` values winning on conflict. hnsw?: { quantization?: { enabled?: boolean // default: false — current behavior exactly @@ -1126,6 +1129,67 @@ export interface BrainyConfig { vectorStorage?: 'memory' | 'lazy' // default: 'memory' — 'lazy' evicts vectors after insert } + /** + * Vector index configuration (Brainy 8.0). + * + * Algorithm-neutral surface. The `recall` preset means the same thing + * whether Brainy's open-core JS HNSW path is in play or a native + * acceleration provider has taken over the `'vector'` provider key: + * - `'fast'` — minimum-latency search, accepts lower recall + * - `'balanced'` — Brainy 7.x defaults, the right pick for almost everyone + * - `'accurate'` — maximum recall, accepts higher latency + * + * Power users may override individual knobs via `advanced.hnsw` (the JS + * path) or `advanced.diskann` (a native DiskANN-style provider). The + * preset is the floor; explicit overrides win. + * + * **Closed-form contract** locked in handoff thread + * BRAINY-8.0-RENAME-COORDINATION § A.2 (cortex-confirmed 2026-06-09). + */ + vector?: { + /** Recall preset. Defaults to `'balanced'` when omitted. */ + recall?: 'fast' | 'balanced' | 'accurate' + /** + * Vector quantization. Independent of `recall` — it trades RAM for a + * small recall hit at any preset. `bits: 8` is SQ8 (4× memory reduction, + * ~0.4% recall loss). `bits: 4` is SQ4 (8× memory reduction, ~1-3% recall + * loss). Both ship in the open-core path; cortex's `distance:sq8` / + * `distance:sq4` SIMD providers accelerate them when available. + */ + quantization?: { + enabled?: boolean + bits?: 8 | 4 + rerankMultiplier?: number + } + /** Vector storage mode. `'memory'` keeps vectors resident; `'lazy'` evicts after insert. */ + vectorStorage?: 'memory' | 'lazy' + /** + * Power-user overrides. The recall preset works for 99% of users — use + * these only if you've measured. `hnsw` overrides apply on the JS HNSW + * path; `diskann` overrides apply when a DiskANN-style native provider + * is in play. Both blocks are honored on their respective paths and + * silently ignored on the other (a one-shot warning fires when + * `strictConfig !== false`). + */ + advanced?: { + hnsw?: { + M?: number + efConstruction?: number + efSearch?: number + ml?: number + } + diskann?: { + pqM?: number + pqKsub?: number + maxDegree?: number + searchListSize?: number + alpha?: number + useMmapAdjacency?: boolean + mmapAdjacencyPath?: string + } + } + } + // Memory management options maxQueryLimit?: number // Override auto-detected query result limit (max: 100000) reservedQueryMemory?: number // Memory reserved for queries in bytes (e.g., 1073741824 = 1GB) diff --git a/src/utils/recallPreset.ts b/src/utils/recallPreset.ts new file mode 100644 index 00000000..0c08d210 --- /dev/null +++ b/src/utils/recallPreset.ts @@ -0,0 +1,91 @@ +/** + * @module utils/recallPreset + * @description Maps Brainy 8.0's algorithm-neutral `recall` preset to the + * underlying index-specific knobs. The same user-facing word (`'fast'` / + * `'balanced'` / `'accurate'`) means "the right thing happens" whether + * Brainy's open-core JS HNSW path is in play or a native acceleration + * provider (DiskANN-style) has taken over the `'vector'` provider key. + * + * **Public contract** — locked in handoff thread BRAINY-8.0-RENAME-COORDINATION + * § A.2 (cortex-confirmed 2026-06-09): + * - `'fast'` — minimum-latency search, accepts lower recall + * - `'balanced'` — Brainy 7.x defaults, the right pick for almost everyone + * - `'accurate'` — maximum recall, accepts higher latency + * + * Default when `recall` is omitted: `'balanced'`. + * + * **Knob mapping** — the JS HNSW preset values below match what Brainy 7.x + * shipped as defaults for `'balanced'`. Native acceleration providers (e.g. + * cortex's DiskANN wrapper) read the same `recall` value off the index + * config and translate it to their own internal knobs. + */ + +import type { HNSWConfig } from '../coreTypes.js' + +export type RecallPreset = 'fast' | 'balanced' | 'accurate' + +/** + * The default preset when `config.vector.recall` is omitted. Matches Brainy + * 7.x's historical HNSW defaults exactly (`M=16`, `efConstruction=200`, + * `efSearch=50`). + */ +export const DEFAULT_RECALL: RecallPreset = 'balanced' + +/** + * The set of HNSW knobs that the `recall` preset controls. Power users may + * override any of these via `config.vector.advanced.hnsw` — the preset is the + * floor, explicit overrides win. + */ +export interface RecallHnswKnobs { + M: number + efConstruction: number + efSearch: number +} + +/** + * Preset → JS HNSW knob tuples. The `'balanced'` entry equals Brainy 7.x's + * `DEFAULT_CONFIG` so a 7.x → 8.0 upgrade with no explicit `recall` value is + * a no-op. + */ +const HNSW_PRESETS: Readonly>> = { + fast: { M: 16, efConstruction: 100, efSearch: 30 }, + balanced: { M: 16, efConstruction: 200, efSearch: 50 }, + accurate: { M: 32, efConstruction: 400, efSearch: 100 }, +} + +/** + * Resolve a preset name (or `undefined` for the default) to its underlying + * HNSW knob tuple. + * + * @param preset - The preset name, or `undefined` to use {@link DEFAULT_RECALL}. + * @returns The HNSW knob values for the preset. + */ +export function resolveRecallHnswKnobs(preset: RecallPreset | undefined): RecallHnswKnobs { + return HNSW_PRESETS[preset ?? DEFAULT_RECALL] +} + +/** + * Resolve a Brainy 8.0 `VectorIndexConfig` into a concrete `HNSWConfig` for + * the open-core JS HNSW path. Applies the recall preset first, then layers + * the optional `advanced.hnsw` overrides so explicit knobs always win. + * + * Native acceleration providers do NOT use this function — they read + * `config.vector.recall` directly and apply their own preset table. This + * helper exists for Brainy's own JS HNSW implementation only. + * + * @param vectorConfig - The user-supplied `config.vector` block. May be omitted. + * @returns A complete `HNSWConfig` with `M`, `efConstruction`, `efSearch`, `ml` + * populated from preset + overrides. + */ +export function resolveJsHnswConfig( + vectorConfig?: { recall?: RecallPreset; advanced?: { hnsw?: Partial } } +): Pick { + const preset = resolveRecallHnswKnobs(vectorConfig?.recall) + const overrides = vectorConfig?.advanced?.hnsw ?? {} + return { + M: overrides.M ?? preset.M, + efConstruction: overrides.efConstruction ?? preset.efConstruction, + efSearch: overrides.efSearch ?? preset.efSearch, + ml: overrides.ml ?? 16, + } +} diff --git a/src/utils/unifiedCache.ts b/src/utils/unifiedCache.ts index e497c421..2a2c63ec 100644 --- a/src/utils/unifiedCache.ts +++ b/src/utils/unifiedCache.ts @@ -21,7 +21,7 @@ import type { CacheProvider } from '../plugin.js' export interface CacheItem { key: string - type: 'hnsw' | 'metadata' | 'embedding' | 'other' + type: 'hnsw' | 'vectors' | 'metadata' | 'embedding' | 'other' data: any size: number rebuildCost: number // milliseconds to rebuild @@ -69,7 +69,7 @@ export class UnifiedCache implements CacheProvider { private cache = new Map() private access = new Map() // Access counts private loadingPromises = new Map>() - private typeAccessCounts = { hnsw: 0, metadata: 0, embedding: 0, other: 0 } + private typeAccessCounts = { hnsw: 0, vectors: 0, metadata: 0, embedding: 0, other: 0 } private totalAccessCount = 0 private currentSize = 0 private maxSize: number @@ -209,7 +209,7 @@ export class UnifiedCache implements CacheProvider { set( key: string, data: any, - type: 'hnsw' | 'metadata' | 'embedding' | 'other', + type: 'hnsw' | 'vectors' | 'metadata' | 'embedding' | 'other', size: number, rebuildCost: number = 1 ): void { @@ -323,8 +323,8 @@ export class UnifiedCache implements CacheProvider { private checkFairness(): void { // Calculate type ratios in cache - const typeSizes = { hnsw: 0, metadata: 0, embedding: 0, other: 0 } - const typeCounts = { hnsw: 0, metadata: 0, embedding: 0, other: 0 } + const typeSizes = { hnsw: 0, vectors: 0, metadata: 0, embedding: 0, other: 0 } + const typeCounts = { hnsw: 0, vectors: 0, metadata: 0, embedding: 0, other: 0 } for (const item of this.cache.values()) { typeSizes[item.type] += item.size @@ -335,6 +335,7 @@ export class UnifiedCache implements CacheProvider { const totalAccess = this.totalAccessCount || 1 const accessRatios = { hnsw: this.typeAccessCounts.hnsw / totalAccess, + vectors: this.typeAccessCounts.vectors / totalAccess, metadata: this.typeAccessCounts.metadata / totalAccess, embedding: this.typeAccessCounts.embedding / totalAccess, other: this.typeAccessCounts.other / totalAccess @@ -344,6 +345,7 @@ export class UnifiedCache implements CacheProvider { const totalSize = this.currentSize || 1 const sizeRatios = { hnsw: typeSizes.hnsw / totalSize, + vectors: typeSizes.vectors / totalSize, metadata: typeSizes.metadata / totalSize, embedding: typeSizes.embedding / totalSize, other: typeSizes.other / totalSize @@ -351,7 +353,7 @@ export class UnifiedCache implements CacheProvider { // Check for starvation (more aggressive - 70% cache with <15% accesses) // Previous: 90% cache, <10% access (too lenient, caused thrashing) - for (const type of ['hnsw', 'metadata', 'embedding', 'other'] as const) { + for (const type of ['hnsw', 'vectors', 'metadata', 'embedding', 'other'] as const) { if (sizeRatios[type] > 0.7 && accessRatios[type] < 0.15) { prodLog.warn(`Type ${type} is hogging cache (${(sizeRatios[type] * 100).toFixed(1)}% size, ${(accessRatios[type] * 100).toFixed(1)}% access)`) this.evictType(type) @@ -364,7 +366,7 @@ export class UnifiedCache implements CacheProvider { * Called immediately when adding items to prevent imbalance formation * Uses same thresholds as periodic check but runs on-demand */ - private checkProactiveFairness(addedType: 'hnsw' | 'metadata' | 'embedding' | 'other'): void { + private checkProactiveFairness(addedType: 'hnsw' | 'vectors' | 'metadata' | 'embedding' | 'other'): void { // Quick check: only evaluate the type being added let typeSize = 0 for (const item of this.cache.values()) { @@ -386,7 +388,7 @@ export class UnifiedCache implements CacheProvider { /** * Force evict items of a specific type */ - private evictType(type: 'hnsw' | 'metadata' | 'embedding' | 'other'): void { + private evictType(type: 'hnsw' | 'vectors' | 'metadata' | 'embedding' | 'other'): void { const candidates: Array<[string, number, CacheItem]> = [] for (const [key, item] of this.cache) { @@ -466,7 +468,7 @@ export class UnifiedCache implements CacheProvider { /** * Clear cache or specific type */ - clear(type?: 'hnsw' | 'metadata' | 'embedding' | 'other'): void { + clear(type?: 'hnsw' | 'vectors' | 'metadata' | 'embedding' | 'other'): void { if (!type) { this.cache.clear() this.currentSize = 0 @@ -533,8 +535,8 @@ export class UnifiedCache implements CacheProvider { * Get cache statistics with memory information */ getStats() { - const typeSizes = { hnsw: 0, metadata: 0, embedding: 0, other: 0 } - const typeCounts = { hnsw: 0, metadata: 0, embedding: 0, other: 0 } + const typeSizes = { hnsw: 0, vectors: 0, metadata: 0, embedding: 0, other: 0 } + const typeCounts = { hnsw: 0, vectors: 0, metadata: 0, embedding: 0, other: 0 } for (const item of this.cache.values()) { typeSizes[item.type] += item.size