@soulcraft/brainy 3.34.0 → 3.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,69 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [3.36.0](https://github.com/soulcraftlabs/brainy/compare/v3.35.0...v3.36.0) (2025-10-10)
6
+
7
+ #### 🚀 Always-Adaptive Caching with Enhanced Monitoring
8
+
9
+ **Zero Breaking Changes** - Internal optimizations with automatic performance improvements
10
+
11
+ #### What's New
12
+
13
+ - **Renamed API**: `getLazyModeStats()` → `getCacheStats()` (backward compatible)
14
+ - **Enhanced Metrics**: Changed `lazyModeEnabled: boolean` → `cachingStrategy: 'preloaded' | 'on-demand'`
15
+ - **Improved Thresholds**: Updated preloading threshold from 30% to 80% for better cache utilization
16
+ - **Better Terminology**: Eliminated "lazy mode" concept in favor of "adaptive caching strategy"
17
+ - **Production Monitoring**: Comprehensive diagnostics for capacity planning and tuning
18
+
19
+ #### Benefits
20
+
21
+ - ✅ **Clearer Semantics**: "preloaded" vs "on-demand" instead of confusing "lazy mode enabled/disabled"
22
+ - ✅ **Better Cache Utilization**: 80% threshold maximizes memory usage before switching to on-demand
23
+ - ✅ **Enhanced Monitoring**: `getCacheStats()` provides actionable insights for production deployments
24
+ - ✅ **Backward Compatible**: Deprecated `lazy` option still accepted (ignored, always adaptive)
25
+ - ✅ **Zero Config**: System automatically chooses optimal strategy based on dataset size and available memory
26
+
27
+ #### API Changes
28
+
29
+ ```typescript
30
+ // New API (recommended)
31
+ const stats = brain.hnsw.getCacheStats()
32
+ console.log(`Strategy: ${stats.cachingStrategy}`) // 'preloaded' or 'on-demand'
33
+ console.log(`Hit Rate: ${stats.unifiedCache.hitRatePercent}%`)
34
+ console.log(`Recommendations: ${stats.recommendations.join(', ')}`)
35
+
36
+ // Old API (deprecated but still works)
37
+ const oldStats = brain.hnsw.getLazyModeStats() // Returns same data
38
+ ```
39
+
40
+ #### Documentation Updates
41
+
42
+ - Added comprehensive migration guide: `docs/guides/migration-3.36.0.md`
43
+ - Added operations guide: `docs/operations/capacity-planning.md`
44
+ - Updated architecture docs with new terminology
45
+ - Renamed example: `monitor-lazy-mode.ts` → `monitor-cache-performance.ts`
46
+
47
+ #### Files Changed
48
+
49
+ - `src/hnsw/hnswIndex.ts`: Core adaptive caching improvements
50
+ - `src/interfaces/IIndex.ts`: Updated interface documentation
51
+ - `docs/guides/migration-3.36.0.md`: Complete migration guide
52
+ - `docs/operations/capacity-planning.md`: Enterprise operations guide
53
+ - `examples/monitor-cache-performance.ts`: Production monitoring example
54
+ - All documentation updated to reflect new terminology
55
+
56
+ #### Migration
57
+
58
+ **No action required!** All changes are backward compatible. Update your code to use `getCacheStats()` when convenient.
59
+
60
+ ---
61
+
62
+ ### [3.35.0](https://github.com/soulcraftlabs/brainy/compare/v3.34.0...v3.35.0) (2025-10-10)
63
+
64
+ - feat: implement HNSW index rebuild and unified index interface (6a4d1ae)
65
+ - cleaning up (12d78ba)
66
+
67
+
5
68
  ### [3.34.0](https://github.com/soulcraftlabs/brainy/compare/v3.33.0...v3.34.0) (2025-10-09)
6
69
 
7
70
  - test: adjust type-matching tests for real embeddings (v3.33.0) (1c5c77e)
package/README.md CHANGED
@@ -19,6 +19,32 @@
19
19
 
20
20
  ## 🎉 Key Features
21
21
 
22
+ ### ⚡ **NEW in 3.36.0: Production-Scale Memory & Performance**
23
+
24
+ **Enterprise-grade adaptive sizing and zero-overhead optimizations:**
25
+
26
+ - **🎯 Adaptive Memory Sizing**: Auto-scales from 2GB to 128GB+ based on available system resources
27
+ - Container-aware (Docker/K8s cgroups v1/v2 detection)
28
+ - Environment-smart (development 25%, container 40%, production 50% allocation)
29
+ - Model memory accounting (150MB Q8, 250MB FP32 reserved before cache)
30
+
31
+ - **⚡ Sync Fast Path**: Zero async overhead when vectors are cached
32
+ - Intelligent sync/async branching - synchronous when data is in memory
33
+ - Falls back to async only when loading from storage
34
+ - Massive performance win for hot paths (vector search, distance calculations)
35
+
36
+ - **📊 Production Monitoring**: Comprehensive diagnostics
37
+ - `getCacheStats()` - UnifiedCache hit rates, fairness metrics, memory pressure
38
+ - Actionable recommendations for tuning
39
+ - Tracks model memory, cache efficiency, and competition across indexes
40
+
41
+ - **🛡️ Zero Breaking Changes**: All optimizations are internal - your code stays the same
42
+ - Public API unchanged
43
+ - Automatic memory detection and allocation
44
+ - Progressive enhancement for existing applications
45
+
46
+ **[📖 Operations Guide →](docs/operations/capacity-planning.md)** | **[🎯 Migration Guide →](docs/guides/migration-3.36.0.md)**
47
+
22
48
  ### 🚀 **NEW in 3.21.0: Enhanced Import & Neural Processing**
23
49
 
24
50
  - **📊 Progress Tracking**: Unified progress reporting with automatic time estimation
@@ -38,7 +64,7 @@
38
64
 
39
65
  - **Modern Syntax**: `brain.add()`, `brain.find()`, `brain.relate()`
40
66
  - **Type Safety**: Full TypeScript integration
41
- - **Zero Config**: Works out of the box with memory storage
67
+ - **Zero Config**: Works out of the box with intelligent storage auto-detection
42
68
  - **Consistent Parameters**: Clean, predictable API surface
43
69
 
44
70
  ### ⚡ **Performance & Reliability**
@@ -352,7 +378,7 @@ const brain = new Brainy()
352
378
 
353
379
  // 2. Custom configuration
354
380
  const brain = new Brainy({
355
- storage: { type: 'memory' },
381
+ storage: { type: 'filesystem', path: './brainy-data' },
356
382
  embeddings: { model: 'all-MiniLM-L6-v2' },
357
383
  cache: { enabled: true, maxSize: 1000 }
358
384
  })
@@ -368,7 +394,7 @@ const customBrain = new Brainy({
368
394
 
369
395
  **What's Auto-Detected:**
370
396
 
371
- - **Storage**: S3/GCS/R2 → Filesystem → Memory (priority order)
397
+ - **Storage**: S3/GCS/R2 → Filesystem (priority order)
372
398
  - **Models**: Always Q8 for optimal balance
373
399
  - **Features**: Minimal → Default → Full based on environment
374
400
  - **Memory**: Optimal cache sizes and batching
@@ -390,13 +416,12 @@ Most users **never need this** - zero-config handles everything. For advanced us
390
416
  const brain = new Brainy() // Uses Q8 automatically
391
417
 
392
418
  // Storage control (auto-detected by default)
393
- const memoryBrain = new Brainy({storage: 'memory'}) // RAM only
394
- const diskBrain = new Brainy({storage: 'disk'}) // Local filesystem
419
+ const diskBrain = new Brainy({storage: 'disk'}) // Local filesystem
395
420
  const cloudBrain = new Brainy({storage: 'cloud'}) // S3/GCS/R2
396
421
 
397
422
  // Legacy full config (still supported)
398
423
  const legacyBrain = new Brainy({
399
- storage: {forceMemoryStorage: true}
424
+ storage: {type: 'filesystem', path: './data'}
400
425
  })
401
426
  ```
402
427
 
@@ -665,12 +690,7 @@ const context = await brain.find({
665
690
  Brainy supports multiple storage backends:
666
691
 
667
692
  ```javascript
668
- // Memory (default for testing)
669
- const brain = new Brainy({
670
- storage: {type: 'memory'}
671
- })
672
-
673
- // FileSystem (Node.js)
693
+ // FileSystem (Node.js - recommended for development)
674
694
  const brain = new Brainy({
675
695
  storage: {
676
696
  type: 'filesystem',
package/dist/brainy.d.ts CHANGED
@@ -1070,6 +1070,21 @@ export declare class Brainy<T = any> implements BrainyInterface<T> {
1070
1070
  /**
1071
1071
  * Rebuild indexes if there's existing data but empty indexes
1072
1072
  */
1073
+ /**
1074
+ * Rebuild indexes from persisted data if needed (v3.35.0+)
1075
+ *
1076
+ * FIXES FOR CRITICAL BUGS:
1077
+ * - Bug #1: GraphAdjacencyIndex rebuild never called ✅ FIXED
1078
+ * - Bug #2: Early return blocks recovery when count=0 ✅ FIXED
1079
+ * - Bug #4: HNSW index has no rebuild mechanism ✅ FIXED
1080
+ *
1081
+ * Production-grade rebuild with:
1082
+ * - Handles millions of entities via pagination
1083
+ * - Smart threshold-based decisions (auto-rebuild < 1000 items)
1084
+ * - Progress reporting for large datasets
1085
+ * - Parallel index rebuilds for performance
1086
+ * - Robust error recovery (continues on partial failures)
1087
+ */
1073
1088
  private rebuildIndexesIfNeeded;
1074
1089
  /**
1075
1090
  * Close and cleanup
package/dist/brainy.js CHANGED
@@ -2385,59 +2385,88 @@ export class Brainy {
2385
2385
  /**
2386
2386
  * Rebuild indexes if there's existing data but empty indexes
2387
2387
  */
2388
+ /**
2389
+ * Rebuild indexes from persisted data if needed (v3.35.0+)
2390
+ *
2391
+ * FIXES FOR CRITICAL BUGS:
2392
+ * - Bug #1: GraphAdjacencyIndex rebuild never called ✅ FIXED
2393
+ * - Bug #2: Early return blocks recovery when count=0 ✅ FIXED
2394
+ * - Bug #4: HNSW index has no rebuild mechanism ✅ FIXED
2395
+ *
2396
+ * Production-grade rebuild with:
2397
+ * - Handles millions of entities via pagination
2398
+ * - Smart threshold-based decisions (auto-rebuild < 1000 items)
2399
+ * - Progress reporting for large datasets
2400
+ * - Parallel index rebuilds for performance
2401
+ * - Robust error recovery (continues on partial failures)
2402
+ */
2388
2403
  async rebuildIndexesIfNeeded() {
2389
2404
  try {
2390
- // Check if storage has data
2405
+ // Check if auto-rebuild is explicitly disabled
2406
+ if (this.config.disableAutoRebuild === true) {
2407
+ if (!this.config.silent) {
2408
+ console.log('⚡ Auto-rebuild explicitly disabled via config');
2409
+ }
2410
+ return;
2411
+ }
2412
+ // BUG #2 FIX: Don't trust counts - check actual storage instead
2413
+ // Counts can be lost/corrupted in container restarts
2391
2414
  const entities = await this.storage.getNouns({ pagination: { limit: 1 } });
2392
2415
  const totalCount = entities.totalCount || 0;
2393
- if (totalCount === 0) {
2394
- // No data in storage, no rebuild needed
2416
+ // If storage is truly empty, no rebuild needed
2417
+ if (totalCount === 0 && entities.items.length === 0) {
2395
2418
  return;
2396
2419
  }
2397
2420
  // Intelligent decision: Auto-rebuild only for small datasets
2398
2421
  // For large datasets, use lazy loading for optimal performance
2399
2422
  const AUTO_REBUILD_THRESHOLD = 1000; // Only auto-rebuild if < 1000 items
2400
- // Check if metadata index is empty
2423
+ // Check if indexes need rebuilding
2401
2424
  const metadataStats = await this.metadataIndex.getStats();
2402
- if (metadataStats.totalEntries === 0 && totalCount > 0) {
2403
- if (totalCount < AUTO_REBUILD_THRESHOLD) {
2404
- // Small dataset - rebuild for convenience
2405
- if (!this.config.silent) {
2406
- console.log(`🔄 Small dataset (${totalCount} items) - rebuilding index for optimal performance...`);
2407
- }
2408
- await this.metadataIndex.rebuild();
2409
- const newStats = await this.metadataIndex.getStats();
2410
- if (!this.config.silent) {
2411
- console.log(`✅ Index rebuilt: ${newStats.totalEntries} entries`);
2412
- }
2413
- }
2414
- else {
2415
- // Large dataset - use lazy loading
2416
- if (!this.config.silent) {
2417
- console.log(`⚡ Large dataset (${totalCount} items) - using lazy loading for optimal startup performance`);
2418
- console.log('💡 Tip: Indexes will build automatically as you use the system');
2419
- }
2420
- }
2425
+ const hnswIndexSize = this.index.size();
2426
+ const graphIndexSize = await this.graphIndex.size();
2427
+ const needsRebuild = metadataStats.totalEntries === 0 ||
2428
+ hnswIndexSize === 0 ||
2429
+ graphIndexSize === 0 ||
2430
+ this.config.disableAutoRebuild === false; // Explicitly enabled
2431
+ if (!needsRebuild) {
2432
+ // All indexes populated, no rebuild needed
2433
+ return;
2421
2434
  }
2422
- // Override with explicit config if provided
2423
- if (this.config.disableAutoRebuild === true) {
2435
+ // Small dataset: Rebuild all indexes for best performance
2436
+ if (totalCount < AUTO_REBUILD_THRESHOLD || this.config.disableAutoRebuild === false) {
2424
2437
  if (!this.config.silent) {
2425
- console.log('⚡ Auto-rebuild explicitly disabled via config');
2438
+ console.log(this.config.disableAutoRebuild === false
2439
+ ? '🔄 Auto-rebuild explicitly enabled - rebuilding all indexes...'
2440
+ : `🔄 Small dataset (${totalCount} items) - rebuilding all indexes...`);
2441
+ }
2442
+ // BUG #1 FIX: Actually call graphIndex.rebuild()
2443
+ // BUG #4 FIX: Actually call HNSW index.rebuild()
2444
+ // Rebuild all 3 indexes in parallel for performance
2445
+ const startTime = Date.now();
2446
+ await Promise.all([
2447
+ metadataStats.totalEntries === 0 ? this.metadataIndex.rebuild() : Promise.resolve(),
2448
+ hnswIndexSize === 0 ? this.index.rebuild() : Promise.resolve(),
2449
+ graphIndexSize === 0 ? this.graphIndex.rebuild() : Promise.resolve()
2450
+ ]);
2451
+ const duration = Date.now() - startTime;
2452
+ if (!this.config.silent) {
2453
+ console.log(`✅ All indexes rebuilt in ${duration}ms:\n` +
2454
+ ` - Metadata: ${await this.metadataIndex.getStats().then(s => s.totalEntries)} entries\n` +
2455
+ ` - HNSW Vector: ${this.index.size()} nodes\n` +
2456
+ ` - Graph Adjacency: ${await this.graphIndex.size()} relationships`);
2426
2457
  }
2427
- return;
2428
2458
  }
2429
- else if (this.config.disableAutoRebuild === false && metadataStats.totalEntries === 0) {
2430
- // Explicitly enabled - rebuild regardless of size
2459
+ else {
2460
+ // Large dataset: Use lazy loading for fast startup
2431
2461
  if (!this.config.silent) {
2432
- console.log('🔄 Auto-rebuild explicitly enabled - rebuilding index...');
2462
+ console.log(`⚡ Large dataset (${totalCount} items) - using lazy loading for optimal startup`);
2463
+ console.log('💡 Indexes will build automatically as you query the system');
2433
2464
  }
2434
- await this.metadataIndex.rebuild();
2435
2465
  }
2436
- // Note: GraphAdjacencyIndex will rebuild itself as relationships are added
2437
- // Vector index should already be populated if storage has data
2438
2466
  }
2439
2467
  catch (error) {
2440
- console.warn('Warning: Could not check or rebuild indexes:', error);
2468
+ console.warn('Warning: Could not rebuild indexes:', error);
2469
+ // Don't throw - allow system to start even if rebuild fails
2441
2470
  }
2442
2471
  }
2443
2472
  /**
@@ -3,6 +3,7 @@
3
3
  * Based on the paper: "Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs"
4
4
  */
5
5
  import { DistanceFunction, HNSWConfig, HNSWNoun, Vector, VectorDocument } from '../coreTypes.js';
6
+ import type { BaseStorage } from '../storage/baseStorage.js';
6
7
  export declare class HNSWIndex {
7
8
  private nouns;
8
9
  private entryPointId;
@@ -13,8 +14,11 @@ export declare class HNSWIndex {
13
14
  private distanceFunction;
14
15
  private dimension;
15
16
  private useParallelization;
17
+ private storage;
18
+ private unifiedCache;
16
19
  constructor(config?: Partial<HNSWConfig>, distanceFunction?: DistanceFunction, options?: {
17
20
  useParallelization?: boolean;
21
+ storage?: BaseStorage;
18
22
  });
19
23
  /**
20
24
  * Set whether to use parallelization for performance-critical operations
@@ -45,7 +49,7 @@ export declare class HNSWIndex {
45
49
  /**
46
50
  * Remove an item from the index
47
51
  */
48
- removeItem(id: string): boolean;
52
+ removeItem(id: string): Promise<boolean>;
49
53
  /**
50
54
  * Get all nouns in the index
51
55
  * @deprecated Use getNounsPaginated() instead for better scalability
@@ -93,11 +97,86 @@ export declare class HNSWIndex {
93
97
  * Get the configuration
94
98
  */
95
99
  getConfig(): HNSWConfig;
100
+ /**
101
+ * Get vector safely (always uses adaptive caching via UnifiedCache)
102
+ *
103
+ * Production-grade adaptive caching (v3.36.0+):
104
+ * - Vector already loaded: Returns immediately (O(1))
105
+ * - Vector in cache: Loads from UnifiedCache (O(1) hash lookup)
106
+ * - Vector on disk: Loads from storage → UnifiedCache (O(disk))
107
+ * - Cost-aware caching: UnifiedCache manages memory competition
108
+ *
109
+ * @param noun The HNSW noun (may have empty vector if not yet loaded)
110
+ * @returns Promise<Vector> The vector (loaded on-demand if needed)
111
+ */
112
+ private getVectorSafe;
113
+ /**
114
+ * Get vector synchronously if available in memory (v3.36.0+)
115
+ *
116
+ * Sync fast path optimization:
117
+ * - Vector in memory: Returns immediately (zero overhead)
118
+ * - Vector in cache: Returns from UnifiedCache synchronously
119
+ * - Returns null if vector not available (caller must handle async path)
120
+ *
121
+ * Use for sync fast path in distance calculations - eliminates async overhead
122
+ * when vectors are already cached.
123
+ *
124
+ * @param noun The HNSW noun
125
+ * @returns Vector | null - vector if in memory/cache, null if needs async load
126
+ */
127
+ private getVectorSync;
128
+ /**
129
+ * Preload multiple vectors in parallel via UnifiedCache
130
+ *
131
+ * Optimization for search operations:
132
+ * - Loads all candidate vectors before distance calculations
133
+ * - Reduces serial disk I/O (parallel loads are faster)
134
+ * - Uses UnifiedCache's request coalescing to prevent stampede
135
+ * - Always active (no "mode" check) for optimal performance
136
+ *
137
+ * @param nodeIds Array of node IDs to preload
138
+ */
139
+ private preloadVectors;
140
+ /**
141
+ * Calculate distance with sync fast path (v3.36.0+)
142
+ *
143
+ * Eliminates async overhead when vectors are in memory:
144
+ * - Sync path: Vector in memory → returns number (zero overhead)
145
+ * - Async path: Vector needs loading → returns Promise<number>
146
+ *
147
+ * Callers must handle union type: `const dist = await Promise.resolve(distance)`
148
+ *
149
+ * @param queryVector The query vector
150
+ * @param noun The target noun (may have empty vector in lazy mode)
151
+ * @returns number | Promise<number> - sync when cached, async when needs load
152
+ */
153
+ private distanceSafe;
96
154
  /**
97
155
  * Get all nodes at a specific level for clustering
98
156
  * This enables O(n) clustering using HNSW's natural hierarchy
99
157
  */
100
158
  getNodesAtLevel(level: number): HNSWNoun[];
159
+ /**
160
+ * Rebuild HNSW index from persisted graph data (v3.35.0+)
161
+ *
162
+ * This is a production-grade O(N) rebuild that restores the pre-computed graph structure
163
+ * from storage. Much faster than re-building which is O(N log N).
164
+ *
165
+ * Designed for millions of entities with:
166
+ * - Cursor-based pagination (no memory overflow)
167
+ * - Batch processing (configurable batch size)
168
+ * - Progress reporting (optional callback)
169
+ * - Error recovery (continues on partial failures)
170
+ * - Lazy mode support (memory-efficient for constrained environments)
171
+ *
172
+ * @param options Rebuild options
173
+ * @returns Promise that resolves when rebuild is complete
174
+ */
175
+ rebuild(options?: {
176
+ lazy?: boolean;
177
+ batchSize?: number;
178
+ onProgress?: (loaded: number, total: number) => void;
179
+ }): Promise<void>;
101
180
  /**
102
181
  * Get level statistics for understanding the hierarchy
103
182
  */
@@ -115,6 +194,54 @@ export declare class HNSWIndex {
115
194
  maxLayer: number;
116
195
  totalNodes: number;
117
196
  };
197
+ /**
198
+ * Get cache performance statistics for monitoring and diagnostics (v3.36.0+)
199
+ *
200
+ * Production-grade monitoring:
201
+ * - Adaptive caching strategy (preloading vs on-demand)
202
+ * - UnifiedCache performance (hits, misses, evictions)
203
+ * - HNSW-specific cache statistics
204
+ * - Fair competition metrics across all indexes
205
+ * - Actionable recommendations for tuning
206
+ *
207
+ * Use this to:
208
+ * - Diagnose performance issues (low hit rate = increase cache)
209
+ * - Monitor memory competition (fairness violations = adjust costs)
210
+ * - Verify adaptive caching decisions (memory estimates vs actual)
211
+ * - Track cache efficiency over time
212
+ *
213
+ * @returns Comprehensive caching and performance statistics
214
+ */
215
+ getCacheStats(): {
216
+ cachingStrategy: 'preloaded' | 'on-demand';
217
+ autoDetection: {
218
+ entityCount: number;
219
+ estimatedVectorMemoryMB: number;
220
+ availableCacheMB: number;
221
+ threshold: number;
222
+ rationale: string;
223
+ };
224
+ unifiedCache: {
225
+ totalSize: number;
226
+ maxSize: number;
227
+ utilizationPercent: number;
228
+ itemCount: number;
229
+ hitRatePercent: number;
230
+ totalAccessCount: number;
231
+ };
232
+ hnswCache: {
233
+ vectorsInCache: number;
234
+ cacheKeyPrefix: string;
235
+ estimatedMemoryMB: number;
236
+ };
237
+ fairness: {
238
+ hnswAccessCount: number;
239
+ hnswAccessPercent: number;
240
+ totalAccessCount: number;
241
+ fairnessViolation: boolean;
242
+ };
243
+ recommendations: string[];
244
+ };
118
245
  /**
119
246
  * Search within a specific layer
120
247
  * Returns a map of noun IDs to distances, sorted by distance