@soulcraft/brainy 3.35.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.
@@ -3,6 +3,8 @@
3
3
  * Based on the paper: "Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs"
4
4
  */
5
5
  import { euclideanDistance, calculateDistancesBatch } from '../utils/index.js';
6
+ import { getGlobalCache } from '../utils/unifiedCache.js';
7
+ import { prodLog } from '../utils/logger.js';
6
8
  // Default HNSW parameters
7
9
  const DEFAULT_CONFIG = {
8
10
  M: 16, // Max number of connections per noun
@@ -11,6 +13,7 @@ const DEFAULT_CONFIG = {
11
13
  ml: 16 // Max level
12
14
  };
13
15
  export class HNSWIndex {
16
+ // Always-adaptive caching (v3.36.0+) - no "mode" concept, system adapts automatically
14
17
  constructor(config = {}, distanceFunction = euclideanDistance, options = {}) {
15
18
  this.nouns = new Map();
16
19
  this.entryPointId = null;
@@ -28,6 +31,8 @@ export class HNSWIndex {
28
31
  ? options.useParallelization
29
32
  : true;
30
33
  this.storage = options.storage || null;
34
+ // Use SAME UnifiedCache as Graph and Metadata for fair memory competition
35
+ this.unifiedCache = getGlobalCache();
31
36
  }
32
37
  /**
33
38
  * Set whether to use parallelization for performance-critical operations
@@ -138,7 +143,8 @@ export class HNSWIndex {
138
143
  return id;
139
144
  }
140
145
  let currObj = entryPoint;
141
- let currDist = this.distanceFunction(vector, entryPoint.vector);
146
+ // Calculate distance to entry point (handles lazy loading + sync fast path)
147
+ let currDist = await Promise.resolve(this.distanceSafe(vector, entryPoint));
142
148
  // Traverse the graph from top to bottom to find the closest noun
143
149
  for (let level = this.maxLevel; level > nounLevel; level--) {
144
150
  let changed = true;
@@ -146,13 +152,17 @@ export class HNSWIndex {
146
152
  changed = false;
147
153
  // Check all neighbors at current level
148
154
  const connections = currObj.connections.get(level) || new Set();
155
+ // OPTIMIZATION: Preload neighbor vectors for parallel loading
156
+ if (connections.size > 0) {
157
+ await this.preloadVectors(Array.from(connections));
158
+ }
149
159
  for (const neighborId of connections) {
150
160
  const neighbor = this.nouns.get(neighborId);
151
161
  if (!neighbor) {
152
162
  // Skip neighbors that don't exist (expected during rapid additions/deletions)
153
163
  continue;
154
164
  }
155
- const distToNeighbor = this.distanceFunction(vector, neighbor.vector);
165
+ const distToNeighbor = await Promise.resolve(this.distanceSafe(vector, neighbor));
156
166
  if (distToNeighbor < currDist) {
157
167
  currDist = distToNeighbor;
158
168
  currObj = neighbor;
@@ -182,7 +192,7 @@ export class HNSWIndex {
182
192
  neighbor.connections.get(level).add(id);
183
193
  // Ensure neighbor doesn't have too many connections
184
194
  if (neighbor.connections.get(level).size > this.config.M) {
185
- this.pruneConnections(neighbor, level);
195
+ await this.pruneConnections(neighbor, level);
186
196
  }
187
197
  // Persist updated neighbor HNSW data (v3.35.0+)
188
198
  if (this.storage) {
@@ -276,7 +286,9 @@ export class HNSWIndex {
276
286
  return [];
277
287
  }
278
288
  let currObj = entryPoint;
279
- let currDist = this.distanceFunction(queryVector, currObj.vector);
289
+ // OPTIMIZATION: Preload entry point vector
290
+ await this.preloadVectors([entryPoint.id]);
291
+ let currDist = await Promise.resolve(this.distanceSafe(queryVector, currObj));
280
292
  // Traverse the graph from top to bottom to find the closest noun
281
293
  for (let level = this.maxLevel; level > 0; level--) {
282
294
  let changed = true;
@@ -284,6 +296,10 @@ export class HNSWIndex {
284
296
  changed = false;
285
297
  // Check all neighbors at current level
286
298
  const connections = currObj.connections.get(level) || new Set();
299
+ // OPTIMIZATION: Preload all neighbor vectors in parallel before distance calculations
300
+ if (connections.size > 0) {
301
+ await this.preloadVectors(Array.from(connections));
302
+ }
287
303
  // If we have enough connections, use parallel distance calculation
288
304
  if (this.useParallelization && connections.size >= 10) {
289
305
  // Prepare vectors for parallel calculation
@@ -292,7 +308,8 @@ export class HNSWIndex {
292
308
  const neighbor = this.nouns.get(neighborId);
293
309
  if (!neighbor)
294
310
  continue;
295
- vectors.push({ id: neighborId, vector: neighbor.vector });
311
+ const neighborVector = await this.getVectorSafe(neighbor);
312
+ vectors.push({ id: neighborId, vector: neighborVector });
296
313
  }
297
314
  // Calculate distances in parallel
298
315
  const distances = await this.calculateDistancesInParallel(queryVector, vectors);
@@ -316,7 +333,7 @@ export class HNSWIndex {
316
333
  // Skip neighbors that don't exist (expected during rapid additions/deletions)
317
334
  continue;
318
335
  }
319
- const distToNeighbor = this.distanceFunction(queryVector, neighbor.vector);
336
+ const distToNeighbor = await Promise.resolve(this.distanceSafe(queryVector, neighbor));
320
337
  if (distToNeighbor < currDist) {
321
338
  currDist = distToNeighbor;
322
339
  currObj = neighbor;
@@ -336,7 +353,7 @@ export class HNSWIndex {
336
353
  /**
337
354
  * Remove an item from the index
338
355
  */
339
- removeItem(id) {
356
+ async removeItem(id) {
340
357
  if (!this.nouns.has(id)) {
341
358
  return false;
342
359
  }
@@ -352,7 +369,7 @@ export class HNSWIndex {
352
369
  if (neighbor.connections.has(level)) {
353
370
  neighbor.connections.get(level).delete(id);
354
371
  // Prune connections after removing this noun to ensure consistency
355
- this.pruneConnections(neighbor, level);
372
+ await this.pruneConnections(neighbor, level);
356
373
  }
357
374
  }
358
375
  }
@@ -364,7 +381,7 @@ export class HNSWIndex {
364
381
  if (connections.has(id)) {
365
382
  connections.delete(id);
366
383
  // Prune connections after removing this reference
367
- this.pruneConnections(otherNoun, level);
384
+ await this.pruneConnections(otherNoun, level);
368
385
  }
369
386
  }
370
387
  }
@@ -473,6 +490,120 @@ export class HNSWIndex {
473
490
  getConfig() {
474
491
  return { ...this.config };
475
492
  }
493
+ /**
494
+ * Get vector safely (always uses adaptive caching via UnifiedCache)
495
+ *
496
+ * Production-grade adaptive caching (v3.36.0+):
497
+ * - Vector already loaded: Returns immediately (O(1))
498
+ * - Vector in cache: Loads from UnifiedCache (O(1) hash lookup)
499
+ * - Vector on disk: Loads from storage → UnifiedCache (O(disk))
500
+ * - Cost-aware caching: UnifiedCache manages memory competition
501
+ *
502
+ * @param noun The HNSW noun (may have empty vector if not yet loaded)
503
+ * @returns Promise<Vector> The vector (loaded on-demand if needed)
504
+ */
505
+ async getVectorSafe(noun) {
506
+ // Vector already in memory
507
+ if (noun.vector.length > 0) {
508
+ return noun.vector;
509
+ }
510
+ // Load from UnifiedCache with storage fallback
511
+ const cacheKey = `hnsw:vector:${noun.id}`;
512
+ const vector = await this.unifiedCache.get(cacheKey, async () => {
513
+ // Cache miss - load from storage
514
+ if (!this.storage) {
515
+ throw new Error('Storage not available for vector loading');
516
+ }
517
+ const loaded = await this.storage.getNounVector(noun.id);
518
+ if (!loaded) {
519
+ throw new Error(`Vector not found for noun ${noun.id}`);
520
+ }
521
+ // Add to UnifiedCache with cost-aware eviction
522
+ // This competes fairly with Graph and Metadata indexes
523
+ this.unifiedCache.set(cacheKey, loaded, 'hnsw', // Type for fairness monitoring
524
+ loaded.length * 4, // Size in bytes (float32)
525
+ 50 // Rebuild cost in ms (moderate priority)
526
+ );
527
+ return loaded;
528
+ });
529
+ return vector;
530
+ }
531
+ /**
532
+ * Get vector synchronously if available in memory (v3.36.0+)
533
+ *
534
+ * Sync fast path optimization:
535
+ * - Vector in memory: Returns immediately (zero overhead)
536
+ * - Vector in cache: Returns from UnifiedCache synchronously
537
+ * - Returns null if vector not available (caller must handle async path)
538
+ *
539
+ * Use for sync fast path in distance calculations - eliminates async overhead
540
+ * when vectors are already cached.
541
+ *
542
+ * @param noun The HNSW noun
543
+ * @returns Vector | null - vector if in memory/cache, null if needs async load
544
+ */
545
+ getVectorSync(noun) {
546
+ // Vector already in memory
547
+ if (noun.vector.length > 0) {
548
+ return noun.vector;
549
+ }
550
+ // Try sync cache lookup
551
+ const cacheKey = `hnsw:vector:${noun.id}`;
552
+ const vector = this.unifiedCache.getSync(cacheKey);
553
+ return vector || null;
554
+ }
555
+ /**
556
+ * Preload multiple vectors in parallel via UnifiedCache
557
+ *
558
+ * Optimization for search operations:
559
+ * - Loads all candidate vectors before distance calculations
560
+ * - Reduces serial disk I/O (parallel loads are faster)
561
+ * - Uses UnifiedCache's request coalescing to prevent stampede
562
+ * - Always active (no "mode" check) for optimal performance
563
+ *
564
+ * @param nodeIds Array of node IDs to preload
565
+ */
566
+ async preloadVectors(nodeIds) {
567
+ if (nodeIds.length === 0)
568
+ return;
569
+ // Use UnifiedCache's request coalescing to prevent duplicate loads
570
+ const promises = nodeIds.map(async (id) => {
571
+ const cacheKey = `hnsw:vector:${id}`;
572
+ return this.unifiedCache.get(cacheKey, async () => {
573
+ if (!this.storage)
574
+ return null;
575
+ const vector = await this.storage.getNounVector(id);
576
+ if (vector) {
577
+ this.unifiedCache.set(cacheKey, vector, 'hnsw', vector.length * 4, 50);
578
+ }
579
+ return vector;
580
+ });
581
+ });
582
+ await Promise.all(promises);
583
+ }
584
+ /**
585
+ * Calculate distance with sync fast path (v3.36.0+)
586
+ *
587
+ * Eliminates async overhead when vectors are in memory:
588
+ * - Sync path: Vector in memory → returns number (zero overhead)
589
+ * - Async path: Vector needs loading → returns Promise<number>
590
+ *
591
+ * Callers must handle union type: `const dist = await Promise.resolve(distance)`
592
+ *
593
+ * @param queryVector The query vector
594
+ * @param noun The target noun (may have empty vector in lazy mode)
595
+ * @returns number | Promise<number> - sync when cached, async when needs load
596
+ */
597
+ distanceSafe(queryVector, noun) {
598
+ // Try sync fast path
599
+ const nounVector = this.getVectorSync(noun);
600
+ if (nounVector !== null) {
601
+ // SYNC PATH: Vector in memory - zero async overhead
602
+ return this.distanceFunction(queryVector, nounVector);
603
+ }
604
+ // ASYNC PATH: Vector needs loading from storage
605
+ return this.getVectorSafe(noun).then(loadedVector => this.distanceFunction(queryVector, loadedVector));
606
+ }
476
607
  /**
477
608
  * Get all nodes at a specific level for clustering
478
609
  * This enables O(n) clustering using HNSW's natural hierarchy
@@ -505,11 +636,10 @@ export class HNSWIndex {
505
636
  */
506
637
  async rebuild(options = {}) {
507
638
  if (!this.storage) {
508
- console.warn('HNSW rebuild skipped: no storage adapter configured');
639
+ prodLog.warn('HNSW rebuild skipped: no storage adapter configured');
509
640
  return;
510
641
  }
511
642
  const batchSize = options.batchSize || 1000;
512
- const lazy = options.lazy || false;
513
643
  try {
514
644
  // Step 1: Clear existing in-memory index
515
645
  this.clear();
@@ -519,7 +649,25 @@ export class HNSWIndex {
519
649
  this.entryPointId = systemData.entryPointId;
520
650
  this.maxLevel = systemData.maxLevel;
521
651
  }
522
- // Step 3: Paginate through all nouns and restore HNSW graph structure
652
+ // Step 3: Determine preloading strategy (adaptive caching)
653
+ // Check if vectors should be preloaded at init or loaded on-demand
654
+ const stats = await this.storage.getStatistics();
655
+ const entityCount = stats?.totalNodes || 0;
656
+ // Estimate memory needed for all vectors (384 dims × 4 bytes = 1536 bytes/vector)
657
+ const vectorMemory = entityCount * 1536;
658
+ // Get available cache size (80% threshold - preload only if fits comfortably)
659
+ const cacheStats = this.unifiedCache.getStats();
660
+ const availableCache = cacheStats.maxSize * 0.80;
661
+ const shouldPreload = vectorMemory < availableCache;
662
+ if (shouldPreload) {
663
+ prodLog.info(`HNSW: Preloading ${entityCount.toLocaleString()} vectors at init ` +
664
+ `(${(vectorMemory / 1024 / 1024).toFixed(1)}MB < ${(availableCache / 1024 / 1024).toFixed(1)}MB cache)`);
665
+ }
666
+ else {
667
+ prodLog.info(`HNSW: Adaptive caching for ${entityCount.toLocaleString()} vectors ` +
668
+ `(${(vectorMemory / 1024 / 1024).toFixed(1)}MB > ${(availableCache / 1024 / 1024).toFixed(1)}MB cache) - loading on-demand`);
669
+ }
670
+ // Step 4: Paginate through all nouns and restore HNSW graph structure
523
671
  let loadedCount = 0;
524
672
  let totalCount = undefined;
525
673
  let hasMore = true;
@@ -546,7 +694,7 @@ export class HNSWIndex {
546
694
  // Create noun object with restored connections
547
695
  const noun = {
548
696
  id: nounData.id,
549
- vector: lazy ? [] : nounData.vector, // Empty vector in lazy mode
697
+ vector: shouldPreload ? nounData.vector : [], // Preload if dataset is small
550
698
  connections: new Map(),
551
699
  level: hnswData.level
552
700
  };
@@ -579,12 +727,14 @@ export class HNSWIndex {
579
727
  hasMore = result.hasMore;
580
728
  cursor = result.nextCursor;
581
729
  }
582
- console.log(`HNSW index rebuilt successfully: ${loadedCount} entities, ` +
583
- `${this.maxLevel + 1} levels, entry point: ${this.entryPointId || 'none'}` +
584
- (lazy ? ' (lazy mode - vectors loaded on-demand)' : ''));
730
+ const cacheInfo = shouldPreload
731
+ ? ` (vectors preloaded)`
732
+ : ` (adaptive caching - vectors loaded on-demand)`;
733
+ prodLog.info(`✅ HNSW index rebuilt: ${loadedCount.toLocaleString()} entities, ` +
734
+ `${this.maxLevel + 1} levels, entry point: ${this.entryPointId || 'none'}${cacheInfo}`);
585
735
  }
586
736
  catch (error) {
587
- console.error('HNSW rebuild failed:', error);
737
+ prodLog.error('HNSW rebuild failed:', error);
588
738
  throw new Error(`Failed to rebuild HNSW index: ${error}`);
589
739
  }
590
740
  }
@@ -632,6 +782,97 @@ export class HNSWIndex {
632
782
  totalNodes
633
783
  };
634
784
  }
785
+ /**
786
+ * Get cache performance statistics for monitoring and diagnostics (v3.36.0+)
787
+ *
788
+ * Production-grade monitoring:
789
+ * - Adaptive caching strategy (preloading vs on-demand)
790
+ * - UnifiedCache performance (hits, misses, evictions)
791
+ * - HNSW-specific cache statistics
792
+ * - Fair competition metrics across all indexes
793
+ * - Actionable recommendations for tuning
794
+ *
795
+ * Use this to:
796
+ * - Diagnose performance issues (low hit rate = increase cache)
797
+ * - Monitor memory competition (fairness violations = adjust costs)
798
+ * - Verify adaptive caching decisions (memory estimates vs actual)
799
+ * - Track cache efficiency over time
800
+ *
801
+ * @returns Comprehensive caching and performance statistics
802
+ */
803
+ getCacheStats() {
804
+ // Get UnifiedCache stats
805
+ const cacheStats = this.unifiedCache.getStats();
806
+ // Calculate entity and memory estimates
807
+ const entityCount = this.nouns.size;
808
+ const vectorDimension = this.dimension || 384;
809
+ const bytesPerVector = vectorDimension * 4; // float32
810
+ const estimatedVectorMemoryMB = (entityCount * bytesPerVector) / (1024 * 1024);
811
+ const availableCacheMB = (cacheStats.maxSize * 0.8) / (1024 * 1024); // 80% threshold
812
+ // Calculate HNSW-specific cache stats
813
+ const vectorsInCache = cacheStats.typeCounts.hnsw || 0;
814
+ const hnswMemoryBytes = cacheStats.typeSizes.hnsw || 0;
815
+ // Calculate fairness metrics
816
+ const hnswAccessCount = cacheStats.typeAccessCounts.hnsw || 0;
817
+ const totalAccessCount = cacheStats.totalAccessCount;
818
+ const hnswAccessPercent = totalAccessCount > 0 ? (hnswAccessCount / totalAccessCount) * 100 : 0;
819
+ // Detect fairness violation (>90% cache with <10% access)
820
+ const hnswCachePercent = cacheStats.maxSize > 0 ? (hnswMemoryBytes / cacheStats.maxSize) * 100 : 0;
821
+ const fairnessViolation = hnswCachePercent > 90 && hnswAccessPercent < 10;
822
+ // Calculate hit rate from cache
823
+ const hitRatePercent = (cacheStats.hitRate * 100) || 0;
824
+ // Determine caching strategy (same logic as rebuild())
825
+ const cachingStrategy = estimatedVectorMemoryMB < availableCacheMB ? 'preloaded' : 'on-demand';
826
+ // Generate actionable recommendations
827
+ const recommendations = [];
828
+ if (cachingStrategy === 'on-demand' && hitRatePercent < 50) {
829
+ recommendations.push(`Low cache hit rate (${hitRatePercent.toFixed(1)}%). Consider increasing UnifiedCache size for better performance`);
830
+ }
831
+ if (cachingStrategy === 'preloaded' && estimatedVectorMemoryMB > availableCacheMB * 0.5) {
832
+ recommendations.push(`Dataset growing (${estimatedVectorMemoryMB.toFixed(1)}MB). May switch to on-demand caching as entities increase`);
833
+ }
834
+ if (fairnessViolation) {
835
+ recommendations.push(`Fairness violation: HNSW using ${hnswCachePercent.toFixed(1)}% cache with only ${hnswAccessPercent.toFixed(1)}% access`);
836
+ }
837
+ if (cacheStats.utilization > 0.95) {
838
+ recommendations.push(`Cache utilization high (${(cacheStats.utilization * 100).toFixed(1)}%). Consider increasing cache size`);
839
+ }
840
+ if (recommendations.length === 0) {
841
+ recommendations.push('All metrics healthy - no action needed');
842
+ }
843
+ return {
844
+ cachingStrategy,
845
+ autoDetection: {
846
+ entityCount,
847
+ estimatedVectorMemoryMB: parseFloat(estimatedVectorMemoryMB.toFixed(2)),
848
+ availableCacheMB: parseFloat(availableCacheMB.toFixed(2)),
849
+ threshold: 0.8, // 80% of UnifiedCache
850
+ rationale: cachingStrategy === 'preloaded'
851
+ ? `Vectors preloaded at init (${estimatedVectorMemoryMB.toFixed(1)}MB < ${availableCacheMB.toFixed(1)}MB threshold)`
852
+ : `Adaptive on-demand loading (${estimatedVectorMemoryMB.toFixed(1)}MB > ${availableCacheMB.toFixed(1)}MB threshold)`
853
+ },
854
+ unifiedCache: {
855
+ totalSize: cacheStats.totalSize,
856
+ maxSize: cacheStats.maxSize,
857
+ utilizationPercent: parseFloat((cacheStats.utilization * 100).toFixed(2)),
858
+ itemCount: cacheStats.itemCount,
859
+ hitRatePercent: parseFloat(hitRatePercent.toFixed(2)),
860
+ totalAccessCount: cacheStats.totalAccessCount
861
+ },
862
+ hnswCache: {
863
+ vectorsInCache,
864
+ cacheKeyPrefix: 'hnsw:vector:',
865
+ estimatedMemoryMB: parseFloat((hnswMemoryBytes / (1024 * 1024)).toFixed(2))
866
+ },
867
+ fairness: {
868
+ hnswAccessCount,
869
+ hnswAccessPercent: parseFloat(hnswAccessPercent.toFixed(2)),
870
+ totalAccessCount,
871
+ fairnessViolation
872
+ },
873
+ recommendations
874
+ };
875
+ }
635
876
  /**
636
877
  * Search within a specific layer
637
878
  * Returns a map of noun IDs to distances, sorted by distance
@@ -639,8 +880,10 @@ export class HNSWIndex {
639
880
  async searchLayer(queryVector, entryPoint, ef, level, filter) {
640
881
  // Set of visited nouns
641
882
  const visited = new Set([entryPoint.id]);
642
- // Check if entry point passes filter
643
- const entryPointDistance = this.distanceFunction(queryVector, entryPoint.vector);
883
+ // OPTIMIZATION: Preload entry point vector
884
+ await this.preloadVectors([entryPoint.id]);
885
+ // Check if entry point passes filter (with sync fast path)
886
+ const entryPointDistance = await Promise.resolve(this.distanceSafe(queryVector, entryPoint));
644
887
  const entryPointPasses = filter ? await filter(entryPoint.id) : true;
645
888
  // Priority queue of candidates (closest first)
646
889
  const candidates = new Map();
@@ -663,10 +906,17 @@ export class HNSWIndex {
663
906
  // Explore neighbors of the closest candidate
664
907
  const noun = this.nouns.get(closestId);
665
908
  if (!noun) {
666
- console.error(`Noun with ID ${closestId} not found in searchLayer`);
909
+ prodLog.error(`Noun with ID ${closestId} not found in searchLayer`);
667
910
  continue;
668
911
  }
669
912
  const connections = noun.connections.get(level) || new Set();
913
+ // OPTIMIZATION: Preload unvisited neighbor vectors in parallel
914
+ if (connections.size > 0) {
915
+ const unvisitedIds = Array.from(connections).filter(id => !visited.has(id));
916
+ if (unvisitedIds.length > 0) {
917
+ await this.preloadVectors(unvisitedIds);
918
+ }
919
+ }
670
920
  // If we have enough connections and parallelization is enabled, use parallel distance calculation
671
921
  if (this.useParallelization && connections.size >= 10) {
672
922
  // Collect unvisited neighbors
@@ -677,7 +927,8 @@ export class HNSWIndex {
677
927
  const neighbor = this.nouns.get(neighborId);
678
928
  if (!neighbor)
679
929
  continue;
680
- unvisitedNeighbors.push({ id: neighborId, vector: neighbor.vector });
930
+ const neighborVector = await this.getVectorSafe(neighbor);
931
+ unvisitedNeighbors.push({ id: neighborId, vector: neighborVector });
681
932
  }
682
933
  }
683
934
  if (unvisitedNeighbors.length > 0) {
@@ -717,7 +968,7 @@ export class HNSWIndex {
717
968
  // Skip neighbors that don't exist (expected during rapid additions/deletions)
718
969
  continue;
719
970
  }
720
- const distToNeighbor = this.distanceFunction(queryVector, neighbor.vector);
971
+ const distToNeighbor = await Promise.resolve(this.distanceSafe(queryVector, neighbor));
721
972
  // Apply filter if provided
722
973
  const passes = filter ? await filter(neighborId) : true;
723
974
  // Always add to candidates for graph traversal
@@ -762,7 +1013,7 @@ export class HNSWIndex {
762
1013
  /**
763
1014
  * Ensure a noun doesn't have too many connections at a given level
764
1015
  */
765
- pruneConnections(noun, level) {
1016
+ async pruneConnections(noun, level) {
766
1017
  const connections = noun.connections.get(level);
767
1018
  if (connections.size <= this.config.M) {
768
1019
  return;
@@ -770,14 +1021,20 @@ export class HNSWIndex {
770
1021
  // Calculate distances to all neighbors
771
1022
  const distances = new Map();
772
1023
  const validNeighborIds = new Set();
1024
+ // OPTIMIZATION: Preload all neighbor vectors
1025
+ if (connections.size > 0) {
1026
+ await this.preloadVectors(Array.from(connections));
1027
+ }
773
1028
  for (const neighborId of connections) {
774
1029
  const neighbor = this.nouns.get(neighborId);
775
1030
  if (!neighbor) {
776
1031
  // Skip neighbors that don't exist (expected during rapid additions/deletions)
777
1032
  continue;
778
1033
  }
779
- // Only add valid neighbors to the distances map
780
- distances.set(neighborId, this.distanceFunction(noun.vector, neighbor.vector));
1034
+ // Only add valid neighbors to the distances map (handles lazy loading + sync fast path)
1035
+ const nounVector = await this.getVectorSafe(noun);
1036
+ const distance = await Promise.resolve(this.distanceSafe(nounVector, neighbor));
1037
+ distances.set(neighborId, distance);
781
1038
  validNeighborIds.add(neighborId);
782
1039
  }
783
1040
  // Only proceed if we have valid neighbors
@@ -94,7 +94,6 @@ export declare class HNSWIndexOptimized extends HNSWIndex {
94
94
  private memoryUsage;
95
95
  private vectorCount;
96
96
  private memoryUpdateLock;
97
- private unifiedCache;
98
97
  constructor(config: Partial<HNSWOptimizedConfig>, distanceFunction: DistanceFunction, storage?: BaseStorage | null);
99
98
  /**
100
99
  * Thread-safe method to update memory usage
@@ -120,7 +119,7 @@ export declare class HNSWIndexOptimized extends HNSWIndex {
120
119
  /**
121
120
  * Remove an item from the index
122
121
  */
123
- removeItem(id: string): boolean;
122
+ removeItem(id: string): Promise<boolean>;
124
123
  /**
125
124
  * Clear the index
126
125
  */
@@ -4,7 +4,6 @@
4
4
  * Uses product quantization for dimensionality reduction and disk-based storage when needed
5
5
  */
6
6
  import { HNSWIndex } from './hnswIndex.js';
7
- import { getGlobalCache } from '../utils/unifiedCache.js';
8
7
  // Default configuration for the optimized HNSW index
9
8
  const DEFAULT_OPTIMIZED_CONFIG = {
10
9
  M: 16,
@@ -230,8 +229,7 @@ export class HNSWIndexOptimized extends HNSWIndex {
230
229
  }
231
230
  // Set disk-based index flag
232
231
  this.useDiskBasedIndex = this.optimizedConfig.useDiskBasedIndex || false;
233
- // Get global unified cache for coordinated memory management
234
- this.unifiedCache = getGlobalCache();
232
+ // Note: UnifiedCache is inherited from base HNSWIndex class
235
233
  }
236
234
  /**
237
235
  * Thread-safe method to update memory usage
@@ -332,7 +330,7 @@ export class HNSWIndexOptimized extends HNSWIndex {
332
330
  /**
333
331
  * Remove an item from the index
334
332
  */
335
- removeItem(id) {
333
+ async removeItem(id) {
336
334
  // If product quantization is active, remove the quantized vector
337
335
  if (this.useProductQuantization) {
338
336
  this.quantizedVectors.delete(id);
@@ -349,7 +347,7 @@ export class HNSWIndexOptimized extends HNSWIndex {
349
347
  console.error('Failed to update memory usage after removal:', error);
350
348
  });
351
349
  // Remove the item from the in-memory index
352
- return super.removeItem(id);
350
+ return await super.removeItem(id);
353
351
  }
354
352
  /**
355
353
  * Clear the index
@@ -274,7 +274,7 @@ export class PartitionedHNSWIndex {
274
274
  async removeItem(id) {
275
275
  // Find which partition contains this item
276
276
  for (const [partitionId, partition] of this.partitions.entries()) {
277
- if (partition.removeItem(id)) {
277
+ if (await partition.removeItem(id)) {
278
278
  // Update metadata
279
279
  const metadata = this.partitionMetadata.get(partitionId);
280
280
  metadata.nodeCount = partition.size();
@@ -45,9 +45,13 @@ export type RebuildProgressCallback = (loaded: number, total: number) => void;
45
45
  */
46
46
  export interface RebuildOptions {
47
47
  /**
48
- * Lazy mode: Load structure only, data on-demand
49
- * Saves memory at cost of first-access latency
50
- * (HNSW: vectors loaded on-demand, Graph: relationships cached, Metadata: lazy field indexing)
48
+ * @deprecated Lazy mode is now auto-detected based on available memory.
49
+ * System automatically chooses between:
50
+ * - Preloading: Small datasets that fit comfortably in cache (< 80% threshold)
51
+ * - On-demand: Large datasets loaded adaptively via UnifiedCache
52
+ *
53
+ * This option is kept for backwards compatibility but is ignored.
54
+ * The system always uses adaptive caching (v3.36.0+).
51
55
  */
52
56
  lazy?: boolean;
53
57
  /**
@@ -86,11 +90,16 @@ export interface IIndex {
86
90
  * - Load data from storage using pagination
87
91
  * - Restore index structure efficiently (O(N) preferred over O(N log N))
88
92
  * - Handle millions of entities via batching
89
- * - Support lazy loading for memory-constrained environments
93
+ * - Auto-detect caching strategy based on dataset size vs available memory
90
94
  * - Provide progress reporting for large datasets
91
95
  * - Recover gracefully from partial failures
92
96
  *
93
- * @param options Rebuild options (lazy mode, batch size, progress callback, force)
97
+ * Adaptive Caching (v3.36.0+):
98
+ * System automatically chooses optimal strategy:
99
+ * - Small datasets: Preload all data at init for zero-latency access
100
+ * - Large datasets: Load on-demand via UnifiedCache for memory efficiency
101
+ *
102
+ * @param options Rebuild options (batch size, progress callback, force)
94
103
  * @returns Promise that resolves when rebuild is complete
95
104
  * @throws Error if rebuild fails critically (should log warnings for partial failures)
96
105
  */