@soulcraft/brainy 0.38.0 → 0.40.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
@@ -4939,7 +4939,8 @@ class HNSWIndex {
4939
4939
  const noun = {
4940
4940
  id,
4941
4941
  vector,
4942
- connections: new Map()
4942
+ connections: new Map(),
4943
+ level: nounLevel
4943
4944
  };
4944
4945
  // Initialize empty connection sets for each level
4945
4946
  for (let level = 0; level <= nounLevel; level++) {
@@ -5264,6 +5265,29 @@ class HNSWIndex {
5264
5265
  getConfig() {
5265
5266
  return { ...this.config };
5266
5267
  }
5268
+ /**
5269
+ * Get index health metrics
5270
+ */
5271
+ getIndexHealth() {
5272
+ let totalConnections = 0;
5273
+ const layerCounts = new Array(this.maxLevel + 1).fill(0);
5274
+ // Count connections and layer distribution
5275
+ this.nouns.forEach(noun => {
5276
+ // Count connections at each layer
5277
+ for (let level = 0; level <= noun.level; level++) {
5278
+ totalConnections += noun.connections.get(level)?.size || 0;
5279
+ layerCounts[level]++;
5280
+ }
5281
+ });
5282
+ const totalNodes = this.nouns.size;
5283
+ const averageConnections = totalNodes > 0 ? totalConnections / totalNodes : 0;
5284
+ return {
5285
+ averageConnections,
5286
+ layerDistribution: layerCounts,
5287
+ maxLayer: this.maxLevel,
5288
+ totalNodes
5289
+ };
5290
+ }
5267
5291
  /**
5268
5292
  * Search within a specific layer
5269
5293
  * Returns a map of noun IDs to distances, sorted by distance
@@ -5721,7 +5745,8 @@ class HNSWIndexOptimized extends HNSWIndex {
5721
5745
  const noun = {
5722
5746
  id,
5723
5747
  vector,
5724
- connections: new Map()
5748
+ connections: new Map(),
5749
+ level: 0
5725
5750
  };
5726
5751
  // Store the noun
5727
5752
  this.storage.saveNoun(noun).catch((error) => {
@@ -6869,7 +6894,8 @@ class MemoryStorage extends BaseStorage {
6869
6894
  const nounCopy = {
6870
6895
  id: noun.id,
6871
6896
  vector: [...noun.vector],
6872
- connections: new Map()
6897
+ connections: new Map(),
6898
+ level: noun.level || 0
6873
6899
  };
6874
6900
  // Copy connections
6875
6901
  for (const [level, connections] of noun.connections.entries()) {
@@ -6892,7 +6918,8 @@ class MemoryStorage extends BaseStorage {
6892
6918
  const nounCopy = {
6893
6919
  id: noun.id,
6894
6920
  vector: [...noun.vector],
6895
- connections: new Map()
6921
+ connections: new Map(),
6922
+ level: noun.level || 0
6896
6923
  };
6897
6924
  // Copy connections
6898
6925
  for (const [level, connections] of noun.connections.entries()) {
@@ -6911,7 +6938,8 @@ class MemoryStorage extends BaseStorage {
6911
6938
  const nounCopy = {
6912
6939
  id: noun.id,
6913
6940
  vector: [...noun.vector],
6914
- connections: new Map()
6941
+ connections: new Map(),
6942
+ level: noun.level || 0
6915
6943
  };
6916
6944
  // Copy connections
6917
6945
  for (const [level, connections] of noun.connections.entries()) {
@@ -6986,7 +7014,8 @@ class MemoryStorage extends BaseStorage {
6986
7014
  const nounCopy = {
6987
7015
  id: noun.id,
6988
7016
  vector: [...noun.vector],
6989
- connections: new Map()
7017
+ connections: new Map(),
7018
+ level: noun.level || 0
6990
7019
  };
6991
7020
  // Copy connections
6992
7021
  for (const [level, connections] of noun.connections.entries()) {
@@ -7294,6 +7323,9 @@ class MemoryStorage extends BaseStorage {
7294
7323
  this.nounMetadata.clear();
7295
7324
  this.verbMetadata.clear();
7296
7325
  this.statistics = null;
7326
+ // Clear the statistics cache
7327
+ this.statisticsCache = null;
7328
+ this.statisticsModified = false;
7297
7329
  }
7298
7330
  /**
7299
7331
  * Get information about storage usage and capacity
@@ -7527,7 +7559,8 @@ class OPFSStorage extends BaseStorage {
7527
7559
  return {
7528
7560
  id: data.id,
7529
7561
  vector: data.vector,
7530
- connections
7562
+ connections,
7563
+ level: data.level || 0
7531
7564
  };
7532
7565
  }
7533
7566
  catch (error) {
@@ -7558,7 +7591,8 @@ class OPFSStorage extends BaseStorage {
7558
7591
  allNouns.push({
7559
7592
  id: data.id,
7560
7593
  vector: data.vector,
7561
- connections
7594
+ connections,
7595
+ level: data.level || 0
7562
7596
  });
7563
7597
  }
7564
7598
  catch (error) {
@@ -7609,7 +7643,8 @@ class OPFSStorage extends BaseStorage {
7609
7643
  nodes.push({
7610
7644
  id: data.id,
7611
7645
  vector: data.vector,
7612
- connections
7646
+ connections,
7647
+ level: data.level || 0
7613
7648
  });
7614
7649
  }
7615
7650
  }
@@ -7979,6 +8014,9 @@ class OPFSStorage extends BaseStorage {
7979
8014
  await removeDirectoryContents(this.verbMetadataDir);
7980
8015
  // Remove all files in the index directory
7981
8016
  await removeDirectoryContents(this.indexDir);
8017
+ // Clear the statistics cache
8018
+ this.statisticsCache = null;
8019
+ this.statisticsModified = false;
7982
8020
  }
7983
8021
  catch (error) {
7984
8022
  console.error('Error clearing storage:', error);
@@ -10306,7 +10344,8 @@ class S3CompatibleStorage extends BaseStorage {
10306
10344
  const node = {
10307
10345
  id: parsedNode.id,
10308
10346
  vector: parsedNode.vector,
10309
- connections
10347
+ connections,
10348
+ level: parsedNode.level || 0
10310
10349
  };
10311
10350
  console.log(`Successfully retrieved node ${id}:`, node);
10312
10351
  return node;
@@ -11204,6 +11243,9 @@ class S3CompatibleStorage extends BaseStorage {
11204
11243
  await deleteObjectsWithPrefix(this.metadataPrefix);
11205
11244
  // Delete all objects in the index directory
11206
11245
  await deleteObjectsWithPrefix(this.indexPrefix);
11246
+ // Clear the statistics cache
11247
+ this.statisticsCache = null;
11248
+ this.statisticsModified = false;
11207
11249
  }
11208
11250
  catch (error) {
11209
11251
  console.error('Failed to clear storage:', error);
@@ -12080,7 +12122,8 @@ class FileSystemStorage extends BaseStorage {
12080
12122
  return {
12081
12123
  id: parsedNode.id,
12082
12124
  vector: parsedNode.vector,
12083
- connections
12125
+ connections,
12126
+ level: parsedNode.level || 0
12084
12127
  };
12085
12128
  }
12086
12129
  catch (error) {
@@ -12111,7 +12154,8 @@ class FileSystemStorage extends BaseStorage {
12111
12154
  allNodes.push({
12112
12155
  id: parsedNode.id,
12113
12156
  vector: parsedNode.vector,
12114
- connections
12157
+ connections,
12158
+ level: parsedNode.level || 0
12115
12159
  });
12116
12160
  }
12117
12161
  }
@@ -12150,7 +12194,8 @@ class FileSystemStorage extends BaseStorage {
12150
12194
  nouns.push({
12151
12195
  id: parsedNode.id,
12152
12196
  vector: parsedNode.vector,
12153
- connections
12197
+ connections,
12198
+ level: parsedNode.level || 0
12154
12199
  });
12155
12200
  }
12156
12201
  }
@@ -12415,6 +12460,9 @@ class FileSystemStorage extends BaseStorage {
12415
12460
  await removeDirectoryContents(this.verbMetadataDir);
12416
12461
  // Remove all files in the index directory
12417
12462
  await removeDirectoryContents(this.indexDir);
12463
+ // Clear the statistics cache
12464
+ this.statisticsCache = null;
12465
+ this.statisticsModified = false;
12418
12466
  }
12419
12467
  /**
12420
12468
  * Get information about storage usage and capacity
@@ -16601,6 +16649,739 @@ class HealthMonitor {
16601
16649
  }
16602
16650
  }
16603
16651
 
16652
+ /**
16653
+ * SearchCache - Caches search results for improved performance
16654
+ */
16655
+ class SearchCache {
16656
+ constructor(config = {}) {
16657
+ this.cache = new Map();
16658
+ // Cache statistics
16659
+ this.hits = 0;
16660
+ this.misses = 0;
16661
+ this.evictions = 0;
16662
+ this.maxAge = config.maxAge ?? 5 * 60 * 1000; // 5 minutes
16663
+ this.maxSize = config.maxSize ?? 100;
16664
+ this.enabled = config.enabled ?? true;
16665
+ this.hitCountWeight = config.hitCountWeight ?? 0.3;
16666
+ }
16667
+ /**
16668
+ * Generate cache key from search parameters
16669
+ */
16670
+ getCacheKey(query, k, options = {}) {
16671
+ // Create a normalized key that ignores order of options
16672
+ const normalizedOptions = Object.keys(options)
16673
+ .sort()
16674
+ .reduce((acc, key) => {
16675
+ // Skip cache-related options
16676
+ if (key === 'skipCache' || key === 'useStreaming')
16677
+ return acc;
16678
+ acc[key] = options[key];
16679
+ return acc;
16680
+ }, {});
16681
+ return JSON.stringify({
16682
+ query: typeof query === 'object' ? JSON.stringify(query) : query,
16683
+ k,
16684
+ ...normalizedOptions
16685
+ });
16686
+ }
16687
+ /**
16688
+ * Get cached results if available and not expired
16689
+ */
16690
+ get(key) {
16691
+ if (!this.enabled)
16692
+ return null;
16693
+ const entry = this.cache.get(key);
16694
+ if (!entry) {
16695
+ this.misses++;
16696
+ return null;
16697
+ }
16698
+ // Check if expired
16699
+ if (Date.now() - entry.timestamp > this.maxAge) {
16700
+ this.cache.delete(key);
16701
+ this.misses++;
16702
+ return null;
16703
+ }
16704
+ // Update hit count and statistics
16705
+ entry.hits++;
16706
+ this.hits++;
16707
+ return entry.results;
16708
+ }
16709
+ /**
16710
+ * Cache search results
16711
+ */
16712
+ set(key, results) {
16713
+ if (!this.enabled)
16714
+ return;
16715
+ // Evict if cache is full
16716
+ if (this.cache.size >= this.maxSize) {
16717
+ this.evictOldest();
16718
+ }
16719
+ this.cache.set(key, {
16720
+ results: [...results], // Deep copy to prevent mutations
16721
+ timestamp: Date.now(),
16722
+ hits: 0
16723
+ });
16724
+ }
16725
+ /**
16726
+ * Evict the oldest entry based on timestamp and hit count
16727
+ */
16728
+ evictOldest() {
16729
+ let oldestKey = null;
16730
+ let oldestScore = Infinity;
16731
+ const now = Date.now();
16732
+ for (const [key, entry] of this.cache.entries()) {
16733
+ // Score combines age and inverse hit count
16734
+ const age = now - entry.timestamp;
16735
+ const hitScore = entry.hits > 0 ? 1 / entry.hits : 1;
16736
+ const score = age + (hitScore * this.hitCountWeight * this.maxAge);
16737
+ if (score < oldestScore) {
16738
+ oldestScore = score;
16739
+ oldestKey = key;
16740
+ }
16741
+ }
16742
+ if (oldestKey) {
16743
+ this.cache.delete(oldestKey);
16744
+ this.evictions++;
16745
+ }
16746
+ }
16747
+ /**
16748
+ * Clear all cached results
16749
+ */
16750
+ clear() {
16751
+ this.cache.clear();
16752
+ this.hits = 0;
16753
+ this.misses = 0;
16754
+ this.evictions = 0;
16755
+ }
16756
+ /**
16757
+ * Invalidate cache entries that might be affected by data changes
16758
+ */
16759
+ invalidate(pattern) {
16760
+ if (!pattern) {
16761
+ this.clear();
16762
+ return;
16763
+ }
16764
+ const keysToDelete = [];
16765
+ for (const key of this.cache.keys()) {
16766
+ const shouldDelete = typeof pattern === 'string'
16767
+ ? key.includes(pattern)
16768
+ : pattern.test(key);
16769
+ if (shouldDelete) {
16770
+ keysToDelete.push(key);
16771
+ }
16772
+ }
16773
+ keysToDelete.forEach(key => this.cache.delete(key));
16774
+ }
16775
+ /**
16776
+ * Smart invalidation for real-time data updates
16777
+ * Only clears cache if it's getting stale or if data changes significantly
16778
+ */
16779
+ invalidateOnDataChange(changeType) {
16780
+ // For now, clear all caches on data changes to ensure consistency
16781
+ // In the future, we could implement more sophisticated invalidation
16782
+ // based on the type of change and affected data
16783
+ this.clear();
16784
+ }
16785
+ /**
16786
+ * Check if cache entries have expired and remove them
16787
+ * This is especially important in distributed scenarios where
16788
+ * real-time updates might be delayed or missed
16789
+ */
16790
+ cleanupExpiredEntries() {
16791
+ const now = Date.now();
16792
+ const keysToDelete = [];
16793
+ for (const [key, entry] of this.cache.entries()) {
16794
+ if (now - entry.timestamp > this.maxAge) {
16795
+ keysToDelete.push(key);
16796
+ }
16797
+ }
16798
+ keysToDelete.forEach(key => this.cache.delete(key));
16799
+ return keysToDelete.length;
16800
+ }
16801
+ /**
16802
+ * Get cache statistics
16803
+ */
16804
+ getStats() {
16805
+ const total = this.hits + this.misses;
16806
+ return {
16807
+ hits: this.hits,
16808
+ misses: this.misses,
16809
+ evictions: this.evictions,
16810
+ hitRate: total > 0 ? this.hits / total : 0,
16811
+ size: this.cache.size,
16812
+ maxSize: this.maxSize,
16813
+ enabled: this.enabled
16814
+ };
16815
+ }
16816
+ /**
16817
+ * Enable or disable caching
16818
+ */
16819
+ setEnabled(enabled) {
16820
+ Object.defineProperty(this, 'enabled', { value: enabled, writable: false });
16821
+ if (!enabled) {
16822
+ this.clear();
16823
+ }
16824
+ }
16825
+ /**
16826
+ * Get memory usage estimate in bytes
16827
+ */
16828
+ getMemoryUsage() {
16829
+ let totalSize = 0;
16830
+ for (const [key, entry] of this.cache.entries()) {
16831
+ // Estimate key size
16832
+ totalSize += key.length * 2; // UTF-16 characters
16833
+ // Estimate entry size
16834
+ totalSize += JSON.stringify(entry.results).length * 2;
16835
+ totalSize += 16; // timestamp + hits (8 bytes each)
16836
+ }
16837
+ return totalSize;
16838
+ }
16839
+ /**
16840
+ * Get current cache configuration
16841
+ */
16842
+ getConfig() {
16843
+ return {
16844
+ enabled: this.enabled,
16845
+ maxSize: this.maxSize,
16846
+ maxAge: this.maxAge,
16847
+ hitCountWeight: this.hitCountWeight
16848
+ };
16849
+ }
16850
+ /**
16851
+ * Update cache configuration dynamically
16852
+ */
16853
+ updateConfig(newConfig) {
16854
+ if (newConfig.enabled !== undefined) {
16855
+ this.enabled = newConfig.enabled;
16856
+ }
16857
+ if (newConfig.maxSize !== undefined) {
16858
+ this.maxSize = newConfig.maxSize;
16859
+ // Trigger eviction if current size exceeds new limit
16860
+ this.evictIfNeeded();
16861
+ }
16862
+ if (newConfig.maxAge !== undefined) {
16863
+ this.maxAge = newConfig.maxAge;
16864
+ // Clean up entries that are now expired with new TTL
16865
+ this.cleanupExpiredEntries();
16866
+ }
16867
+ if (newConfig.hitCountWeight !== undefined) {
16868
+ this.hitCountWeight = newConfig.hitCountWeight;
16869
+ }
16870
+ }
16871
+ /**
16872
+ * Evict entries if cache exceeds maxSize
16873
+ */
16874
+ evictIfNeeded() {
16875
+ if (this.cache.size <= this.maxSize) {
16876
+ return;
16877
+ }
16878
+ // Calculate eviction score for each entry (same logic as existing eviction)
16879
+ const entries = Array.from(this.cache.entries()).map(([key, entry]) => {
16880
+ const age = Date.now() - entry.timestamp;
16881
+ const hitCount = entry.hits;
16882
+ // Eviction score: lower is more likely to be evicted
16883
+ // Combines age and hit count (weighted by hitCountWeight)
16884
+ const ageScore = age / this.maxAge;
16885
+ const hitScore = 1 / (hitCount + 1); // Inverse of hits (more hits = lower score)
16886
+ const score = ageScore * (1 - this.hitCountWeight) + hitScore * this.hitCountWeight;
16887
+ return { key, entry, score };
16888
+ });
16889
+ // Sort by score (lowest first - these will be evicted)
16890
+ entries.sort((a, b) => a.score - b.score);
16891
+ // Evict entries until we're under the limit
16892
+ const toEvict = entries.slice(0, this.cache.size - this.maxSize);
16893
+ toEvict.forEach(({ key }) => {
16894
+ this.cache.delete(key);
16895
+ this.evictions++;
16896
+ });
16897
+ }
16898
+ }
16899
+
16900
+ /**
16901
+ * Intelligent cache auto-configuration system
16902
+ * Adapts cache settings based on environment, usage patterns, and storage type
16903
+ */
16904
+ class CacheAutoConfigurator {
16905
+ constructor() {
16906
+ this.stats = {
16907
+ totalQueries: 0,
16908
+ repeatQueries: 0,
16909
+ avgQueryTime: 50,
16910
+ memoryPressure: 0,
16911
+ storageType: 'memory',
16912
+ isDistributed: false,
16913
+ changeFrequency: 0,
16914
+ readWriteRatio: 10,
16915
+ };
16916
+ this.configHistory = [];
16917
+ this.lastOptimization = 0;
16918
+ }
16919
+ /**
16920
+ * Auto-detect optimal cache configuration based on current conditions
16921
+ */
16922
+ autoDetectOptimalConfig(storageConfig, currentStats) {
16923
+ // Update stats with current information
16924
+ if (currentStats) {
16925
+ this.stats = { ...this.stats, ...currentStats };
16926
+ }
16927
+ // Detect environment characteristics
16928
+ this.detectEnvironment(storageConfig);
16929
+ // Generate optimal configuration
16930
+ const result = this.generateOptimalConfig();
16931
+ // Store for learning
16932
+ this.configHistory.push(result);
16933
+ this.lastOptimization = Date.now();
16934
+ return result;
16935
+ }
16936
+ /**
16937
+ * Dynamically adjust configuration based on runtime performance
16938
+ */
16939
+ adaptConfiguration(currentConfig, performanceMetrics) {
16940
+ const reasoning = [];
16941
+ let needsUpdate = false;
16942
+ // Check if we should update (don't over-optimize)
16943
+ if (Date.now() - this.lastOptimization < 60000) {
16944
+ return null; // Wait at least 1 minute between optimizations
16945
+ }
16946
+ // Analyze performance patterns
16947
+ const adaptations = {};
16948
+ // Low hit rate → adjust cache size or TTL
16949
+ if (performanceMetrics.hitRate < 0.3) {
16950
+ if (performanceMetrics.externalChangesDetected > 5) {
16951
+ // Too many external changes → shorter TTL
16952
+ adaptations.maxAge = Math.max(60000, currentConfig.maxAge * 0.7);
16953
+ reasoning.push('Reduced cache TTL due to frequent external changes');
16954
+ needsUpdate = true;
16955
+ }
16956
+ else {
16957
+ // Expand cache size for better hit rate
16958
+ adaptations.maxSize = Math.min(500, (currentConfig.maxSize || 100) * 1.5);
16959
+ reasoning.push('Increased cache size due to low hit rate');
16960
+ needsUpdate = true;
16961
+ }
16962
+ }
16963
+ // High hit rate but slow responses → might need cache warming
16964
+ if (performanceMetrics.hitRate > 0.8 && performanceMetrics.avgResponseTime > 100) {
16965
+ reasoning.push('High hit rate but slow responses - consider cache warming');
16966
+ }
16967
+ // Memory pressure → reduce cache size
16968
+ if (performanceMetrics.memoryUsage > 100 * 1024 * 1024) { // 100MB
16969
+ adaptations.maxSize = Math.max(20, (currentConfig.maxSize || 100) * 0.7);
16970
+ reasoning.push('Reduced cache size due to memory pressure');
16971
+ needsUpdate = true;
16972
+ }
16973
+ // Recent external changes → adaptive TTL
16974
+ if (performanceMetrics.timeSinceLastChange < 30000) { // 30 seconds
16975
+ adaptations.maxAge = Math.max(30000, currentConfig.maxAge * 0.8);
16976
+ reasoning.push('Shortened TTL due to recent external changes');
16977
+ needsUpdate = true;
16978
+ }
16979
+ if (!needsUpdate) {
16980
+ return null;
16981
+ }
16982
+ const newCacheConfig = {
16983
+ ...currentConfig,
16984
+ ...adaptations
16985
+ };
16986
+ const newRealtimeConfig = this.calculateRealtimeConfig();
16987
+ return {
16988
+ cacheConfig: newCacheConfig,
16989
+ realtimeConfig: newRealtimeConfig,
16990
+ reasoning
16991
+ };
16992
+ }
16993
+ /**
16994
+ * Get recommended configuration for specific use case
16995
+ */
16996
+ getRecommendedConfig(useCase) {
16997
+ const configs = {
16998
+ 'high-consistency': {
16999
+ cache: { maxAge: 120000, maxSize: 50 },
17000
+ realtime: { interval: 15000, enabled: true },
17001
+ reasoning: ['Optimized for data consistency and real-time updates']
17002
+ },
17003
+ 'balanced': {
17004
+ cache: { maxAge: 300000, maxSize: 100 },
17005
+ realtime: { interval: 30000, enabled: true },
17006
+ reasoning: ['Balanced performance and consistency']
17007
+ },
17008
+ 'performance-first': {
17009
+ cache: { maxAge: 600000, maxSize: 200 },
17010
+ realtime: { interval: 60000, enabled: true },
17011
+ reasoning: ['Optimized for maximum cache performance']
17012
+ }
17013
+ };
17014
+ const config = configs[useCase];
17015
+ return {
17016
+ cacheConfig: {
17017
+ enabled: true,
17018
+ ...config.cache
17019
+ },
17020
+ realtimeConfig: {
17021
+ updateIndex: true,
17022
+ updateStatistics: true,
17023
+ ...config.realtime
17024
+ },
17025
+ reasoning: config.reasoning
17026
+ };
17027
+ }
17028
+ /**
17029
+ * Learn from usage patterns and improve recommendations
17030
+ */
17031
+ learnFromUsage(usageData) {
17032
+ // Update internal stats for better future recommendations
17033
+ this.stats.totalQueries += usageData.totalQueries;
17034
+ this.stats.repeatQueries += usageData.cacheHits;
17035
+ this.stats.avgQueryTime = (this.stats.avgQueryTime + usageData.responseTime) / 2;
17036
+ this.stats.changeFrequency = usageData.dataChanges / (usageData.timeWindow / 60000);
17037
+ // Calculate read/write ratio
17038
+ const writes = usageData.dataChanges;
17039
+ const reads = usageData.totalQueries;
17040
+ this.stats.readWriteRatio = reads > 0 ? reads / Math.max(writes, 1) : 10;
17041
+ }
17042
+ detectEnvironment(storageConfig) {
17043
+ // Detect storage type
17044
+ if (storageConfig?.s3Storage || storageConfig?.customS3Storage) {
17045
+ this.stats.storageType = 's3';
17046
+ this.stats.isDistributed = true;
17047
+ }
17048
+ else if (storageConfig?.forceFileSystemStorage) {
17049
+ this.stats.storageType = 'filesystem';
17050
+ }
17051
+ else if (storageConfig?.forceMemoryStorage) {
17052
+ this.stats.storageType = 'memory';
17053
+ }
17054
+ else {
17055
+ // Auto-detect browser vs Node.js
17056
+ this.stats.storageType = typeof window !== 'undefined' ? 'opfs' : 'filesystem';
17057
+ }
17058
+ // Detect distributed mode indicators
17059
+ this.stats.isDistributed = this.stats.isDistributed ||
17060
+ Boolean(storageConfig?.s3Storage || storageConfig?.customS3Storage);
17061
+ }
17062
+ generateOptimalConfig() {
17063
+ const reasoning = [];
17064
+ // Base configuration
17065
+ let cacheConfig = {
17066
+ enabled: true,
17067
+ maxSize: 100,
17068
+ maxAge: 300000, // 5 minutes
17069
+ hitCountWeight: 0.3
17070
+ };
17071
+ let realtimeConfig = {
17072
+ enabled: false,
17073
+ interval: 60000,
17074
+ updateIndex: true,
17075
+ updateStatistics: true
17076
+ };
17077
+ // Adjust for storage type
17078
+ if (this.stats.storageType === 's3' || this.stats.isDistributed) {
17079
+ cacheConfig.maxAge = 180000; // 3 minutes for distributed
17080
+ realtimeConfig.enabled = true;
17081
+ realtimeConfig.interval = 30000; // 30 seconds
17082
+ reasoning.push('Distributed storage detected - enabled real-time updates');
17083
+ reasoning.push('Reduced cache TTL for distributed consistency');
17084
+ }
17085
+ // Adjust for read/write patterns
17086
+ if (this.stats.readWriteRatio > 20) {
17087
+ // Read-heavy workload
17088
+ cacheConfig.maxSize = Math.min(300, (cacheConfig.maxSize || 100) * 2);
17089
+ cacheConfig.maxAge = Math.min(900000, (cacheConfig.maxAge || 300000) * 1.5); // Up to 15 minutes
17090
+ reasoning.push('Read-heavy workload detected - increased cache size and TTL');
17091
+ }
17092
+ else if (this.stats.readWriteRatio < 5) {
17093
+ // Write-heavy workload
17094
+ cacheConfig.maxSize = Math.max(50, (cacheConfig.maxSize || 100) * 0.7);
17095
+ cacheConfig.maxAge = Math.max(60000, (cacheConfig.maxAge || 300000) * 0.6);
17096
+ reasoning.push('Write-heavy workload detected - reduced cache size and TTL');
17097
+ }
17098
+ // Adjust for change frequency
17099
+ if (this.stats.changeFrequency > 10) { // More than 10 changes per minute
17100
+ realtimeConfig.interval = Math.max(10000, realtimeConfig.interval * 0.5);
17101
+ cacheConfig.maxAge = Math.max(30000, (cacheConfig.maxAge || 300000) * 0.5);
17102
+ reasoning.push('High change frequency detected - increased update frequency');
17103
+ }
17104
+ // Memory constraints
17105
+ if (this.detectMemoryConstraints()) {
17106
+ cacheConfig.maxSize = Math.max(20, (cacheConfig.maxSize || 100) * 0.6);
17107
+ reasoning.push('Memory constraints detected - reduced cache size');
17108
+ }
17109
+ // Performance optimization
17110
+ if (this.stats.avgQueryTime > 200) {
17111
+ cacheConfig.maxSize = Math.min(500, (cacheConfig.maxSize || 100) * 1.5);
17112
+ reasoning.push('Slow queries detected - increased cache size');
17113
+ }
17114
+ return {
17115
+ cacheConfig,
17116
+ realtimeConfig,
17117
+ reasoning
17118
+ };
17119
+ }
17120
+ calculateRealtimeConfig() {
17121
+ return {
17122
+ enabled: this.stats.isDistributed || this.stats.changeFrequency > 1,
17123
+ interval: this.stats.isDistributed ? 30000 : 60000,
17124
+ updateIndex: true,
17125
+ updateStatistics: true
17126
+ };
17127
+ }
17128
+ detectMemoryConstraints() {
17129
+ // Simple heuristic for memory constraints
17130
+ try {
17131
+ if (typeof performance !== 'undefined' && 'memory' in performance) {
17132
+ const memInfo = performance.memory;
17133
+ return memInfo.usedJSHeapSize > memInfo.jsHeapSizeLimit * 0.8;
17134
+ }
17135
+ }
17136
+ catch (e) {
17137
+ // Ignore errors
17138
+ }
17139
+ // Default assumption for constrained environments
17140
+ return false;
17141
+ }
17142
+ /**
17143
+ * Get human-readable explanation of current configuration
17144
+ */
17145
+ getConfigExplanation(config) {
17146
+ const lines = [
17147
+ '🤖 Brainy Auto-Configuration:',
17148
+ '',
17149
+ `📊 Cache: ${config.cacheConfig.maxSize} queries, ${config.cacheConfig.maxAge / 1000}s TTL`,
17150
+ `🔄 Updates: ${config.realtimeConfig.enabled ? `Every ${(config.realtimeConfig.interval || 30000) / 1000}s` : 'Disabled'}`,
17151
+ '',
17152
+ '🎯 Optimizations applied:'
17153
+ ];
17154
+ config.reasoning.forEach(reason => {
17155
+ lines.push(` • ${reason}`);
17156
+ });
17157
+ return lines.join('\n');
17158
+ }
17159
+ }
17160
+
17161
+ /**
17162
+ * Lightweight statistics collector for Brainy
17163
+ * Designed to have minimal performance impact even with millions of entries
17164
+ */
17165
+ class StatisticsCollector {
17166
+ constructor() {
17167
+ // Content type tracking (lightweight counters)
17168
+ this.contentTypes = new Map();
17169
+ // Data freshness tracking (only track timestamps, not full data)
17170
+ this.oldestTimestamp = Date.now();
17171
+ this.newestTimestamp = Date.now();
17172
+ this.updateTimestamps = [];
17173
+ // Search performance tracking (rolling window)
17174
+ this.searchMetrics = {
17175
+ totalSearches: 0,
17176
+ totalSearchTimeMs: 0,
17177
+ searchTimestamps: [],
17178
+ topSearchTerms: new Map()
17179
+ };
17180
+ // Verb type tracking
17181
+ this.verbTypes = new Map();
17182
+ // Storage size estimates (updated periodically, not on every operation)
17183
+ this.storageSizeCache = {
17184
+ lastUpdated: 0,
17185
+ sizes: {
17186
+ nouns: 0,
17187
+ verbs: 0,
17188
+ metadata: 0,
17189
+ index: 0
17190
+ }
17191
+ };
17192
+ this.MAX_TIMESTAMPS = 1000; // Keep last 1000 timestamps
17193
+ this.MAX_SEARCH_TERMS = 100; // Track top 100 search terms
17194
+ this.SIZE_UPDATE_INTERVAL = 60000; // Update sizes every minute
17195
+ }
17196
+ /**
17197
+ * Track content type (very lightweight)
17198
+ */
17199
+ trackContentType(type) {
17200
+ this.contentTypes.set(type, (this.contentTypes.get(type) || 0) + 1);
17201
+ }
17202
+ /**
17203
+ * Track data update timestamp (lightweight)
17204
+ */
17205
+ trackUpdate(timestamp) {
17206
+ const ts = timestamp || Date.now();
17207
+ // Update oldest/newest
17208
+ if (ts < this.oldestTimestamp)
17209
+ this.oldestTimestamp = ts;
17210
+ if (ts > this.newestTimestamp)
17211
+ this.newestTimestamp = ts;
17212
+ // Add to rolling window
17213
+ this.updateTimestamps.push({ timestamp: ts, count: 1 });
17214
+ // Keep window size limited
17215
+ if (this.updateTimestamps.length > this.MAX_TIMESTAMPS) {
17216
+ this.updateTimestamps.shift();
17217
+ }
17218
+ }
17219
+ /**
17220
+ * Track search performance (lightweight)
17221
+ */
17222
+ trackSearch(searchTerm, durationMs) {
17223
+ this.searchMetrics.totalSearches++;
17224
+ this.searchMetrics.totalSearchTimeMs += durationMs;
17225
+ // Add to rolling window
17226
+ this.searchMetrics.searchTimestamps.push({
17227
+ timestamp: Date.now(),
17228
+ count: 1
17229
+ });
17230
+ // Keep window size limited
17231
+ if (this.searchMetrics.searchTimestamps.length > this.MAX_TIMESTAMPS) {
17232
+ this.searchMetrics.searchTimestamps.shift();
17233
+ }
17234
+ // Track search term (limit to top N)
17235
+ const termCount = (this.searchMetrics.topSearchTerms.get(searchTerm) || 0) + 1;
17236
+ this.searchMetrics.topSearchTerms.set(searchTerm, termCount);
17237
+ // Prune if too many terms
17238
+ if (this.searchMetrics.topSearchTerms.size > this.MAX_SEARCH_TERMS * 2) {
17239
+ this.pruneSearchTerms();
17240
+ }
17241
+ }
17242
+ /**
17243
+ * Track verb type (lightweight)
17244
+ */
17245
+ trackVerbType(type) {
17246
+ this.verbTypes.set(type, (this.verbTypes.get(type) || 0) + 1);
17247
+ }
17248
+ /**
17249
+ * Update storage size estimates (called periodically, not on every operation)
17250
+ */
17251
+ updateStorageSizes(sizes) {
17252
+ this.storageSizeCache = {
17253
+ lastUpdated: Date.now(),
17254
+ sizes
17255
+ };
17256
+ }
17257
+ /**
17258
+ * Get comprehensive statistics
17259
+ */
17260
+ getStatistics() {
17261
+ const now = Date.now();
17262
+ const hourAgo = now - 3600000;
17263
+ const dayAgo = now - 86400000;
17264
+ const weekAgo = now - 604800000;
17265
+ const monthAgo = now - 2592000000;
17266
+ // Calculate data freshness
17267
+ const updatesLastHour = this.updateTimestamps.filter(t => t.timestamp > hourAgo).length;
17268
+ const updatesLastDay = this.updateTimestamps.filter(t => t.timestamp > dayAgo).length;
17269
+ // Calculate age distribution
17270
+ const ageDistribution = {
17271
+ last24h: 0,
17272
+ last7d: 0,
17273
+ last30d: 0,
17274
+ older: 0
17275
+ };
17276
+ // Estimate based on update patterns (not scanning all data)
17277
+ const totalUpdates = this.updateTimestamps.length;
17278
+ if (totalUpdates > 0) {
17279
+ const recentUpdates = this.updateTimestamps.filter(t => t.timestamp > dayAgo).length;
17280
+ const weekUpdates = this.updateTimestamps.filter(t => t.timestamp > weekAgo).length;
17281
+ const monthUpdates = this.updateTimestamps.filter(t => t.timestamp > monthAgo).length;
17282
+ ageDistribution.last24h = Math.round((recentUpdates / totalUpdates) * 100);
17283
+ ageDistribution.last7d = Math.round(((weekUpdates - recentUpdates) / totalUpdates) * 100);
17284
+ ageDistribution.last30d = Math.round(((monthUpdates - weekUpdates) / totalUpdates) * 100);
17285
+ ageDistribution.older = 100 - ageDistribution.last24h - ageDistribution.last7d - ageDistribution.last30d;
17286
+ }
17287
+ // Calculate search metrics
17288
+ const searchesLastHour = this.searchMetrics.searchTimestamps.filter(t => t.timestamp > hourAgo).length;
17289
+ const searchesLastDay = this.searchMetrics.searchTimestamps.filter(t => t.timestamp > dayAgo).length;
17290
+ const avgSearchTime = this.searchMetrics.totalSearches > 0
17291
+ ? this.searchMetrics.totalSearchTimeMs / this.searchMetrics.totalSearches
17292
+ : 0;
17293
+ // Get top search terms
17294
+ const topSearchTerms = Array.from(this.searchMetrics.topSearchTerms.entries())
17295
+ .sort((a, b) => b[1] - a[1])
17296
+ .slice(0, 10)
17297
+ .map(([term]) => term);
17298
+ // Calculate storage metrics
17299
+ const totalSize = Object.values(this.storageSizeCache.sizes).reduce((a, b) => a + b, 0);
17300
+ return {
17301
+ contentTypes: Object.fromEntries(this.contentTypes),
17302
+ dataFreshness: {
17303
+ oldestEntry: new Date(this.oldestTimestamp).toISOString(),
17304
+ newestEntry: new Date(this.newestTimestamp).toISOString(),
17305
+ updatesLastHour,
17306
+ updatesLastDay,
17307
+ ageDistribution
17308
+ },
17309
+ storageMetrics: {
17310
+ totalSizeBytes: totalSize,
17311
+ nounsSizeBytes: this.storageSizeCache.sizes.nouns,
17312
+ verbsSizeBytes: this.storageSizeCache.sizes.verbs,
17313
+ metadataSizeBytes: this.storageSizeCache.sizes.metadata,
17314
+ indexSizeBytes: this.storageSizeCache.sizes.index
17315
+ },
17316
+ searchMetrics: {
17317
+ totalSearches: this.searchMetrics.totalSearches,
17318
+ averageSearchTimeMs: avgSearchTime,
17319
+ searchesLastHour,
17320
+ searchesLastDay,
17321
+ topSearchTerms
17322
+ },
17323
+ verbStatistics: {
17324
+ totalVerbs: Array.from(this.verbTypes.values()).reduce((a, b) => a + b, 0),
17325
+ verbTypes: Object.fromEntries(this.verbTypes),
17326
+ averageConnectionsPerVerb: 2 // Verbs connect 2 nouns
17327
+ }
17328
+ };
17329
+ }
17330
+ /**
17331
+ * Merge statistics from storage (for distributed systems)
17332
+ */
17333
+ mergeFromStorage(stored) {
17334
+ // Merge content types
17335
+ if (stored.contentTypes) {
17336
+ for (const [type, count] of Object.entries(stored.contentTypes)) {
17337
+ this.contentTypes.set(type, count);
17338
+ }
17339
+ }
17340
+ // Merge verb types
17341
+ if (stored.verbStatistics?.verbTypes) {
17342
+ for (const [type, count] of Object.entries(stored.verbStatistics.verbTypes)) {
17343
+ this.verbTypes.set(type, count);
17344
+ }
17345
+ }
17346
+ // Merge search metrics
17347
+ if (stored.searchMetrics) {
17348
+ this.searchMetrics.totalSearches = stored.searchMetrics.totalSearches || 0;
17349
+ this.searchMetrics.totalSearchTimeMs = (stored.searchMetrics.averageSearchTimeMs || 0) * this.searchMetrics.totalSearches;
17350
+ }
17351
+ // Merge data freshness
17352
+ if (stored.dataFreshness) {
17353
+ this.oldestTimestamp = new Date(stored.dataFreshness.oldestEntry).getTime();
17354
+ this.newestTimestamp = new Date(stored.dataFreshness.newestEntry).getTime();
17355
+ }
17356
+ }
17357
+ /**
17358
+ * Reset statistics (for testing)
17359
+ */
17360
+ reset() {
17361
+ this.contentTypes.clear();
17362
+ this.verbTypes.clear();
17363
+ this.updateTimestamps = [];
17364
+ this.searchMetrics = {
17365
+ totalSearches: 0,
17366
+ totalSearchTimeMs: 0,
17367
+ searchTimestamps: [],
17368
+ topSearchTerms: new Map()
17369
+ };
17370
+ this.oldestTimestamp = Date.now();
17371
+ this.newestTimestamp = Date.now();
17372
+ }
17373
+ pruneSearchTerms() {
17374
+ // Keep only top N search terms
17375
+ const sorted = Array.from(this.searchMetrics.topSearchTerms.entries())
17376
+ .sort((a, b) => b[1] - a[1])
17377
+ .slice(0, this.MAX_SEARCH_TERMS);
17378
+ this.searchMetrics.topSearchTerms.clear();
17379
+ for (const [term, count] of sorted) {
17380
+ this.searchMetrics.topSearchTerms.set(term, count);
17381
+ }
17382
+ }
17383
+ }
17384
+
16604
17385
  /**
16605
17386
  * BrainyData
16606
17387
  * Main class that provides the vector database functionality
@@ -16661,6 +17442,8 @@ class BrainyData {
16661
17442
  this.operationalMode = null;
16662
17443
  this.domainDetector = null;
16663
17444
  this.healthMonitor = null;
17445
+ // Statistics collector
17446
+ this.statisticsCollector = new StatisticsCollector();
16664
17447
  // Set dimensions to fixed value of 512 (Universal Sentence Encoder dimension)
16665
17448
  this._dimensions = 512;
16666
17449
  // Set distance function
@@ -16758,6 +17541,26 @@ class BrainyData {
16758
17541
  this.distributedConfig = config.distributed;
16759
17542
  }
16760
17543
  }
17544
+ // Initialize cache auto-configurator first
17545
+ this.cacheAutoConfigurator = new CacheAutoConfigurator();
17546
+ // Auto-detect optimal cache configuration if not explicitly provided
17547
+ let finalSearchCacheConfig = config.searchCache;
17548
+ if (!config.searchCache || Object.keys(config.searchCache).length === 0) {
17549
+ const autoConfig = this.cacheAutoConfigurator.autoDetectOptimalConfig(config.storage);
17550
+ finalSearchCacheConfig = autoConfig.cacheConfig;
17551
+ // Apply auto-detected real-time update configuration if not explicitly set
17552
+ if (!config.realtimeUpdates && autoConfig.realtimeConfig.enabled) {
17553
+ this.realtimeUpdateConfig = {
17554
+ ...this.realtimeUpdateConfig,
17555
+ ...autoConfig.realtimeConfig
17556
+ };
17557
+ }
17558
+ if (this.loggingConfig?.verbose) {
17559
+ console.log(this.cacheAutoConfigurator.getConfigExplanation(autoConfig));
17560
+ }
17561
+ }
17562
+ // Initialize search cache with final configuration
17563
+ this.searchCache = new SearchCache(finalSearchCacheConfig);
16761
17564
  }
16762
17565
  /**
16763
17566
  * Check if the database is in read-only mode and throw an error if it is
@@ -16897,6 +17700,18 @@ class BrainyData {
16897
17700
  await this.applyChangesFromFullScan();
16898
17701
  }
16899
17702
  }
17703
+ // Cleanup expired cache entries (defensive mechanism for distributed scenarios)
17704
+ const expiredCount = this.searchCache.cleanupExpiredEntries();
17705
+ if (expiredCount > 0 && this.loggingConfig?.verbose) {
17706
+ console.log(`Cleaned up ${expiredCount} expired cache entries`);
17707
+ }
17708
+ // Adapt cache configuration based on performance (every few updates)
17709
+ // Only adapt every 5th update to avoid over-optimization
17710
+ const updateCount = Math.floor((Date.now() - (this.lastUpdateTime || 0)) /
17711
+ this.realtimeUpdateConfig.interval);
17712
+ if (updateCount % 5 === 0) {
17713
+ this.adaptCacheConfiguration();
17714
+ }
16900
17715
  // Update the last update time
16901
17716
  this.lastUpdateTime = Date.now();
16902
17717
  if (this.loggingConfig?.verbose) {
@@ -16971,6 +17786,13 @@ class BrainyData {
16971
17786
  (addedCount > 0 || updatedCount > 0 || deletedCount > 0)) {
16972
17787
  console.log(`Real-time update: Added ${addedCount}, updated ${updatedCount}, deleted ${deletedCount} nouns using change log`);
16973
17788
  }
17789
+ // Invalidate search cache if any external changes were detected
17790
+ if (addedCount > 0 || updatedCount > 0 || deletedCount > 0) {
17791
+ this.searchCache.invalidateOnDataChange('update');
17792
+ if (this.loggingConfig?.verbose) {
17793
+ console.log('Search cache invalidated due to external data changes');
17794
+ }
17795
+ }
16974
17796
  // Update the last known noun count
16975
17797
  this.lastKnownNounCount = await this.getNounCount();
16976
17798
  }
@@ -17014,6 +17836,13 @@ class BrainyData {
17014
17836
  }
17015
17837
  // Update the last known noun count
17016
17838
  this.lastKnownNounCount = currentCount;
17839
+ // Invalidate search cache if new nouns were detected
17840
+ if (newNouns.length > 0) {
17841
+ this.searchCache.invalidateOnDataChange('add');
17842
+ if (this.loggingConfig?.verbose) {
17843
+ console.log('Search cache invalidated due to external data changes');
17844
+ }
17845
+ }
17017
17846
  if (this.loggingConfig?.verbose && newNouns.length > 0) {
17018
17847
  console.log(`Real-time update: Added ${newNouns.length} new nouns to index using full scan`);
17019
17848
  }
@@ -17197,6 +18026,16 @@ class BrainyData {
17197
18026
  // Continue initialization even if remote connection fails
17198
18027
  }
17199
18028
  }
18029
+ // Initialize statistics collector with existing data
18030
+ try {
18031
+ const existingStats = await this.storage.getStatistics();
18032
+ if (existingStats) {
18033
+ this.statisticsCollector.mergeFromStorage(existingStats);
18034
+ }
18035
+ }
18036
+ catch (e) {
18037
+ // Ignore errors loading existing statistics
18038
+ }
17200
18039
  this.isInitialized = true;
17201
18040
  this.isInitializing = false;
17202
18041
  // Start real-time updates if enabled
@@ -17408,13 +18247,15 @@ class BrainyData {
17408
18247
  try {
17409
18248
  if (this.writeOnly) {
17410
18249
  // In write-only mode, check storage directly
17411
- existingNoun = await this.storage.getNoun(options.id) ?? undefined;
18250
+ existingNoun =
18251
+ (await this.storage.getNoun(options.id)) ?? undefined;
17412
18252
  }
17413
18253
  else {
17414
18254
  // In normal mode, check index first, then storage
17415
18255
  existingNoun = this.index.getNouns().get(options.id);
17416
18256
  if (!existingNoun) {
17417
- existingNoun = await this.storage.getNoun(options.id) ?? undefined;
18257
+ existingNoun =
18258
+ (await this.storage.getNoun(options.id)) ?? undefined;
17418
18259
  }
17419
18260
  }
17420
18261
  if (existingNoun) {
@@ -17449,6 +18290,7 @@ class BrainyData {
17449
18290
  id,
17450
18291
  vector,
17451
18292
  connections: new Map(),
18293
+ level: 0, // Default level for new nodes
17452
18294
  metadata: undefined // Will be set separately
17453
18295
  };
17454
18296
  }
@@ -17522,17 +18364,24 @@ class BrainyData {
17522
18364
  // Domain already specified, keep it
17523
18365
  const domainInfo = this.domainDetector.detectDomain(metadataToSave);
17524
18366
  if (domainInfo.domainMetadata) {
17525
- metadataToSave.domainMetadata = domainInfo.domainMetadata;
18367
+ ;
18368
+ metadataToSave.domainMetadata =
18369
+ domainInfo.domainMetadata;
17526
18370
  }
17527
18371
  }
17528
18372
  else {
17529
18373
  // Try to detect domain from the data
17530
- const dataToAnalyze = Array.isArray(vectorOrData) ? metadata : vectorOrData;
18374
+ const dataToAnalyze = Array.isArray(vectorOrData)
18375
+ ? metadata
18376
+ : vectorOrData;
17531
18377
  const domainInfo = this.domainDetector.detectDomain(dataToAnalyze);
17532
18378
  if (domainInfo.domain) {
18379
+ ;
17533
18380
  metadataToSave.domain = domainInfo.domain;
17534
18381
  if (domainInfo.domainMetadata) {
17535
- metadataToSave.domainMetadata = domainInfo.domainMetadata;
18382
+ ;
18383
+ metadataToSave.domainMetadata =
18384
+ domainInfo.domainMetadata;
17536
18385
  }
17537
18386
  }
17538
18387
  }
@@ -17547,10 +18396,19 @@ class BrainyData {
17547
18396
  // Track metadata statistics
17548
18397
  const metadataService = this.getServiceName(options);
17549
18398
  await this.storage.incrementStatistic('metadata', metadataService);
18399
+ // Track content type if it's a GraphNoun
18400
+ if (metadataToSave &&
18401
+ typeof metadataToSave === 'object' &&
18402
+ 'noun' in metadataToSave) {
18403
+ this.statisticsCollector.trackContentType(metadataToSave.noun);
18404
+ }
18405
+ // Track update timestamp
18406
+ this.statisticsCollector.trackUpdate();
17550
18407
  }
17551
18408
  }
17552
- // Update HNSW index size (excluding verbs)
17553
- await this.storage.updateHnswIndexSize(await this.getNounCount());
18409
+ // Update HNSW index size with actual index size
18410
+ const indexSize = this.index.size();
18411
+ await this.storage.updateHnswIndexSize(indexSize);
17554
18412
  // Update health metrics if in distributed mode
17555
18413
  if (this.healthMonitor) {
17556
18414
  const vectorCount = await this.getNounCount();
@@ -17565,6 +18423,8 @@ class BrainyData {
17565
18423
  console.warn(`Failed to add to remote server: ${remoteError}. Continuing with local add.`);
17566
18424
  }
17567
18425
  }
18426
+ // Invalidate search cache since data has changed
18427
+ this.searchCache.invalidateOnDataChange('add');
17568
18428
  return id;
17569
18429
  }
17570
18430
  catch (error) {
@@ -17822,11 +18682,16 @@ class BrainyData {
17822
18682
  console.log(`Lazy loading mode: Added ${limitedNouns.length} nodes to index for search`);
17823
18683
  }
17824
18684
  }
17825
- // Search in the index
17826
- const results = await this.index.search(queryVector, k);
18685
+ // When using offset, we need to fetch more results and then slice
18686
+ const offset = options.offset || 0;
18687
+ const totalNeeded = k + offset;
18688
+ // Search in the index for totalNeeded results
18689
+ const results = await this.index.search(queryVector, totalNeeded);
18690
+ // Skip the offset number of results
18691
+ const paginatedResults = results.slice(offset, offset + k);
17827
18692
  // Get metadata for each result
17828
18693
  const searchResults = [];
17829
- for (const [id, score] of results) {
18694
+ for (const [id, score] of paginatedResults) {
17830
18695
  const noun = this.index.getNouns().get(id);
17831
18696
  if (!noun) {
17832
18697
  continue;
@@ -17867,8 +18732,9 @@ class BrainyData {
17867
18732
  }
17868
18733
  // Sort by distance (ascending)
17869
18734
  results.sort((a, b) => a[1] - b[1]);
17870
- // Take top k results
17871
- const topResults = results.slice(0, k);
18735
+ // Apply offset and take k results
18736
+ const offset = options.offset || 0;
18737
+ const topResults = results.slice(offset, offset + k);
17872
18738
  // Get metadata for each result
17873
18739
  const searchResults = [];
17874
18740
  for (const [id, score] of topResults) {
@@ -17962,12 +18828,29 @@ class BrainyData {
17962
18828
  }
17963
18829
  // Default behavior (backward compatible): search locally
17964
18830
  try {
18831
+ // Check cache first (transparent to user)
18832
+ const cacheKey = this.searchCache.getCacheKey(queryVectorOrData, k, options);
18833
+ const cachedResults = this.searchCache.get(cacheKey);
18834
+ if (cachedResults) {
18835
+ // Track cache hit in health monitor
18836
+ if (this.healthMonitor) {
18837
+ const latency = Date.now() - startTime;
18838
+ this.healthMonitor.recordRequest(latency, false);
18839
+ this.healthMonitor.recordCacheAccess(true);
18840
+ }
18841
+ return cachedResults;
18842
+ }
18843
+ // Cache miss - perform actual search
17965
18844
  const results = await this.searchLocal(queryVectorOrData, k, options);
18845
+ // Cache results for future queries (unless explicitly disabled)
18846
+ if (!options.skipCache) {
18847
+ this.searchCache.set(cacheKey, results);
18848
+ }
17966
18849
  // Track successful search in health monitor
17967
18850
  if (this.healthMonitor) {
17968
18851
  const latency = Date.now() - startTime;
17969
18852
  this.healthMonitor.recordRequest(latency, false);
17970
- this.healthMonitor.recordCacheAccess(results.length > 0);
18853
+ this.healthMonitor.recordCacheAccess(false);
17971
18854
  }
17972
18855
  return results;
17973
18856
  }
@@ -17980,6 +18863,59 @@ class BrainyData {
17980
18863
  throw error;
17981
18864
  }
17982
18865
  }
18866
+ /**
18867
+ * Search with cursor-based pagination for better performance on large datasets
18868
+ * @param queryVectorOrData Query vector or data to search for
18869
+ * @param k Number of results to return
18870
+ * @param options Additional options including cursor for pagination
18871
+ * @returns Paginated search results with cursor for next page
18872
+ */
18873
+ async searchWithCursor(queryVectorOrData, k = 10, options = {}) {
18874
+ // For cursor-based search, we need to fetch more results and filter
18875
+ const searchK = options.cursor ? k + 20 : k; // Get extra results for filtering
18876
+ // Perform regular search
18877
+ const allResults = await this.search(queryVectorOrData, searchK, {
18878
+ ...options,
18879
+ skipCache: options.skipCache
18880
+ });
18881
+ let results = allResults;
18882
+ let startIndex = 0;
18883
+ // If cursor provided, find starting position
18884
+ if (options.cursor) {
18885
+ startIndex = allResults.findIndex((r) => r.id === options.cursor.lastId &&
18886
+ Math.abs(r.score - options.cursor.lastScore) < 0.0001);
18887
+ if (startIndex >= 0) {
18888
+ startIndex += 1; // Start after the cursor position
18889
+ results = allResults.slice(startIndex, startIndex + k);
18890
+ }
18891
+ else {
18892
+ // Cursor not found, might be stale - return from beginning
18893
+ results = allResults.slice(0, k);
18894
+ startIndex = 0;
18895
+ }
18896
+ }
18897
+ else {
18898
+ results = allResults.slice(0, k);
18899
+ }
18900
+ // Create cursor for next page
18901
+ let nextCursor;
18902
+ const hasMoreResults = startIndex + results.length < allResults.length ||
18903
+ allResults.length >= searchK;
18904
+ if (results.length > 0 && hasMoreResults) {
18905
+ const lastResult = results[results.length - 1];
18906
+ nextCursor = {
18907
+ lastId: lastResult.id,
18908
+ lastScore: lastResult.score,
18909
+ position: startIndex + results.length
18910
+ };
18911
+ }
18912
+ return {
18913
+ results,
18914
+ cursor: nextCursor,
18915
+ hasMore: !!nextCursor,
18916
+ totalEstimate: allResults.length > searchK ? undefined : allResults.length
18917
+ };
18918
+ }
17983
18919
  /**
17984
18920
  * Search the local database for similar vectors
17985
18921
  * @param queryVectorOrData Query vector or data to search for
@@ -18035,18 +18971,20 @@ class BrainyData {
18035
18971
  if (options.nounTypes && options.nounTypes.length > 0) {
18036
18972
  searchResults = await this.searchByNounTypes(queryToUse, k, options.nounTypes, {
18037
18973
  forceEmbed: options.forceEmbed,
18038
- service: options.service
18974
+ service: options.service,
18975
+ offset: options.offset
18039
18976
  });
18040
18977
  }
18041
18978
  else {
18042
18979
  // Otherwise, search all GraphNouns
18043
18980
  searchResults = await this.searchByNounTypes(queryToUse, k, null, {
18044
18981
  forceEmbed: options.forceEmbed,
18045
- service: options.service
18982
+ service: options.service,
18983
+ offset: options.offset
18046
18984
  });
18047
18985
  }
18048
18986
  // Filter out placeholder nouns from search results
18049
- searchResults = searchResults.filter(result => {
18987
+ searchResults = searchResults.filter((result) => {
18050
18988
  if (result.metadata && typeof result.metadata === 'object') {
18051
18989
  const metadata = result.metadata;
18052
18990
  // Exclude placeholder nouns from search results
@@ -18154,7 +19092,7 @@ class BrainyData {
18154
19092
  // In write-only mode, query storage directly since index is not loaded
18155
19093
  if (this.writeOnly) {
18156
19094
  try {
18157
- noun = await this.storage.getNoun(id) ?? undefined;
19095
+ noun = (await this.storage.getNoun(id)) ?? undefined;
18158
19096
  }
18159
19097
  catch (storageError) {
18160
19098
  // If storage lookup fails, return null (noun doesn't exist)
@@ -18167,7 +19105,7 @@ class BrainyData {
18167
19105
  // If not found in index, fallback to storage (for race conditions)
18168
19106
  if (!noun && this.storage) {
18169
19107
  try {
18170
- noun = await this.storage.getNoun(id) ?? undefined;
19108
+ noun = (await this.storage.getNoun(id)) ?? undefined;
18171
19109
  }
18172
19110
  catch (storageError) {
18173
19111
  // Storage lookup failed, noun doesn't exist
@@ -18376,6 +19314,8 @@ class BrainyData {
18376
19314
  catch (error) {
18377
19315
  // Ignore
18378
19316
  }
19317
+ // Invalidate search cache since data has changed
19318
+ this.searchCache.invalidateOnDataChange('delete');
18379
19319
  return true;
18380
19320
  }
18381
19321
  catch (error) {
@@ -18459,6 +19399,8 @@ class BrainyData {
18459
19399
  // Track metadata statistics
18460
19400
  const service = this.getServiceName(options);
18461
19401
  await this.storage.incrementStatistic('metadata', service);
19402
+ // Invalidate search cache since metadata has changed
19403
+ this.searchCache.invalidateOnDataChange('update');
18462
19404
  return true;
18463
19405
  }
18464
19406
  catch (error) {
@@ -18555,6 +19497,7 @@ class BrainyData {
18555
19497
  id: sourceId,
18556
19498
  vector: sourcePlaceholderVector,
18557
19499
  connections: new Map(),
19500
+ level: 0,
18558
19501
  metadata: sourceMetadata
18559
19502
  };
18560
19503
  // Create placeholder target noun
@@ -18575,6 +19518,7 @@ class BrainyData {
18575
19518
  id: targetId,
18576
19519
  vector: targetPlaceholderVector,
18577
19520
  connections: new Map(),
19521
+ level: 0,
18578
19522
  metadata: targetMetadata
18579
19523
  };
18580
19524
  // Save placeholder nouns to storage (but skip indexing for speed)
@@ -18811,8 +19755,13 @@ class BrainyData {
18811
19755
  // Track verb statistics
18812
19756
  const serviceForStats = this.getServiceName(options);
18813
19757
  await this.storage.incrementStatistic('verb', serviceForStats);
18814
- // Update HNSW index size (excluding verbs)
18815
- await this.storage.updateHnswIndexSize(await this.getNounCount());
19758
+ // Track verb type
19759
+ this.statisticsCollector.trackVerbType(verbMetadata.verb);
19760
+ // Update HNSW index size with actual index size
19761
+ const indexSize = this.index.size();
19762
+ await this.storage.updateHnswIndexSize(indexSize);
19763
+ // Invalidate search cache since verb data has changed
19764
+ this.searchCache.invalidateOnDataChange('add');
18816
19765
  return id;
18817
19766
  }
18818
19767
  catch (error) {
@@ -19035,6 +19984,10 @@ class BrainyData {
19035
19984
  await this.index.clear();
19036
19985
  // Clear storage
19037
19986
  await this.storage.clear();
19987
+ // Reset statistics collector
19988
+ this.statisticsCollector = new StatisticsCollector();
19989
+ // Clear search cache since all data has been removed
19990
+ this.searchCache.invalidateOnDataChange('delete');
19038
19991
  }
19039
19992
  catch (error) {
19040
19993
  console.error('Failed to clear vector database:', error);
@@ -19047,6 +20000,67 @@ class BrainyData {
19047
20000
  size() {
19048
20001
  return this.index.size();
19049
20002
  }
20003
+ /**
20004
+ * Get search cache statistics for performance monitoring
20005
+ * @returns Cache statistics including hit rate and memory usage
20006
+ */
20007
+ getCacheStats() {
20008
+ return {
20009
+ search: this.searchCache.getStats(),
20010
+ searchMemoryUsage: this.searchCache.getMemoryUsage()
20011
+ };
20012
+ }
20013
+ /**
20014
+ * Clear search cache manually (useful for testing or memory management)
20015
+ */
20016
+ clearCache() {
20017
+ this.searchCache.clear();
20018
+ }
20019
+ /**
20020
+ * Adapt cache configuration based on current performance metrics
20021
+ * This method analyzes usage patterns and automatically optimizes cache settings
20022
+ * @private
20023
+ */
20024
+ adaptCacheConfiguration() {
20025
+ const stats = this.searchCache.getStats();
20026
+ const memoryUsage = this.searchCache.getMemoryUsage();
20027
+ const currentConfig = this.searchCache.getConfig();
20028
+ // Prepare performance metrics for adaptation
20029
+ const performanceMetrics = {
20030
+ hitRate: stats.hitRate,
20031
+ avgResponseTime: 50, // Would be measured in real implementation
20032
+ memoryUsage: memoryUsage,
20033
+ externalChangesDetected: 0, // Would be tracked from real-time updates
20034
+ timeSinceLastChange: Date.now() - this.lastUpdateTime
20035
+ };
20036
+ // Try to adapt configuration
20037
+ const newConfig = this.cacheAutoConfigurator.adaptConfiguration(currentConfig, performanceMetrics);
20038
+ if (newConfig) {
20039
+ // Apply new cache configuration
20040
+ this.searchCache.updateConfig(newConfig.cacheConfig);
20041
+ // Apply new real-time update configuration if needed
20042
+ if (newConfig.realtimeConfig.enabled !==
20043
+ this.realtimeUpdateConfig.enabled ||
20044
+ newConfig.realtimeConfig.interval !== this.realtimeUpdateConfig.interval) {
20045
+ const wasEnabled = this.realtimeUpdateConfig.enabled;
20046
+ this.realtimeUpdateConfig = {
20047
+ ...this.realtimeUpdateConfig,
20048
+ ...newConfig.realtimeConfig
20049
+ };
20050
+ // Restart real-time updates with new configuration
20051
+ if (wasEnabled) {
20052
+ this.stopRealtimeUpdates();
20053
+ }
20054
+ if (this.realtimeUpdateConfig.enabled && this.isInitialized) {
20055
+ this.startRealtimeUpdates();
20056
+ }
20057
+ }
20058
+ if (this.loggingConfig?.verbose) {
20059
+ console.log('🔧 Auto-adapted cache configuration:');
20060
+ console.log(this.cacheAutoConfigurator.getConfigExplanation(newConfig));
20061
+ }
20062
+ }
20063
+ }
19050
20064
  /**
19051
20065
  * Get the number of nouns in the database (excluding verbs)
19052
20066
  * This is used for statistics reporting to match the expected behavior in tests
@@ -19116,6 +20130,41 @@ class BrainyData {
19116
20130
  // Call the flushStatisticsToStorage method on the storage adapter
19117
20131
  await this.storage.flushStatisticsToStorage();
19118
20132
  }
20133
+ /**
20134
+ * Update storage sizes if needed (called periodically for performance)
20135
+ */
20136
+ async updateStorageSizesIfNeeded() {
20137
+ // Only update every minute to avoid performance impact
20138
+ const now = Date.now();
20139
+ const lastUpdate = this.lastStorageSizeUpdate || 0;
20140
+ if (now - lastUpdate < 60000) {
20141
+ return; // Skip if updated recently
20142
+ }
20143
+ this.lastStorageSizeUpdate = now;
20144
+ try {
20145
+ // Estimate sizes based on counts and average sizes
20146
+ const stats = await this.storage.getStatistics();
20147
+ if (stats) {
20148
+ const avgNounSize = 2048; // ~2KB per noun (vector + metadata)
20149
+ const avgVerbSize = 512; // ~0.5KB per verb
20150
+ const avgMetadataSize = 256; // ~0.25KB per metadata entry
20151
+ const avgIndexEntrySize = 128; // ~128 bytes per index entry
20152
+ // Calculate total counts
20153
+ const totalNouns = Object.values(stats.nounCount).reduce((a, b) => a + b, 0);
20154
+ const totalVerbs = Object.values(stats.verbCount).reduce((a, b) => a + b, 0);
20155
+ const totalMetadata = Object.values(stats.metadataCount).reduce((a, b) => a + b, 0);
20156
+ this.statisticsCollector.updateStorageSizes({
20157
+ nouns: totalNouns * avgNounSize,
20158
+ verbs: totalVerbs * avgVerbSize,
20159
+ metadata: totalMetadata * avgMetadataSize,
20160
+ index: stats.hnswIndexSize * avgIndexEntrySize
20161
+ });
20162
+ }
20163
+ }
20164
+ catch (error) {
20165
+ // Ignore errors in size calculation
20166
+ }
20167
+ }
19119
20168
  /**
19120
20169
  * Get statistics about the current state of the database
19121
20170
  * @param options Additional options for retrieving statistics
@@ -19190,6 +20239,40 @@ class BrainyData {
19190
20239
  relate: result.verbCount,
19191
20240
  total: result.nounCount + result.verbCount + result.metadataCount
19192
20241
  };
20242
+ // Add extended statistics if requested
20243
+ if (true) {
20244
+ // Always include for now
20245
+ // Add index health metrics
20246
+ try {
20247
+ const indexHealth = this.index.getIndexHealth();
20248
+ result.indexHealth = indexHealth;
20249
+ }
20250
+ catch (e) {
20251
+ // Index health not available
20252
+ }
20253
+ // Add cache metrics
20254
+ try {
20255
+ const cacheStats = this.searchCache.getStats();
20256
+ result.cacheMetrics = cacheStats;
20257
+ }
20258
+ catch (e) {
20259
+ // Cache stats not available
20260
+ }
20261
+ // Add memory usage
20262
+ if (typeof process !== 'undefined' && process.memoryUsage) {
20263
+ ;
20264
+ result.memoryUsage = process.memoryUsage().heapUsed;
20265
+ }
20266
+ // Add last updated timestamp
20267
+ ;
20268
+ result.lastUpdated =
20269
+ stats.lastUpdated || new Date().toISOString();
20270
+ // Add enhanced statistics from collector
20271
+ const collectorStats = this.statisticsCollector.getStatistics();
20272
+ Object.assign(result, collectorStats);
20273
+ // Update storage sizes if needed (only periodically for performance)
20274
+ await this.updateStorageSizesIfNeeded();
20275
+ }
19193
20276
  return result;
19194
20277
  }
19195
20278
  // If statistics are not available, return zeros instead of calculating on-demand
@@ -19549,15 +20632,20 @@ class BrainyData {
19549
20632
  await this.ensureInitialized();
19550
20633
  // Check if database is in write-only mode
19551
20634
  this.checkWriteOnly();
20635
+ const searchStartTime = Date.now();
19552
20636
  try {
19553
20637
  // Embed the query text
19554
20638
  const queryVector = await this.embed(query);
19555
20639
  // Search using the embedded vector
19556
- return await this.search(queryVector, k, {
20640
+ const results = await this.search(queryVector, k, {
19557
20641
  nounTypes: options.nounTypes,
19558
20642
  includeVerbs: options.includeVerbs,
19559
20643
  searchMode: options.searchMode
19560
20644
  });
20645
+ // Track search performance
20646
+ const duration = Date.now() - searchStartTime;
20647
+ this.statisticsCollector.trackSearch(query, duration);
20648
+ return results;
19561
20649
  }
19562
20650
  catch (error) {
19563
20651
  console.error('Failed to search with text query:', error);
@@ -19593,12 +20681,17 @@ class BrainyData {
19593
20681
  if (!this.serverSearchConduit || !this.serverConnection) {
19594
20682
  throw new Error('Server search conduit or connection is not initialized');
19595
20683
  }
19596
- // Search the remote server
19597
- const searchResult = await this.serverSearchConduit.searchServer(this.serverConnection.connectionId, query, k);
20684
+ // When using offset, fetch more results and slice
20685
+ const offset = options.offset || 0;
20686
+ const totalNeeded = k + offset;
20687
+ // Search the remote server for totalNeeded results
20688
+ const searchResult = await this.serverSearchConduit.searchServer(this.serverConnection.connectionId, query, totalNeeded);
19598
20689
  if (!searchResult.success) {
19599
20690
  throw new Error(`Remote search failed: ${searchResult.error}`);
19600
20691
  }
19601
- return searchResult.data;
20692
+ // Apply offset to remote results
20693
+ const allResults = searchResult.data;
20694
+ return allResults.slice(offset, offset + k);
19602
20695
  }
19603
20696
  catch (error) {
19604
20697
  console.error('Failed to search remote server:', error);