@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/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)) / this.realtimeUpdateConfig.interval);
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 = await this.storage.getNoun(options.id) ?? undefined;
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 = await this.storage.getNoun(options.id) ?? undefined;
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
- metadataToSave.domainMetadata = domainInfo.domainMetadata;
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) ? metadata : 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
- metadataToSave.domainMetadata = domainInfo.domainMetadata;
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 (excluding verbs)
18107
- await this.storage.updateHnswIndexSize(await this.getNounCount());
18409
+ // Update HNSW index size with actual index size
18410
+ const indexSize = this.index.size();
18411
+ await this.storage.updateHnswIndexSize(indexSize);
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 = (startIndex + results.length) < allResults.length || allResults.length >= searchK;
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
- // Update HNSW index size (excluding verbs)
19452
- await this.storage.updateHnswIndexSize(await this.getNounCount());
19758
+ // Track verb type
19759
+ this.statisticsCollector.trackVerbType(verbMetadata.verb);
19760
+ // Update HNSW index size with actual index size
19761
+ const indexSize = this.index.size();
19762
+ await this.storage.updateHnswIndexSize(indexSize);
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 !== this.realtimeUpdateConfig.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
- return await this.search(queryVector, k, {
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);