@soulcraft/brainy 0.39.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/brainyData.d.ts +5 -0
- package/dist/coreTypes.d.ts +48 -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 +417 -30
- package/dist/unified.min.js +990 -990
- 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
|
|
@@ -17110,6 +17158,230 @@ class CacheAutoConfigurator {
|
|
|
17110
17158
|
}
|
|
17111
17159
|
}
|
|
17112
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
|
+
|
|
17113
17385
|
/**
|
|
17114
17386
|
* BrainyData
|
|
17115
17387
|
* Main class that provides the vector database functionality
|
|
@@ -17170,6 +17442,8 @@ class BrainyData {
|
|
|
17170
17442
|
this.operationalMode = null;
|
|
17171
17443
|
this.domainDetector = null;
|
|
17172
17444
|
this.healthMonitor = null;
|
|
17445
|
+
// Statistics collector
|
|
17446
|
+
this.statisticsCollector = new StatisticsCollector();
|
|
17173
17447
|
// Set dimensions to fixed value of 512 (Universal Sentence Encoder dimension)
|
|
17174
17448
|
this._dimensions = 512;
|
|
17175
17449
|
// Set distance function
|
|
@@ -17433,7 +17707,8 @@ class BrainyData {
|
|
|
17433
17707
|
}
|
|
17434
17708
|
// Adapt cache configuration based on performance (every few updates)
|
|
17435
17709
|
// Only adapt every 5th update to avoid over-optimization
|
|
17436
|
-
const updateCount = Math.floor((Date.now() - (this.lastUpdateTime || 0)) /
|
|
17710
|
+
const updateCount = Math.floor((Date.now() - (this.lastUpdateTime || 0)) /
|
|
17711
|
+
this.realtimeUpdateConfig.interval);
|
|
17437
17712
|
if (updateCount % 5 === 0) {
|
|
17438
17713
|
this.adaptCacheConfiguration();
|
|
17439
17714
|
}
|
|
@@ -17751,6 +18026,16 @@ class BrainyData {
|
|
|
17751
18026
|
// Continue initialization even if remote connection fails
|
|
17752
18027
|
}
|
|
17753
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
|
+
}
|
|
17754
18039
|
this.isInitialized = true;
|
|
17755
18040
|
this.isInitializing = false;
|
|
17756
18041
|
// Start real-time updates if enabled
|
|
@@ -17962,13 +18247,15 @@ class BrainyData {
|
|
|
17962
18247
|
try {
|
|
17963
18248
|
if (this.writeOnly) {
|
|
17964
18249
|
// In write-only mode, check storage directly
|
|
17965
|
-
existingNoun =
|
|
18250
|
+
existingNoun =
|
|
18251
|
+
(await this.storage.getNoun(options.id)) ?? undefined;
|
|
17966
18252
|
}
|
|
17967
18253
|
else {
|
|
17968
18254
|
// In normal mode, check index first, then storage
|
|
17969
18255
|
existingNoun = this.index.getNouns().get(options.id);
|
|
17970
18256
|
if (!existingNoun) {
|
|
17971
|
-
existingNoun =
|
|
18257
|
+
existingNoun =
|
|
18258
|
+
(await this.storage.getNoun(options.id)) ?? undefined;
|
|
17972
18259
|
}
|
|
17973
18260
|
}
|
|
17974
18261
|
if (existingNoun) {
|
|
@@ -18003,6 +18290,7 @@ class BrainyData {
|
|
|
18003
18290
|
id,
|
|
18004
18291
|
vector,
|
|
18005
18292
|
connections: new Map(),
|
|
18293
|
+
level: 0, // Default level for new nodes
|
|
18006
18294
|
metadata: undefined // Will be set separately
|
|
18007
18295
|
};
|
|
18008
18296
|
}
|
|
@@ -18076,17 +18364,24 @@ class BrainyData {
|
|
|
18076
18364
|
// Domain already specified, keep it
|
|
18077
18365
|
const domainInfo = this.domainDetector.detectDomain(metadataToSave);
|
|
18078
18366
|
if (domainInfo.domainMetadata) {
|
|
18079
|
-
|
|
18367
|
+
;
|
|
18368
|
+
metadataToSave.domainMetadata =
|
|
18369
|
+
domainInfo.domainMetadata;
|
|
18080
18370
|
}
|
|
18081
18371
|
}
|
|
18082
18372
|
else {
|
|
18083
18373
|
// Try to detect domain from the data
|
|
18084
|
-
const dataToAnalyze = Array.isArray(vectorOrData)
|
|
18374
|
+
const dataToAnalyze = Array.isArray(vectorOrData)
|
|
18375
|
+
? metadata
|
|
18376
|
+
: vectorOrData;
|
|
18085
18377
|
const domainInfo = this.domainDetector.detectDomain(dataToAnalyze);
|
|
18086
18378
|
if (domainInfo.domain) {
|
|
18379
|
+
;
|
|
18087
18380
|
metadataToSave.domain = domainInfo.domain;
|
|
18088
18381
|
if (domainInfo.domainMetadata) {
|
|
18089
|
-
|
|
18382
|
+
;
|
|
18383
|
+
metadataToSave.domainMetadata =
|
|
18384
|
+
domainInfo.domainMetadata;
|
|
18090
18385
|
}
|
|
18091
18386
|
}
|
|
18092
18387
|
}
|
|
@@ -18101,10 +18396,19 @@ class BrainyData {
|
|
|
18101
18396
|
// Track metadata statistics
|
|
18102
18397
|
const metadataService = this.getServiceName(options);
|
|
18103
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();
|
|
18104
18407
|
}
|
|
18105
18408
|
}
|
|
18106
|
-
// Update HNSW index size
|
|
18107
|
-
|
|
18409
|
+
// Update HNSW index size with actual index size
|
|
18410
|
+
const indexSize = this.index.size();
|
|
18411
|
+
await this.storage.updateHnswIndexSize(indexSize);
|
|
18108
18412
|
// Update health metrics if in distributed mode
|
|
18109
18413
|
if (this.healthMonitor) {
|
|
18110
18414
|
const vectorCount = await this.getNounCount();
|
|
@@ -18578,7 +18882,7 @@ class BrainyData {
|
|
|
18578
18882
|
let startIndex = 0;
|
|
18579
18883
|
// If cursor provided, find starting position
|
|
18580
18884
|
if (options.cursor) {
|
|
18581
|
-
startIndex = allResults.findIndex(r => r.id === options.cursor.lastId &&
|
|
18885
|
+
startIndex = allResults.findIndex((r) => r.id === options.cursor.lastId &&
|
|
18582
18886
|
Math.abs(r.score - options.cursor.lastScore) < 0.0001);
|
|
18583
18887
|
if (startIndex >= 0) {
|
|
18584
18888
|
startIndex += 1; // Start after the cursor position
|
|
@@ -18595,7 +18899,8 @@ class BrainyData {
|
|
|
18595
18899
|
}
|
|
18596
18900
|
// Create cursor for next page
|
|
18597
18901
|
let nextCursor;
|
|
18598
|
-
const hasMoreResults =
|
|
18902
|
+
const hasMoreResults = startIndex + results.length < allResults.length ||
|
|
18903
|
+
allResults.length >= searchK;
|
|
18599
18904
|
if (results.length > 0 && hasMoreResults) {
|
|
18600
18905
|
const lastResult = results[results.length - 1];
|
|
18601
18906
|
nextCursor = {
|
|
@@ -18679,7 +18984,7 @@ class BrainyData {
|
|
|
18679
18984
|
});
|
|
18680
18985
|
}
|
|
18681
18986
|
// Filter out placeholder nouns from search results
|
|
18682
|
-
searchResults = searchResults.filter(result => {
|
|
18987
|
+
searchResults = searchResults.filter((result) => {
|
|
18683
18988
|
if (result.metadata && typeof result.metadata === 'object') {
|
|
18684
18989
|
const metadata = result.metadata;
|
|
18685
18990
|
// Exclude placeholder nouns from search results
|
|
@@ -18787,7 +19092,7 @@ class BrainyData {
|
|
|
18787
19092
|
// In write-only mode, query storage directly since index is not loaded
|
|
18788
19093
|
if (this.writeOnly) {
|
|
18789
19094
|
try {
|
|
18790
|
-
noun = await this.storage.getNoun(id) ?? undefined;
|
|
19095
|
+
noun = (await this.storage.getNoun(id)) ?? undefined;
|
|
18791
19096
|
}
|
|
18792
19097
|
catch (storageError) {
|
|
18793
19098
|
// If storage lookup fails, return null (noun doesn't exist)
|
|
@@ -18800,7 +19105,7 @@ class BrainyData {
|
|
|
18800
19105
|
// If not found in index, fallback to storage (for race conditions)
|
|
18801
19106
|
if (!noun && this.storage) {
|
|
18802
19107
|
try {
|
|
18803
|
-
noun = await this.storage.getNoun(id) ?? undefined;
|
|
19108
|
+
noun = (await this.storage.getNoun(id)) ?? undefined;
|
|
18804
19109
|
}
|
|
18805
19110
|
catch (storageError) {
|
|
18806
19111
|
// Storage lookup failed, noun doesn't exist
|
|
@@ -19192,6 +19497,7 @@ class BrainyData {
|
|
|
19192
19497
|
id: sourceId,
|
|
19193
19498
|
vector: sourcePlaceholderVector,
|
|
19194
19499
|
connections: new Map(),
|
|
19500
|
+
level: 0,
|
|
19195
19501
|
metadata: sourceMetadata
|
|
19196
19502
|
};
|
|
19197
19503
|
// Create placeholder target noun
|
|
@@ -19212,6 +19518,7 @@ class BrainyData {
|
|
|
19212
19518
|
id: targetId,
|
|
19213
19519
|
vector: targetPlaceholderVector,
|
|
19214
19520
|
connections: new Map(),
|
|
19521
|
+
level: 0,
|
|
19215
19522
|
metadata: targetMetadata
|
|
19216
19523
|
};
|
|
19217
19524
|
// Save placeholder nouns to storage (but skip indexing for speed)
|
|
@@ -19448,8 +19755,11 @@ class BrainyData {
|
|
|
19448
19755
|
// Track verb statistics
|
|
19449
19756
|
const serviceForStats = this.getServiceName(options);
|
|
19450
19757
|
await this.storage.incrementStatistic('verb', serviceForStats);
|
|
19451
|
-
//
|
|
19452
|
-
|
|
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);
|
|
19453
19763
|
// Invalidate search cache since verb data has changed
|
|
19454
19764
|
this.searchCache.invalidateOnDataChange('add');
|
|
19455
19765
|
return id;
|
|
@@ -19674,6 +19984,8 @@ class BrainyData {
|
|
|
19674
19984
|
await this.index.clear();
|
|
19675
19985
|
// Clear storage
|
|
19676
19986
|
await this.storage.clear();
|
|
19987
|
+
// Reset statistics collector
|
|
19988
|
+
this.statisticsCollector = new StatisticsCollector();
|
|
19677
19989
|
// Clear search cache since all data has been removed
|
|
19678
19990
|
this.searchCache.invalidateOnDataChange('delete');
|
|
19679
19991
|
}
|
|
@@ -19727,7 +20039,8 @@ class BrainyData {
|
|
|
19727
20039
|
// Apply new cache configuration
|
|
19728
20040
|
this.searchCache.updateConfig(newConfig.cacheConfig);
|
|
19729
20041
|
// Apply new real-time update configuration if needed
|
|
19730
|
-
if (newConfig.realtimeConfig.enabled !==
|
|
20042
|
+
if (newConfig.realtimeConfig.enabled !==
|
|
20043
|
+
this.realtimeUpdateConfig.enabled ||
|
|
19731
20044
|
newConfig.realtimeConfig.interval !== this.realtimeUpdateConfig.interval) {
|
|
19732
20045
|
const wasEnabled = this.realtimeUpdateConfig.enabled;
|
|
19733
20046
|
this.realtimeUpdateConfig = {
|
|
@@ -19817,6 +20130,41 @@ class BrainyData {
|
|
|
19817
20130
|
// Call the flushStatisticsToStorage method on the storage adapter
|
|
19818
20131
|
await this.storage.flushStatisticsToStorage();
|
|
19819
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
|
+
}
|
|
19820
20168
|
/**
|
|
19821
20169
|
* Get statistics about the current state of the database
|
|
19822
20170
|
* @param options Additional options for retrieving statistics
|
|
@@ -19891,6 +20239,40 @@ class BrainyData {
|
|
|
19891
20239
|
relate: result.verbCount,
|
|
19892
20240
|
total: result.nounCount + result.verbCount + result.metadataCount
|
|
19893
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
|
+
}
|
|
19894
20276
|
return result;
|
|
19895
20277
|
}
|
|
19896
20278
|
// If statistics are not available, return zeros instead of calculating on-demand
|
|
@@ -20250,15 +20632,20 @@ class BrainyData {
|
|
|
20250
20632
|
await this.ensureInitialized();
|
|
20251
20633
|
// Check if database is in write-only mode
|
|
20252
20634
|
this.checkWriteOnly();
|
|
20635
|
+
const searchStartTime = Date.now();
|
|
20253
20636
|
try {
|
|
20254
20637
|
// Embed the query text
|
|
20255
20638
|
const queryVector = await this.embed(query);
|
|
20256
20639
|
// Search using the embedded vector
|
|
20257
|
-
|
|
20640
|
+
const results = await this.search(queryVector, k, {
|
|
20258
20641
|
nounTypes: options.nounTypes,
|
|
20259
20642
|
includeVerbs: options.includeVerbs,
|
|
20260
20643
|
searchMode: options.searchMode
|
|
20261
20644
|
});
|
|
20645
|
+
// Track search performance
|
|
20646
|
+
const duration = Date.now() - searchStartTime;
|
|
20647
|
+
this.statisticsCollector.trackSearch(query, duration);
|
|
20648
|
+
return results;
|
|
20262
20649
|
}
|
|
20263
20650
|
catch (error) {
|
|
20264
20651
|
console.error('Failed to search with text query:', error);
|