@soulcraft/brainy 0.38.0 → 0.39.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/dist/unified.js CHANGED
@@ -16601,6 +16601,515 @@ class HealthMonitor {
16601
16601
  }
16602
16602
  }
16603
16603
 
16604
+ /**
16605
+ * SearchCache - Caches search results for improved performance
16606
+ */
16607
+ class SearchCache {
16608
+ constructor(config = {}) {
16609
+ this.cache = new Map();
16610
+ // Cache statistics
16611
+ this.hits = 0;
16612
+ this.misses = 0;
16613
+ this.evictions = 0;
16614
+ this.maxAge = config.maxAge ?? 5 * 60 * 1000; // 5 minutes
16615
+ this.maxSize = config.maxSize ?? 100;
16616
+ this.enabled = config.enabled ?? true;
16617
+ this.hitCountWeight = config.hitCountWeight ?? 0.3;
16618
+ }
16619
+ /**
16620
+ * Generate cache key from search parameters
16621
+ */
16622
+ getCacheKey(query, k, options = {}) {
16623
+ // Create a normalized key that ignores order of options
16624
+ const normalizedOptions = Object.keys(options)
16625
+ .sort()
16626
+ .reduce((acc, key) => {
16627
+ // Skip cache-related options
16628
+ if (key === 'skipCache' || key === 'useStreaming')
16629
+ return acc;
16630
+ acc[key] = options[key];
16631
+ return acc;
16632
+ }, {});
16633
+ return JSON.stringify({
16634
+ query: typeof query === 'object' ? JSON.stringify(query) : query,
16635
+ k,
16636
+ ...normalizedOptions
16637
+ });
16638
+ }
16639
+ /**
16640
+ * Get cached results if available and not expired
16641
+ */
16642
+ get(key) {
16643
+ if (!this.enabled)
16644
+ return null;
16645
+ const entry = this.cache.get(key);
16646
+ if (!entry) {
16647
+ this.misses++;
16648
+ return null;
16649
+ }
16650
+ // Check if expired
16651
+ if (Date.now() - entry.timestamp > this.maxAge) {
16652
+ this.cache.delete(key);
16653
+ this.misses++;
16654
+ return null;
16655
+ }
16656
+ // Update hit count and statistics
16657
+ entry.hits++;
16658
+ this.hits++;
16659
+ return entry.results;
16660
+ }
16661
+ /**
16662
+ * Cache search results
16663
+ */
16664
+ set(key, results) {
16665
+ if (!this.enabled)
16666
+ return;
16667
+ // Evict if cache is full
16668
+ if (this.cache.size >= this.maxSize) {
16669
+ this.evictOldest();
16670
+ }
16671
+ this.cache.set(key, {
16672
+ results: [...results], // Deep copy to prevent mutations
16673
+ timestamp: Date.now(),
16674
+ hits: 0
16675
+ });
16676
+ }
16677
+ /**
16678
+ * Evict the oldest entry based on timestamp and hit count
16679
+ */
16680
+ evictOldest() {
16681
+ let oldestKey = null;
16682
+ let oldestScore = Infinity;
16683
+ const now = Date.now();
16684
+ for (const [key, entry] of this.cache.entries()) {
16685
+ // Score combines age and inverse hit count
16686
+ const age = now - entry.timestamp;
16687
+ const hitScore = entry.hits > 0 ? 1 / entry.hits : 1;
16688
+ const score = age + (hitScore * this.hitCountWeight * this.maxAge);
16689
+ if (score < oldestScore) {
16690
+ oldestScore = score;
16691
+ oldestKey = key;
16692
+ }
16693
+ }
16694
+ if (oldestKey) {
16695
+ this.cache.delete(oldestKey);
16696
+ this.evictions++;
16697
+ }
16698
+ }
16699
+ /**
16700
+ * Clear all cached results
16701
+ */
16702
+ clear() {
16703
+ this.cache.clear();
16704
+ this.hits = 0;
16705
+ this.misses = 0;
16706
+ this.evictions = 0;
16707
+ }
16708
+ /**
16709
+ * Invalidate cache entries that might be affected by data changes
16710
+ */
16711
+ invalidate(pattern) {
16712
+ if (!pattern) {
16713
+ this.clear();
16714
+ return;
16715
+ }
16716
+ const keysToDelete = [];
16717
+ for (const key of this.cache.keys()) {
16718
+ const shouldDelete = typeof pattern === 'string'
16719
+ ? key.includes(pattern)
16720
+ : pattern.test(key);
16721
+ if (shouldDelete) {
16722
+ keysToDelete.push(key);
16723
+ }
16724
+ }
16725
+ keysToDelete.forEach(key => this.cache.delete(key));
16726
+ }
16727
+ /**
16728
+ * Smart invalidation for real-time data updates
16729
+ * Only clears cache if it's getting stale or if data changes significantly
16730
+ */
16731
+ invalidateOnDataChange(changeType) {
16732
+ // For now, clear all caches on data changes to ensure consistency
16733
+ // In the future, we could implement more sophisticated invalidation
16734
+ // based on the type of change and affected data
16735
+ this.clear();
16736
+ }
16737
+ /**
16738
+ * Check if cache entries have expired and remove them
16739
+ * This is especially important in distributed scenarios where
16740
+ * real-time updates might be delayed or missed
16741
+ */
16742
+ cleanupExpiredEntries() {
16743
+ const now = Date.now();
16744
+ const keysToDelete = [];
16745
+ for (const [key, entry] of this.cache.entries()) {
16746
+ if (now - entry.timestamp > this.maxAge) {
16747
+ keysToDelete.push(key);
16748
+ }
16749
+ }
16750
+ keysToDelete.forEach(key => this.cache.delete(key));
16751
+ return keysToDelete.length;
16752
+ }
16753
+ /**
16754
+ * Get cache statistics
16755
+ */
16756
+ getStats() {
16757
+ const total = this.hits + this.misses;
16758
+ return {
16759
+ hits: this.hits,
16760
+ misses: this.misses,
16761
+ evictions: this.evictions,
16762
+ hitRate: total > 0 ? this.hits / total : 0,
16763
+ size: this.cache.size,
16764
+ maxSize: this.maxSize,
16765
+ enabled: this.enabled
16766
+ };
16767
+ }
16768
+ /**
16769
+ * Enable or disable caching
16770
+ */
16771
+ setEnabled(enabled) {
16772
+ Object.defineProperty(this, 'enabled', { value: enabled, writable: false });
16773
+ if (!enabled) {
16774
+ this.clear();
16775
+ }
16776
+ }
16777
+ /**
16778
+ * Get memory usage estimate in bytes
16779
+ */
16780
+ getMemoryUsage() {
16781
+ let totalSize = 0;
16782
+ for (const [key, entry] of this.cache.entries()) {
16783
+ // Estimate key size
16784
+ totalSize += key.length * 2; // UTF-16 characters
16785
+ // Estimate entry size
16786
+ totalSize += JSON.stringify(entry.results).length * 2;
16787
+ totalSize += 16; // timestamp + hits (8 bytes each)
16788
+ }
16789
+ return totalSize;
16790
+ }
16791
+ /**
16792
+ * Get current cache configuration
16793
+ */
16794
+ getConfig() {
16795
+ return {
16796
+ enabled: this.enabled,
16797
+ maxSize: this.maxSize,
16798
+ maxAge: this.maxAge,
16799
+ hitCountWeight: this.hitCountWeight
16800
+ };
16801
+ }
16802
+ /**
16803
+ * Update cache configuration dynamically
16804
+ */
16805
+ updateConfig(newConfig) {
16806
+ if (newConfig.enabled !== undefined) {
16807
+ this.enabled = newConfig.enabled;
16808
+ }
16809
+ if (newConfig.maxSize !== undefined) {
16810
+ this.maxSize = newConfig.maxSize;
16811
+ // Trigger eviction if current size exceeds new limit
16812
+ this.evictIfNeeded();
16813
+ }
16814
+ if (newConfig.maxAge !== undefined) {
16815
+ this.maxAge = newConfig.maxAge;
16816
+ // Clean up entries that are now expired with new TTL
16817
+ this.cleanupExpiredEntries();
16818
+ }
16819
+ if (newConfig.hitCountWeight !== undefined) {
16820
+ this.hitCountWeight = newConfig.hitCountWeight;
16821
+ }
16822
+ }
16823
+ /**
16824
+ * Evict entries if cache exceeds maxSize
16825
+ */
16826
+ evictIfNeeded() {
16827
+ if (this.cache.size <= this.maxSize) {
16828
+ return;
16829
+ }
16830
+ // Calculate eviction score for each entry (same logic as existing eviction)
16831
+ const entries = Array.from(this.cache.entries()).map(([key, entry]) => {
16832
+ const age = Date.now() - entry.timestamp;
16833
+ const hitCount = entry.hits;
16834
+ // Eviction score: lower is more likely to be evicted
16835
+ // Combines age and hit count (weighted by hitCountWeight)
16836
+ const ageScore = age / this.maxAge;
16837
+ const hitScore = 1 / (hitCount + 1); // Inverse of hits (more hits = lower score)
16838
+ const score = ageScore * (1 - this.hitCountWeight) + hitScore * this.hitCountWeight;
16839
+ return { key, entry, score };
16840
+ });
16841
+ // Sort by score (lowest first - these will be evicted)
16842
+ entries.sort((a, b) => a.score - b.score);
16843
+ // Evict entries until we're under the limit
16844
+ const toEvict = entries.slice(0, this.cache.size - this.maxSize);
16845
+ toEvict.forEach(({ key }) => {
16846
+ this.cache.delete(key);
16847
+ this.evictions++;
16848
+ });
16849
+ }
16850
+ }
16851
+
16852
+ /**
16853
+ * Intelligent cache auto-configuration system
16854
+ * Adapts cache settings based on environment, usage patterns, and storage type
16855
+ */
16856
+ class CacheAutoConfigurator {
16857
+ constructor() {
16858
+ this.stats = {
16859
+ totalQueries: 0,
16860
+ repeatQueries: 0,
16861
+ avgQueryTime: 50,
16862
+ memoryPressure: 0,
16863
+ storageType: 'memory',
16864
+ isDistributed: false,
16865
+ changeFrequency: 0,
16866
+ readWriteRatio: 10,
16867
+ };
16868
+ this.configHistory = [];
16869
+ this.lastOptimization = 0;
16870
+ }
16871
+ /**
16872
+ * Auto-detect optimal cache configuration based on current conditions
16873
+ */
16874
+ autoDetectOptimalConfig(storageConfig, currentStats) {
16875
+ // Update stats with current information
16876
+ if (currentStats) {
16877
+ this.stats = { ...this.stats, ...currentStats };
16878
+ }
16879
+ // Detect environment characteristics
16880
+ this.detectEnvironment(storageConfig);
16881
+ // Generate optimal configuration
16882
+ const result = this.generateOptimalConfig();
16883
+ // Store for learning
16884
+ this.configHistory.push(result);
16885
+ this.lastOptimization = Date.now();
16886
+ return result;
16887
+ }
16888
+ /**
16889
+ * Dynamically adjust configuration based on runtime performance
16890
+ */
16891
+ adaptConfiguration(currentConfig, performanceMetrics) {
16892
+ const reasoning = [];
16893
+ let needsUpdate = false;
16894
+ // Check if we should update (don't over-optimize)
16895
+ if (Date.now() - this.lastOptimization < 60000) {
16896
+ return null; // Wait at least 1 minute between optimizations
16897
+ }
16898
+ // Analyze performance patterns
16899
+ const adaptations = {};
16900
+ // Low hit rate → adjust cache size or TTL
16901
+ if (performanceMetrics.hitRate < 0.3) {
16902
+ if (performanceMetrics.externalChangesDetected > 5) {
16903
+ // Too many external changes → shorter TTL
16904
+ adaptations.maxAge = Math.max(60000, currentConfig.maxAge * 0.7);
16905
+ reasoning.push('Reduced cache TTL due to frequent external changes');
16906
+ needsUpdate = true;
16907
+ }
16908
+ else {
16909
+ // Expand cache size for better hit rate
16910
+ adaptations.maxSize = Math.min(500, (currentConfig.maxSize || 100) * 1.5);
16911
+ reasoning.push('Increased cache size due to low hit rate');
16912
+ needsUpdate = true;
16913
+ }
16914
+ }
16915
+ // High hit rate but slow responses → might need cache warming
16916
+ if (performanceMetrics.hitRate > 0.8 && performanceMetrics.avgResponseTime > 100) {
16917
+ reasoning.push('High hit rate but slow responses - consider cache warming');
16918
+ }
16919
+ // Memory pressure → reduce cache size
16920
+ if (performanceMetrics.memoryUsage > 100 * 1024 * 1024) { // 100MB
16921
+ adaptations.maxSize = Math.max(20, (currentConfig.maxSize || 100) * 0.7);
16922
+ reasoning.push('Reduced cache size due to memory pressure');
16923
+ needsUpdate = true;
16924
+ }
16925
+ // Recent external changes → adaptive TTL
16926
+ if (performanceMetrics.timeSinceLastChange < 30000) { // 30 seconds
16927
+ adaptations.maxAge = Math.max(30000, currentConfig.maxAge * 0.8);
16928
+ reasoning.push('Shortened TTL due to recent external changes');
16929
+ needsUpdate = true;
16930
+ }
16931
+ if (!needsUpdate) {
16932
+ return null;
16933
+ }
16934
+ const newCacheConfig = {
16935
+ ...currentConfig,
16936
+ ...adaptations
16937
+ };
16938
+ const newRealtimeConfig = this.calculateRealtimeConfig();
16939
+ return {
16940
+ cacheConfig: newCacheConfig,
16941
+ realtimeConfig: newRealtimeConfig,
16942
+ reasoning
16943
+ };
16944
+ }
16945
+ /**
16946
+ * Get recommended configuration for specific use case
16947
+ */
16948
+ getRecommendedConfig(useCase) {
16949
+ const configs = {
16950
+ 'high-consistency': {
16951
+ cache: { maxAge: 120000, maxSize: 50 },
16952
+ realtime: { interval: 15000, enabled: true },
16953
+ reasoning: ['Optimized for data consistency and real-time updates']
16954
+ },
16955
+ 'balanced': {
16956
+ cache: { maxAge: 300000, maxSize: 100 },
16957
+ realtime: { interval: 30000, enabled: true },
16958
+ reasoning: ['Balanced performance and consistency']
16959
+ },
16960
+ 'performance-first': {
16961
+ cache: { maxAge: 600000, maxSize: 200 },
16962
+ realtime: { interval: 60000, enabled: true },
16963
+ reasoning: ['Optimized for maximum cache performance']
16964
+ }
16965
+ };
16966
+ const config = configs[useCase];
16967
+ return {
16968
+ cacheConfig: {
16969
+ enabled: true,
16970
+ ...config.cache
16971
+ },
16972
+ realtimeConfig: {
16973
+ updateIndex: true,
16974
+ updateStatistics: true,
16975
+ ...config.realtime
16976
+ },
16977
+ reasoning: config.reasoning
16978
+ };
16979
+ }
16980
+ /**
16981
+ * Learn from usage patterns and improve recommendations
16982
+ */
16983
+ learnFromUsage(usageData) {
16984
+ // Update internal stats for better future recommendations
16985
+ this.stats.totalQueries += usageData.totalQueries;
16986
+ this.stats.repeatQueries += usageData.cacheHits;
16987
+ this.stats.avgQueryTime = (this.stats.avgQueryTime + usageData.responseTime) / 2;
16988
+ this.stats.changeFrequency = usageData.dataChanges / (usageData.timeWindow / 60000);
16989
+ // Calculate read/write ratio
16990
+ const writes = usageData.dataChanges;
16991
+ const reads = usageData.totalQueries;
16992
+ this.stats.readWriteRatio = reads > 0 ? reads / Math.max(writes, 1) : 10;
16993
+ }
16994
+ detectEnvironment(storageConfig) {
16995
+ // Detect storage type
16996
+ if (storageConfig?.s3Storage || storageConfig?.customS3Storage) {
16997
+ this.stats.storageType = 's3';
16998
+ this.stats.isDistributed = true;
16999
+ }
17000
+ else if (storageConfig?.forceFileSystemStorage) {
17001
+ this.stats.storageType = 'filesystem';
17002
+ }
17003
+ else if (storageConfig?.forceMemoryStorage) {
17004
+ this.stats.storageType = 'memory';
17005
+ }
17006
+ else {
17007
+ // Auto-detect browser vs Node.js
17008
+ this.stats.storageType = typeof window !== 'undefined' ? 'opfs' : 'filesystem';
17009
+ }
17010
+ // Detect distributed mode indicators
17011
+ this.stats.isDistributed = this.stats.isDistributed ||
17012
+ Boolean(storageConfig?.s3Storage || storageConfig?.customS3Storage);
17013
+ }
17014
+ generateOptimalConfig() {
17015
+ const reasoning = [];
17016
+ // Base configuration
17017
+ let cacheConfig = {
17018
+ enabled: true,
17019
+ maxSize: 100,
17020
+ maxAge: 300000, // 5 minutes
17021
+ hitCountWeight: 0.3
17022
+ };
17023
+ let realtimeConfig = {
17024
+ enabled: false,
17025
+ interval: 60000,
17026
+ updateIndex: true,
17027
+ updateStatistics: true
17028
+ };
17029
+ // Adjust for storage type
17030
+ if (this.stats.storageType === 's3' || this.stats.isDistributed) {
17031
+ cacheConfig.maxAge = 180000; // 3 minutes for distributed
17032
+ realtimeConfig.enabled = true;
17033
+ realtimeConfig.interval = 30000; // 30 seconds
17034
+ reasoning.push('Distributed storage detected - enabled real-time updates');
17035
+ reasoning.push('Reduced cache TTL for distributed consistency');
17036
+ }
17037
+ // Adjust for read/write patterns
17038
+ if (this.stats.readWriteRatio > 20) {
17039
+ // Read-heavy workload
17040
+ cacheConfig.maxSize = Math.min(300, (cacheConfig.maxSize || 100) * 2);
17041
+ cacheConfig.maxAge = Math.min(900000, (cacheConfig.maxAge || 300000) * 1.5); // Up to 15 minutes
17042
+ reasoning.push('Read-heavy workload detected - increased cache size and TTL');
17043
+ }
17044
+ else if (this.stats.readWriteRatio < 5) {
17045
+ // Write-heavy workload
17046
+ cacheConfig.maxSize = Math.max(50, (cacheConfig.maxSize || 100) * 0.7);
17047
+ cacheConfig.maxAge = Math.max(60000, (cacheConfig.maxAge || 300000) * 0.6);
17048
+ reasoning.push('Write-heavy workload detected - reduced cache size and TTL');
17049
+ }
17050
+ // Adjust for change frequency
17051
+ if (this.stats.changeFrequency > 10) { // More than 10 changes per minute
17052
+ realtimeConfig.interval = Math.max(10000, realtimeConfig.interval * 0.5);
17053
+ cacheConfig.maxAge = Math.max(30000, (cacheConfig.maxAge || 300000) * 0.5);
17054
+ reasoning.push('High change frequency detected - increased update frequency');
17055
+ }
17056
+ // Memory constraints
17057
+ if (this.detectMemoryConstraints()) {
17058
+ cacheConfig.maxSize = Math.max(20, (cacheConfig.maxSize || 100) * 0.6);
17059
+ reasoning.push('Memory constraints detected - reduced cache size');
17060
+ }
17061
+ // Performance optimization
17062
+ if (this.stats.avgQueryTime > 200) {
17063
+ cacheConfig.maxSize = Math.min(500, (cacheConfig.maxSize || 100) * 1.5);
17064
+ reasoning.push('Slow queries detected - increased cache size');
17065
+ }
17066
+ return {
17067
+ cacheConfig,
17068
+ realtimeConfig,
17069
+ reasoning
17070
+ };
17071
+ }
17072
+ calculateRealtimeConfig() {
17073
+ return {
17074
+ enabled: this.stats.isDistributed || this.stats.changeFrequency > 1,
17075
+ interval: this.stats.isDistributed ? 30000 : 60000,
17076
+ updateIndex: true,
17077
+ updateStatistics: true
17078
+ };
17079
+ }
17080
+ detectMemoryConstraints() {
17081
+ // Simple heuristic for memory constraints
17082
+ try {
17083
+ if (typeof performance !== 'undefined' && 'memory' in performance) {
17084
+ const memInfo = performance.memory;
17085
+ return memInfo.usedJSHeapSize > memInfo.jsHeapSizeLimit * 0.8;
17086
+ }
17087
+ }
17088
+ catch (e) {
17089
+ // Ignore errors
17090
+ }
17091
+ // Default assumption for constrained environments
17092
+ return false;
17093
+ }
17094
+ /**
17095
+ * Get human-readable explanation of current configuration
17096
+ */
17097
+ getConfigExplanation(config) {
17098
+ const lines = [
17099
+ '🤖 Brainy Auto-Configuration:',
17100
+ '',
17101
+ `📊 Cache: ${config.cacheConfig.maxSize} queries, ${config.cacheConfig.maxAge / 1000}s TTL`,
17102
+ `🔄 Updates: ${config.realtimeConfig.enabled ? `Every ${(config.realtimeConfig.interval || 30000) / 1000}s` : 'Disabled'}`,
17103
+ '',
17104
+ '🎯 Optimizations applied:'
17105
+ ];
17106
+ config.reasoning.forEach(reason => {
17107
+ lines.push(` • ${reason}`);
17108
+ });
17109
+ return lines.join('\n');
17110
+ }
17111
+ }
17112
+
16604
17113
  /**
16605
17114
  * BrainyData
16606
17115
  * Main class that provides the vector database functionality
@@ -16758,6 +17267,26 @@ class BrainyData {
16758
17267
  this.distributedConfig = config.distributed;
16759
17268
  }
16760
17269
  }
17270
+ // Initialize cache auto-configurator first
17271
+ this.cacheAutoConfigurator = new CacheAutoConfigurator();
17272
+ // Auto-detect optimal cache configuration if not explicitly provided
17273
+ let finalSearchCacheConfig = config.searchCache;
17274
+ if (!config.searchCache || Object.keys(config.searchCache).length === 0) {
17275
+ const autoConfig = this.cacheAutoConfigurator.autoDetectOptimalConfig(config.storage);
17276
+ finalSearchCacheConfig = autoConfig.cacheConfig;
17277
+ // Apply auto-detected real-time update configuration if not explicitly set
17278
+ if (!config.realtimeUpdates && autoConfig.realtimeConfig.enabled) {
17279
+ this.realtimeUpdateConfig = {
17280
+ ...this.realtimeUpdateConfig,
17281
+ ...autoConfig.realtimeConfig
17282
+ };
17283
+ }
17284
+ if (this.loggingConfig?.verbose) {
17285
+ console.log(this.cacheAutoConfigurator.getConfigExplanation(autoConfig));
17286
+ }
17287
+ }
17288
+ // Initialize search cache with final configuration
17289
+ this.searchCache = new SearchCache(finalSearchCacheConfig);
16761
17290
  }
16762
17291
  /**
16763
17292
  * Check if the database is in read-only mode and throw an error if it is
@@ -16897,6 +17426,17 @@ class BrainyData {
16897
17426
  await this.applyChangesFromFullScan();
16898
17427
  }
16899
17428
  }
17429
+ // Cleanup expired cache entries (defensive mechanism for distributed scenarios)
17430
+ const expiredCount = this.searchCache.cleanupExpiredEntries();
17431
+ if (expiredCount > 0 && this.loggingConfig?.verbose) {
17432
+ console.log(`Cleaned up ${expiredCount} expired cache entries`);
17433
+ }
17434
+ // Adapt cache configuration based on performance (every few updates)
17435
+ // Only adapt every 5th update to avoid over-optimization
17436
+ const updateCount = Math.floor((Date.now() - (this.lastUpdateTime || 0)) / this.realtimeUpdateConfig.interval);
17437
+ if (updateCount % 5 === 0) {
17438
+ this.adaptCacheConfiguration();
17439
+ }
16900
17440
  // Update the last update time
16901
17441
  this.lastUpdateTime = Date.now();
16902
17442
  if (this.loggingConfig?.verbose) {
@@ -16971,6 +17511,13 @@ class BrainyData {
16971
17511
  (addedCount > 0 || updatedCount > 0 || deletedCount > 0)) {
16972
17512
  console.log(`Real-time update: Added ${addedCount}, updated ${updatedCount}, deleted ${deletedCount} nouns using change log`);
16973
17513
  }
17514
+ // Invalidate search cache if any external changes were detected
17515
+ if (addedCount > 0 || updatedCount > 0 || deletedCount > 0) {
17516
+ this.searchCache.invalidateOnDataChange('update');
17517
+ if (this.loggingConfig?.verbose) {
17518
+ console.log('Search cache invalidated due to external data changes');
17519
+ }
17520
+ }
16974
17521
  // Update the last known noun count
16975
17522
  this.lastKnownNounCount = await this.getNounCount();
16976
17523
  }
@@ -17014,6 +17561,13 @@ class BrainyData {
17014
17561
  }
17015
17562
  // Update the last known noun count
17016
17563
  this.lastKnownNounCount = currentCount;
17564
+ // Invalidate search cache if new nouns were detected
17565
+ if (newNouns.length > 0) {
17566
+ this.searchCache.invalidateOnDataChange('add');
17567
+ if (this.loggingConfig?.verbose) {
17568
+ console.log('Search cache invalidated due to external data changes');
17569
+ }
17570
+ }
17017
17571
  if (this.loggingConfig?.verbose && newNouns.length > 0) {
17018
17572
  console.log(`Real-time update: Added ${newNouns.length} new nouns to index using full scan`);
17019
17573
  }
@@ -17565,6 +18119,8 @@ class BrainyData {
17565
18119
  console.warn(`Failed to add to remote server: ${remoteError}. Continuing with local add.`);
17566
18120
  }
17567
18121
  }
18122
+ // Invalidate search cache since data has changed
18123
+ this.searchCache.invalidateOnDataChange('add');
17568
18124
  return id;
17569
18125
  }
17570
18126
  catch (error) {
@@ -17822,11 +18378,16 @@ class BrainyData {
17822
18378
  console.log(`Lazy loading mode: Added ${limitedNouns.length} nodes to index for search`);
17823
18379
  }
17824
18380
  }
17825
- // Search in the index
17826
- const results = await this.index.search(queryVector, k);
18381
+ // When using offset, we need to fetch more results and then slice
18382
+ const offset = options.offset || 0;
18383
+ const totalNeeded = k + offset;
18384
+ // Search in the index for totalNeeded results
18385
+ const results = await this.index.search(queryVector, totalNeeded);
18386
+ // Skip the offset number of results
18387
+ const paginatedResults = results.slice(offset, offset + k);
17827
18388
  // Get metadata for each result
17828
18389
  const searchResults = [];
17829
- for (const [id, score] of results) {
18390
+ for (const [id, score] of paginatedResults) {
17830
18391
  const noun = this.index.getNouns().get(id);
17831
18392
  if (!noun) {
17832
18393
  continue;
@@ -17867,8 +18428,9 @@ class BrainyData {
17867
18428
  }
17868
18429
  // Sort by distance (ascending)
17869
18430
  results.sort((a, b) => a[1] - b[1]);
17870
- // Take top k results
17871
- const topResults = results.slice(0, k);
18431
+ // Apply offset and take k results
18432
+ const offset = options.offset || 0;
18433
+ const topResults = results.slice(offset, offset + k);
17872
18434
  // Get metadata for each result
17873
18435
  const searchResults = [];
17874
18436
  for (const [id, score] of topResults) {
@@ -17962,12 +18524,29 @@ class BrainyData {
17962
18524
  }
17963
18525
  // Default behavior (backward compatible): search locally
17964
18526
  try {
18527
+ // Check cache first (transparent to user)
18528
+ const cacheKey = this.searchCache.getCacheKey(queryVectorOrData, k, options);
18529
+ const cachedResults = this.searchCache.get(cacheKey);
18530
+ if (cachedResults) {
18531
+ // Track cache hit in health monitor
18532
+ if (this.healthMonitor) {
18533
+ const latency = Date.now() - startTime;
18534
+ this.healthMonitor.recordRequest(latency, false);
18535
+ this.healthMonitor.recordCacheAccess(true);
18536
+ }
18537
+ return cachedResults;
18538
+ }
18539
+ // Cache miss - perform actual search
17965
18540
  const results = await this.searchLocal(queryVectorOrData, k, options);
18541
+ // Cache results for future queries (unless explicitly disabled)
18542
+ if (!options.skipCache) {
18543
+ this.searchCache.set(cacheKey, results);
18544
+ }
17966
18545
  // Track successful search in health monitor
17967
18546
  if (this.healthMonitor) {
17968
18547
  const latency = Date.now() - startTime;
17969
18548
  this.healthMonitor.recordRequest(latency, false);
17970
- this.healthMonitor.recordCacheAccess(results.length > 0);
18549
+ this.healthMonitor.recordCacheAccess(false);
17971
18550
  }
17972
18551
  return results;
17973
18552
  }
@@ -17980,6 +18559,58 @@ class BrainyData {
17980
18559
  throw error;
17981
18560
  }
17982
18561
  }
18562
+ /**
18563
+ * Search with cursor-based pagination for better performance on large datasets
18564
+ * @param queryVectorOrData Query vector or data to search for
18565
+ * @param k Number of results to return
18566
+ * @param options Additional options including cursor for pagination
18567
+ * @returns Paginated search results with cursor for next page
18568
+ */
18569
+ async searchWithCursor(queryVectorOrData, k = 10, options = {}) {
18570
+ // For cursor-based search, we need to fetch more results and filter
18571
+ const searchK = options.cursor ? k + 20 : k; // Get extra results for filtering
18572
+ // Perform regular search
18573
+ const allResults = await this.search(queryVectorOrData, searchK, {
18574
+ ...options,
18575
+ skipCache: options.skipCache
18576
+ });
18577
+ let results = allResults;
18578
+ let startIndex = 0;
18579
+ // If cursor provided, find starting position
18580
+ if (options.cursor) {
18581
+ startIndex = allResults.findIndex(r => r.id === options.cursor.lastId &&
18582
+ Math.abs(r.score - options.cursor.lastScore) < 0.0001);
18583
+ if (startIndex >= 0) {
18584
+ startIndex += 1; // Start after the cursor position
18585
+ results = allResults.slice(startIndex, startIndex + k);
18586
+ }
18587
+ else {
18588
+ // Cursor not found, might be stale - return from beginning
18589
+ results = allResults.slice(0, k);
18590
+ startIndex = 0;
18591
+ }
18592
+ }
18593
+ else {
18594
+ results = allResults.slice(0, k);
18595
+ }
18596
+ // Create cursor for next page
18597
+ let nextCursor;
18598
+ const hasMoreResults = (startIndex + results.length) < allResults.length || allResults.length >= searchK;
18599
+ if (results.length > 0 && hasMoreResults) {
18600
+ const lastResult = results[results.length - 1];
18601
+ nextCursor = {
18602
+ lastId: lastResult.id,
18603
+ lastScore: lastResult.score,
18604
+ position: startIndex + results.length
18605
+ };
18606
+ }
18607
+ return {
18608
+ results,
18609
+ cursor: nextCursor,
18610
+ hasMore: !!nextCursor,
18611
+ totalEstimate: allResults.length > searchK ? undefined : allResults.length
18612
+ };
18613
+ }
17983
18614
  /**
17984
18615
  * Search the local database for similar vectors
17985
18616
  * @param queryVectorOrData Query vector or data to search for
@@ -18035,14 +18666,16 @@ class BrainyData {
18035
18666
  if (options.nounTypes && options.nounTypes.length > 0) {
18036
18667
  searchResults = await this.searchByNounTypes(queryToUse, k, options.nounTypes, {
18037
18668
  forceEmbed: options.forceEmbed,
18038
- service: options.service
18669
+ service: options.service,
18670
+ offset: options.offset
18039
18671
  });
18040
18672
  }
18041
18673
  else {
18042
18674
  // Otherwise, search all GraphNouns
18043
18675
  searchResults = await this.searchByNounTypes(queryToUse, k, null, {
18044
18676
  forceEmbed: options.forceEmbed,
18045
- service: options.service
18677
+ service: options.service,
18678
+ offset: options.offset
18046
18679
  });
18047
18680
  }
18048
18681
  // Filter out placeholder nouns from search results
@@ -18376,6 +19009,8 @@ class BrainyData {
18376
19009
  catch (error) {
18377
19010
  // Ignore
18378
19011
  }
19012
+ // Invalidate search cache since data has changed
19013
+ this.searchCache.invalidateOnDataChange('delete');
18379
19014
  return true;
18380
19015
  }
18381
19016
  catch (error) {
@@ -18459,6 +19094,8 @@ class BrainyData {
18459
19094
  // Track metadata statistics
18460
19095
  const service = this.getServiceName(options);
18461
19096
  await this.storage.incrementStatistic('metadata', service);
19097
+ // Invalidate search cache since metadata has changed
19098
+ this.searchCache.invalidateOnDataChange('update');
18462
19099
  return true;
18463
19100
  }
18464
19101
  catch (error) {
@@ -18813,6 +19450,8 @@ class BrainyData {
18813
19450
  await this.storage.incrementStatistic('verb', serviceForStats);
18814
19451
  // Update HNSW index size (excluding verbs)
18815
19452
  await this.storage.updateHnswIndexSize(await this.getNounCount());
19453
+ // Invalidate search cache since verb data has changed
19454
+ this.searchCache.invalidateOnDataChange('add');
18816
19455
  return id;
18817
19456
  }
18818
19457
  catch (error) {
@@ -19035,6 +19674,8 @@ class BrainyData {
19035
19674
  await this.index.clear();
19036
19675
  // Clear storage
19037
19676
  await this.storage.clear();
19677
+ // Clear search cache since all data has been removed
19678
+ this.searchCache.invalidateOnDataChange('delete');
19038
19679
  }
19039
19680
  catch (error) {
19040
19681
  console.error('Failed to clear vector database:', error);
@@ -19047,6 +19688,66 @@ class BrainyData {
19047
19688
  size() {
19048
19689
  return this.index.size();
19049
19690
  }
19691
+ /**
19692
+ * Get search cache statistics for performance monitoring
19693
+ * @returns Cache statistics including hit rate and memory usage
19694
+ */
19695
+ getCacheStats() {
19696
+ return {
19697
+ search: this.searchCache.getStats(),
19698
+ searchMemoryUsage: this.searchCache.getMemoryUsage()
19699
+ };
19700
+ }
19701
+ /**
19702
+ * Clear search cache manually (useful for testing or memory management)
19703
+ */
19704
+ clearCache() {
19705
+ this.searchCache.clear();
19706
+ }
19707
+ /**
19708
+ * Adapt cache configuration based on current performance metrics
19709
+ * This method analyzes usage patterns and automatically optimizes cache settings
19710
+ * @private
19711
+ */
19712
+ adaptCacheConfiguration() {
19713
+ const stats = this.searchCache.getStats();
19714
+ const memoryUsage = this.searchCache.getMemoryUsage();
19715
+ const currentConfig = this.searchCache.getConfig();
19716
+ // Prepare performance metrics for adaptation
19717
+ const performanceMetrics = {
19718
+ hitRate: stats.hitRate,
19719
+ avgResponseTime: 50, // Would be measured in real implementation
19720
+ memoryUsage: memoryUsage,
19721
+ externalChangesDetected: 0, // Would be tracked from real-time updates
19722
+ timeSinceLastChange: Date.now() - this.lastUpdateTime
19723
+ };
19724
+ // Try to adapt configuration
19725
+ const newConfig = this.cacheAutoConfigurator.adaptConfiguration(currentConfig, performanceMetrics);
19726
+ if (newConfig) {
19727
+ // Apply new cache configuration
19728
+ this.searchCache.updateConfig(newConfig.cacheConfig);
19729
+ // Apply new real-time update configuration if needed
19730
+ if (newConfig.realtimeConfig.enabled !== this.realtimeUpdateConfig.enabled ||
19731
+ newConfig.realtimeConfig.interval !== this.realtimeUpdateConfig.interval) {
19732
+ const wasEnabled = this.realtimeUpdateConfig.enabled;
19733
+ this.realtimeUpdateConfig = {
19734
+ ...this.realtimeUpdateConfig,
19735
+ ...newConfig.realtimeConfig
19736
+ };
19737
+ // Restart real-time updates with new configuration
19738
+ if (wasEnabled) {
19739
+ this.stopRealtimeUpdates();
19740
+ }
19741
+ if (this.realtimeUpdateConfig.enabled && this.isInitialized) {
19742
+ this.startRealtimeUpdates();
19743
+ }
19744
+ }
19745
+ if (this.loggingConfig?.verbose) {
19746
+ console.log('🔧 Auto-adapted cache configuration:');
19747
+ console.log(this.cacheAutoConfigurator.getConfigExplanation(newConfig));
19748
+ }
19749
+ }
19750
+ }
19050
19751
  /**
19051
19752
  * Get the number of nouns in the database (excluding verbs)
19052
19753
  * This is used for statistics reporting to match the expected behavior in tests
@@ -19593,12 +20294,17 @@ class BrainyData {
19593
20294
  if (!this.serverSearchConduit || !this.serverConnection) {
19594
20295
  throw new Error('Server search conduit or connection is not initialized');
19595
20296
  }
19596
- // Search the remote server
19597
- const searchResult = await this.serverSearchConduit.searchServer(this.serverConnection.connectionId, query, k);
20297
+ // When using offset, fetch more results and slice
20298
+ const offset = options.offset || 0;
20299
+ const totalNeeded = k + offset;
20300
+ // Search the remote server for totalNeeded results
20301
+ const searchResult = await this.serverSearchConduit.searchServer(this.serverConnection.connectionId, query, totalNeeded);
19598
20302
  if (!searchResult.success) {
19599
20303
  throw new Error(`Remote search failed: ${searchResult.error}`);
19600
20304
  }
19601
- return searchResult.data;
20305
+ // Apply offset to remote results
20306
+ const allResults = searchResult.data;
20307
+ return allResults.slice(offset, offset + k);
19602
20308
  }
19603
20309
  catch (error) {
19604
20310
  console.error('Failed to search remote server:', error);