@mastra/lance 0.1.1-alpha.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.
@@ -0,0 +1,1493 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
+ import { LanceVectorStore } from './index';
3
+
4
+ describe('Lance vector store tests', () => {
5
+ let vectorDB: LanceVectorStore;
6
+ const connectionString = process.env.DB_URL || 'lancedb-vector';
7
+
8
+ beforeAll(async () => {
9
+ // Giving directory path to connect to in memory db
10
+ // Give remote db url to connect to remote db such as s3 or lancedb cloud
11
+ vectorDB = await LanceVectorStore.create(connectionString);
12
+ });
13
+
14
+ afterAll(async () => {
15
+ try {
16
+ await vectorDB.deleteAllTables();
17
+ console.log('All tables have been deleted');
18
+ } catch (error) {
19
+ console.warn('Failed to delete tables during cleanup:', error);
20
+ } finally {
21
+ vectorDB.close();
22
+ }
23
+ });
24
+
25
+ describe('Index operations', () => {
26
+ const testTableName = 'test-table' + Date.now();
27
+ const indexOnColumn = 'vector';
28
+
29
+ beforeAll(async () => {
30
+ const generateTableData = (numRows: number) => {
31
+ return Array.from({ length: numRows }, (_, i) => ({
32
+ id: String(i + 1),
33
+ vector: Array.from({ length: 3 }, () => Math.random()),
34
+ }));
35
+ };
36
+
37
+ // lancedb requires to create more than 256 rows for index creation
38
+ // otherwise it will throw an error
39
+ await vectorDB.createTable(testTableName, generateTableData(300));
40
+ });
41
+
42
+ describe('create index', () => {
43
+ it('should create an index with specified dimensions', async () => {
44
+ await vectorDB.createIndex({
45
+ indexConfig: {
46
+ type: 'ivfflat',
47
+ numPartitions: 1,
48
+ numSubVectors: 1,
49
+ },
50
+ indexName: indexOnColumn,
51
+ dimension: 2,
52
+ tableName: testTableName,
53
+ });
54
+
55
+ const stats = await vectorDB.describeIndex({ indexName: indexOnColumn + '_idx' });
56
+
57
+ expect(stats?.dimension).toBe(3);
58
+ expect(stats?.count).toBe(300);
59
+ });
60
+
61
+ it('should create an index for hnsw', async () => {
62
+ await vectorDB.createIndex({
63
+ indexConfig: {
64
+ type: 'hnsw',
65
+ hnsw: {
66
+ m: 16,
67
+ efConstruction: 100,
68
+ },
69
+ },
70
+ indexName: indexOnColumn,
71
+ metric: 'euclidean',
72
+ dimension: 2,
73
+ tableName: testTableName,
74
+ });
75
+
76
+ const stats = await vectorDB.describeIndex({ indexName: indexOnColumn + '_idx' });
77
+
78
+ expect(stats?.metric).toBe('l2');
79
+ });
80
+ });
81
+
82
+ describe('list indexes', () => {
83
+ const listIndexTestTable = 'list-index-test-table' + Date.now();
84
+ const indexColumnName = 'vector';
85
+
86
+ afterAll(async () => {
87
+ try {
88
+ await vectorDB.deleteIndex({ indexName: indexColumnName + '_idx' });
89
+ } catch (error) {
90
+ console.warn('Failed to delete index during cleanup:', error);
91
+ }
92
+ });
93
+
94
+ it('should list available indexes', async () => {
95
+ const generateTableData = (numRows: number) => {
96
+ return Array.from({ length: numRows }, (_, i) => ({
97
+ id: String(i + 1),
98
+ vector: Array.from({ length: 3 }, () => Math.random()),
99
+ }));
100
+ };
101
+
102
+ await vectorDB.createTable(listIndexTestTable, generateTableData(300));
103
+
104
+ await vectorDB.createIndex({
105
+ indexConfig: {
106
+ type: 'ivfflat',
107
+ numPartitions: 1,
108
+ numSubVectors: 1,
109
+ },
110
+ indexName: indexColumnName,
111
+ dimension: 3,
112
+ tableName: listIndexTestTable,
113
+ });
114
+
115
+ const indexes = await vectorDB.listIndexes();
116
+
117
+ expect(indexes).toContain(indexColumnName + '_idx');
118
+ });
119
+ });
120
+
121
+ describe('describe index', () => {
122
+ const describeIndexTestTable = 'describe-index-test-table' + Date.now();
123
+ const indexColumnName = 'vector';
124
+
125
+ afterAll(async () => {
126
+ try {
127
+ await vectorDB.deleteIndex({ indexName: indexColumnName + '_idx' });
128
+ } catch (error) {
129
+ console.warn('Failed to delete index during cleanup:', error);
130
+ }
131
+ });
132
+ it('should describe an existing index', async () => {
133
+ const generateTableData = (numRows: number) => {
134
+ return Array.from({ length: numRows }, (_, i) => ({
135
+ id: String(i + 1),
136
+ vector: Array.from({ length: 3 }, () => Math.random()),
137
+ }));
138
+ };
139
+
140
+ await vectorDB.createTable(describeIndexTestTable, generateTableData(300));
141
+
142
+ await vectorDB.createIndex({
143
+ indexConfig: {
144
+ type: 'ivfflat',
145
+ numPartitions: 1,
146
+ numSubVectors: 1,
147
+ },
148
+ indexName: indexColumnName,
149
+ dimension: 3,
150
+ metric: 'euclidean',
151
+ tableName: describeIndexTestTable,
152
+ });
153
+
154
+ const stats = await vectorDB.describeIndex({ indexName: indexColumnName + '_idx' });
155
+
156
+ expect(stats).toBeDefined();
157
+ expect(stats?.dimension).toBe(3);
158
+ expect(stats?.count).toBe(300);
159
+ expect(stats?.metric).toBe('l2');
160
+ });
161
+
162
+ it('should throw error for non-existent index', async () => {
163
+ const nonExistentIndex = 'non-existent-index-' + Date.now();
164
+
165
+ await expect(vectorDB.describeIndex({ indexName: nonExistentIndex })).rejects.toThrow('not found');
166
+ });
167
+ });
168
+
169
+ describe('delete index', () => {
170
+ const deleteIndexTestTable = 'delete-index-test-table' + Date.now();
171
+ const indexColumnName = 'vector';
172
+
173
+ beforeAll(async () => {
174
+ vectorDB.deleteAllTables();
175
+ });
176
+
177
+ it('should delete an existing index', async () => {
178
+ const generateTableData = (numRows: number) => {
179
+ return Array.from({ length: numRows }, (_, i) => ({
180
+ id: String(i + 1),
181
+ vector: Array.from({ length: 3 }, () => Math.random()),
182
+ }));
183
+ };
184
+
185
+ await vectorDB.createTable(deleteIndexTestTable, generateTableData(300));
186
+
187
+ await vectorDB.createIndex({
188
+ indexConfig: {
189
+ type: 'ivfflat',
190
+ numPartitions: 1,
191
+ numSubVectors: 1,
192
+ },
193
+ indexName: indexColumnName,
194
+ dimension: 3,
195
+ tableName: deleteIndexTestTable,
196
+ });
197
+
198
+ const indexesBefore = await vectorDB.listIndexes();
199
+ expect(indexesBefore).toContain(indexColumnName + '_idx');
200
+
201
+ await vectorDB.deleteIndex({ indexName: indexColumnName + '_idx' });
202
+
203
+ const indexesAfter = await vectorDB.listIndexes();
204
+ expect(indexesAfter).not.toContain(indexColumnName + '_idx');
205
+ });
206
+
207
+ it('should throw error when deleting non-existent index', async () => {
208
+ const nonExistentIndex = 'non-existent-index-' + Date.now();
209
+
210
+ await expect(vectorDB.deleteIndex({ indexName: nonExistentIndex })).rejects.toThrow('not found');
211
+ });
212
+ });
213
+ });
214
+
215
+ describe('Create table operations', () => {
216
+ const testTableName = 'test-table' + Date.now();
217
+
218
+ beforeAll(async () => {
219
+ vectorDB.deleteAllTables();
220
+ });
221
+
222
+ it('should throw error when no data is provided', async () => {
223
+ await expect(vectorDB.createTable(testTableName, [])).rejects.toThrow(
224
+ 'Failed to create table: At least one record or a schema needs to be provided',
225
+ );
226
+ });
227
+
228
+ it('should create a new table', async () => {
229
+ await vectorDB.createTable(testTableName, [{ id: '1', vector: [0.1, 0.2, 0.3] }]);
230
+
231
+ const tables = await vectorDB.listTables();
232
+ expect(tables).toContain(testTableName);
233
+
234
+ const schema = await vectorDB.getTableSchema(testTableName);
235
+ expect(schema.fields.map(field => field.name)).toEqual(['id', 'vector']);
236
+ });
237
+
238
+ it('should throw error when creating existing table', async () => {
239
+ const tableName = 'test-table' + Date.now();
240
+ await vectorDB.createTable(tableName, [{ id: '1', vector: [0.1, 0.2, 0.3] }]);
241
+
242
+ await expect(vectorDB.createTable(tableName, [{ id: '1', vector: [0.1, 0.2, 0.3] }])).rejects.toThrow(
243
+ 'already exists',
244
+ );
245
+ });
246
+
247
+ it('should create a table with single level nested metadata object by flattening it', async () => {
248
+ const tableName = 'test-table' + Date.now();
249
+ await vectorDB.createTable(tableName, [{ id: '1', vector: [0.1, 0.2, 0.3], metadata_text: 'test' }]);
250
+
251
+ const schema = await vectorDB.getTableSchema(tableName);
252
+ expect(schema.fields.map((field: any) => field.name)).toEqual(['id', 'vector', 'metadata_text']);
253
+ });
254
+
255
+ it('should create a table with multi level nested metadata object by flattening it', async () => {
256
+ const tableName = 'test-table' + Date.now();
257
+ await vectorDB.createTable(tableName, [
258
+ { id: '1', vector: [0.1, 0.2, 0.3], metadata: { text: 'test', newText: 'test' } },
259
+ ]);
260
+
261
+ const schema = await vectorDB.getTableSchema(tableName);
262
+ expect(schema.fields.map((field: any) => field.name)).toEqual([
263
+ 'id',
264
+ 'vector',
265
+ 'metadata_text',
266
+ 'metadata_newText',
267
+ ]);
268
+ });
269
+ });
270
+
271
+ describe('Vector operations', () => {
272
+ describe('upsert operations', () => {
273
+ const testTableName = 'test-table-test' + Date.now();
274
+ const testTableIndexColumn = 'vector';
275
+
276
+ beforeAll(async () => {
277
+ const generateTableData = (numRows: number) => {
278
+ return Array.from({ length: numRows }, (_, i) => ({
279
+ id: String(i + 1),
280
+ vector: Array.from({ length: 3 }, () => Math.random()),
281
+ metadata: { text: 'test' },
282
+ }));
283
+ };
284
+
285
+ await vectorDB.createTable(testTableName, generateTableData(300));
286
+
287
+ await vectorDB.createIndex({
288
+ indexConfig: {
289
+ type: 'ivfflat',
290
+ numPartitions: 1,
291
+ numSubVectors: 1,
292
+ },
293
+ indexName: testTableIndexColumn,
294
+ dimension: 3,
295
+ tableName: testTableName,
296
+ });
297
+ });
298
+
299
+ afterAll(async () => {
300
+ vectorDB.deleteTable(testTableName);
301
+ });
302
+
303
+ it('should upsert vectors in an existing table', async () => {
304
+ const testVectors = [
305
+ [0.1, 0.2, 0.3],
306
+ [0.4, 0.5, 0.6],
307
+ [0.7, 0.8, 0.9],
308
+ ];
309
+
310
+ const testMetadata = [{ text: 'First vector' }, { text: 'Second vector' }, { text: 'Third vector' }];
311
+
312
+ const ids = await vectorDB.upsert({
313
+ indexName: testTableIndexColumn,
314
+ tableName: testTableName,
315
+ vectors: testVectors,
316
+ metadata: testMetadata,
317
+ });
318
+
319
+ expect(ids).toHaveLength(3);
320
+ expect(ids.every(id => typeof id === 'string')).toBe(true);
321
+
322
+ // Test upsert with provided IDs (update existing vectors)
323
+ const updatedVectors = [
324
+ [1.1, 1.2, 1.3],
325
+ [1.4, 1.5, 1.6],
326
+ [1.7, 1.8, 1.9],
327
+ ];
328
+
329
+ const updatedMetadata = [
330
+ { text: 'First vector updated' },
331
+ { text: 'Second vector updated' },
332
+ { text: 'Third vector updated' },
333
+ ];
334
+
335
+ const updatedIds = await vectorDB.upsert({
336
+ indexName: testTableIndexColumn,
337
+ tableName: testTableName,
338
+ vectors: updatedVectors,
339
+ metadata: updatedMetadata,
340
+ ids,
341
+ });
342
+
343
+ expect(updatedIds).toEqual(ids);
344
+ });
345
+
346
+ it('should throw error when upserting to non-existent table', async () => {
347
+ const nonExistentTable = 'non-existent-table-' + Date.now();
348
+
349
+ await expect(
350
+ vectorDB.upsert({
351
+ indexName: testTableIndexColumn,
352
+ tableName: nonExistentTable,
353
+ vectors: [[0.1, 0.2, 0.3]],
354
+ }),
355
+ ).rejects.toThrow('does not exist');
356
+ });
357
+ });
358
+
359
+ describe('query operations', () => {
360
+ const testTableName = 'test-table-query' + Date.now();
361
+ const testTableIndexColumn = 'vector';
362
+
363
+ beforeAll(async () => {
364
+ const generateTableData = (numRows: number) => {
365
+ return Array.from({ length: numRows }, (_, i) => ({
366
+ id: String(i + 1),
367
+ vector: Array.from({ length: 3 }, () => Math.random()),
368
+ metadata: { text: 'test' },
369
+ }));
370
+ };
371
+
372
+ await vectorDB.createTable(testTableName, generateTableData(300));
373
+
374
+ await vectorDB.createIndex({
375
+ indexConfig: {
376
+ type: 'ivfflat',
377
+ numPartitions: 1,
378
+ numSubVectors: 1,
379
+ },
380
+ indexName: testTableIndexColumn,
381
+ dimension: 3,
382
+ tableName: testTableName,
383
+ });
384
+ });
385
+
386
+ afterAll(async () => {
387
+ vectorDB.deleteTable(testTableName);
388
+ });
389
+
390
+ it('should query vectors from an existing table', async () => {
391
+ const testVectors = [
392
+ [0.1, 0.2, 0.3],
393
+ [0.4, 0.5, 0.6],
394
+ [0.7, 0.8, 0.9],
395
+ ];
396
+
397
+ const testMetadata = [{ text: 'First vector' }, { text: 'Second vector' }, { text: 'Third vector' }];
398
+
399
+ const ids = await vectorDB.upsert({
400
+ indexName: testTableIndexColumn,
401
+ tableName: testTableName,
402
+ vectors: testVectors,
403
+ metadata: testMetadata,
404
+ });
405
+
406
+ expect(ids).toHaveLength(3);
407
+ expect(ids.every(id => typeof id === 'string')).toBe(true);
408
+
409
+ const results = await vectorDB.query({
410
+ indexName: testTableIndexColumn,
411
+ tableName: testTableName,
412
+ queryVector: testVectors[0],
413
+ columns: ['id', 'metadata_text', 'vector'],
414
+ topK: 3,
415
+ includeVector: true,
416
+ });
417
+
418
+ expect(results).toHaveLength(3);
419
+ const sortedResultIds = results.map(res => res.id).sort();
420
+ const sortedIds = ids.sort();
421
+ expect(sortedResultIds).to.deep.equal(sortedIds);
422
+ expect(results[0].metadata?.text).toBe('First vector');
423
+ expect(results[1].metadata?.text).toBe('Second vector');
424
+ expect(results[2].metadata?.text).toBe('Third vector');
425
+ });
426
+
427
+ it('should throw error when querying from non-existent table', async () => {
428
+ const nonExistentTable = 'non-existent-table-' + Date.now();
429
+
430
+ await expect(
431
+ vectorDB.query({
432
+ indexName: testTableIndexColumn,
433
+ tableName: nonExistentTable,
434
+ columns: ['id', 'vector', 'metadata'],
435
+ queryVector: [0.1, 0.2, 0.3],
436
+ }),
437
+ ).rejects.toThrow(`Failed to query vectors: Table '${nonExistentTable}' was not found`);
438
+ });
439
+ });
440
+
441
+ describe('update operations', () => {
442
+ const testTableName = 'test-table-updates' + Date.now();
443
+ const testTableIndexColumn = 'vector';
444
+
445
+ beforeAll(async () => {
446
+ const generateTableData = (numRows: number) => {
447
+ return Array.from({ length: numRows }, (_, i) => ({
448
+ id: String(i + 1),
449
+ vector: Array.from({ length: 3 }, () => Math.random()),
450
+ metadata: { text: 'test' },
451
+ }));
452
+ };
453
+
454
+ await vectorDB.createTable(testTableName, generateTableData(300));
455
+
456
+ await vectorDB.createIndex({
457
+ indexConfig: {
458
+ type: 'ivfflat',
459
+ numPartitions: 1,
460
+ numSubVectors: 1,
461
+ },
462
+ indexName: testTableIndexColumn,
463
+ dimension: 3,
464
+ tableName: testTableName,
465
+ });
466
+ });
467
+
468
+ afterAll(async () => {
469
+ vectorDB.deleteTable(testTableName);
470
+ });
471
+
472
+ it('should update vector and metadata by id', async () => {
473
+ const ids = await vectorDB.upsert({
474
+ indexName: testTableIndexColumn,
475
+ tableName: testTableName,
476
+ vectors: [[0.1, 0.2, 0.3]],
477
+ metadata: [{ text: 'First vector' }],
478
+ });
479
+
480
+ expect(ids).toHaveLength(1);
481
+ expect(ids.every(id => typeof id === 'string')).toBe(true);
482
+
483
+ await vectorDB.updateVector({
484
+ indexName: testTableIndexColumn,
485
+ id: ids[0],
486
+ update: {
487
+ vector: [0.4, 0.5, 0.6],
488
+ metadata: { text: 'Updated vector' },
489
+ },
490
+ });
491
+
492
+ const res = await vectorDB.query({
493
+ indexName: testTableIndexColumn,
494
+ tableName: testTableName,
495
+ queryVector: [0.4, 0.5, 0.6],
496
+ columns: ['id', 'metadata_text', 'vector'],
497
+ topK: 3,
498
+ includeVector: true,
499
+ });
500
+
501
+ expect(res).toHaveLength(1);
502
+ expect(res[0].id).toBe(ids[0]);
503
+ expect(res[0].metadata?.text).to.equal('Updated vector');
504
+
505
+ // Fix decimal points in the response vector
506
+ const fixedVector = res[0].vector?.map(num => Number(num.toFixed(1)));
507
+ expect(fixedVector).toEqual([0.4, 0.5, 0.6]);
508
+ });
509
+
510
+ it('should only update existing vector', async () => {
511
+ const ids = await vectorDB.upsert({
512
+ indexName: testTableIndexColumn,
513
+ tableName: testTableName,
514
+ vectors: [[0.1, 0.2, 0.3]],
515
+ metadata: [{ text: 'Vector only update test' }],
516
+ });
517
+
518
+ expect(ids).toHaveLength(1);
519
+ expect(ids.every(id => typeof id === 'string')).toBe(true);
520
+
521
+ await vectorDB.updateVector({
522
+ indexName: testTableIndexColumn,
523
+ id: ids[0],
524
+ update: {
525
+ vector: [0.4, 0.5, 0.6],
526
+ },
527
+ });
528
+
529
+ const res = await vectorDB.query({
530
+ indexName: testTableIndexColumn,
531
+ tableName: testTableName,
532
+ queryVector: [0.4, 0.5, 0.6],
533
+ columns: ['id', 'metadata_text', 'vector'],
534
+ topK: 3,
535
+ includeVector: true,
536
+ });
537
+
538
+ expect(res).toHaveLength(1);
539
+ expect(res[0].id).toBe(ids[0]);
540
+ expect(res[0].metadata?.text).to.equal('Vector only update test');
541
+
542
+ // Fix decimal points in the response vector
543
+ const fixedVector = res[0].vector?.map(num => Number(num.toFixed(1)));
544
+ expect(fixedVector).toEqual([0.4, 0.5, 0.6]);
545
+ });
546
+
547
+ it('should only update existing vector metadata', async () => {
548
+ const ids = await vectorDB.upsert({
549
+ indexName: testTableIndexColumn,
550
+ tableName: testTableName,
551
+ vectors: [[0.1, 0.2, 0.3]],
552
+ metadata: [{ text: 'Metadata only update test' }],
553
+ });
554
+
555
+ expect(ids).toHaveLength(1);
556
+ expect(ids.every(id => typeof id === 'string')).toBe(true);
557
+
558
+ await vectorDB.updateVector({
559
+ indexName: testTableIndexColumn,
560
+ id: ids[0],
561
+ update: {
562
+ metadata: { text: 'Updated metadata' },
563
+ },
564
+ });
565
+
566
+ const res = await vectorDB.query({
567
+ indexName: testTableIndexColumn,
568
+ tableName: testTableName,
569
+ queryVector: [0.1, 0.2, 0.3],
570
+ columns: ['id', 'metadata_text', 'vector'],
571
+ topK: 3,
572
+ includeVector: true,
573
+ });
574
+
575
+ expect(res).toHaveLength(1);
576
+ expect(res[0].id).toBe(ids[0]);
577
+ expect(res[0].metadata?.text).to.equal('Updated metadata');
578
+
579
+ // Fix decimal points in the response vector
580
+ const fixedVector = res[0].vector?.map(num => Number(num.toFixed(1)));
581
+ expect(fixedVector).toEqual([0.1, 0.2, 0.3]);
582
+ });
583
+ });
584
+
585
+ describe('delete operations', () => {
586
+ const testTableName = 'test-table-delete' + Date.now();
587
+ const testTableIndexColumn = 'vector';
588
+
589
+ beforeAll(async () => {
590
+ vectorDB.deleteAllTables();
591
+
592
+ const generateTableData = (numRows: number) => {
593
+ return Array.from({ length: numRows }, (_, i) => ({
594
+ id: String(i + 1),
595
+ vector: Array.from({ length: 3 }, () => Math.random()),
596
+ metadata: { text: 'test' },
597
+ }));
598
+ };
599
+
600
+ await vectorDB.createTable(testTableName, generateTableData(300));
601
+
602
+ await vectorDB.createIndex({
603
+ indexConfig: {
604
+ type: 'ivfflat',
605
+ numPartitions: 1,
606
+ numSubVectors: 1,
607
+ },
608
+ indexName: testTableIndexColumn,
609
+ dimension: 3,
610
+ tableName: testTableName,
611
+ });
612
+ });
613
+
614
+ afterAll(async () => {
615
+ vectorDB.deleteTable(testTableName);
616
+ });
617
+
618
+ it('should delete vector and metadata by id', async () => {
619
+ const testVectors = [
620
+ [0.1, 0.2, 0.3],
621
+ [0.4, 0.5, 0.6],
622
+ ];
623
+
624
+ const ids = await vectorDB.upsert({
625
+ indexName: testTableIndexColumn,
626
+ tableName: testTableName,
627
+ vectors: testVectors,
628
+ metadata: [{ text: 'First vector' }, { text: 'Second vector' }],
629
+ });
630
+
631
+ expect(ids).toHaveLength(2);
632
+
633
+ let results = await vectorDB.query({
634
+ indexName: testTableIndexColumn,
635
+ tableName: testTableName,
636
+ queryVector: [0.1, 0.2, 0.3],
637
+ columns: ['id', 'metadata_text'],
638
+ topK: 3,
639
+ includeVector: true,
640
+ });
641
+
642
+ expect(results).toHaveLength(2);
643
+
644
+ await vectorDB.deleteVector({
645
+ indexName: testTableIndexColumn,
646
+ id: ids[0],
647
+ });
648
+
649
+ results = await vectorDB.query({
650
+ indexName: testTableIndexColumn,
651
+ tableName: testTableName,
652
+ queryVector: [0.1, 0.2, 0.3],
653
+ columns: ['id', 'metadata_text'],
654
+ topK: 3,
655
+ includeVector: true,
656
+ });
657
+
658
+ expect(results).toHaveLength(1);
659
+ expect(results[0].id).toBe(ids[1]);
660
+ });
661
+ });
662
+ });
663
+
664
+ describe('Basic query operations', () => {
665
+ const testTableName = 'test-table-basic' + Date.now();
666
+ const testTableIndexColumn = 'vector';
667
+
668
+ beforeAll(async () => {
669
+ const generateTableData = (numRows: number) => {
670
+ return Array.from({ length: numRows }, (_, i) => ({
671
+ id: String(i + 1),
672
+ vector: Array.from({ length: 3 }, () => Math.random()),
673
+ metadata_text: 'test',
674
+ metadata_newText: 'test',
675
+ }));
676
+ };
677
+
678
+ await vectorDB.createTable(testTableName, generateTableData(300));
679
+
680
+ await vectorDB.createIndex({
681
+ indexConfig: {
682
+ type: 'ivfflat',
683
+ numPartitions: 1,
684
+ numSubVectors: 1,
685
+ },
686
+ indexName: testTableIndexColumn,
687
+ dimension: 3,
688
+ tableName: testTableName,
689
+ });
690
+ });
691
+
692
+ afterAll(async () => {
693
+ vectorDB.deleteTable(testTableName);
694
+ });
695
+
696
+ it('should query vectors with metadata', async () => {
697
+ const testVectors = [[0.1, 0.2, 0.3]];
698
+ const ids = await vectorDB.upsert({
699
+ indexName: testTableIndexColumn,
700
+ tableName: testTableName,
701
+ vectors: testVectors,
702
+ metadata: [{ text: 'First vector', newText: 'hi' }],
703
+ });
704
+
705
+ expect(ids).toHaveLength(1);
706
+ expect(ids.every(id => typeof id === 'string')).toBe(true);
707
+
708
+ const res = await vectorDB.query({
709
+ indexName: testTableIndexColumn,
710
+ tableName: testTableName,
711
+ queryVector: testVectors[0],
712
+ columns: ['id', 'metadata_text', 'metadata_newText', 'vector'],
713
+ topK: 3,
714
+ includeVector: true,
715
+ });
716
+
717
+ expect(res).toHaveLength(1);
718
+ expect(res[0].id).toBe(ids[0]);
719
+ expect(res[0].metadata?.text).to.equal('First vector');
720
+ expect(res[0].metadata?.newText).to.equal('hi');
721
+ });
722
+
723
+ it('should query vectors with filter', async () => {
724
+ const testVectors = [[0.1, 0.2, 0.3]];
725
+ const ids = await vectorDB.upsert({
726
+ indexName: testTableIndexColumn,
727
+ tableName: testTableName,
728
+ vectors: testVectors,
729
+ metadata: [{ text: 'First vector', newText: 'hi' }],
730
+ });
731
+
732
+ expect(ids).toHaveLength(1);
733
+ expect(ids.every(id => typeof id === 'string')).toBe(true);
734
+
735
+ const res = await vectorDB.query({
736
+ indexName: testTableIndexColumn,
737
+ tableName: testTableName,
738
+ queryVector: testVectors[0],
739
+ columns: ['id', 'metadata_text', 'metadata_newText', 'vector'],
740
+ topK: 3,
741
+ includeVector: true,
742
+ filter: { text: 'First vector' },
743
+ });
744
+
745
+ expect(res).toHaveLength(1);
746
+ expect(res[0].id).toBe(ids[0]);
747
+ expect(res[0].metadata?.text).to.equal('First vector');
748
+ expect(res[0].metadata?.newText).to.equal('hi');
749
+ });
750
+
751
+ it('should query vectors if filter columns array is not provided', async () => {
752
+ const testVectors = [[0.1, 0.2, 0.3]];
753
+ const ids = await vectorDB.upsert({
754
+ indexName: testTableIndexColumn,
755
+ tableName: testTableName,
756
+ vectors: testVectors,
757
+ metadata: [{ text: 'First vector', newText: 'hi' }],
758
+ });
759
+
760
+ expect(ids).toHaveLength(1);
761
+ expect(ids.every(id => typeof id === 'string')).toBe(true);
762
+
763
+ const res = await vectorDB.query({
764
+ indexName: testTableIndexColumn,
765
+ tableName: testTableName,
766
+ queryVector: testVectors[0],
767
+ topK: 3,
768
+ includeVector: true,
769
+ filter: { text: 'First vector' },
770
+ });
771
+
772
+ expect(res).toHaveLength(1);
773
+ expect(res[0].id).toBe(ids[0]);
774
+ expect(res[0].metadata?.text).toBeUndefined();
775
+ expect(res[0].metadata?.newText).toBeUndefined();
776
+ });
777
+
778
+ it('should query vectors with all columns when the include all columns flag is true', async () => {
779
+ const testVectors = [[0.1, 0.2, 0.3]];
780
+ const ids = await vectorDB.upsert({
781
+ indexName: testTableIndexColumn,
782
+ tableName: testTableName,
783
+ vectors: testVectors,
784
+ metadata: [{ text: 'First vector', newText: 'hi' }],
785
+ });
786
+
787
+ expect(ids).toHaveLength(1);
788
+ expect(ids.every(id => typeof id === 'string')).toBe(true);
789
+
790
+ const res = await vectorDB.query({
791
+ indexName: testTableIndexColumn,
792
+ tableName: testTableName,
793
+ queryVector: testVectors[0],
794
+ topK: 3,
795
+ includeVector: true,
796
+ filter: { text: 'First vector' },
797
+ includeAllColumns: true,
798
+ });
799
+
800
+ const tableSchema = await vectorDB.getTableSchema(testTableName);
801
+ const expectedColumns = tableSchema.fields.map((column: any) => column.name);
802
+ expect(['id', 'vector', 'metadata_text', 'metadata_newText']).toEqual(expectedColumns);
803
+
804
+ expect(res).toHaveLength(1);
805
+ expect(res[0].id).toBe(ids[0]);
806
+ expect(res[0].metadata?.text).toBe('First vector');
807
+ expect(res[0].metadata?.newText).toBe('hi');
808
+ });
809
+ });
810
+
811
+ describe('Advanced query operations', () => {
812
+ const testTableName = 'test-table-advanced' + Date.now();
813
+ const testTableIndexColumn = 'vector';
814
+
815
+ beforeAll(async () => {
816
+ const generateTableData = (numRows: number) => {
817
+ return Array.from({ length: numRows }, (_, i) => ({
818
+ id: String(i + 1),
819
+ vector: Array.from({ length: 3 }, () => Math.random()),
820
+ metadata: { name: 'test', details: { text: 'test' } },
821
+ }));
822
+ };
823
+
824
+ await vectorDB.createTable(testTableName, generateTableData(300));
825
+
826
+ await vectorDB.createIndex({
827
+ indexConfig: {
828
+ type: 'ivfflat',
829
+ numPartitions: 1,
830
+ numSubVectors: 1,
831
+ },
832
+ indexName: testTableIndexColumn,
833
+ dimension: 3,
834
+ tableName: testTableName,
835
+ });
836
+ });
837
+
838
+ afterAll(async () => {
839
+ vectorDB.deleteTable(testTableName);
840
+ });
841
+
842
+ describe('Simple queries', () => {
843
+ it('should query vectors with nested metadata filter', async () => {
844
+ const testVectors = [[0.1, 0.2, 0.3]];
845
+ const ids = await vectorDB.upsert({
846
+ indexName: testTableIndexColumn,
847
+ tableName: testTableName,
848
+ vectors: testVectors,
849
+ metadata: [{ name: 'test2', details: { text: 'test2' } }],
850
+ });
851
+
852
+ expect(ids).toHaveLength(1);
853
+ expect(ids.every(id => typeof id === 'string')).toBe(true);
854
+
855
+ const res = await vectorDB.query({
856
+ indexName: testTableIndexColumn,
857
+ tableName: testTableName,
858
+ queryVector: testVectors[0],
859
+ columns: ['id', 'metadata_name', 'metadata_details_text', 'vector'],
860
+ topK: 3,
861
+ includeVector: true,
862
+ filter: { name: 'test2' },
863
+ });
864
+
865
+ expect(res).toHaveLength(1);
866
+ expect(res[0].id).toBe(ids[0]);
867
+ expect(res[0].metadata?.name).to.equal('test2');
868
+ expect(res[0].metadata?.details?.text).to.equal('test2');
869
+ });
870
+
871
+ it('should not throw error when filter is not provided', async () => {
872
+ const res = await vectorDB.query({
873
+ indexName: testTableIndexColumn,
874
+ tableName: testTableName,
875
+ queryVector: [0.1, 0.2, 0.3],
876
+ topK: 3,
877
+ includeVector: true,
878
+ includeAllColumns: true,
879
+ });
880
+
881
+ expect(res).toHaveLength(1);
882
+ });
883
+ });
884
+
885
+ describe('Query with $ne operator', () => {
886
+ const testTableName = 'test-ne-operator';
887
+
888
+ beforeAll(async () => {
889
+ const generateTableData = (numRows: number) => {
890
+ return Array.from({ length: numRows }, (_, i) => ({
891
+ id: String(i + 1),
892
+ vector: Array.from({ length: 3 }, () => Math.random()),
893
+ metadata: {
894
+ category: i % 3 === 0 ? 'A' : i % 3 === 1 ? 'B' : 'C',
895
+ count: i + 1,
896
+ active: i % 2 === 0,
897
+ },
898
+ }));
899
+ };
900
+
901
+ await vectorDB.createTable(testTableName, generateTableData(300));
902
+
903
+ await vectorDB.createIndex({
904
+ indexConfig: {
905
+ type: 'ivfflat',
906
+ numPartitions: 1,
907
+ numSubVectors: 1,
908
+ },
909
+ indexName: testTableIndexColumn,
910
+ dimension: 3,
911
+ tableName: testTableName,
912
+ });
913
+ });
914
+
915
+ afterAll(async () => {
916
+ vectorDB.deleteTable(testTableName);
917
+ });
918
+
919
+ it('should filter with negated equality (equivalent to $not)', async () => {
920
+ const res = await vectorDB.query({
921
+ indexName: testTableIndexColumn,
922
+ tableName: testTableName,
923
+ queryVector: [0.5, 0.5, 0.5],
924
+ topK: 30,
925
+ includeAllColumns: true,
926
+ filter: {
927
+ category: { $ne: 'A' },
928
+ },
929
+ });
930
+
931
+ // Should only include categories B and C
932
+ expect(res.length).toBeGreaterThan(0);
933
+ res.forEach(item => {
934
+ expect(item.metadata?.category).not.toBe('A');
935
+ });
936
+ });
937
+
938
+ it('should filter with negated comparison (equivalent to $not $gt)', async () => {
939
+ const res = await vectorDB.query({
940
+ indexName: testTableIndexColumn,
941
+ tableName: testTableName,
942
+ queryVector: [0.5, 0.5, 0.5],
943
+ topK: 30,
944
+ includeAllColumns: true,
945
+ filter: {
946
+ count: { $lte: 15 },
947
+ },
948
+ });
949
+
950
+ // Should only include counts <= 15
951
+ expect(res.length).toBeGreaterThan(0);
952
+ res.forEach(item => {
953
+ expect(Number(item.metadata?.count)).toBeLessThanOrEqual(15);
954
+ });
955
+ });
956
+
957
+ it('should combine negated filters with other operators in complex queries', async () => {
958
+ const res = await vectorDB.query({
959
+ indexName: testTableIndexColumn,
960
+ tableName: testTableName,
961
+ queryVector: [0.5, 0.5, 0.5],
962
+ topK: 30,
963
+ includeAllColumns: true,
964
+ filter: {
965
+ $and: [{ category: { $ne: 'A' } }, { active: true }],
966
+ },
967
+ });
968
+
969
+ // Should only include active items with categories B and C
970
+ expect(res.length).toBeGreaterThan(0);
971
+ res.forEach(item => {
972
+ expect(item.metadata?.category).not.toBe('A');
973
+ expect(item.metadata?.active).toBe(true);
974
+ });
975
+ });
976
+ });
977
+
978
+ describe('Query with $or operator', () => {
979
+ const testTableName = 'test-or-operator';
980
+ beforeAll(async () => {
981
+ const generateTableData = (numRows: number) => {
982
+ return Array.from({ length: numRows }, (_, i) => ({
983
+ id: String(i + 1),
984
+ vector: Array.from({ length: 3 }, () => Math.random()),
985
+ metadata: { name: 'category_test', tag: 'important' },
986
+ }));
987
+ };
988
+
989
+ await vectorDB.createTable(testTableName, generateTableData(300));
990
+
991
+ await vectorDB.createIndex({
992
+ indexConfig: {
993
+ type: 'ivfflat',
994
+ numPartitions: 1,
995
+ numSubVectors: 1,
996
+ },
997
+ indexName: testTableIndexColumn,
998
+ dimension: 3,
999
+ tableName: testTableName,
1000
+ });
1001
+ });
1002
+
1003
+ afterAll(async () => {
1004
+ vectorDB.deleteTable(testTableName);
1005
+ });
1006
+
1007
+ it('should query with logical $or operator for metadata filtering', async () => {
1008
+ const testVectors = [
1009
+ [0.4, 0.5, 0.6],
1010
+ [0.7, 0.8, 0.9],
1011
+ ];
1012
+
1013
+ const ids = await vectorDB.upsert({
1014
+ indexName: testTableIndexColumn,
1015
+ tableName: testTableName,
1016
+ vectors: testVectors,
1017
+ metadata: [
1018
+ { name: 'category_a', tag: 'important' },
1019
+ { name: 'category_b', tag: 'urgent' },
1020
+ ],
1021
+ });
1022
+
1023
+ expect(ids).toHaveLength(2);
1024
+
1025
+ const res = await vectorDB.query({
1026
+ indexName: testTableIndexColumn,
1027
+ tableName: testTableName,
1028
+ queryVector: [0.5, 0.6, 0.7],
1029
+ topK: 5,
1030
+ includeVector: true,
1031
+ includeAllColumns: true,
1032
+ filter: {
1033
+ $or: [{ name: 'category_a' }, { name: 'category_b' }],
1034
+ },
1035
+ });
1036
+
1037
+ expect(res.length).toBeGreaterThanOrEqual(2);
1038
+ const foundIds = res.map(item => item.id);
1039
+ expect(foundIds).toContain(ids[0]);
1040
+ expect(foundIds).toContain(ids[1]);
1041
+ });
1042
+ });
1043
+
1044
+ describe('Query with $and operator', () => {
1045
+ const testTableName = 'test-and-operator';
1046
+ beforeAll(async () => {
1047
+ const generateTableData = (numRows: number) => {
1048
+ return Array.from({ length: numRows }, (_, i) => ({
1049
+ id: String(i + 1),
1050
+ vector: Array.from({ length: 3 }, () => Math.random()),
1051
+ metadata: { score: 10, dateAdded: Date.now() },
1052
+ }));
1053
+ };
1054
+
1055
+ await vectorDB.createTable(testTableName, generateTableData(300));
1056
+
1057
+ await vectorDB.createIndex({
1058
+ indexConfig: {
1059
+ type: 'ivfflat',
1060
+ numPartitions: 1,
1061
+ numSubVectors: 1,
1062
+ },
1063
+ indexName: testTableIndexColumn,
1064
+ dimension: 3,
1065
+ tableName: testTableName,
1066
+ });
1067
+ });
1068
+
1069
+ afterAll(async () => {
1070
+ vectorDB.deleteTable(testTableName);
1071
+ });
1072
+
1073
+ it('should query with $and operator using comparison operators', async () => {
1074
+ const testVectors = [
1075
+ [0.1, 0.1, 0.1],
1076
+ [0.2, 0.2, 0.2],
1077
+ [0.3, 0.3, 0.3],
1078
+ ];
1079
+
1080
+ const ids = await vectorDB.upsert({
1081
+ indexName: testTableIndexColumn,
1082
+ tableName: testTableName,
1083
+ vectors: testVectors,
1084
+ metadata: [
1085
+ { score: 85, dateAdded: new Date('2023-01-15') },
1086
+ { score: 92, dateAdded: new Date('2023-02-20') },
1087
+ { score: 78, dateAdded: new Date('2023-03-10') },
1088
+ ],
1089
+ });
1090
+
1091
+ expect(ids).toHaveLength(3);
1092
+
1093
+ const res = await vectorDB.query({
1094
+ indexName: testTableIndexColumn,
1095
+ tableName: testTableName,
1096
+ queryVector: [0.2, 0.2, 0.2],
1097
+ topK: 10,
1098
+ includeAllColumns: true,
1099
+ includeVector: true,
1100
+ filter: {
1101
+ $and: [{ score: { $gte: 80 } }, { score: { $lte: 95 } }],
1102
+ },
1103
+ });
1104
+
1105
+ // should find the score between 80 and 95
1106
+ expect(res.length).toBeGreaterThanOrEqual(2);
1107
+
1108
+ const scoresFound = res.map(item => item.metadata?.score);
1109
+ expect(scoresFound).toContain(85);
1110
+ expect(scoresFound).toContain(92);
1111
+ expect(scoresFound).not.toContain(78);
1112
+ });
1113
+ });
1114
+
1115
+ describe('Query with $in operator', () => {
1116
+ const testTableName = 'test-in-operator';
1117
+ beforeAll(async () => {
1118
+ const generateTableData = (numRows: number) => {
1119
+ return Array.from({ length: numRows }, (_, i) => ({
1120
+ id: String(i + 1),
1121
+ vector: Array.from({ length: 3 }, () => Math.random()),
1122
+ metadata: { region: 'north', status: 'active' },
1123
+ }));
1124
+ };
1125
+
1126
+ await vectorDB.createTable(testTableName, generateTableData(300));
1127
+
1128
+ await vectorDB.createIndex({
1129
+ indexConfig: {
1130
+ type: 'ivfflat',
1131
+ numPartitions: 1,
1132
+ numSubVectors: 1,
1133
+ },
1134
+ indexName: testTableIndexColumn,
1135
+ dimension: 3,
1136
+ tableName: testTableName,
1137
+ });
1138
+ });
1139
+
1140
+ afterAll(async () => {
1141
+ vectorDB.deleteTable(testTableName);
1142
+ });
1143
+
1144
+ it('should query with array $in operator', async () => {
1145
+ const testVectors = [
1146
+ [0.4, 0.4, 0.4],
1147
+ [0.5, 0.5, 0.5],
1148
+ [0.6, 0.6, 0.6],
1149
+ ];
1150
+
1151
+ const ids = await vectorDB.upsert({
1152
+ indexName: testTableIndexColumn,
1153
+ tableName: testTableName,
1154
+ vectors: testVectors,
1155
+ metadata: [
1156
+ { region: 'north', status: 'active' },
1157
+ { region: 'south', status: 'pending' },
1158
+ { region: 'east', status: 'inactive' },
1159
+ ],
1160
+ });
1161
+
1162
+ expect(ids).toHaveLength(3);
1163
+
1164
+ const res = await vectorDB.query({
1165
+ indexName: testTableIndexColumn,
1166
+ tableName: testTableName,
1167
+ queryVector: [0.5, 0.5, 0.5],
1168
+ topK: 10,
1169
+ includeAllColumns: true,
1170
+ includeVector: true,
1171
+ filter: {
1172
+ region: { $in: ['north', 'south'] },
1173
+ },
1174
+ });
1175
+
1176
+ expect(res.length).toBeGreaterThanOrEqual(2);
1177
+
1178
+ const regionsFound = res.map(item => item.metadata?.region);
1179
+ expect(regionsFound).toContain('north');
1180
+ expect(regionsFound).toContain('south');
1181
+ expect(regionsFound).not.toContain('east');
1182
+
1183
+ const statusFound = res.map(item => item.metadata?.status);
1184
+ expect(statusFound).toContain('active');
1185
+ expect(statusFound).toContain('pending');
1186
+ expect(statusFound).not.toContain('inactive');
1187
+ });
1188
+ });
1189
+
1190
+ describe('Query with nested comparison', () => {
1191
+ const testTableName = 'test-nested-table';
1192
+
1193
+ beforeAll(async () => {
1194
+ const generateTableData = (numRows: number) => {
1195
+ return Array.from({ length: numRows }, (_, i) => ({
1196
+ id: String(i + 1),
1197
+ vector: Array.from({ length: 3 }, () => Math.random()),
1198
+ metadata: {
1199
+ profile: {
1200
+ username: 'john_doe',
1201
+ email: 'john@example.com',
1202
+ metrics: { visits: 42, likes: 156 },
1203
+ },
1204
+ },
1205
+ }));
1206
+ };
1207
+
1208
+ await vectorDB.createTable(testTableName, generateTableData(300));
1209
+
1210
+ await vectorDB.createIndex({
1211
+ indexConfig: {
1212
+ type: 'ivfflat',
1213
+ numPartitions: 1,
1214
+ numSubVectors: 1,
1215
+ },
1216
+ indexName: testTableIndexColumn,
1217
+ dimension: 3,
1218
+ tableName: testTableName,
1219
+ });
1220
+ });
1221
+
1222
+ afterAll(async () => {
1223
+ vectorDB.deleteTable(testTableName);
1224
+ });
1225
+
1226
+ it('should query with nested comparison and pattern matching', async () => {
1227
+ const testTableName = 'test-nested-table';
1228
+
1229
+ const testVectors = [
1230
+ [0.7, 0.7, 0.7],
1231
+ [0.8, 0.8, 0.8],
1232
+ ];
1233
+
1234
+ const ids = await vectorDB.upsert({
1235
+ indexName: testTableIndexColumn,
1236
+ tableName: testTableName,
1237
+ vectors: testVectors,
1238
+ metadata: [
1239
+ {
1240
+ profile: {
1241
+ username: 'john_doe',
1242
+ email: 'john@example.com',
1243
+ metrics: { visits: 42, likes: 156 },
1244
+ },
1245
+ },
1246
+ {
1247
+ profile: {
1248
+ username: 'jane_smith',
1249
+ email: 'jane@example.com',
1250
+ metrics: { visits: 64, likes: 89 },
1251
+ },
1252
+ },
1253
+ ],
1254
+ });
1255
+
1256
+ expect(ids).toHaveLength(2);
1257
+
1258
+ const res = await vectorDB.query({
1259
+ indexName: testTableIndexColumn,
1260
+ tableName: testTableName,
1261
+ queryVector: [0.75, 0.75, 0.75],
1262
+ topK: 10,
1263
+ includeAllColumns: true,
1264
+ includeVector: true,
1265
+ filter: {
1266
+ $and: [{ 'profile.metrics.visits': { $gt: 40 } }, { 'profile.email': { $like: '%example.com' } }],
1267
+ },
1268
+ });
1269
+
1270
+ expect(res.length).toBeGreaterThanOrEqual(2);
1271
+
1272
+ const usernamesFound = res.map(item => item.metadata?.profile?.username);
1273
+ expect(usernamesFound).toContain('john_doe');
1274
+ expect(usernamesFound).toContain('jane_smith');
1275
+ });
1276
+ });
1277
+
1278
+ describe('Query with regex matching', () => {
1279
+ const testTableName = 'test-regex-table';
1280
+
1281
+ beforeAll(async () => {
1282
+ const generateTableData = (numRows: number) => {
1283
+ return Array.from({ length: numRows }, (_, i) => ({
1284
+ id: String(i + 1),
1285
+ vector: Array.from({ length: 3 }, () => Math.random()),
1286
+ metadata: { code: 'US-CA-123', description: 'California office' },
1287
+ }));
1288
+ };
1289
+
1290
+ await vectorDB.createTable(testTableName, generateTableData(300));
1291
+
1292
+ await vectorDB.createIndex({
1293
+ indexConfig: {
1294
+ type: 'ivfflat',
1295
+ numPartitions: 1,
1296
+ numSubVectors: 1,
1297
+ },
1298
+ indexName: testTableIndexColumn,
1299
+ dimension: 3,
1300
+ tableName: testTableName,
1301
+ });
1302
+ });
1303
+
1304
+ afterAll(async () => {
1305
+ vectorDB.deleteTable(testTableName);
1306
+ });
1307
+
1308
+ it('should query with regex pattern matching', async () => {
1309
+ const testVectors = [
1310
+ [0.9, 0.9, 0.9],
1311
+ [1.0, 1.0, 1.0],
1312
+ [1.1, 1.1, 1.1],
1313
+ ];
1314
+
1315
+ const ids = await vectorDB.upsert({
1316
+ indexName: testTableIndexColumn,
1317
+ tableName: testTableName,
1318
+ vectors: testVectors,
1319
+ metadata: [
1320
+ { code: 'US-CA-123', description: 'California office' },
1321
+ { code: 'UK-LN-456', description: 'London office' },
1322
+ { code: 'US-NY-789', description: 'New York office' },
1323
+ ],
1324
+ });
1325
+
1326
+ expect(ids).toHaveLength(3);
1327
+
1328
+ const res = await vectorDB.query({
1329
+ indexName: testTableIndexColumn,
1330
+ tableName: testTableName,
1331
+ queryVector: [1.0, 1.0, 1.0],
1332
+ topK: 10,
1333
+ includeAllColumns: true,
1334
+ includeVector: true,
1335
+ filter: {
1336
+ code: { $regex: '^US-' },
1337
+ },
1338
+ });
1339
+
1340
+ expect(res.length).toBeGreaterThanOrEqual(2);
1341
+
1342
+ const codesFound = res.map(item => item.metadata?.code);
1343
+ expect(codesFound).toContain('US-CA-123');
1344
+ expect(codesFound).toContain('US-NY-789');
1345
+ expect(codesFound).not.toContain('UK-LN-456');
1346
+ });
1347
+ });
1348
+
1349
+ describe('Queries to check null fields', () => {
1350
+ const testTableName = 'test-null-fields-table';
1351
+
1352
+ beforeAll(async () => {
1353
+ // Create data with some null fields for testing
1354
+ const data = [
1355
+ {
1356
+ id: '1',
1357
+ vector: [0.1, 0.2, 0.3],
1358
+ metadata: {
1359
+ title: 'Document with all fields',
1360
+ description: 'This document has all fields populated',
1361
+ status: 'active',
1362
+ tags: ['important', 'reviewed'],
1363
+ },
1364
+ },
1365
+ {
1366
+ id: '2',
1367
+ vector: [0.4, 0.5, 0.6],
1368
+ metadata: {
1369
+ title: 'Document with null description',
1370
+ description: null,
1371
+ status: 'active',
1372
+ tags: ['draft'],
1373
+ },
1374
+ },
1375
+ {
1376
+ id: '3',
1377
+ vector: [0.7, 0.8, 0.9],
1378
+ metadata: {
1379
+ title: 'Document with null status',
1380
+ description: 'This document has a null status field',
1381
+ status: null,
1382
+ tags: ['important'],
1383
+ },
1384
+ },
1385
+ {
1386
+ id: '4',
1387
+ vector: [0.2, 0.3, 0.4],
1388
+ metadata: {
1389
+ title: 'Document with empty tags',
1390
+ description: 'This document has empty tags array',
1391
+ status: 'inactive',
1392
+ tags: [],
1393
+ },
1394
+ },
1395
+ {
1396
+ id: '5',
1397
+ vector: [0.5, 0.6, 0.7],
1398
+ metadata: {
1399
+ title: 'Document with null tags',
1400
+ description: 'This document has null tags',
1401
+ status: 'pending',
1402
+ tags: null,
1403
+ },
1404
+ },
1405
+ ];
1406
+
1407
+ await vectorDB.createTable(testTableName, data);
1408
+ });
1409
+
1410
+ afterAll(async () => {
1411
+ vectorDB.deleteTable(testTableName);
1412
+ });
1413
+
1414
+ it('should find documents with null fields using direct null comparison', async () => {
1415
+ const res = await vectorDB.query({
1416
+ indexName: testTableIndexColumn,
1417
+ tableName: testTableName,
1418
+ queryVector: [0.5, 0.5, 0.5],
1419
+ topK: 10,
1420
+ includeAllColumns: true,
1421
+ filter: {
1422
+ description: null,
1423
+ },
1424
+ });
1425
+
1426
+ // Should find documents where description is null
1427
+ expect(res.length).toBeGreaterThan(0);
1428
+ res.forEach(item => {
1429
+ expect(item.metadata?.description).toBeNull();
1430
+ });
1431
+ });
1432
+
1433
+ it('should find documents with non-null fields using $ne null comparison', async () => {
1434
+ const res = await vectorDB.query({
1435
+ indexName: testTableIndexColumn,
1436
+ tableName: testTableName,
1437
+ queryVector: [0.5, 0.5, 0.5],
1438
+ topK: 10,
1439
+ includeAllColumns: true,
1440
+ filter: {
1441
+ status: { $ne: null },
1442
+ },
1443
+ });
1444
+
1445
+ // Should find documents where status is not null
1446
+ expect(res.length).toBeGreaterThan(0);
1447
+ res.forEach(item => {
1448
+ expect(item.metadata?.status).not.toBeNull();
1449
+ });
1450
+ });
1451
+
1452
+ it('should find documents with null fields in complex queries', async () => {
1453
+ const res = await vectorDB.query({
1454
+ indexName: testTableIndexColumn,
1455
+ tableName: testTableName,
1456
+ queryVector: [0.5, 0.5, 0.5],
1457
+ topK: 10,
1458
+ includeAllColumns: true,
1459
+ filter: {
1460
+ $and: [{ description: { $ne: null } }, { status: null }],
1461
+ },
1462
+ });
1463
+
1464
+ // Should find documents where description is not null and status is null
1465
+ expect(res.length).toBeGreaterThan(0);
1466
+ res.forEach(item => {
1467
+ expect(item.metadata?.description).not.toBeNull();
1468
+ expect(item.metadata?.status).toBeNull();
1469
+ });
1470
+ });
1471
+
1472
+ it('should combine null checks with other operators', async () => {
1473
+ const res = await vectorDB.query({
1474
+ indexName: testTableIndexColumn,
1475
+ tableName: testTableName,
1476
+ queryVector: [0.5, 0.5, 0.5],
1477
+ topK: 10,
1478
+ includeAllColumns: true,
1479
+ filter: {
1480
+ $or: [{ status: 'active' }, { tags: null }],
1481
+ },
1482
+ });
1483
+
1484
+ // Should find documents where either status is active or tags is null
1485
+ expect(res.length).toBeGreaterThan(0);
1486
+ res.forEach(item => {
1487
+ const isMatch = item.metadata?.status === 'active' || item.metadata?.tags === null;
1488
+ expect(isMatch).toBe(true);
1489
+ });
1490
+ });
1491
+ });
1492
+ });
1493
+ });