@dxos/index-core 0.8.4-main.fcfe5033a5 → 0.9.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/LICENSE +102 -5
- package/README.md +1 -1
- package/dist/lib/neutral/index.mjs +190 -101
- package/dist/lib/neutral/index.mjs.map +4 -4
- package/dist/lib/neutral/meta.json +1 -1
- package/dist/types/src/index-engine.d.ts +31 -17
- package/dist/types/src/index-engine.d.ts.map +1 -1
- package/dist/types/src/index-tracker.d.ts +3 -3
- package/dist/types/src/index.d.ts +3 -3
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/indexes/entity-meta-index.d.ts +113 -0
- package/dist/types/src/indexes/entity-meta-index.d.ts.map +1 -0
- package/dist/types/src/indexes/entity-meta-index.test.d.ts +2 -0
- package/dist/types/src/indexes/entity-meta-index.test.d.ts.map +1 -0
- package/dist/types/src/indexes/fts-index.d.ts +7 -6
- package/dist/types/src/indexes/fts-index.d.ts.map +1 -1
- package/dist/types/src/indexes/index.d.ts +1 -1
- package/dist/types/src/indexes/interface.d.ts +15 -4
- package/dist/types/src/indexes/interface.d.ts.map +1 -1
- package/dist/types/src/indexes/reverse-ref-index.d.ts +3 -2
- package/dist/types/src/indexes/reverse-ref-index.d.ts.map +1 -1
- package/dist/types/src/utils.d.ts +3 -3
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +13 -18
- package/src/index-engine.test.ts +138 -16
- package/src/index-engine.ts +123 -58
- package/src/index.ts +9 -3
- package/src/indexes/{object-meta-index.test.ts → entity-meta-index.test.ts} +114 -53
- package/src/indexes/{object-meta-index.ts → entity-meta-index.ts} +140 -71
- package/src/indexes/fts-index.test.ts +188 -34
- package/src/indexes/fts-index.ts +32 -15
- package/src/indexes/index.ts +1 -1
- package/src/indexes/interface.ts +16 -4
- package/src/indexes/reverse-ref-index.test.ts +57 -43
- package/src/indexes/reverse-ref-index.ts +22 -14
- package/src/utils.ts +5 -5
- package/dist/types/src/indexes/object-meta-index.d.ts +0 -88
- package/dist/types/src/indexes/object-meta-index.d.ts.map +0 -1
- package/dist/types/src/indexes/object-meta-index.test.d.ts +0 -2
- package/dist/types/src/indexes/object-meta-index.test.d.ts.map +0 -1
|
@@ -10,14 +10,14 @@ import * as Effect from 'effect/Effect';
|
|
|
10
10
|
import * as Layer from 'effect/Layer';
|
|
11
11
|
|
|
12
12
|
import { ATTR_TYPE } from '@dxos/echo/internal';
|
|
13
|
-
import { DXN,
|
|
13
|
+
import { DXN, EntityId, SpaceId } from '@dxos/keys';
|
|
14
14
|
|
|
15
|
+
import { EntityMetaIndex } from './entity-meta-index';
|
|
15
16
|
import { FtsIndex } from './fts-index';
|
|
16
17
|
import type { IndexerObject } from './interface';
|
|
17
|
-
import { ObjectMetaIndex } from './object-meta-index';
|
|
18
18
|
|
|
19
|
-
const TYPE_PERSON = DXN.
|
|
20
|
-
const TYPE_DEFAULT = DXN.
|
|
19
|
+
const TYPE_PERSON = DXN.make('com.example.type.person', '0.1.0');
|
|
20
|
+
const TYPE_DEFAULT = DXN.make('com.example.type.Type', '0.1.0');
|
|
21
21
|
|
|
22
22
|
const TestLayer = Layer.merge(
|
|
23
23
|
SqliteClient.layer({
|
|
@@ -46,7 +46,7 @@ describe('FtsIndex', () => {
|
|
|
46
46
|
'should insert snapshots and query them via MATCH',
|
|
47
47
|
Effect.fnUntraced(function* () {
|
|
48
48
|
const index = new FtsIndex();
|
|
49
|
-
const metaIndex = new
|
|
49
|
+
const metaIndex = new EntityMetaIndex();
|
|
50
50
|
yield* index.migrate();
|
|
51
51
|
yield* metaIndex.migrate();
|
|
52
52
|
|
|
@@ -55,11 +55,13 @@ describe('FtsIndex', () => {
|
|
|
55
55
|
{
|
|
56
56
|
spaceId,
|
|
57
57
|
queueId: null,
|
|
58
|
+
queueNamespace: null,
|
|
58
59
|
documentId: 'doc-1',
|
|
59
60
|
recordId: null,
|
|
61
|
+
createdAt: null,
|
|
60
62
|
updatedAt: Date.now(),
|
|
61
63
|
data: {
|
|
62
|
-
id:
|
|
64
|
+
id: EntityId.random(),
|
|
63
65
|
[ATTR_TYPE]: TYPE_PERSON,
|
|
64
66
|
title: 'Hello Effect',
|
|
65
67
|
body: 'This is a message about Effect and SQL.',
|
|
@@ -89,23 +91,25 @@ describe('FtsIndex', () => {
|
|
|
89
91
|
'should upsert objects on update',
|
|
90
92
|
Effect.fnUntraced(function* () {
|
|
91
93
|
const index = new FtsIndex();
|
|
92
|
-
const metaIndex = new
|
|
94
|
+
const metaIndex = new EntityMetaIndex();
|
|
93
95
|
yield* index.migrate();
|
|
94
96
|
yield* metaIndex.migrate();
|
|
95
97
|
|
|
96
98
|
const spaceId = SpaceId.random();
|
|
97
|
-
const objectId =
|
|
99
|
+
const objectId = EntityId.random();
|
|
98
100
|
|
|
99
101
|
// Initial insert.
|
|
100
102
|
const obj1: IndexerObject = {
|
|
101
103
|
spaceId,
|
|
102
104
|
queueId: null,
|
|
105
|
+
queueNamespace: null,
|
|
103
106
|
documentId: 'doc-1',
|
|
104
107
|
recordId: null,
|
|
108
|
+
createdAt: null,
|
|
105
109
|
updatedAt: Date.now(),
|
|
106
110
|
data: {
|
|
107
111
|
id: objectId,
|
|
108
|
-
[ATTR_TYPE]:
|
|
112
|
+
[ATTR_TYPE]: TYPE_PERSON,
|
|
109
113
|
title: 'Original Title',
|
|
110
114
|
},
|
|
111
115
|
};
|
|
@@ -120,8 +124,10 @@ describe('FtsIndex', () => {
|
|
|
120
124
|
const obj2: IndexerObject = {
|
|
121
125
|
spaceId,
|
|
122
126
|
queueId: null,
|
|
127
|
+
queueNamespace: null,
|
|
123
128
|
documentId: 'doc-1',
|
|
124
129
|
recordId: null,
|
|
130
|
+
createdAt: null,
|
|
125
131
|
updatedAt: Date.now(),
|
|
126
132
|
data: {
|
|
127
133
|
id: objectId,
|
|
@@ -149,7 +155,7 @@ describe('FtsIndex', () => {
|
|
|
149
155
|
'should handle non-sequential recordIds',
|
|
150
156
|
Effect.fnUntraced(function* () {
|
|
151
157
|
const index = new FtsIndex();
|
|
152
|
-
const metaIndex = new
|
|
158
|
+
const metaIndex = new EntityMetaIndex();
|
|
153
159
|
yield* index.migrate();
|
|
154
160
|
yield* metaIndex.migrate();
|
|
155
161
|
|
|
@@ -158,11 +164,13 @@ describe('FtsIndex', () => {
|
|
|
158
164
|
{
|
|
159
165
|
spaceId,
|
|
160
166
|
queueId: null,
|
|
167
|
+
queueNamespace: null,
|
|
161
168
|
documentId: 'doc-100',
|
|
162
169
|
recordId: null,
|
|
170
|
+
createdAt: null,
|
|
163
171
|
updatedAt: Date.now(),
|
|
164
172
|
data: {
|
|
165
|
-
id:
|
|
173
|
+
id: EntityId.random(),
|
|
166
174
|
[ATTR_TYPE]: TYPE_PERSON,
|
|
167
175
|
title: 'Alpha Document',
|
|
168
176
|
},
|
|
@@ -170,11 +178,13 @@ describe('FtsIndex', () => {
|
|
|
170
178
|
{
|
|
171
179
|
spaceId,
|
|
172
180
|
queueId: null,
|
|
181
|
+
queueNamespace: null,
|
|
173
182
|
documentId: 'doc-200',
|
|
174
183
|
recordId: null,
|
|
184
|
+
createdAt: null,
|
|
175
185
|
updatedAt: Date.now(),
|
|
176
186
|
data: {
|
|
177
|
-
id:
|
|
187
|
+
id: EntityId.random(),
|
|
178
188
|
[ATTR_TYPE]: TYPE_PERSON,
|
|
179
189
|
title: 'Beta Document',
|
|
180
190
|
},
|
|
@@ -182,11 +192,13 @@ describe('FtsIndex', () => {
|
|
|
182
192
|
{
|
|
183
193
|
spaceId,
|
|
184
194
|
queueId: null,
|
|
195
|
+
queueNamespace: null,
|
|
185
196
|
documentId: 'doc-1000',
|
|
186
197
|
recordId: null,
|
|
198
|
+
createdAt: null,
|
|
187
199
|
updatedAt: Date.now(),
|
|
188
200
|
data: {
|
|
189
|
-
id:
|
|
201
|
+
id: EntityId.random(),
|
|
190
202
|
[ATTR_TYPE]: TYPE_PERSON,
|
|
191
203
|
title: 'Gamma Document',
|
|
192
204
|
},
|
|
@@ -221,7 +233,7 @@ describe('FtsIndex', () => {
|
|
|
221
233
|
'should query from one space only',
|
|
222
234
|
Effect.fnUntraced(function* () {
|
|
223
235
|
const index = new FtsIndex();
|
|
224
|
-
const metaIndex = new
|
|
236
|
+
const metaIndex = new EntityMetaIndex();
|
|
225
237
|
yield* index.migrate();
|
|
226
238
|
yield* metaIndex.migrate();
|
|
227
239
|
|
|
@@ -231,11 +243,13 @@ describe('FtsIndex', () => {
|
|
|
231
243
|
const obj1: IndexerObject = {
|
|
232
244
|
spaceId: space1,
|
|
233
245
|
queueId: null,
|
|
246
|
+
queueNamespace: null,
|
|
234
247
|
documentId: 'doc-s1',
|
|
235
248
|
recordId: null,
|
|
249
|
+
createdAt: null,
|
|
236
250
|
updatedAt: Date.now(),
|
|
237
251
|
data: {
|
|
238
|
-
id:
|
|
252
|
+
id: EntityId.random(),
|
|
239
253
|
[ATTR_TYPE]: TYPE_PERSON,
|
|
240
254
|
title: 'Space One Content',
|
|
241
255
|
},
|
|
@@ -244,11 +258,13 @@ describe('FtsIndex', () => {
|
|
|
244
258
|
const obj2: IndexerObject = {
|
|
245
259
|
spaceId: space2,
|
|
246
260
|
queueId: null,
|
|
261
|
+
queueNamespace: null,
|
|
247
262
|
documentId: 'doc-s2',
|
|
248
263
|
recordId: null,
|
|
264
|
+
createdAt: null,
|
|
249
265
|
updatedAt: Date.now(),
|
|
250
266
|
data: {
|
|
251
|
-
id:
|
|
267
|
+
id: EntityId.random(),
|
|
252
268
|
[ATTR_TYPE]: TYPE_PERSON,
|
|
253
269
|
title: 'Space Two Content',
|
|
254
270
|
},
|
|
@@ -294,7 +310,7 @@ describe('FtsIndex', () => {
|
|
|
294
310
|
'partial word matches',
|
|
295
311
|
Effect.fnUntraced(function* () {
|
|
296
312
|
const index = new FtsIndex();
|
|
297
|
-
const metaIndex = new
|
|
313
|
+
const metaIndex = new EntityMetaIndex();
|
|
298
314
|
yield* index.migrate();
|
|
299
315
|
yield* metaIndex.migrate();
|
|
300
316
|
|
|
@@ -303,11 +319,13 @@ describe('FtsIndex', () => {
|
|
|
303
319
|
{
|
|
304
320
|
spaceId,
|
|
305
321
|
queueId: null,
|
|
322
|
+
queueNamespace: null,
|
|
306
323
|
documentId: 'doc-1',
|
|
307
324
|
recordId: null,
|
|
325
|
+
createdAt: null,
|
|
308
326
|
updatedAt: Date.now(),
|
|
309
327
|
data: {
|
|
310
|
-
id:
|
|
328
|
+
id: EntityId.random(),
|
|
311
329
|
[ATTR_TYPE]: TYPE_PERSON,
|
|
312
330
|
title: 'Programming in TypeScript',
|
|
313
331
|
body: 'Learn about functional programming patterns.',
|
|
@@ -316,11 +334,13 @@ describe('FtsIndex', () => {
|
|
|
316
334
|
{
|
|
317
335
|
spaceId,
|
|
318
336
|
queueId: null,
|
|
337
|
+
queueNamespace: null,
|
|
319
338
|
documentId: 'doc-2',
|
|
320
339
|
recordId: null,
|
|
340
|
+
createdAt: null,
|
|
321
341
|
updatedAt: Date.now(),
|
|
322
342
|
data: {
|
|
323
|
-
id:
|
|
343
|
+
id: EntityId.random(),
|
|
324
344
|
[ATTR_TYPE]: TYPE_PERSON,
|
|
325
345
|
title: 'Database Design',
|
|
326
346
|
body: 'Understanding program architecture.',
|
|
@@ -375,22 +395,24 @@ describe('FtsIndex', () => {
|
|
|
375
395
|
'should query from specific queues',
|
|
376
396
|
Effect.fnUntraced(function* () {
|
|
377
397
|
const index = new FtsIndex();
|
|
378
|
-
const metaIndex = new
|
|
398
|
+
const metaIndex = new EntityMetaIndex();
|
|
379
399
|
yield* index.migrate();
|
|
380
400
|
yield* metaIndex.migrate();
|
|
381
401
|
|
|
382
402
|
const spaceId = SpaceId.random();
|
|
383
|
-
const queue1 =
|
|
384
|
-
const queue2 =
|
|
403
|
+
const queue1 = EntityId.random();
|
|
404
|
+
const queue2 = EntityId.random();
|
|
385
405
|
|
|
386
406
|
const spaceObj: IndexerObject = {
|
|
387
407
|
spaceId,
|
|
388
408
|
queueId: null,
|
|
409
|
+
queueNamespace: null,
|
|
389
410
|
documentId: 'doc-space',
|
|
390
411
|
recordId: null,
|
|
412
|
+
createdAt: null,
|
|
391
413
|
updatedAt: Date.now(),
|
|
392
414
|
data: {
|
|
393
|
-
id:
|
|
415
|
+
id: EntityId.random(),
|
|
394
416
|
[ATTR_TYPE]: TYPE_PERSON,
|
|
395
417
|
title: 'Space Content',
|
|
396
418
|
},
|
|
@@ -399,11 +421,13 @@ describe('FtsIndex', () => {
|
|
|
399
421
|
const queue1Obj: IndexerObject = {
|
|
400
422
|
spaceId,
|
|
401
423
|
queueId: queue1,
|
|
424
|
+
queueNamespace: 'data',
|
|
402
425
|
documentId: null,
|
|
403
426
|
recordId: null,
|
|
427
|
+
createdAt: null,
|
|
404
428
|
updatedAt: Date.now(),
|
|
405
429
|
data: {
|
|
406
|
-
id:
|
|
430
|
+
id: EntityId.random(),
|
|
407
431
|
[ATTR_TYPE]: TYPE_PERSON,
|
|
408
432
|
title: 'Queue One Content',
|
|
409
433
|
},
|
|
@@ -412,11 +436,13 @@ describe('FtsIndex', () => {
|
|
|
412
436
|
const queue2Obj: IndexerObject = {
|
|
413
437
|
spaceId,
|
|
414
438
|
queueId: queue2,
|
|
439
|
+
queueNamespace: 'data',
|
|
415
440
|
documentId: null,
|
|
416
441
|
recordId: null,
|
|
442
|
+
createdAt: null,
|
|
417
443
|
updatedAt: Date.now(),
|
|
418
444
|
data: {
|
|
419
|
-
id:
|
|
445
|
+
id: EntityId.random(),
|
|
420
446
|
[ATTR_TYPE]: TYPE_PERSON,
|
|
421
447
|
title: 'Queue Two Content',
|
|
422
448
|
},
|
|
@@ -451,21 +477,23 @@ describe('FtsIndex', () => {
|
|
|
451
477
|
'should query with includeAllQueues',
|
|
452
478
|
Effect.fnUntraced(function* () {
|
|
453
479
|
const index = new FtsIndex();
|
|
454
|
-
const metaIndex = new
|
|
480
|
+
const metaIndex = new EntityMetaIndex();
|
|
455
481
|
yield* index.migrate();
|
|
456
482
|
yield* metaIndex.migrate();
|
|
457
483
|
|
|
458
484
|
const spaceId = SpaceId.random();
|
|
459
|
-
const queueId =
|
|
485
|
+
const queueId = EntityId.random();
|
|
460
486
|
|
|
461
487
|
const spaceObj: IndexerObject = {
|
|
462
488
|
spaceId,
|
|
463
489
|
queueId: null,
|
|
490
|
+
queueNamespace: null,
|
|
464
491
|
documentId: 'doc-space',
|
|
465
492
|
recordId: null,
|
|
493
|
+
createdAt: null,
|
|
466
494
|
updatedAt: Date.now(),
|
|
467
495
|
data: {
|
|
468
|
-
id:
|
|
496
|
+
id: EntityId.random(),
|
|
469
497
|
[ATTR_TYPE]: TYPE_PERSON,
|
|
470
498
|
title: 'Space Content',
|
|
471
499
|
},
|
|
@@ -474,11 +502,13 @@ describe('FtsIndex', () => {
|
|
|
474
502
|
const queueObj: IndexerObject = {
|
|
475
503
|
spaceId,
|
|
476
504
|
queueId,
|
|
505
|
+
queueNamespace: 'data',
|
|
477
506
|
documentId: null,
|
|
478
507
|
recordId: null,
|
|
508
|
+
createdAt: null,
|
|
479
509
|
updatedAt: Date.now(),
|
|
480
510
|
data: {
|
|
481
|
-
id:
|
|
511
|
+
id: EntityId.random(),
|
|
482
512
|
[ATTR_TYPE]: TYPE_PERSON,
|
|
483
513
|
title: 'Queue Content',
|
|
484
514
|
},
|
|
@@ -513,22 +543,24 @@ describe('FtsIndex', () => {
|
|
|
513
543
|
'should OR space and queue constraints',
|
|
514
544
|
Effect.fnUntraced(function* () {
|
|
515
545
|
const index = new FtsIndex();
|
|
516
|
-
const metaIndex = new
|
|
546
|
+
const metaIndex = new EntityMetaIndex();
|
|
517
547
|
yield* index.migrate();
|
|
518
548
|
yield* metaIndex.migrate();
|
|
519
549
|
|
|
520
550
|
const space1 = SpaceId.random();
|
|
521
551
|
const space2 = SpaceId.random();
|
|
522
|
-
const queueInSpace2 =
|
|
552
|
+
const queueInSpace2 = EntityId.random();
|
|
523
553
|
|
|
524
554
|
const space1Obj: IndexerObject = {
|
|
525
555
|
spaceId: space1,
|
|
526
556
|
queueId: null,
|
|
557
|
+
queueNamespace: null,
|
|
527
558
|
documentId: 'doc-s1',
|
|
528
559
|
recordId: null,
|
|
560
|
+
createdAt: null,
|
|
529
561
|
updatedAt: Date.now(),
|
|
530
562
|
data: {
|
|
531
|
-
id:
|
|
563
|
+
id: EntityId.random(),
|
|
532
564
|
[ATTR_TYPE]: TYPE_PERSON,
|
|
533
565
|
title: 'Space One Content',
|
|
534
566
|
},
|
|
@@ -537,11 +569,13 @@ describe('FtsIndex', () => {
|
|
|
537
569
|
const space2Obj: IndexerObject = {
|
|
538
570
|
spaceId: space2,
|
|
539
571
|
queueId: null,
|
|
572
|
+
queueNamespace: null,
|
|
540
573
|
documentId: 'doc-s2',
|
|
541
574
|
recordId: null,
|
|
575
|
+
createdAt: null,
|
|
542
576
|
updatedAt: Date.now(),
|
|
543
577
|
data: {
|
|
544
|
-
id:
|
|
578
|
+
id: EntityId.random(),
|
|
545
579
|
[ATTR_TYPE]: TYPE_PERSON,
|
|
546
580
|
title: 'Space Two Content',
|
|
547
581
|
},
|
|
@@ -550,11 +584,13 @@ describe('FtsIndex', () => {
|
|
|
550
584
|
const queueObj: IndexerObject = {
|
|
551
585
|
spaceId: space2,
|
|
552
586
|
queueId: queueInSpace2,
|
|
587
|
+
queueNamespace: 'data',
|
|
553
588
|
documentId: null,
|
|
554
589
|
recordId: null,
|
|
590
|
+
createdAt: null,
|
|
555
591
|
updatedAt: Date.now(),
|
|
556
592
|
data: {
|
|
557
|
-
id:
|
|
593
|
+
id: EntityId.random(),
|
|
558
594
|
[ATTR_TYPE]: TYPE_PERSON,
|
|
559
595
|
title: 'Queue Content',
|
|
560
596
|
},
|
|
@@ -579,4 +615,122 @@ describe('FtsIndex', () => {
|
|
|
579
615
|
expect(objectIds).not.toContain(space2Obj.data.id);
|
|
580
616
|
}, Effect.provide(TestLayer)),
|
|
581
617
|
);
|
|
618
|
+
|
|
619
|
+
describe('querySnapshotsJSON', () => {
|
|
620
|
+
it.effect(
|
|
621
|
+
'returns snapshots for all present recordIds',
|
|
622
|
+
Effect.fnUntraced(function* () {
|
|
623
|
+
const index = new FtsIndex();
|
|
624
|
+
const metaIndex = new EntityMetaIndex();
|
|
625
|
+
yield* index.migrate();
|
|
626
|
+
yield* metaIndex.migrate();
|
|
627
|
+
|
|
628
|
+
const spaceId = SpaceId.random();
|
|
629
|
+
const objects: IndexerObject[] = [
|
|
630
|
+
{
|
|
631
|
+
spaceId,
|
|
632
|
+
queueId: EntityId.random(),
|
|
633
|
+
queueNamespace: 'data',
|
|
634
|
+
documentId: null,
|
|
635
|
+
recordId: null,
|
|
636
|
+
createdAt: null,
|
|
637
|
+
updatedAt: Date.now(),
|
|
638
|
+
data: { id: EntityId.random(), [ATTR_TYPE]: TYPE_PERSON, value: 'alpha' },
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
spaceId,
|
|
642
|
+
queueId: EntityId.random(),
|
|
643
|
+
queueNamespace: 'data',
|
|
644
|
+
documentId: null,
|
|
645
|
+
recordId: null,
|
|
646
|
+
createdAt: null,
|
|
647
|
+
updatedAt: Date.now(),
|
|
648
|
+
data: { id: EntityId.random(), [ATTR_TYPE]: TYPE_PERSON, value: 'beta' },
|
|
649
|
+
},
|
|
650
|
+
];
|
|
651
|
+
|
|
652
|
+
yield* metaIndex.update(objects);
|
|
653
|
+
yield* metaIndex.lookupRecordIds(objects);
|
|
654
|
+
yield* index.update(objects);
|
|
655
|
+
|
|
656
|
+
const recordIds = objects.map((o) => o.recordId!);
|
|
657
|
+
const snapshots = yield* index.querySnapshotsJSON(recordIds);
|
|
658
|
+
|
|
659
|
+
expect(snapshots).toHaveLength(2);
|
|
660
|
+
const snapshotMap = new Map(snapshots.map((s) => [s.recordId, s.snapshot]));
|
|
661
|
+
expect((snapshotMap.get(objects[0].recordId!) as any).value).toBe('alpha');
|
|
662
|
+
expect((snapshotMap.get(objects[1].recordId!) as any).value).toBe('beta');
|
|
663
|
+
}, Effect.provide(TestLayer)),
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
it.effect(
|
|
667
|
+
'omits stale recordIds not present in FTS index',
|
|
668
|
+
Effect.fnUntraced(function* () {
|
|
669
|
+
const index = new FtsIndex();
|
|
670
|
+
const metaIndex = new EntityMetaIndex();
|
|
671
|
+
yield* index.migrate();
|
|
672
|
+
yield* metaIndex.migrate();
|
|
673
|
+
|
|
674
|
+
const spaceId = SpaceId.random();
|
|
675
|
+
const object: IndexerObject = {
|
|
676
|
+
spaceId,
|
|
677
|
+
queueId: EntityId.random(),
|
|
678
|
+
queueNamespace: 'data',
|
|
679
|
+
documentId: null,
|
|
680
|
+
recordId: null,
|
|
681
|
+
createdAt: null,
|
|
682
|
+
updatedAt: Date.now(),
|
|
683
|
+
data: { id: EntityId.random(), [ATTR_TYPE]: TYPE_PERSON, value: 'present' },
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
yield* metaIndex.update([object]);
|
|
687
|
+
yield* metaIndex.lookupRecordIds([object]);
|
|
688
|
+
yield* index.update([object]);
|
|
689
|
+
|
|
690
|
+
// Query with the real id plus a stale/non-existent id.
|
|
691
|
+
const staleId = 99999;
|
|
692
|
+
const snapshots = yield* index.querySnapshotsJSON([object.recordId!, staleId]);
|
|
693
|
+
|
|
694
|
+
expect(snapshots).toHaveLength(1);
|
|
695
|
+
expect(snapshots[0].recordId).toBe(object.recordId!);
|
|
696
|
+
expect((snapshots[0].snapshot as any).value).toBe('present');
|
|
697
|
+
}, Effect.provide(TestLayer)),
|
|
698
|
+
);
|
|
699
|
+
|
|
700
|
+
it.effect(
|
|
701
|
+
'handles more than 999 recordIds without exceeding SQLite variable limit',
|
|
702
|
+
Effect.fnUntraced(function* () {
|
|
703
|
+
const index = new FtsIndex();
|
|
704
|
+
const metaIndex = new EntityMetaIndex();
|
|
705
|
+
yield* index.migrate();
|
|
706
|
+
yield* metaIndex.migrate();
|
|
707
|
+
|
|
708
|
+
const spaceId = SpaceId.random();
|
|
709
|
+
const count = 1100;
|
|
710
|
+
const objects: IndexerObject[] = Array.from({ length: count }, (_, i) => ({
|
|
711
|
+
spaceId,
|
|
712
|
+
queueId: EntityId.random(),
|
|
713
|
+
queueNamespace: 'data',
|
|
714
|
+
documentId: null,
|
|
715
|
+
recordId: null,
|
|
716
|
+
createdAt: null,
|
|
717
|
+
updatedAt: Date.now(),
|
|
718
|
+
data: { id: EntityId.random(), [ATTR_TYPE]: TYPE_PERSON, index: i },
|
|
719
|
+
}));
|
|
720
|
+
|
|
721
|
+
yield* metaIndex.update(objects);
|
|
722
|
+
yield* metaIndex.lookupRecordIds(objects);
|
|
723
|
+
yield* index.update(objects);
|
|
724
|
+
|
|
725
|
+
const recordIds = objects.map((o) => o.recordId!);
|
|
726
|
+
const snapshots = yield* index.querySnapshotsJSON(recordIds);
|
|
727
|
+
|
|
728
|
+
expect(snapshots).toHaveLength(count);
|
|
729
|
+
const returnedIds = new Set(snapshots.map((s) => s.recordId));
|
|
730
|
+
for (const id of recordIds) {
|
|
731
|
+
expect(returnedIds.has(id)).toBe(true);
|
|
732
|
+
}
|
|
733
|
+
}, Effect.provide(TestLayer)),
|
|
734
|
+
);
|
|
735
|
+
});
|
|
582
736
|
});
|
package/src/indexes/fts-index.ts
CHANGED
|
@@ -8,10 +8,14 @@ import type * as Statement from '@effect/sql/Statement';
|
|
|
8
8
|
import * as Effect from 'effect/Effect';
|
|
9
9
|
|
|
10
10
|
import type { Obj } from '@dxos/echo';
|
|
11
|
-
import type {
|
|
11
|
+
import type { EntityId, SpaceId } from '@dxos/keys';
|
|
12
12
|
|
|
13
|
+
import type { EntityMeta } from './entity-meta-index';
|
|
13
14
|
import type { Index, IndexerObject } from './interface';
|
|
14
|
-
|
|
15
|
+
|
|
16
|
+
// SQLite bound-variable limit (SQLITE_LIMIT_VARIABLE_NUMBER) is 999 in most builds.
|
|
17
|
+
// Use 500 as a safe chunk size for IN (...) clauses.
|
|
18
|
+
const SQL_CHUNK_SIZE = 500;
|
|
15
19
|
|
|
16
20
|
/**
|
|
17
21
|
* The space and queue constrains are combined together using a logical OR.
|
|
@@ -35,13 +39,13 @@ export interface FtsQuery {
|
|
|
35
39
|
/**
|
|
36
40
|
* Queue IDs to search within.
|
|
37
41
|
*/
|
|
38
|
-
queueIds: readonly
|
|
42
|
+
queueIds: readonly EntityId[] | null;
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
/**
|
|
42
46
|
* Result of FTS query including the indexed snapshot data.
|
|
43
47
|
*/
|
|
44
|
-
export interface FtsResult extends
|
|
48
|
+
export interface FtsResult extends EntityMeta {
|
|
45
49
|
/**
|
|
46
50
|
* The indexed snapshot data (JSON string).
|
|
47
51
|
* Used to load queue objects without going through document loading.
|
|
@@ -52,7 +56,7 @@ export interface FtsResult extends ObjectMeta {
|
|
|
52
56
|
/**
|
|
53
57
|
* Result of FTS query with rank.
|
|
54
58
|
*/
|
|
55
|
-
export interface FtsQueryResult extends
|
|
59
|
+
export interface FtsQueryResult extends EntityMeta {
|
|
56
60
|
/**
|
|
57
61
|
* Relevance rank from FTS5.
|
|
58
62
|
* Higher values indicate better matches.
|
|
@@ -157,7 +161,7 @@ export class FtsIndex implements Index {
|
|
|
157
161
|
// BM25 returns negative values, negate to get higher = better match.
|
|
158
162
|
// Order by rank descending so best matches come first.
|
|
159
163
|
// Note: bm25() requires the actual table name, not an alias.
|
|
160
|
-
const rows = yield* sql<
|
|
164
|
+
const rows = yield* sql<EntityMeta & { rank: number }>`
|
|
161
165
|
SELECT m.*, -bm25(ftsIndex) AS rank
|
|
162
166
|
FROM ftsIndex AS f
|
|
163
167
|
JOIN objectMeta AS m ON f.rowid = m.recordId
|
|
@@ -167,7 +171,7 @@ export class FtsIndex implements Index {
|
|
|
167
171
|
return rows;
|
|
168
172
|
} else {
|
|
169
173
|
// LIKE fallback - no ranking available, default to 1.
|
|
170
|
-
const rows = yield* sql<
|
|
174
|
+
const rows = yield* sql<EntityMeta>`
|
|
171
175
|
SELECT m.*
|
|
172
176
|
FROM ftsIndex AS f
|
|
173
177
|
JOIN objectMeta AS m ON f.rowid = m.recordId
|
|
@@ -181,6 +185,7 @@ export class FtsIndex implements Index {
|
|
|
181
185
|
/**
|
|
182
186
|
* Query snapshots by recordIds.
|
|
183
187
|
* Returns the parsed JSON snapshots for queue objects.
|
|
188
|
+
* RecordIds not present in the FTS index are silently omitted from the result.
|
|
184
189
|
*/
|
|
185
190
|
querySnapshotsJSON(
|
|
186
191
|
recordIds: number[],
|
|
@@ -190,14 +195,26 @@ export class FtsIndex implements Index {
|
|
|
190
195
|
return [];
|
|
191
196
|
}
|
|
192
197
|
const sql = yield* SqlClient.SqlClient;
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
198
|
+
|
|
199
|
+
// Chunk to avoid SQLite bound-variable limit (SQLITE_LIMIT_VARIABLE_NUMBER,
|
|
200
|
+
// typically 999 in wasm builds). 500 gives a safe margin.
|
|
201
|
+
const chunks: number[][] = [];
|
|
202
|
+
for (let i = 0; i < recordIds.length; i += SQL_CHUNK_SIZE) {
|
|
203
|
+
chunks.push(recordIds.slice(i, i + SQL_CHUNK_SIZE));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const allResults: { recordId: number; snapshot: Obj.JSON }[] = [];
|
|
207
|
+
for (const chunk of chunks) {
|
|
208
|
+
const rows = yield* sql<{
|
|
209
|
+
rowid: number;
|
|
210
|
+
snapshot: string;
|
|
211
|
+
}>`SELECT rowid, snapshot FROM ftsIndex WHERE rowid IN ${sql.in(chunk)}`;
|
|
212
|
+
for (const r of rows) {
|
|
213
|
+
allResults.push({ recordId: r.rowid, snapshot: JSON.parse(r.snapshot) });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return allResults;
|
|
201
218
|
});
|
|
202
219
|
}
|
|
203
220
|
|
package/src/indexes/index.ts
CHANGED
package/src/indexes/interface.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type * as SqlError from '@effect/sql/SqlError';
|
|
|
7
7
|
import type * as Effect from 'effect/Effect';
|
|
8
8
|
|
|
9
9
|
import type { Obj } from '@dxos/echo';
|
|
10
|
-
import type {
|
|
10
|
+
import type { EntityId, SpaceId } from '@dxos/keys';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Data describing objects returned from sources to the indexer.
|
|
@@ -18,7 +18,12 @@ export interface IndexerObject {
|
|
|
18
18
|
* Queue id if object is from the queue.
|
|
19
19
|
* If null, `documentId` must be set.
|
|
20
20
|
*/
|
|
21
|
-
queueId:
|
|
21
|
+
queueId: EntityId | null;
|
|
22
|
+
/**
|
|
23
|
+
* Queue subspace namespace (e.g. 'data', 'trace') the object lives in.
|
|
24
|
+
* Set together with `queueId`; null for non-queue objects.
|
|
25
|
+
*/
|
|
26
|
+
queueNamespace: string | null;
|
|
22
27
|
/**
|
|
23
28
|
* Document id if object is from the automerge document.
|
|
24
29
|
* If null, `queueId` must be set.
|
|
@@ -27,8 +32,8 @@ export interface IndexerObject {
|
|
|
27
32
|
|
|
28
33
|
/**
|
|
29
34
|
* Record id from the objectMeta index.
|
|
30
|
-
* `Null` before the object is stored in the
|
|
31
|
-
* Enriched by the IndexEngine after the object is stored in the
|
|
35
|
+
* `Null` before the object is stored in the EntityMetaIndex.
|
|
36
|
+
* Enriched by the IndexEngine after the object is stored in the EntityMetaIndex.
|
|
32
37
|
*/
|
|
33
38
|
recordId: number | null;
|
|
34
39
|
|
|
@@ -37,6 +42,13 @@ export interface IndexerObject {
|
|
|
37
42
|
*/
|
|
38
43
|
data: Obj.JSON;
|
|
39
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Unix ms timestamp when this object was first created.
|
|
47
|
+
* Sourced from system.createdAt in the automerge document; null for legacy objects
|
|
48
|
+
* created before this field was introduced.
|
|
49
|
+
*/
|
|
50
|
+
createdAt: number | null;
|
|
51
|
+
|
|
40
52
|
/**
|
|
41
53
|
* Timestamp of the last update of the object.
|
|
42
54
|
*/
|