@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/README.md +182 -105
- package/dist/brainyData.d.ts +66 -1
- package/dist/coreTypes.d.ts +65 -0
- package/dist/hnsw/hnswIndex.d.ts +9 -0
- package/dist/hnsw/hnswIndex.d.ts.map +1 -1
- package/dist/hnsw/hnswIndexOptimized.d.ts.map +1 -1
- package/dist/storage/adapters/fileSystemStorage.d.ts.map +1 -1
- package/dist/storage/adapters/memoryStorage.d.ts.map +1 -1
- package/dist/storage/adapters/opfsStorage.d.ts.map +1 -1
- package/dist/storage/adapters/s3CompatibleStorage.d.ts.map +1 -1
- package/dist/storage/cacheManager.d.ts +2 -2
- package/dist/storage/cacheManager.d.ts.map +1 -1
- package/dist/unified.js +1130 -37
- package/dist/unified.min.js +991 -991
- package/dist/utils/cacheAutoConfig.d.ts +63 -0
- package/dist/utils/cacheAutoConfig.d.ts.map +1 -0
- package/dist/utils/searchCache.d.ts +93 -0
- package/dist/utils/searchCache.d.ts.map +1 -0
- package/dist/utils/statisticsCollector.d.ts +55 -0
- package/dist/utils/statisticsCollector.d.ts.map +1 -0
- package/package.json +1 -1
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 =
|
|
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 =
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
|
17553
|
-
|
|
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
|
-
//
|
|
17826
|
-
const
|
|
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
|
|
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
|
-
//
|
|
17871
|
-
const
|
|
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(
|
|
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
|
-
//
|
|
18815
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
19597
|
-
const
|
|
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
|
-
|
|
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);
|