@soulcraft/brainy 4.5.3 → 4.7.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/brainy.d.ts +10 -5
- package/dist/brainy.js +89 -31
- package/dist/cli/commands/core.js +2 -4
- package/dist/types/brainy.types.d.ts +4 -12
- package/dist/utils/metadataIndex.d.ts +82 -0
- package/dist/utils/metadataIndex.js +220 -37
- package/dist/vfs/PathResolver.js +6 -7
- package/dist/vfs/VirtualFileSystem.js +4 -8
- package/dist/vfs/semantic/projections/AuthorProjection.js +5 -8
- package/dist/vfs/semantic/projections/TagProjection.js +7 -10
- package/dist/vfs/semantic/projections/TemporalProjection.js +6 -8
- package/package.json +1 -1
package/dist/brainy.d.ts
CHANGED
|
@@ -545,18 +545,23 @@ export declare class Brainy<T = any> implements BrainyInterface<T> {
|
|
|
545
545
|
* // Returns only knowledge entities, VFS files excluded
|
|
546
546
|
*
|
|
547
547
|
* @example
|
|
548
|
-
* //
|
|
548
|
+
* // v4.7.0: VFS entities included by default
|
|
549
549
|
* const everything = await brainy.find({
|
|
550
|
-
* query: 'documentation'
|
|
551
|
-
* includeVFS: true // Opt-in to include VFS files
|
|
550
|
+
* query: 'documentation'
|
|
552
551
|
* })
|
|
553
552
|
* // Returns both knowledge entities AND VFS files
|
|
554
553
|
*
|
|
555
554
|
* @example
|
|
556
555
|
* // Search only VFS files
|
|
557
556
|
* const files = await brainy.find({
|
|
558
|
-
* where: { vfsType: 'file', extension: '.md' }
|
|
559
|
-
*
|
|
557
|
+
* where: { vfsType: 'file', extension: '.md' }
|
|
558
|
+
* })
|
|
559
|
+
*
|
|
560
|
+
* @example
|
|
561
|
+
* // Exclude VFS entities (if needed)
|
|
562
|
+
* const concepts = await brainy.find({
|
|
563
|
+
* query: 'machine learning',
|
|
564
|
+
* excludeVFS: true // v4.7.0: Exclude VFS files
|
|
560
565
|
* })
|
|
561
566
|
*/
|
|
562
567
|
find(query: string | FindParams<T>): Promise<Result<T>[]>;
|
package/dist/brainy.js
CHANGED
|
@@ -446,7 +446,13 @@ export class Brainy {
|
|
|
446
446
|
id: noun.id,
|
|
447
447
|
vector: noun.vector,
|
|
448
448
|
type: nounType || NounType.Thing,
|
|
449
|
-
metadata
|
|
449
|
+
// Preserve timestamps in metadata for indexing (v4.5.4 fix)
|
|
450
|
+
// Metadata index needs these fields to enable sorting and range queries
|
|
451
|
+
metadata: {
|
|
452
|
+
...userMetadata,
|
|
453
|
+
...(createdAt !== undefined && { createdAt }),
|
|
454
|
+
...(updatedAt !== undefined && { updatedAt })
|
|
455
|
+
},
|
|
450
456
|
service: service,
|
|
451
457
|
createdAt: createdAt || Date.now(),
|
|
452
458
|
updatedAt: updatedAt
|
|
@@ -846,13 +852,8 @@ export class Brainy {
|
|
|
846
852
|
if (params.service) {
|
|
847
853
|
filter.service = params.service;
|
|
848
854
|
}
|
|
849
|
-
// v4.
|
|
850
|
-
// VFS
|
|
851
|
-
// Only include VFS relationships if explicitly requested
|
|
852
|
-
if (params.includeVFS !== true) {
|
|
853
|
-
filter.metadata = filter.metadata || {};
|
|
854
|
-
filter.metadata.isVFS = { notEquals: true };
|
|
855
|
-
}
|
|
855
|
+
// v4.7.0: VFS relationships are no longer filtered
|
|
856
|
+
// VFS is part of the knowledge graph - users can filter explicitly if needed
|
|
856
857
|
// Fetch from storage with pagination at storage layer (efficient!)
|
|
857
858
|
const result = await this.storage.getVerbs({
|
|
858
859
|
pagination: {
|
|
@@ -1027,18 +1028,23 @@ export class Brainy {
|
|
|
1027
1028
|
* // Returns only knowledge entities, VFS files excluded
|
|
1028
1029
|
*
|
|
1029
1030
|
* @example
|
|
1030
|
-
* //
|
|
1031
|
+
* // v4.7.0: VFS entities included by default
|
|
1031
1032
|
* const everything = await brainy.find({
|
|
1032
|
-
* query: 'documentation'
|
|
1033
|
-
* includeVFS: true // Opt-in to include VFS files
|
|
1033
|
+
* query: 'documentation'
|
|
1034
1034
|
* })
|
|
1035
1035
|
* // Returns both knowledge entities AND VFS files
|
|
1036
1036
|
*
|
|
1037
1037
|
* @example
|
|
1038
1038
|
* // Search only VFS files
|
|
1039
1039
|
* const files = await brainy.find({
|
|
1040
|
-
* where: { vfsType: 'file', extension: '.md' }
|
|
1041
|
-
*
|
|
1040
|
+
* where: { vfsType: 'file', extension: '.md' }
|
|
1041
|
+
* })
|
|
1042
|
+
*
|
|
1043
|
+
* @example
|
|
1044
|
+
* // Exclude VFS entities (if needed)
|
|
1045
|
+
* const concepts = await brainy.find({
|
|
1046
|
+
* query: 'machine learning',
|
|
1047
|
+
* excludeVFS: true // v4.7.0: Exclude VFS files
|
|
1042
1048
|
* })
|
|
1043
1049
|
*/
|
|
1044
1050
|
async find(query) {
|
|
@@ -1084,11 +1090,10 @@ export class Brainy {
|
|
|
1084
1090
|
Object.assign(filter, params.where);
|
|
1085
1091
|
if (params.service)
|
|
1086
1092
|
filter.service = params.service;
|
|
1087
|
-
// v4.
|
|
1088
|
-
//
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
filter.isVFS = { notEquals: true };
|
|
1093
|
+
// v4.7.0: excludeVFS helper for cleaner UX
|
|
1094
|
+
// Use vfsType field (more semantic than isVFS)
|
|
1095
|
+
if (params.excludeVFS === true) {
|
|
1096
|
+
filter.vfsType = { exists: false };
|
|
1092
1097
|
}
|
|
1093
1098
|
if (params.type) {
|
|
1094
1099
|
const types = Array.isArray(params.type) ? params.type : [params.type];
|
|
@@ -1104,8 +1109,17 @@ export class Brainy {
|
|
|
1104
1109
|
};
|
|
1105
1110
|
}
|
|
1106
1111
|
}
|
|
1107
|
-
//
|
|
1108
|
-
|
|
1112
|
+
// v4.5.4: Apply sorting if requested, otherwise just filter
|
|
1113
|
+
let filteredIds;
|
|
1114
|
+
if (params.orderBy) {
|
|
1115
|
+
// Get sorted IDs using production-scale sorted filtering
|
|
1116
|
+
filteredIds = await this.metadataIndex.getSortedIdsForFilter(filter, params.orderBy, params.order || 'asc');
|
|
1117
|
+
}
|
|
1118
|
+
else {
|
|
1119
|
+
// Just filter without sorting
|
|
1120
|
+
filteredIds = await this.metadataIndex.getIdsForFilter(filter);
|
|
1121
|
+
}
|
|
1122
|
+
// Paginate BEFORE loading entities (production-scale!)
|
|
1109
1123
|
const limit = params.limit || 10;
|
|
1110
1124
|
const offset = params.offset || 0;
|
|
1111
1125
|
const pageIds = filteredIds.slice(offset, offset + limit);
|
|
@@ -1122,12 +1136,12 @@ export class Brainy {
|
|
|
1122
1136
|
if (!hasVectorSearchCriteria && !hasFilterCriteria && !hasGraphCriteria) {
|
|
1123
1137
|
const limit = params.limit || 20;
|
|
1124
1138
|
const offset = params.offset || 0;
|
|
1125
|
-
// v4.
|
|
1139
|
+
// v4.7.0: excludeVFS helper
|
|
1126
1140
|
let filter = {};
|
|
1127
|
-
if (params.
|
|
1128
|
-
filter.
|
|
1141
|
+
if (params.excludeVFS === true) {
|
|
1142
|
+
filter.vfsType = { exists: false };
|
|
1129
1143
|
}
|
|
1130
|
-
// Use metadata index if we need to filter
|
|
1144
|
+
// Use metadata index if we need to filter
|
|
1131
1145
|
if (Object.keys(filter).length > 0) {
|
|
1132
1146
|
const filteredIds = await this.metadataIndex.getIdsForFilter(filter);
|
|
1133
1147
|
const pageIds = filteredIds.slice(offset, offset + limit);
|
|
@@ -1182,7 +1196,7 @@ export class Brainy {
|
|
|
1182
1196
|
results = Array.from(uniqueResults.values());
|
|
1183
1197
|
}
|
|
1184
1198
|
// Apply O(log n) metadata filtering using core MetadataIndexManager
|
|
1185
|
-
if (params.where || params.type || params.service || params.
|
|
1199
|
+
if (params.where || params.type || params.service || params.excludeVFS) {
|
|
1186
1200
|
// Build filter object for metadata index
|
|
1187
1201
|
let filter = {};
|
|
1188
1202
|
// Base filter from where and service
|
|
@@ -1190,10 +1204,9 @@ export class Brainy {
|
|
|
1190
1204
|
Object.assign(filter, params.where);
|
|
1191
1205
|
if (params.service)
|
|
1192
1206
|
filter.service = params.service;
|
|
1193
|
-
// v4.
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
filter.isVFS = { notEquals: true };
|
|
1207
|
+
// v4.7.0: excludeVFS helper for cleaner UX
|
|
1208
|
+
if (params.excludeVFS === true) {
|
|
1209
|
+
filter.vfsType = { exists: false };
|
|
1197
1210
|
}
|
|
1198
1211
|
if (params.type) {
|
|
1199
1212
|
const types = Array.isArray(params.type) ? params.type : [params.type];
|
|
@@ -1252,6 +1265,23 @@ export class Brainy {
|
|
|
1252
1265
|
}
|
|
1253
1266
|
// Early return for metadata-only queries with pagination applied
|
|
1254
1267
|
if (!params.query && !params.connected) {
|
|
1268
|
+
// v4.5.4: Apply sorting if requested for metadata-only queries
|
|
1269
|
+
if (params.orderBy) {
|
|
1270
|
+
const sortedIds = await this.metadataIndex.getSortedIdsForFilter(filter, params.orderBy, params.order || 'asc');
|
|
1271
|
+
// Paginate sorted IDs BEFORE loading entities (production-scale!)
|
|
1272
|
+
const limit = params.limit || 10;
|
|
1273
|
+
const offset = params.offset || 0;
|
|
1274
|
+
const pageIds = sortedIds.slice(offset, offset + limit);
|
|
1275
|
+
// Load entities for paginated results only
|
|
1276
|
+
const sortedResults = [];
|
|
1277
|
+
for (const id of pageIds) {
|
|
1278
|
+
const entity = await this.get(id);
|
|
1279
|
+
if (entity) {
|
|
1280
|
+
sortedResults.push(this.createResult(id, 1.0, entity));
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
return sortedResults;
|
|
1284
|
+
}
|
|
1255
1285
|
return results;
|
|
1256
1286
|
}
|
|
1257
1287
|
}
|
|
@@ -1265,7 +1295,35 @@ export class Brainy {
|
|
|
1265
1295
|
results = this.applyFusionScoring(results, params.fusion);
|
|
1266
1296
|
}
|
|
1267
1297
|
// OPTIMIZED: Sort first, then apply efficient pagination
|
|
1268
|
-
|
|
1298
|
+
// v4.5.4: Support custom orderBy for vector + metadata queries
|
|
1299
|
+
if (params.orderBy && results.length > 0) {
|
|
1300
|
+
// For vector + metadata queries, sort by specified field instead of score
|
|
1301
|
+
// Load sort field values for all results (small set, already filtered)
|
|
1302
|
+
const resultsWithValues = await Promise.all(results.map(async (r) => ({
|
|
1303
|
+
result: r,
|
|
1304
|
+
value: await this.metadataIndex.getFieldValueForEntity(r.id, params.orderBy)
|
|
1305
|
+
})));
|
|
1306
|
+
// Sort by field value
|
|
1307
|
+
resultsWithValues.sort((a, b) => {
|
|
1308
|
+
// Handle null/undefined
|
|
1309
|
+
if (a.value == null && b.value == null)
|
|
1310
|
+
return 0;
|
|
1311
|
+
if (a.value == null)
|
|
1312
|
+
return (params.order || 'asc') === 'asc' ? 1 : -1;
|
|
1313
|
+
if (b.value == null)
|
|
1314
|
+
return (params.order || 'asc') === 'asc' ? -1 : 1;
|
|
1315
|
+
// Compare values
|
|
1316
|
+
if (a.value === b.value)
|
|
1317
|
+
return 0;
|
|
1318
|
+
const comparison = a.value < b.value ? -1 : 1;
|
|
1319
|
+
return (params.order || 'asc') === 'asc' ? comparison : -comparison;
|
|
1320
|
+
});
|
|
1321
|
+
results = resultsWithValues.map(({ result }) => result);
|
|
1322
|
+
}
|
|
1323
|
+
else {
|
|
1324
|
+
// Default: sort by relevance score
|
|
1325
|
+
results.sort((a, b) => b.score - a.score);
|
|
1326
|
+
}
|
|
1269
1327
|
const limit = params.limit || 10;
|
|
1270
1328
|
const offset = params.offset || 0;
|
|
1271
1329
|
// Efficient pagination - only slice what we need
|
|
@@ -1420,7 +1478,7 @@ export class Brainy {
|
|
|
1420
1478
|
type: params.type,
|
|
1421
1479
|
where: params.where,
|
|
1422
1480
|
service: params.service,
|
|
1423
|
-
|
|
1481
|
+
excludeVFS: params.excludeVFS // v4.7.0: Pass through VFS filtering
|
|
1424
1482
|
});
|
|
1425
1483
|
}
|
|
1426
1484
|
// ============= BATCH OPERATIONS =============
|
|
@@ -274,10 +274,8 @@ export const coreCommands = {
|
|
|
274
274
|
if (options.includeRelations) {
|
|
275
275
|
searchParams.includeRelations = true;
|
|
276
276
|
}
|
|
277
|
-
//
|
|
278
|
-
if
|
|
279
|
-
searchParams.includeVFS = true;
|
|
280
|
-
}
|
|
277
|
+
// v4.7.0: VFS is now part of the knowledge graph (included by default)
|
|
278
|
+
// Users can exclude VFS with --where vfsType exists:false if needed
|
|
281
279
|
// Triple Intelligence Fusion - custom weighting
|
|
282
280
|
if (options.fusion || options.vectorWeight || options.graphWeight || options.fieldWeight) {
|
|
283
281
|
searchParams.fusion = {
|
|
@@ -143,10 +143,12 @@ export interface FindParams<T = any> {
|
|
|
143
143
|
limit?: number;
|
|
144
144
|
offset?: number;
|
|
145
145
|
cursor?: string;
|
|
146
|
+
orderBy?: string;
|
|
147
|
+
order?: 'asc' | 'desc';
|
|
146
148
|
mode?: SearchMode;
|
|
147
149
|
explain?: boolean;
|
|
148
150
|
includeRelations?: boolean;
|
|
149
|
-
|
|
151
|
+
excludeVFS?: boolean;
|
|
150
152
|
service?: string;
|
|
151
153
|
fusion?: {
|
|
152
154
|
strategy?: 'adaptive' | 'weighted' | 'progressive';
|
|
@@ -184,7 +186,7 @@ export interface SimilarParams<T = any> {
|
|
|
184
186
|
type?: NounType | NounType[];
|
|
185
187
|
where?: Partial<T>;
|
|
186
188
|
service?: string;
|
|
187
|
-
|
|
189
|
+
excludeVFS?: boolean;
|
|
188
190
|
}
|
|
189
191
|
/**
|
|
190
192
|
* Parameters for getting relationships
|
|
@@ -266,16 +268,6 @@ export interface GetRelationsParams {
|
|
|
266
268
|
* Only return relationships belonging to this service.
|
|
267
269
|
*/
|
|
268
270
|
service?: string;
|
|
269
|
-
/**
|
|
270
|
-
* Include VFS relationships (v4.5.1)
|
|
271
|
-
*
|
|
272
|
-
* By default, getRelations() excludes VFS relationships (since v4.4.0).
|
|
273
|
-
* Set this to true when you need to traverse VFS structure.
|
|
274
|
-
*
|
|
275
|
-
* @default false
|
|
276
|
-
* @since v4.5.1
|
|
277
|
-
*/
|
|
278
|
-
includeVFS?: boolean;
|
|
279
271
|
}
|
|
280
272
|
/**
|
|
281
273
|
* Batch add parameters
|
|
@@ -155,6 +155,7 @@ export declare class MetadataIndexManager {
|
|
|
155
155
|
/**
|
|
156
156
|
* Get IDs for a range using chunked sparse index with zone maps and roaring bitmaps (v3.43.0)
|
|
157
157
|
* v3.44.1: Now fully lazy-loaded via UnifiedCache (no local sparseIndices Map)
|
|
158
|
+
* v4.5.4: Normalize min/max for timestamp bucketing before comparison
|
|
158
159
|
*/
|
|
159
160
|
private getIdsFromChunksForRange;
|
|
160
161
|
/**
|
|
@@ -271,6 +272,87 @@ export declare class MetadataIndexManager {
|
|
|
271
272
|
* Get IDs matching Brainy Field Operator metadata filter using indexes where possible
|
|
272
273
|
*/
|
|
273
274
|
getIdsForFilter(filter: any): Promise<string[]>;
|
|
275
|
+
/**
|
|
276
|
+
* Get filtered IDs sorted by a field (production-scale sorting)
|
|
277
|
+
*
|
|
278
|
+
* **Performance Characteristics** (designed for billions of entities):
|
|
279
|
+
* - **Filtering**: O(log n) using roaring bitmaps with SIMD acceleration
|
|
280
|
+
* - **Field Loading**: O(k) where k = filtered result count (NOT O(n))
|
|
281
|
+
* - **Sorting**: O(k log k) in-memory (IDs + sort values only, NOT full entities)
|
|
282
|
+
* - **Memory**: O(k) for k filtered results, independent of total entity count
|
|
283
|
+
*
|
|
284
|
+
* **Scalability**:
|
|
285
|
+
* - Total entities: Billions (memory usage unaffected)
|
|
286
|
+
* - Filtered set: Up to 10M (reasonable for in-memory sort of ID+value pairs)
|
|
287
|
+
* - Pagination: Happens AFTER sorting, so only page entities are loaded
|
|
288
|
+
*
|
|
289
|
+
* **Example**:
|
|
290
|
+
* ```typescript
|
|
291
|
+
* // Production-scale: 1B entities, 100K match filter, sort by createdAt
|
|
292
|
+
* const sortedIds = await metadataIndex.getSortedIdsForFilter(
|
|
293
|
+
* { status: 'published', category: 'AI' },
|
|
294
|
+
* 'createdAt',
|
|
295
|
+
* 'desc'
|
|
296
|
+
* )
|
|
297
|
+
* // Returns: 100K sorted IDs
|
|
298
|
+
* // Memory: ~5MB (100K IDs + 100K timestamps)
|
|
299
|
+
* // Then caller paginates: sortedIds.slice(0, 20) and loads only 20 entities
|
|
300
|
+
* ```
|
|
301
|
+
*
|
|
302
|
+
* @param filter - Metadata filter criteria (uses roaring bitmaps)
|
|
303
|
+
* @param orderBy - Field name to sort by (e.g., 'createdAt', 'title')
|
|
304
|
+
* @param order - Sort direction: 'asc' (default) or 'desc'
|
|
305
|
+
* @returns Promise<string[]> - Entity IDs sorted by specified field
|
|
306
|
+
*
|
|
307
|
+
* @since v4.5.4
|
|
308
|
+
*/
|
|
309
|
+
getSortedIdsForFilter(filter: any, orderBy: string, order?: 'asc' | 'desc'): Promise<string[]>;
|
|
310
|
+
/**
|
|
311
|
+
* Get field value for a specific entity (helper for sorted queries)
|
|
312
|
+
*
|
|
313
|
+
* **IMPORTANT**: For timestamp fields (createdAt, updatedAt), this loads
|
|
314
|
+
* the ACTUAL value from entity metadata, NOT the bucketed index value.
|
|
315
|
+
* This is required because timestamp bucketing (1-minute precision) loses
|
|
316
|
+
* precision needed for accurate sorting.
|
|
317
|
+
*
|
|
318
|
+
* For non-timestamp fields, loads from the chunked sparse index without
|
|
319
|
+
* loading the full entity. This is critical for production-scale sorting.
|
|
320
|
+
*
|
|
321
|
+
* **Performance**:
|
|
322
|
+
* - Timestamp fields: O(1) metadata load from storage (cached)
|
|
323
|
+
* - Other fields: O(chunks) roaring bitmap lookup (typically 1-10 chunks)
|
|
324
|
+
*
|
|
325
|
+
* @param entityId - Entity UUID to get field value for
|
|
326
|
+
* @param field - Field name to retrieve (e.g., 'createdAt', 'title')
|
|
327
|
+
* @returns Promise<any> - Field value or undefined if not found
|
|
328
|
+
*
|
|
329
|
+
* @public (called from brainy.ts for sorted queries)
|
|
330
|
+
* @since v4.5.4
|
|
331
|
+
*/
|
|
332
|
+
getFieldValueForEntity(entityId: string, field: string): Promise<any>;
|
|
333
|
+
/**
|
|
334
|
+
* Denormalize a value (reverse of normalizeValue)
|
|
335
|
+
*
|
|
336
|
+
* Converts normalized/stringified values back to their original type.
|
|
337
|
+
* For most fields, this just parses numbers or returns strings as-is.
|
|
338
|
+
*
|
|
339
|
+
* **NOTE**: This is NOT used for timestamp sorting! Timestamp fields
|
|
340
|
+
* (createdAt, updatedAt) are loaded directly from entity metadata by
|
|
341
|
+
* getFieldValueForEntity() to avoid precision loss from bucketing.
|
|
342
|
+
*
|
|
343
|
+
* **Timestamp Bucketing (for range queries only)**:
|
|
344
|
+
* - Indexed as: Math.floor(timestamp / 60000) * 60000
|
|
345
|
+
* - Used for: Range queries (gte, lte) where 1-minute precision is acceptable
|
|
346
|
+
* - NOT used for: Sorting (requires exact millisecond precision)
|
|
347
|
+
*
|
|
348
|
+
* @param normalized - Normalized value string from index
|
|
349
|
+
* @param field - Field name (used for type inference)
|
|
350
|
+
* @returns Denormalized value in original type
|
|
351
|
+
*
|
|
352
|
+
* @private
|
|
353
|
+
* @since v4.5.4
|
|
354
|
+
*/
|
|
355
|
+
private denormalizeValue;
|
|
274
356
|
/**
|
|
275
357
|
* DEPRECATED - Old implementation for backward compatibility
|
|
276
358
|
*/
|
|
@@ -463,6 +463,7 @@ export class MetadataIndexManager {
|
|
|
463
463
|
/**
|
|
464
464
|
* Get IDs for a range using chunked sparse index with zone maps and roaring bitmaps (v3.43.0)
|
|
465
465
|
* v3.44.1: Now fully lazy-loaded via UnifiedCache (no local sparseIndices Map)
|
|
466
|
+
* v4.5.4: Normalize min/max for timestamp bucketing before comparison
|
|
466
467
|
*/
|
|
467
468
|
async getIdsFromChunksForRange(field, min, max, includeMin = true, includeMax = true) {
|
|
468
469
|
// Load sparse index via UnifiedCache (lazy loading)
|
|
@@ -470,8 +471,12 @@ export class MetadataIndexManager {
|
|
|
470
471
|
if (!sparseIndex) {
|
|
471
472
|
return []; // No chunked index exists yet
|
|
472
473
|
}
|
|
474
|
+
// v4.5.4: Normalize min/max for consistent comparison with indexed values
|
|
475
|
+
// (indexed values are bucketed for timestamps, so we must bucket the query bounds too)
|
|
476
|
+
const normalizedMin = min !== undefined ? this.normalizeValue(min, field) : undefined;
|
|
477
|
+
const normalizedMax = max !== undefined ? this.normalizeValue(max, field) : undefined;
|
|
473
478
|
// Find candidate chunks using zone maps
|
|
474
|
-
const candidateChunkIds = sparseIndex.findChunksForRange(
|
|
479
|
+
const candidateChunkIds = sparseIndex.findChunksForRange(normalizedMin, normalizedMax);
|
|
475
480
|
if (candidateChunkIds.length === 0) {
|
|
476
481
|
return [];
|
|
477
482
|
}
|
|
@@ -481,13 +486,13 @@ export class MetadataIndexManager {
|
|
|
481
486
|
const chunk = await this.chunkManager.loadChunk(field, chunkId);
|
|
482
487
|
if (chunk) {
|
|
483
488
|
for (const [value, bitmap] of chunk.entries) {
|
|
484
|
-
// Check if value is in range
|
|
489
|
+
// Check if value is in range (both value and normalized bounds are now bucketed)
|
|
485
490
|
let inRange = true;
|
|
486
|
-
if (
|
|
487
|
-
inRange = inRange && (includeMin ? value >=
|
|
491
|
+
if (normalizedMin !== undefined) {
|
|
492
|
+
inRange = inRange && (includeMin ? value >= normalizedMin : value > normalizedMin);
|
|
488
493
|
}
|
|
489
|
-
if (
|
|
490
|
-
inRange = inRange && (includeMax ? value <=
|
|
494
|
+
if (normalizedMax !== undefined) {
|
|
495
|
+
inRange = inRange && (includeMax ? value <= normalizedMax : value < normalizedMax);
|
|
491
496
|
}
|
|
492
497
|
if (inRange) {
|
|
493
498
|
// Iterate through roaring bitmap integers
|
|
@@ -1204,17 +1209,36 @@ export class MetadataIndexManager {
|
|
|
1204
1209
|
continue;
|
|
1205
1210
|
let fieldResults = [];
|
|
1206
1211
|
if (condition && typeof condition === 'object' && !Array.isArray(condition)) {
|
|
1207
|
-
// Handle Brainy Field Operators
|
|
1212
|
+
// Handle Brainy Field Operators (v4.5.4: canonical operators defined)
|
|
1213
|
+
// See docs/api/README.md for complete operator reference
|
|
1208
1214
|
for (const [op, operand] of Object.entries(condition)) {
|
|
1209
1215
|
switch (op) {
|
|
1210
|
-
//
|
|
1211
|
-
|
|
1212
|
-
case 'is':
|
|
1216
|
+
// ===== EQUALITY OPERATORS =====
|
|
1217
|
+
// Canonical: 'eq' | Alias: 'equals' | Deprecated: 'is' (remove in v5.0.0)
|
|
1218
|
+
case 'is': // DEPRECATED (v4.5.4): Use 'eq' instead
|
|
1219
|
+
case 'equals': // Alias for 'eq'
|
|
1213
1220
|
case 'eq':
|
|
1214
1221
|
fieldResults = await this.getIds(field, operand);
|
|
1215
1222
|
break;
|
|
1216
|
-
//
|
|
1217
|
-
|
|
1223
|
+
// ===== NEGATION OPERATORS =====
|
|
1224
|
+
// Canonical: 'ne' | Alias: 'notEquals' | Deprecated: 'isNot' (remove in v5.0.0)
|
|
1225
|
+
case 'isNot': // DEPRECATED (v4.5.4): Use 'ne' instead
|
|
1226
|
+
case 'notEquals': // Alias for 'ne'
|
|
1227
|
+
case 'ne':
|
|
1228
|
+
// For notEquals, we need all IDs EXCEPT those matching the value
|
|
1229
|
+
// This is especially important for soft delete: deleted !== true
|
|
1230
|
+
// should include items without a deleted field
|
|
1231
|
+
// First, get all IDs in the database
|
|
1232
|
+
const allItemIds = await this.getAllIds();
|
|
1233
|
+
// Then get IDs that match the value we want to exclude
|
|
1234
|
+
const excludeIds = await this.getIds(field, operand);
|
|
1235
|
+
const excludeSet = new Set(excludeIds);
|
|
1236
|
+
// Return all IDs except those to exclude
|
|
1237
|
+
fieldResults = allItemIds.filter(id => !excludeSet.has(id));
|
|
1238
|
+
break;
|
|
1239
|
+
// ===== MULTI-VALUE OPERATORS =====
|
|
1240
|
+
// Canonical: 'in' | Alias: 'oneOf'
|
|
1241
|
+
case 'oneOf': // Alias for 'in'
|
|
1218
1242
|
case 'in':
|
|
1219
1243
|
if (Array.isArray(operand)) {
|
|
1220
1244
|
const unionIds = new Set();
|
|
@@ -1225,35 +1249,46 @@ export class MetadataIndexManager {
|
|
|
1225
1249
|
fieldResults = Array.from(unionIds);
|
|
1226
1250
|
}
|
|
1227
1251
|
break;
|
|
1228
|
-
//
|
|
1229
|
-
|
|
1252
|
+
// ===== GREATER THAN OPERATORS =====
|
|
1253
|
+
// Canonical: 'gt' | Alias: 'greaterThan'
|
|
1254
|
+
case 'greaterThan': // Alias for 'gt'
|
|
1230
1255
|
case 'gt':
|
|
1231
1256
|
fieldResults = await this.getIdsForRange(field, operand, undefined, false, true);
|
|
1232
1257
|
break;
|
|
1233
|
-
|
|
1258
|
+
// ===== GREATER THAN OR EQUAL OPERATORS =====
|
|
1259
|
+
// Canonical: 'gte' | Alias: 'greaterThanOrEqual' | Deprecated: 'greaterEqual' (remove in v5.0.0)
|
|
1260
|
+
case 'greaterEqual': // DEPRECATED (v4.5.4): Use 'gte' instead
|
|
1261
|
+
case 'greaterThanOrEqual': // Alias for 'gte'
|
|
1234
1262
|
case 'gte':
|
|
1235
|
-
case 'greaterThanOrEqual':
|
|
1236
1263
|
fieldResults = await this.getIdsForRange(field, operand, undefined, true, true);
|
|
1237
1264
|
break;
|
|
1238
|
-
|
|
1265
|
+
// ===== LESS THAN OPERATORS =====
|
|
1266
|
+
// Canonical: 'lt' | Alias: 'lessThan'
|
|
1267
|
+
case 'lessThan': // Alias for 'lt'
|
|
1239
1268
|
case 'lt':
|
|
1240
1269
|
fieldResults = await this.getIdsForRange(field, undefined, operand, true, false);
|
|
1241
1270
|
break;
|
|
1242
|
-
|
|
1271
|
+
// ===== LESS THAN OR EQUAL OPERATORS =====
|
|
1272
|
+
// Canonical: 'lte' | Alias: 'lessThanOrEqual' | Deprecated: 'lessEqual' (remove in v5.0.0)
|
|
1273
|
+
case 'lessEqual': // DEPRECATED (v4.5.4): Use 'lte' instead
|
|
1274
|
+
case 'lessThanOrEqual': // Alias for 'lte'
|
|
1243
1275
|
case 'lte':
|
|
1244
|
-
case 'lessThanOrEqual':
|
|
1245
1276
|
fieldResults = await this.getIdsForRange(field, undefined, operand, true, true);
|
|
1246
1277
|
break;
|
|
1278
|
+
// ===== RANGE OPERATOR =====
|
|
1279
|
+
// between: [min, max] - inclusive range query
|
|
1247
1280
|
case 'between':
|
|
1248
1281
|
if (Array.isArray(operand) && operand.length === 2) {
|
|
1249
1282
|
fieldResults = await this.getIdsForRange(field, operand[0], operand[1], true, true);
|
|
1250
1283
|
}
|
|
1251
1284
|
break;
|
|
1252
|
-
//
|
|
1285
|
+
// ===== ARRAY CONTAINS OPERATOR =====
|
|
1286
|
+
// contains: value - check if array field contains value
|
|
1253
1287
|
case 'contains':
|
|
1254
1288
|
fieldResults = await this.getIds(field, operand);
|
|
1255
1289
|
break;
|
|
1256
|
-
//
|
|
1290
|
+
// ===== EXISTENCE OPERATOR =====
|
|
1291
|
+
// exists: boolean - check if field exists (any value)
|
|
1257
1292
|
case 'exists':
|
|
1258
1293
|
if (operand) {
|
|
1259
1294
|
// Get all IDs that have this field (any value) from chunked sparse index with roaring bitmaps (v3.43.0)
|
|
@@ -1279,26 +1314,11 @@ export class MetadataIndexManager {
|
|
|
1279
1314
|
fieldResults = this.idMapper.intsIterableToUuids(allIntIds);
|
|
1280
1315
|
}
|
|
1281
1316
|
break;
|
|
1282
|
-
// Negation operators
|
|
1283
|
-
case 'notEquals':
|
|
1284
|
-
case 'isNot':
|
|
1285
|
-
case 'ne':
|
|
1286
|
-
// For notEquals, we need all IDs EXCEPT those matching the value
|
|
1287
|
-
// This is especially important for soft delete: deleted !== true
|
|
1288
|
-
// should include items without a deleted field
|
|
1289
|
-
// First, get all IDs in the database
|
|
1290
|
-
const allItemIds = await this.getAllIds();
|
|
1291
|
-
// Then get IDs that match the value we want to exclude
|
|
1292
|
-
const excludeIds = await this.getIds(field, operand);
|
|
1293
|
-
const excludeSet = new Set(excludeIds);
|
|
1294
|
-
// Return all IDs except those to exclude
|
|
1295
|
-
fieldResults = allItemIds.filter(id => !excludeSet.has(id));
|
|
1296
|
-
break;
|
|
1297
1317
|
}
|
|
1298
1318
|
}
|
|
1299
1319
|
}
|
|
1300
1320
|
else {
|
|
1301
|
-
// Direct value match (shorthand for
|
|
1321
|
+
// Direct value match (shorthand for 'eq' operator)
|
|
1302
1322
|
fieldResults = await this.getIds(field, condition);
|
|
1303
1323
|
}
|
|
1304
1324
|
if (fieldResults.length > 0) {
|
|
@@ -1316,6 +1336,169 @@ export class MetadataIndexManager {
|
|
|
1316
1336
|
// Intersection of all field criteria (implicit AND)
|
|
1317
1337
|
return idSets.reduce((intersection, currentSet) => intersection.filter(id => currentSet.includes(id)));
|
|
1318
1338
|
}
|
|
1339
|
+
/**
|
|
1340
|
+
* Get filtered IDs sorted by a field (production-scale sorting)
|
|
1341
|
+
*
|
|
1342
|
+
* **Performance Characteristics** (designed for billions of entities):
|
|
1343
|
+
* - **Filtering**: O(log n) using roaring bitmaps with SIMD acceleration
|
|
1344
|
+
* - **Field Loading**: O(k) where k = filtered result count (NOT O(n))
|
|
1345
|
+
* - **Sorting**: O(k log k) in-memory (IDs + sort values only, NOT full entities)
|
|
1346
|
+
* - **Memory**: O(k) for k filtered results, independent of total entity count
|
|
1347
|
+
*
|
|
1348
|
+
* **Scalability**:
|
|
1349
|
+
* - Total entities: Billions (memory usage unaffected)
|
|
1350
|
+
* - Filtered set: Up to 10M (reasonable for in-memory sort of ID+value pairs)
|
|
1351
|
+
* - Pagination: Happens AFTER sorting, so only page entities are loaded
|
|
1352
|
+
*
|
|
1353
|
+
* **Example**:
|
|
1354
|
+
* ```typescript
|
|
1355
|
+
* // Production-scale: 1B entities, 100K match filter, sort by createdAt
|
|
1356
|
+
* const sortedIds = await metadataIndex.getSortedIdsForFilter(
|
|
1357
|
+
* { status: 'published', category: 'AI' },
|
|
1358
|
+
* 'createdAt',
|
|
1359
|
+
* 'desc'
|
|
1360
|
+
* )
|
|
1361
|
+
* // Returns: 100K sorted IDs
|
|
1362
|
+
* // Memory: ~5MB (100K IDs + 100K timestamps)
|
|
1363
|
+
* // Then caller paginates: sortedIds.slice(0, 20) and loads only 20 entities
|
|
1364
|
+
* ```
|
|
1365
|
+
*
|
|
1366
|
+
* @param filter - Metadata filter criteria (uses roaring bitmaps)
|
|
1367
|
+
* @param orderBy - Field name to sort by (e.g., 'createdAt', 'title')
|
|
1368
|
+
* @param order - Sort direction: 'asc' (default) or 'desc'
|
|
1369
|
+
* @returns Promise<string[]> - Entity IDs sorted by specified field
|
|
1370
|
+
*
|
|
1371
|
+
* @since v4.5.4
|
|
1372
|
+
*/
|
|
1373
|
+
async getSortedIdsForFilter(filter, orderBy, order = 'asc') {
|
|
1374
|
+
// 1. Get filtered IDs using existing roaring bitmap implementation (fast!)
|
|
1375
|
+
const filteredIds = await this.getIdsForFilter(filter);
|
|
1376
|
+
if (filteredIds.length === 0) {
|
|
1377
|
+
return [];
|
|
1378
|
+
}
|
|
1379
|
+
// 2. Load sort field values for filtered IDs ONLY
|
|
1380
|
+
// This is O(k) not O(n) where k = filtered count
|
|
1381
|
+
// We only load the ONE field needed for sorting, not full entities
|
|
1382
|
+
const idValuePairs = [];
|
|
1383
|
+
for (const id of filteredIds) {
|
|
1384
|
+
const value = await this.getFieldValueForEntity(id, orderBy);
|
|
1385
|
+
idValuePairs.push({ id, value });
|
|
1386
|
+
}
|
|
1387
|
+
// 3. Sort by value (in-memory BUT only IDs + sort values)
|
|
1388
|
+
// This is acceptable because we're sorting the FILTERED set, not all entities
|
|
1389
|
+
// Even 1M filtered results = ~50MB (IDs + values), manageable in-memory
|
|
1390
|
+
idValuePairs.sort((a, b) => {
|
|
1391
|
+
// Handle null/undefined (always sort to end)
|
|
1392
|
+
if (a.value == null && b.value == null)
|
|
1393
|
+
return 0;
|
|
1394
|
+
if (a.value == null)
|
|
1395
|
+
return order === 'asc' ? 1 : -1;
|
|
1396
|
+
if (b.value == null)
|
|
1397
|
+
return order === 'asc' ? -1 : 1;
|
|
1398
|
+
// Compare values
|
|
1399
|
+
if (a.value === b.value)
|
|
1400
|
+
return 0;
|
|
1401
|
+
const comparison = a.value < b.value ? -1 : 1;
|
|
1402
|
+
return order === 'asc' ? comparison : -comparison;
|
|
1403
|
+
});
|
|
1404
|
+
// 4. Return sorted IDs (caller handles pagination BEFORE loading entities)
|
|
1405
|
+
return idValuePairs.map(p => p.id);
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Get field value for a specific entity (helper for sorted queries)
|
|
1409
|
+
*
|
|
1410
|
+
* **IMPORTANT**: For timestamp fields (createdAt, updatedAt), this loads
|
|
1411
|
+
* the ACTUAL value from entity metadata, NOT the bucketed index value.
|
|
1412
|
+
* This is required because timestamp bucketing (1-minute precision) loses
|
|
1413
|
+
* precision needed for accurate sorting.
|
|
1414
|
+
*
|
|
1415
|
+
* For non-timestamp fields, loads from the chunked sparse index without
|
|
1416
|
+
* loading the full entity. This is critical for production-scale sorting.
|
|
1417
|
+
*
|
|
1418
|
+
* **Performance**:
|
|
1419
|
+
* - Timestamp fields: O(1) metadata load from storage (cached)
|
|
1420
|
+
* - Other fields: O(chunks) roaring bitmap lookup (typically 1-10 chunks)
|
|
1421
|
+
*
|
|
1422
|
+
* @param entityId - Entity UUID to get field value for
|
|
1423
|
+
* @param field - Field name to retrieve (e.g., 'createdAt', 'title')
|
|
1424
|
+
* @returns Promise<any> - Field value or undefined if not found
|
|
1425
|
+
*
|
|
1426
|
+
* @public (called from brainy.ts for sorted queries)
|
|
1427
|
+
* @since v4.5.4
|
|
1428
|
+
*/
|
|
1429
|
+
async getFieldValueForEntity(entityId, field) {
|
|
1430
|
+
// For timestamp fields, load ACTUAL value from entity metadata
|
|
1431
|
+
// (index has bucketed values which lose precision for sorting)
|
|
1432
|
+
if (field === 'createdAt' || field === 'updatedAt' || field === 'accessed' || field === 'modified') {
|
|
1433
|
+
try {
|
|
1434
|
+
const noun = await this.storage.getNoun(entityId);
|
|
1435
|
+
if (noun && noun.metadata) {
|
|
1436
|
+
return noun.metadata[field];
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
catch (err) {
|
|
1440
|
+
// If metadata load fails, fall back to index (bucketed value)
|
|
1441
|
+
console.warn(`[MetadataIndex] Failed to load ${field} from metadata for ${entityId}, using bucketed value`);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
// For non-timestamp fields, use the sparse index (no bucketing issues)
|
|
1445
|
+
const intId = this.idMapper.getInt(entityId);
|
|
1446
|
+
if (intId === undefined) {
|
|
1447
|
+
return undefined;
|
|
1448
|
+
}
|
|
1449
|
+
// Load sparse index for this field (cached via UnifiedCache)
|
|
1450
|
+
const sparseIndex = await this.loadSparseIndex(field);
|
|
1451
|
+
if (!sparseIndex) {
|
|
1452
|
+
return undefined;
|
|
1453
|
+
}
|
|
1454
|
+
// Search through chunks to find which value this entity has
|
|
1455
|
+
// Typically 1-10 chunks per field, so this is fast
|
|
1456
|
+
for (const chunkId of sparseIndex.getAllChunkIds()) {
|
|
1457
|
+
const chunk = await this.chunkManager.loadChunk(field, chunkId);
|
|
1458
|
+
if (!chunk)
|
|
1459
|
+
continue;
|
|
1460
|
+
// Check each value's roaring bitmap for our entity ID
|
|
1461
|
+
// Roaring bitmap .has() is O(1) with SIMD optimization
|
|
1462
|
+
for (const [value, bitmap] of chunk.entries) {
|
|
1463
|
+
if (bitmap.has(intId)) {
|
|
1464
|
+
// Found it! Denormalize the value (no bucketing for non-timestamps)
|
|
1465
|
+
return this.denormalizeValue(value, field);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
return undefined;
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Denormalize a value (reverse of normalizeValue)
|
|
1473
|
+
*
|
|
1474
|
+
* Converts normalized/stringified values back to their original type.
|
|
1475
|
+
* For most fields, this just parses numbers or returns strings as-is.
|
|
1476
|
+
*
|
|
1477
|
+
* **NOTE**: This is NOT used for timestamp sorting! Timestamp fields
|
|
1478
|
+
* (createdAt, updatedAt) are loaded directly from entity metadata by
|
|
1479
|
+
* getFieldValueForEntity() to avoid precision loss from bucketing.
|
|
1480
|
+
*
|
|
1481
|
+
* **Timestamp Bucketing (for range queries only)**:
|
|
1482
|
+
* - Indexed as: Math.floor(timestamp / 60000) * 60000
|
|
1483
|
+
* - Used for: Range queries (gte, lte) where 1-minute precision is acceptable
|
|
1484
|
+
* - NOT used for: Sorting (requires exact millisecond precision)
|
|
1485
|
+
*
|
|
1486
|
+
* @param normalized - Normalized value string from index
|
|
1487
|
+
* @param field - Field name (used for type inference)
|
|
1488
|
+
* @returns Denormalized value in original type
|
|
1489
|
+
*
|
|
1490
|
+
* @private
|
|
1491
|
+
* @since v4.5.4
|
|
1492
|
+
*/
|
|
1493
|
+
denormalizeValue(normalized, field) {
|
|
1494
|
+
// Try parsing as number (timestamps, integers, floats)
|
|
1495
|
+
const asNumber = Number(normalized);
|
|
1496
|
+
if (!isNaN(asNumber)) {
|
|
1497
|
+
return asNumber;
|
|
1498
|
+
}
|
|
1499
|
+
// For strings, return as-is (already denormalized)
|
|
1500
|
+
return normalized;
|
|
1501
|
+
}
|
|
1319
1502
|
/**
|
|
1320
1503
|
* DEPRECATED - Old implementation for backward compatibility
|
|
1321
1504
|
*/
|
package/dist/vfs/PathResolver.js
CHANGED
|
@@ -131,12 +131,11 @@ export class PathResolver {
|
|
|
131
131
|
// Use cached knowledge to quickly find the child
|
|
132
132
|
// Still need to verify it exists
|
|
133
133
|
}
|
|
134
|
-
// Use proper graph traversal to find children
|
|
135
|
-
//
|
|
134
|
+
// v4.7.0: Use proper graph traversal to find children
|
|
135
|
+
// VFS relationships are now part of the knowledge graph
|
|
136
136
|
const relations = await this.brain.getRelations({
|
|
137
137
|
from: parentId,
|
|
138
|
-
type: VerbType.Contains
|
|
139
|
-
includeVFS: true // v4.5.1: Required to see VFS relationships
|
|
138
|
+
type: VerbType.Contains
|
|
140
139
|
});
|
|
141
140
|
// Find the child with matching name
|
|
142
141
|
for (const relation of relations) {
|
|
@@ -157,11 +156,11 @@ export class PathResolver {
|
|
|
157
156
|
* Uses proper graph relationships to traverse the tree
|
|
158
157
|
*/
|
|
159
158
|
async getChildren(dirId) {
|
|
160
|
-
//
|
|
159
|
+
// v4.7.0: Use O(1) graph relationships (VFS creates these in mkdir/writeFile)
|
|
160
|
+
// VFS relationships are now part of the knowledge graph (no special filtering needed)
|
|
161
161
|
const relations = await this.brain.getRelations({
|
|
162
162
|
from: dirId,
|
|
163
|
-
type: VerbType.Contains
|
|
164
|
-
includeVFS: true // v4.5.1: Required to see VFS relationships
|
|
163
|
+
type: VerbType.Contains
|
|
165
164
|
});
|
|
166
165
|
const validChildren = [];
|
|
167
166
|
const childNames = new Set();
|
|
@@ -98,8 +98,7 @@ export class VirtualFileSystem {
|
|
|
98
98
|
path: '/', // ✅ Correct field name
|
|
99
99
|
vfsType: 'directory' // ✅ Correct field name
|
|
100
100
|
},
|
|
101
|
-
limit: 10
|
|
102
|
-
includeVFS: true // v4.4.0: CRITICAL - Must find VFS root entity!
|
|
101
|
+
limit: 10
|
|
103
102
|
});
|
|
104
103
|
if (existing.length > 0) {
|
|
105
104
|
// Handle duplicate roots (Workshop team reported ~10 duplicates!)
|
|
@@ -780,9 +779,8 @@ export class VirtualFileSystem {
|
|
|
780
779
|
limit: options?.limit || 10,
|
|
781
780
|
offset: options?.offset,
|
|
782
781
|
explain: options?.explain,
|
|
783
|
-
includeVFS: true, // v4.4.0: VFS search must include VFS entities!
|
|
784
782
|
where: {
|
|
785
|
-
vfsType: 'file' // v4.
|
|
783
|
+
vfsType: 'file' // v4.7.0: Search VFS files
|
|
786
784
|
}
|
|
787
785
|
};
|
|
788
786
|
// Add path filter if specified
|
|
@@ -824,9 +822,8 @@ export class VirtualFileSystem {
|
|
|
824
822
|
limit: options?.limit || 10,
|
|
825
823
|
threshold: options?.threshold || 0.7,
|
|
826
824
|
type: [NounType.File, NounType.Document, NounType.Media],
|
|
827
|
-
includeVFS: true, // v4.4.0: VFS similarity search must include VFS entities!
|
|
828
825
|
where: {
|
|
829
|
-
vfsType: 'file' // v4.
|
|
826
|
+
vfsType: 'file' // v4.7.0: Find similar VFS files
|
|
830
827
|
}
|
|
831
828
|
});
|
|
832
829
|
return results.map(r => {
|
|
@@ -1969,8 +1966,7 @@ export class VirtualFileSystem {
|
|
|
1969
1966
|
...query.where,
|
|
1970
1967
|
vfsType: 'entity'
|
|
1971
1968
|
},
|
|
1972
|
-
limit: query.limit || 100
|
|
1973
|
-
includeVFS: true // v4.4.0: VFS entity search must include VFS entities!
|
|
1969
|
+
limit: query.limit || 100
|
|
1974
1970
|
};
|
|
1975
1971
|
if (query.type) {
|
|
1976
1972
|
searchQuery.where.entityType = query.type;
|
|
@@ -27,8 +27,7 @@ export class AuthorProjection extends BaseProjectionStrategy {
|
|
|
27
27
|
vfsType: 'file',
|
|
28
28
|
owner: authorName
|
|
29
29
|
},
|
|
30
|
-
limit: 1000
|
|
31
|
-
includeVFS: true // v4.4.0: Must include VFS entities!
|
|
30
|
+
limit: 1000
|
|
32
31
|
};
|
|
33
32
|
// Filter by filename if subpath specified
|
|
34
33
|
if (subpath) {
|
|
@@ -46,14 +45,13 @@ export class AuthorProjection extends BaseProjectionStrategy {
|
|
|
46
45
|
* Resolve author to entity IDs using REAL Brainy.find()
|
|
47
46
|
*/
|
|
48
47
|
async resolve(brain, vfs, authorName) {
|
|
49
|
-
//
|
|
48
|
+
// v4.7.0: VFS entities are part of the knowledge graph
|
|
50
49
|
const results = await brain.find({
|
|
51
50
|
where: {
|
|
52
51
|
vfsType: 'file',
|
|
53
52
|
owner: authorName
|
|
54
53
|
},
|
|
55
|
-
limit: 1000
|
|
56
|
-
includeVFS: true // v4.4.0: Must include VFS entities!
|
|
54
|
+
limit: 1000
|
|
57
55
|
});
|
|
58
56
|
return this.extractIds(results);
|
|
59
57
|
}
|
|
@@ -66,10 +64,9 @@ export class AuthorProjection extends BaseProjectionStrategy {
|
|
|
66
64
|
const results = await brain.find({
|
|
67
65
|
where: {
|
|
68
66
|
vfsType: 'file',
|
|
69
|
-
owner: {
|
|
67
|
+
owner: { exists: true }
|
|
70
68
|
},
|
|
71
|
-
limit
|
|
72
|
-
includeVFS: true // v4.4.0: Must include VFS entities!
|
|
69
|
+
limit
|
|
73
70
|
});
|
|
74
71
|
return results.map(r => r.entity);
|
|
75
72
|
}
|
|
@@ -25,10 +25,9 @@ export class TagProjection extends BaseProjectionStrategy {
|
|
|
25
25
|
const query = {
|
|
26
26
|
where: {
|
|
27
27
|
vfsType: 'file',
|
|
28
|
-
tags: { contains: tagName } //
|
|
28
|
+
tags: { contains: tagName } // contains operator for array search
|
|
29
29
|
},
|
|
30
|
-
limit: 1000
|
|
31
|
-
includeVFS: true // v4.4.0: Must include VFS entities!
|
|
30
|
+
limit: 1000
|
|
32
31
|
};
|
|
33
32
|
// Filter by filename if subpath specified
|
|
34
33
|
if (subpath) {
|
|
@@ -46,14 +45,13 @@ export class TagProjection extends BaseProjectionStrategy {
|
|
|
46
45
|
* Resolve tag to entity IDs using REAL Brainy.find()
|
|
47
46
|
*/
|
|
48
47
|
async resolve(brain, vfs, tagName) {
|
|
49
|
-
//
|
|
48
|
+
// v4.7.0: VFS entities are part of the knowledge graph
|
|
50
49
|
const results = await brain.find({
|
|
51
50
|
where: {
|
|
52
51
|
vfsType: 'file',
|
|
53
|
-
tags: { contains: tagName }
|
|
52
|
+
tags: { contains: tagName }
|
|
54
53
|
},
|
|
55
|
-
limit: 1000
|
|
56
|
-
includeVFS: true // v4.4.0: Must include VFS entities!
|
|
54
|
+
limit: 1000
|
|
57
55
|
});
|
|
58
56
|
return this.extractIds(results);
|
|
59
57
|
}
|
|
@@ -65,10 +63,9 @@ export class TagProjection extends BaseProjectionStrategy {
|
|
|
65
63
|
const results = await brain.find({
|
|
66
64
|
where: {
|
|
67
65
|
vfsType: 'file',
|
|
68
|
-
tags: { exists: true } //
|
|
66
|
+
tags: { exists: true } // exists operator
|
|
69
67
|
},
|
|
70
|
-
limit
|
|
71
|
-
includeVFS: true // v4.4.0: Must include VFS entities!
|
|
68
|
+
limit
|
|
72
69
|
});
|
|
73
70
|
return results.map(r => r.entity);
|
|
74
71
|
}
|
|
@@ -58,17 +58,16 @@ export class TemporalProjection extends BaseProjectionStrategy {
|
|
|
58
58
|
startOfDay.setHours(0, 0, 0, 0);
|
|
59
59
|
const endOfDay = new Date(date);
|
|
60
60
|
endOfDay.setHours(23, 59, 59, 999);
|
|
61
|
-
//
|
|
61
|
+
// v4.7.0: VFS entities are part of the knowledge graph
|
|
62
62
|
const results = await brain.find({
|
|
63
63
|
where: {
|
|
64
64
|
vfsType: 'file',
|
|
65
65
|
modified: {
|
|
66
|
-
greaterEqual: startOfDay.getTime(),
|
|
67
|
-
lessEqual: endOfDay.getTime()
|
|
66
|
+
greaterEqual: startOfDay.getTime(),
|
|
67
|
+
lessEqual: endOfDay.getTime()
|
|
68
68
|
}
|
|
69
69
|
},
|
|
70
|
-
limit: 1000
|
|
71
|
-
includeVFS: true // v4.4.0: Must include VFS entities!
|
|
70
|
+
limit: 1000
|
|
72
71
|
});
|
|
73
72
|
return this.extractIds(results);
|
|
74
73
|
}
|
|
@@ -80,10 +79,9 @@ export class TemporalProjection extends BaseProjectionStrategy {
|
|
|
80
79
|
const results = await brain.find({
|
|
81
80
|
where: {
|
|
82
81
|
vfsType: 'file',
|
|
83
|
-
modified: { greaterEqual: oneDayAgo }
|
|
82
|
+
modified: { greaterEqual: oneDayAgo }
|
|
84
83
|
},
|
|
85
|
-
limit
|
|
86
|
-
includeVFS: true // v4.4.0: Must include VFS entities!
|
|
84
|
+
limit
|
|
87
85
|
});
|
|
88
86
|
return results.map(r => r.entity);
|
|
89
87
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soulcraft/brainy",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.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",
|