@soulcraft/brainy 3.1.1 → 3.2.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.
@@ -1,11 +1,19 @@
1
1
  /**
2
2
  * Metrics Augmentation - Optional Performance & Usage Metrics
3
3
  *
4
- * Replaces the hardcoded StatisticsCollector in Brainy with an optional augmentation.
5
- * Tracks performance metrics, usage patterns, and system statistics.
4
+ * IMPORTANT: This is SEPARATE from core counting (brain.counts.*) which is always enabled.
5
+ *
6
+ * Core counting provides O(1) entity/relationship counts for scalability.
7
+ * This augmentation provides performance analytics and observability.
8
+ *
9
+ * Features:
10
+ * - Search performance tracking (latency, throughput)
11
+ * - Cache hit/miss rates
12
+ * - Operation timing analysis
13
+ * - Usage pattern insights
6
14
  *
7
15
  * Zero-config: Automatically enabled for observability
8
- * Can be disabled or customized via augmentation registry
16
+ * Can be disabled via augmentation registry without affecting core performance
9
17
  */
10
18
  import { BaseAugmentation } from './brainyAugmentation.js';
11
19
  export interface MetricsConfig {
@@ -71,10 +79,16 @@ export declare class MetricsAugmentation extends BaseAugmentation {
71
79
  */
72
80
  private persistMetrics;
73
81
  /**
74
- * Get current metrics
82
+ * Get current metrics (performance analytics only)
83
+ *
84
+ * NOTE: For production counting (entities, relationships), use:
85
+ * - brain.counts.entities() - O(1) total count
86
+ * - brain.counts.byType() - O(1) type-specific counts
87
+ * - brain.counts.relationships() - O(1) relationship counts
75
88
  */
76
89
  getStatistics(): {
77
90
  enabled: boolean;
91
+ note: string;
78
92
  totalSearches: number;
79
93
  totalUpdates: number;
80
94
  contentTypes: {};
@@ -166,6 +180,7 @@ export declare class MetricsAugmentation extends BaseAugmentation {
166
180
  lastUpdated?: string | undefined;
167
181
  distributedConfig?: import("../types/distributedTypes.js").SharedConfig | undefined;
168
182
  enabled: boolean;
183
+ note: string;
169
184
  totalSearches?: undefined;
170
185
  totalUpdates?: undefined;
171
186
  verbTypes?: undefined;
@@ -1,11 +1,19 @@
1
1
  /**
2
2
  * Metrics Augmentation - Optional Performance & Usage Metrics
3
3
  *
4
- * Replaces the hardcoded StatisticsCollector in Brainy with an optional augmentation.
5
- * Tracks performance metrics, usage patterns, and system statistics.
4
+ * IMPORTANT: This is SEPARATE from core counting (brain.counts.*) which is always enabled.
5
+ *
6
+ * Core counting provides O(1) entity/relationship counts for scalability.
7
+ * This augmentation provides performance analytics and observability.
8
+ *
9
+ * Features:
10
+ * - Search performance tracking (latency, throughput)
11
+ * - Cache hit/miss rates
12
+ * - Operation timing analysis
13
+ * - Usage pattern insights
6
14
  *
7
15
  * Zero-config: Automatically enabled for observability
8
- * Can be disabled or customized via augmentation registry
16
+ * Can be disabled via augmentation registry without affecting core performance
9
17
  */
10
18
  import { BaseAugmentation } from './brainyAugmentation.js';
11
19
  import { StatisticsCollector } from '../utils/statisticsCollector.js';
@@ -224,12 +232,18 @@ export class MetricsAugmentation extends BaseAugmentation {
224
232
  }
225
233
  }
226
234
  /**
227
- * Get current metrics
235
+ * Get current metrics (performance analytics only)
236
+ *
237
+ * NOTE: For production counting (entities, relationships), use:
238
+ * - brain.counts.entities() - O(1) total count
239
+ * - brain.counts.byType() - O(1) type-specific counts
240
+ * - brain.counts.relationships() - O(1) relationship counts
228
241
  */
229
242
  getStatistics() {
230
243
  if (!this.statisticsCollector) {
231
244
  return {
232
245
  enabled: false,
246
+ note: 'For core counting, use brain.counts.* APIs which are always available',
233
247
  totalSearches: 0,
234
248
  totalUpdates: 0,
235
249
  contentTypes: {},
@@ -243,6 +257,7 @@ export class MetricsAugmentation extends BaseAugmentation {
243
257
  }
244
258
  return {
245
259
  enabled: true,
260
+ note: 'Performance analytics only. Core counting available via brain.counts.*',
246
261
  ...this.statisticsCollector.getStatistics()
247
262
  };
248
263
  }
package/dist/brainy.d.ts CHANGED
@@ -195,6 +195,82 @@ export declare class Brainy<T = any> {
195
195
  services: string[];
196
196
  density: number;
197
197
  }>;
198
+ /**
199
+ * Efficient Pagination API - Production-scale pagination using index-first approach
200
+ * Automatically optimizes based on query type and applies pagination at the index level
201
+ */
202
+ get pagination(): {
203
+ find: (params: FindParams<T> & {
204
+ page?: number;
205
+ pageSize?: number;
206
+ }) => Promise<Result<T>[]>;
207
+ count: (params: Omit<FindParams<T>, "limit" | "offset">) => Promise<number>;
208
+ meta: (params: FindParams<T> & {
209
+ page?: number;
210
+ pageSize?: number;
211
+ }) => Promise<{
212
+ page: number;
213
+ pageSize: number;
214
+ totalCount: number;
215
+ totalPages: number;
216
+ hasNext: boolean;
217
+ hasPrev: boolean;
218
+ }>;
219
+ };
220
+ /**
221
+ * Streaming API - Process millions of entities with constant memory using existing Pipeline
222
+ * Integrates with index-based optimizations for maximum efficiency
223
+ */
224
+ get streaming(): {
225
+ entities: (filter?: Partial<FindParams<T>>) => AsyncGenerator<Entity<T>>;
226
+ search: (params: FindParams<T>, batchSize?: number) => AsyncGenerator<{
227
+ id: string;
228
+ score: number;
229
+ entity: Entity<T>;
230
+ }>;
231
+ relationships: (filter?: {
232
+ type?: string;
233
+ sourceId?: string;
234
+ targetId?: string;
235
+ }) => AsyncGenerator<any>;
236
+ pipeline: (source: AsyncIterable<any>) => any;
237
+ process: (processor: (entity: Entity<T>) => Promise<Entity<T>>, filter?: Partial<FindParams<T>>, options?: {
238
+ batchSize: number;
239
+ parallel: number;
240
+ }) => Promise<void>;
241
+ };
242
+ /**
243
+ * O(1) Count API - Production-scale counting using existing indexes
244
+ * Works across all storage adapters (FileSystem, OPFS, S3, Memory)
245
+ */
246
+ get counts(): {
247
+ entities: () => number;
248
+ relationships: () => number;
249
+ byType: (type?: string) => number | {
250
+ [k: string]: number;
251
+ };
252
+ byRelationshipType: (type?: string) => number | {
253
+ [k: string]: number;
254
+ };
255
+ byCriteria: (field: string, value: any) => Promise<number>;
256
+ getAllTypeCounts: () => Map<string, number>;
257
+ getStats: () => {
258
+ entities: {
259
+ total: number;
260
+ byType: {
261
+ [k: string]: number;
262
+ };
263
+ };
264
+ relationships: {
265
+ totalRelationships: number;
266
+ relationshipsByType: Record<string, number>;
267
+ uniqueSourceNodes: number;
268
+ uniqueTargetNodes: number;
269
+ totalNodes: number;
270
+ };
271
+ density: number;
272
+ };
273
+ };
198
274
  /**
199
275
  * Augmentations API - Clean and simple
200
276
  */
package/dist/brainy.js CHANGED
@@ -16,6 +16,7 @@ import { NaturalLanguageProcessor } from './neural/naturalLanguageProcessor.js';
16
16
  import { TripleIntelligenceSystem } from './triple/TripleIntelligenceSystem.js';
17
17
  import { MetadataIndexManager } from './utils/metadataIndex.js';
18
18
  import { GraphAdjacencyIndex } from './graph/graphAdjacencyIndex.js';
19
+ import { createPipeline } from './streaming/pipeline.js';
19
20
  import { configureLogger, LogLevel } from './utils/logger.js';
20
21
  import { NounType } from './types/graphTypes.js';
21
22
  /**
@@ -468,22 +469,51 @@ export class Brainy {
468
469
  const filteredIds = await this.metadataIndex.getIdsForFilter(filter);
469
470
  // CRITICAL FIX: Handle both cases properly
470
471
  if (results.length > 0) {
471
- // Filter existing results (from vector search)
472
+ // OPTIMIZED: Filter existing results (from vector search) efficiently
472
473
  const filteredIdSet = new Set(filteredIds);
473
474
  results = results.filter((r) => filteredIdSet.has(r.id));
475
+ // Apply early pagination for vector + metadata queries
476
+ const limit = params.limit || 10;
477
+ const offset = params.offset || 0;
478
+ // If we have enough filtered results, sort and paginate early
479
+ if (results.length >= offset + limit) {
480
+ results.sort((a, b) => b.score - a.score);
481
+ results = results.slice(offset, offset + limit);
482
+ // Load entities only for the paginated results
483
+ for (const result of results) {
484
+ if (!result.entity) {
485
+ const entity = await this.get(result.id);
486
+ if (entity) {
487
+ result.entity = entity;
488
+ }
489
+ }
490
+ }
491
+ // Early return if no other processing needed
492
+ if (!params.connected && !params.fusion) {
493
+ return results;
494
+ }
495
+ }
474
496
  }
475
497
  else {
476
- // Create results from metadata matches (metadata-only query)
477
- for (const id of filteredIds) {
498
+ // OPTIMIZED: Apply pagination to filtered IDs BEFORE loading entities
499
+ const limit = params.limit || 10;
500
+ const offset = params.offset || 0;
501
+ const pageIds = filteredIds.slice(offset, offset + limit);
502
+ // Load only entities for current page - O(page_size) instead of O(total_results)
503
+ for (const id of pageIds) {
478
504
  const entity = await this.get(id);
479
505
  if (entity) {
480
506
  results.push({
481
507
  id,
482
508
  score: 1.0, // All metadata matches are equally relevant
483
- entity
509
+ entity: entity
484
510
  });
485
511
  }
486
512
  }
513
+ // Early return for metadata-only queries with pagination applied
514
+ if (!params.query && !params.connected) {
515
+ return results;
516
+ }
487
517
  }
488
518
  }
489
519
  // Graph search component with O(1) traversal
@@ -494,10 +524,11 @@ export class Brainy {
494
524
  if (params.fusion && results.length > 0) {
495
525
  results = this.applyFusionScoring(results, params.fusion);
496
526
  }
497
- // Sort by score and apply pagination
527
+ // OPTIMIZED: Sort first, then apply efficient pagination
498
528
  results.sort((a, b) => b.score - a.score);
499
529
  const limit = params.limit || 10;
500
530
  const offset = params.offset || 0;
531
+ // Efficient pagination - only slice what we need
501
532
  return results.slice(offset, offset + limit);
502
533
  });
503
534
  // Record performance for auto-tuning
@@ -863,24 +894,16 @@ export class Brainy {
863
894
  */
864
895
  async insights() {
865
896
  await this.ensureInitialized();
866
- // Get all entities count - use getNouns with high limit
867
- const entitiesResult = await this.storage.getNouns({
868
- pagination: { limit: 10000 }
869
- });
870
- const entities = entitiesResult.totalCount || entitiesResult.items.length;
871
- // Get relationships count - use getVerbs with high limit
872
- const verbsResult = await this.storage.getVerbs({
873
- pagination: { limit: 10000 }
874
- });
875
- const relationships = verbsResult.totalCount || verbsResult.items.length;
876
- // Count by type
877
- const types = {};
878
- for (const entity of entitiesResult.items) {
879
- const type = entity.metadata?.noun || 'unknown';
880
- types[type] = (types[type] || 0) + 1;
881
- }
882
- // Get unique services
883
- const services = [...new Set(entitiesResult.items.map((e) => e.metadata?.service).filter(Boolean))];
897
+ // O(1) entity counting using existing MetadataIndexManager
898
+ const entities = this.metadataIndex.getTotalEntityCount();
899
+ // O(1) count by type using existing index tracking
900
+ const typeCountsMap = this.metadataIndex.getAllEntityCounts();
901
+ const types = Object.fromEntries(typeCountsMap);
902
+ // O(1) relationships count using GraphAdjacencyIndex
903
+ const relationships = this.graphIndex.getTotalRelationshipCount();
904
+ // Get unique services - O(log n) using index
905
+ const serviceValues = await this.metadataIndex.getFilterValues('service');
906
+ const services = serviceValues.filter(Boolean);
884
907
  // Calculate density (relationships per entity)
885
908
  const density = entities > 0 ? relationships / entities : 0;
886
909
  return {
@@ -891,6 +914,229 @@ export class Brainy {
891
914
  density
892
915
  };
893
916
  }
917
+ /**
918
+ * Efficient Pagination API - Production-scale pagination using index-first approach
919
+ * Automatically optimizes based on query type and applies pagination at the index level
920
+ */
921
+ get pagination() {
922
+ return {
923
+ // Get paginated results with automatic optimization
924
+ find: async (params) => {
925
+ const page = params.page || 1;
926
+ const pageSize = params.pageSize || 10;
927
+ const offset = (page - 1) * pageSize;
928
+ return this.find({
929
+ ...params,
930
+ limit: pageSize,
931
+ offset
932
+ });
933
+ },
934
+ // Get total count for pagination UI (O(1) when possible)
935
+ count: async (params) => {
936
+ // For simple type queries, use O(1) index counting
937
+ if (params.type && !params.query && !params.where && !params.connected) {
938
+ const types = Array.isArray(params.type) ? params.type : [params.type];
939
+ return types.reduce((sum, type) => sum + this.metadataIndex.getEntityCountByType(type), 0);
940
+ }
941
+ // For complex queries, use metadata index for efficient counting
942
+ if (params.where || params.service) {
943
+ let filter = {};
944
+ if (params.where)
945
+ Object.assign(filter, params.where);
946
+ if (params.service)
947
+ filter.service = params.service;
948
+ if (params.type) {
949
+ const types = Array.isArray(params.type) ? params.type : [params.type];
950
+ if (types.length === 1) {
951
+ filter.noun = types[0];
952
+ }
953
+ else {
954
+ const baseFilter = { ...filter };
955
+ filter = {
956
+ anyOf: types.map(type => ({ noun: type, ...baseFilter }))
957
+ };
958
+ }
959
+ }
960
+ const filteredIds = await this.metadataIndex.getIdsForFilter(filter);
961
+ return filteredIds.length;
962
+ }
963
+ // Fallback: total entity count
964
+ return this.metadataIndex.getTotalEntityCount();
965
+ },
966
+ // Get pagination metadata
967
+ meta: async (params) => {
968
+ const page = params.page || 1;
969
+ const pageSize = params.pageSize || 10;
970
+ const totalCount = await this.pagination.count(params);
971
+ const totalPages = Math.ceil(totalCount / pageSize);
972
+ return {
973
+ page,
974
+ pageSize,
975
+ totalCount,
976
+ totalPages,
977
+ hasNext: page < totalPages,
978
+ hasPrev: page > 1
979
+ };
980
+ }
981
+ };
982
+ }
983
+ /**
984
+ * Streaming API - Process millions of entities with constant memory using existing Pipeline
985
+ * Integrates with index-based optimizations for maximum efficiency
986
+ */
987
+ get streaming() {
988
+ return {
989
+ // Stream all entities with optional filtering
990
+ entities: async function* (filter) {
991
+ if (filter?.type || filter?.where || filter?.service) {
992
+ // Use MetadataIndexManager for efficient filtered streaming
993
+ let filterObj = {};
994
+ if (filter.where)
995
+ Object.assign(filterObj, filter.where);
996
+ if (filter.service)
997
+ filterObj.service = filter.service;
998
+ if (filter.type) {
999
+ const types = Array.isArray(filter.type) ? filter.type : [filter.type];
1000
+ if (types.length === 1) {
1001
+ filterObj.noun = types[0];
1002
+ }
1003
+ else {
1004
+ const baseFilterObj = { ...filterObj };
1005
+ filterObj = {
1006
+ anyOf: types.map(type => ({ noun: type, ...baseFilterObj }))
1007
+ };
1008
+ }
1009
+ }
1010
+ const filteredIds = await this.metadataIndex.getIdsForFilter(filterObj);
1011
+ // Stream filtered entities in batches for memory efficiency
1012
+ const batchSize = 100;
1013
+ for (let i = 0; i < filteredIds.length; i += batchSize) {
1014
+ const batchIds = filteredIds.slice(i, i + batchSize);
1015
+ for (const id of batchIds) {
1016
+ const entity = await this.get(id);
1017
+ if (entity)
1018
+ yield entity;
1019
+ }
1020
+ }
1021
+ }
1022
+ else {
1023
+ // Stream all entities using storage adapter pagination
1024
+ let offset = 0;
1025
+ const batchSize = 100;
1026
+ let hasMore = true;
1027
+ while (hasMore) {
1028
+ const result = await this.storage.getNouns({
1029
+ pagination: { offset, limit: batchSize }
1030
+ });
1031
+ for (const noun of result.items) {
1032
+ // Convert HNSWNoun to Entity<T>
1033
+ yield noun;
1034
+ }
1035
+ hasMore = result.hasMore;
1036
+ offset += batchSize;
1037
+ }
1038
+ }
1039
+ }.bind(this),
1040
+ // Stream search results efficiently
1041
+ search: async function* (params, batchSize = 50) {
1042
+ const originalLimit = params.limit;
1043
+ let offset = 0;
1044
+ let hasMore = true;
1045
+ while (hasMore) {
1046
+ const batchResults = await this.find({
1047
+ ...params,
1048
+ limit: batchSize,
1049
+ offset
1050
+ });
1051
+ for (const result of batchResults) {
1052
+ yield result;
1053
+ }
1054
+ hasMore = batchResults.length === batchSize;
1055
+ offset += batchSize;
1056
+ // Respect original limit if specified
1057
+ if (originalLimit && offset >= originalLimit) {
1058
+ break;
1059
+ }
1060
+ }
1061
+ }.bind(this),
1062
+ // Stream relationships efficiently
1063
+ relationships: async function* (filter) {
1064
+ let offset = 0;
1065
+ const batchSize = 100;
1066
+ let hasMore = true;
1067
+ while (hasMore) {
1068
+ const result = await this.storage.getVerbs({
1069
+ pagination: { offset, limit: batchSize },
1070
+ filter
1071
+ });
1072
+ for (const verb of result.items) {
1073
+ yield verb;
1074
+ }
1075
+ hasMore = result.hasMore;
1076
+ offset += batchSize;
1077
+ }
1078
+ }.bind(this),
1079
+ // Create processing pipeline from stream
1080
+ pipeline: (source) => {
1081
+ return createPipeline(this).source(source);
1082
+ },
1083
+ // Batch process entities with Pipeline system
1084
+ process: async function (processor, filter, options = { batchSize: 50, parallel: 4 }) {
1085
+ return createPipeline(this)
1086
+ .source(this.streaming.entities(filter))
1087
+ .batch(options.batchSize)
1088
+ .parallelSink(async (batch) => {
1089
+ await Promise.all(batch.map(processor));
1090
+ }, options.parallel)
1091
+ .run();
1092
+ }.bind(this)
1093
+ };
1094
+ }
1095
+ /**
1096
+ * O(1) Count API - Production-scale counting using existing indexes
1097
+ * Works across all storage adapters (FileSystem, OPFS, S3, Memory)
1098
+ */
1099
+ get counts() {
1100
+ return {
1101
+ // O(1) total entity count
1102
+ entities: () => this.metadataIndex.getTotalEntityCount(),
1103
+ // O(1) total relationship count
1104
+ relationships: () => this.graphIndex.getTotalRelationshipCount(),
1105
+ // O(1) count by type
1106
+ byType: (type) => {
1107
+ if (type) {
1108
+ return this.metadataIndex.getEntityCountByType(type);
1109
+ }
1110
+ return Object.fromEntries(this.metadataIndex.getAllEntityCounts());
1111
+ },
1112
+ // O(1) count by relationship type
1113
+ byRelationshipType: (type) => {
1114
+ if (type) {
1115
+ return this.graphIndex.getRelationshipCountByType(type);
1116
+ }
1117
+ return Object.fromEntries(this.graphIndex.getAllRelationshipCounts());
1118
+ },
1119
+ // O(1) count by field-value criteria
1120
+ byCriteria: async (field, value) => {
1121
+ return this.metadataIndex.getCountForCriteria(field, value);
1122
+ },
1123
+ // Get all type counts as Map for performance-critical operations
1124
+ getAllTypeCounts: () => this.metadataIndex.getAllEntityCounts(),
1125
+ // Get complete statistics
1126
+ getStats: () => {
1127
+ const entityStats = {
1128
+ total: this.metadataIndex.getTotalEntityCount(),
1129
+ byType: Object.fromEntries(this.metadataIndex.getAllEntityCounts())
1130
+ };
1131
+ const relationshipStats = this.graphIndex.getRelationshipStats();
1132
+ return {
1133
+ entities: entityStats,
1134
+ relationships: relationshipStats,
1135
+ density: entityStats.total > 0 ? relationshipStats.totalRelationships / entityStats.total : 0
1136
+ };
1137
+ }
1138
+ };
1139
+ }
894
1140
  /**
895
1141
  * Augmentations API - Clean and simple
896
1142
  */
@@ -42,6 +42,7 @@ export declare class GraphAdjacencyIndex {
42
42
  private flushTimer?;
43
43
  private rebuildStartTime;
44
44
  private totalRelationshipsIndexed;
45
+ private relationshipCountsByType;
45
46
  constructor(storage: StorageAdapter, config?: GraphIndexConfig);
46
47
  /**
47
48
  * Core API - O(1) neighbor lookup
@@ -49,9 +50,31 @@ export declare class GraphAdjacencyIndex {
49
50
  */
50
51
  getNeighbors(id: string, direction?: 'in' | 'out' | 'both'): Promise<string[]>;
51
52
  /**
52
- * Get relationship count
53
+ * Get total relationship count - O(1) operation
53
54
  */
54
55
  size(): number;
56
+ /**
57
+ * Get relationship count by type - O(1) operation using existing tracking
58
+ */
59
+ getRelationshipCountByType(type: string): number;
60
+ /**
61
+ * Get total relationship count - O(1) operation
62
+ */
63
+ getTotalRelationshipCount(): number;
64
+ /**
65
+ * Get all relationship types and their counts - O(1) operation
66
+ */
67
+ getAllRelationshipCounts(): Map<string, number>;
68
+ /**
69
+ * Get relationship statistics with enhanced counting information
70
+ */
71
+ getRelationshipStats(): {
72
+ totalRelationships: number;
73
+ relationshipsByType: Record<string, number>;
74
+ uniqueSourceNodes: number;
75
+ uniqueTargetNodes: number;
76
+ totalNodes: number;
77
+ };
55
78
  /**
56
79
  * Add relationship to index - O(1) amortized
57
80
  */
@@ -28,6 +28,8 @@ export class GraphAdjacencyIndex {
28
28
  this.isRebuilding = false;
29
29
  this.rebuildStartTime = 0;
30
30
  this.totalRelationshipsIndexed = 0;
31
+ // Production-scale relationship counting by type
32
+ this.relationshipCountsByType = new Map();
31
33
  this.storage = storage;
32
34
  this.config = {
33
35
  maxIndexSize: config.maxIndexSize ?? 100000,
@@ -70,11 +72,50 @@ export class GraphAdjacencyIndex {
70
72
  return result;
71
73
  }
72
74
  /**
73
- * Get relationship count
75
+ * Get total relationship count - O(1) operation
74
76
  */
75
77
  size() {
76
78
  return this.verbIndex.size;
77
79
  }
80
+ /**
81
+ * Get relationship count by type - O(1) operation using existing tracking
82
+ */
83
+ getRelationshipCountByType(type) {
84
+ return this.relationshipCountsByType.get(type) || 0;
85
+ }
86
+ /**
87
+ * Get total relationship count - O(1) operation
88
+ */
89
+ getTotalRelationshipCount() {
90
+ return this.verbIndex.size;
91
+ }
92
+ /**
93
+ * Get all relationship types and their counts - O(1) operation
94
+ */
95
+ getAllRelationshipCounts() {
96
+ return new Map(this.relationshipCountsByType);
97
+ }
98
+ /**
99
+ * Get relationship statistics with enhanced counting information
100
+ */
101
+ getRelationshipStats() {
102
+ const totalRelationships = this.verbIndex.size;
103
+ const relationshipsByType = Object.fromEntries(this.relationshipCountsByType);
104
+ const uniqueSourceNodes = this.sourceIndex.size;
105
+ const uniqueTargetNodes = this.targetIndex.size;
106
+ // Calculate total unique nodes (source ∪ target)
107
+ const allNodes = new Set();
108
+ this.sourceIndex.keys().forEach(id => allNodes.add(id));
109
+ this.targetIndex.keys().forEach(id => allNodes.add(id));
110
+ const totalNodes = allNodes.size;
111
+ return {
112
+ totalRelationships,
113
+ relationshipsByType,
114
+ uniqueSourceNodes,
115
+ uniqueTargetNodes,
116
+ totalNodes
117
+ };
118
+ }
78
119
  /**
79
120
  * Add relationship to index - O(1) amortized
80
121
  */
@@ -98,6 +139,9 @@ export class GraphAdjacencyIndex {
98
139
  // Cache immediately for hot data
99
140
  await this.cacheIndexEntry(verb.sourceId, 'source');
100
141
  await this.cacheIndexEntry(verb.targetId, 'target');
142
+ // Update type-specific counts atomically
143
+ const verbType = verb.type || 'unknown';
144
+ this.relationshipCountsByType.set(verbType, (this.relationshipCountsByType.get(verbType) || 0) + 1);
101
145
  const elapsed = performance.now() - startTime;
102
146
  this.totalRelationshipsIndexed++;
103
147
  // Performance assertion
@@ -115,6 +159,15 @@ export class GraphAdjacencyIndex {
115
159
  const startTime = performance.now();
116
160
  // Remove from verb cache
117
161
  this.verbIndex.delete(verbId);
162
+ // Update type-specific counts atomically
163
+ const verbType = verb.type || 'unknown';
164
+ const currentCount = this.relationshipCountsByType.get(verbType) || 0;
165
+ if (currentCount > 1) {
166
+ this.relationshipCountsByType.set(verbType, currentCount - 1);
167
+ }
168
+ else {
169
+ this.relationshipCountsByType.delete(verbType);
170
+ }
118
171
  // Remove from source index
119
172
  const sourceNeighbors = this.sourceIndex.get(verb.sourceId);
120
173
  if (sourceNeighbors) {
@@ -224,7 +224,24 @@ export declare class MetadataIndexManager {
224
224
  */
225
225
  private loadSortedIndex;
226
226
  /**
227
- * Get index statistics
227
+ * Get count of entities by type - O(1) operation using existing tracking
228
+ * This exposes the production-ready counting that's already maintained
229
+ */
230
+ getEntityCountByType(type: string): number;
231
+ /**
232
+ * Get total count of all entities - O(1) operation
233
+ */
234
+ getTotalEntityCount(): number;
235
+ /**
236
+ * Get all entity types and their counts - O(1) operation
237
+ */
238
+ getAllEntityCounts(): Map<string, number>;
239
+ /**
240
+ * Get count of entities matching field-value criteria - O(1) lookup from existing indexes
241
+ */
242
+ getCountForCriteria(field: string, value: any): Promise<number>;
243
+ /**
244
+ * Get index statistics with enhanced counting information
228
245
  */
229
246
  getStats(): Promise<MetadataIndexStats>;
230
247
  /**
@@ -1209,7 +1209,45 @@ export class MetadataIndexManager {
1209
1209
  return cached;
1210
1210
  }
1211
1211
  /**
1212
- * Get index statistics
1212
+ * Get count of entities by type - O(1) operation using existing tracking
1213
+ * This exposes the production-ready counting that's already maintained
1214
+ */
1215
+ getEntityCountByType(type) {
1216
+ return this.totalEntitiesByType.get(type) || 0;
1217
+ }
1218
+ /**
1219
+ * Get total count of all entities - O(1) operation
1220
+ */
1221
+ getTotalEntityCount() {
1222
+ let total = 0;
1223
+ for (const count of this.totalEntitiesByType.values()) {
1224
+ total += count;
1225
+ }
1226
+ return total;
1227
+ }
1228
+ /**
1229
+ * Get all entity types and their counts - O(1) operation
1230
+ */
1231
+ getAllEntityCounts() {
1232
+ return new Map(this.totalEntitiesByType);
1233
+ }
1234
+ /**
1235
+ * Get count of entities matching field-value criteria - O(1) lookup from existing indexes
1236
+ */
1237
+ async getCountForCriteria(field, value) {
1238
+ const key = this.getIndexKey(field, value);
1239
+ let entry = this.indexCache.get(key);
1240
+ if (!entry) {
1241
+ const loadedEntry = await this.loadIndexEntry(key);
1242
+ if (loadedEntry) {
1243
+ entry = loadedEntry;
1244
+ this.indexCache.set(key, entry);
1245
+ }
1246
+ }
1247
+ return entry ? entry.ids.size : 0;
1248
+ }
1249
+ /**
1250
+ * Get index statistics with enhanced counting information
1213
1251
  */
1214
1252
  async getStats() {
1215
1253
  const fields = new Set();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulcraft/brainy",
3
- "version": "3.1.1",
3
+ "version": "3.2.0",
4
4
  "description": "Universal Knowledge Protocol™ - World's first Triple Intelligence database unifying vector, graph, and document search in one API. 31 nouns × 40 verbs for infinite expressiveness.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",