@mastra/opensearch 0.11.7 → 0.11.8-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,661 +0,0 @@
1
- import { beforeEach, describe, expect, it } from 'vitest';
2
-
3
- import type { OpenSearchVectorFilter } from './filter';
4
- import { OpenSearchFilterTranslator } from './filter';
5
-
6
- describe('OpenSearchFilterTranslator', () => {
7
- let translator: OpenSearchFilterTranslator;
8
-
9
- beforeEach(() => {
10
- translator = new OpenSearchFilterTranslator();
11
- });
12
-
13
- // Basic Filter Operations
14
- describe('basic operations', () => {
15
- it('handles empty filters', () => {
16
- expect(translator.translate({})).toEqual(undefined);
17
- expect(translator.translate(null)).toEqual(undefined);
18
- expect(translator.translate(undefined)).toEqual(undefined);
19
- });
20
-
21
- it('translates simple field equality to term query', () => {
22
- const filter: OpenSearchVectorFilter = { field: 'value' };
23
- expect(translator.translate(filter)).toEqual({
24
- term: { 'metadata.field.keyword': 'value' },
25
- });
26
- });
27
-
28
- it('translates multiple top-level fields to bool must', () => {
29
- const filter: OpenSearchVectorFilter = { field1: 'value1', field2: 'value2' };
30
- expect(translator.translate(filter)).toEqual({
31
- bool: {
32
- must: [{ term: { 'metadata.field1.keyword': 'value1' } }, { term: { 'metadata.field2.keyword': 'value2' } }],
33
- },
34
- });
35
- });
36
-
37
- it('handles nested objects', () => {
38
- const filter = {
39
- user: {
40
- profile: {
41
- age: 25,
42
- name: 'John',
43
- },
44
- },
45
- };
46
- expect(translator.translate(filter)).toEqual({
47
- bool: {
48
- must: [
49
- {
50
- bool: {
51
- must: [
52
- { term: { 'metadata.user.profile.age': 25 } },
53
- { term: { 'metadata.user.profile.name.keyword': 'John' } },
54
- ],
55
- },
56
- },
57
- ],
58
- },
59
- });
60
- });
61
- });
62
-
63
- // Comparison Operators
64
- describe('comparison operators', () => {
65
- it('translates $eq operator', () => {
66
- const filter: OpenSearchVectorFilter = { field: { $eq: 'value' } };
67
- expect(translator.translate(filter)).toEqual({
68
- term: { 'metadata.field.keyword': 'value' },
69
- });
70
- });
71
-
72
- it('translates $ne operator', () => {
73
- const filter: OpenSearchVectorFilter = { field: { $ne: 'value' } };
74
- expect(translator.translate(filter)).toEqual({
75
- bool: {
76
- must_not: [{ term: { 'metadata.field.keyword': 'value' } }],
77
- },
78
- });
79
- });
80
-
81
- it('handles date values', () => {
82
- const date = new Date('2024-01-01');
83
- const filter: OpenSearchVectorFilter = { timestamp: { $gt: date } };
84
- expect(translator.translate(filter)).toEqual({
85
- range: { 'metadata.timestamp': { gt: date.toISOString() } },
86
- });
87
- });
88
- });
89
-
90
- // Logical Operators
91
- describe('logical operators', () => {
92
- it('translates $and operator', () => {
93
- const filter: OpenSearchVectorFilter = {
94
- $and: [{ field1: 'value1' }, { field2: 'value2' }],
95
- };
96
- expect(translator.translate(filter)).toEqual({
97
- bool: {
98
- must: [{ term: { 'metadata.field1.keyword': 'value1' } }, { term: { 'metadata.field2.keyword': 'value2' } }],
99
- },
100
- });
101
- });
102
-
103
- it('translates $or operator', () => {
104
- const filter: OpenSearchVectorFilter = {
105
- $or: [{ field1: 'value1' }, { field2: 'value2' }],
106
- };
107
- expect(translator.translate(filter)).toEqual({
108
- bool: {
109
- should: [
110
- { term: { 'metadata.field1.keyword': 'value1' } },
111
- { term: { 'metadata.field2.keyword': 'value2' } },
112
- ],
113
- },
114
- });
115
- });
116
-
117
- it('translates $not operator', () => {
118
- const filter: OpenSearchVectorFilter = {
119
- $not: { field: 'value' },
120
- };
121
- expect(translator.translate(filter)).toEqual({
122
- bool: {
123
- must_not: [{ term: { 'metadata.field.keyword': 'value' } }],
124
- },
125
- });
126
- });
127
-
128
- it('translates $not with $eq operator', () => {
129
- const filter: OpenSearchVectorFilter = { field: { $not: { $eq: 'value' } } };
130
- expect(translator.translate(filter)).toEqual({
131
- bool: {
132
- must_not: [{ term: { 'metadata.field.keyword': 'value' } }],
133
- },
134
- });
135
- });
136
-
137
- it('translates $not with $ne operator', () => {
138
- const filter: OpenSearchVectorFilter = { field: { $not: { $ne: 'value' } } };
139
- expect(translator.translate(filter)).toEqual({
140
- bool: {
141
- must_not: [
142
- {
143
- bool: {
144
- must_not: [{ term: { 'metadata.field.keyword': 'value' } }],
145
- },
146
- },
147
- ],
148
- },
149
- });
150
- });
151
-
152
- it('translates $not with $eq null', () => {
153
- const filter: OpenSearchVectorFilter = { field: { $not: { $eq: null } } };
154
- expect(translator.translate(filter)).toEqual({
155
- exists: { field: 'metadata.field' },
156
- });
157
- });
158
-
159
- it('translates $not with $ne null', () => {
160
- const filter: OpenSearchVectorFilter = { field: { $not: { $ne: null } } };
161
- expect(translator.translate(filter)).toEqual({
162
- bool: {
163
- must_not: [{ exists: { field: 'metadata.field' } }],
164
- },
165
- });
166
- });
167
-
168
- it('translates $not with nested fields', () => {
169
- const filter: OpenSearchVectorFilter = { 'user.profile.age': { $not: { $gt: 25 } } };
170
- expect(translator.translate(filter)).toEqual({
171
- bool: {
172
- must_not: [
173
- {
174
- range: { 'metadata.user.profile.age': { gt: 25 } },
175
- },
176
- ],
177
- },
178
- });
179
- });
180
-
181
- it('translates $not with multiple operators', () => {
182
- const filter: OpenSearchVectorFilter = { price: { $not: { $gte: 30, $lte: 70 } } };
183
- expect(translator.translate(filter)).toEqual({
184
- bool: {
185
- must_not: [
186
- {
187
- range: { 'metadata.price': { gte: 30, lte: 70 } },
188
- },
189
- ],
190
- },
191
- });
192
- });
193
-
194
- it('handles empty $and array', () => {
195
- const filter: OpenSearchVectorFilter = {
196
- $and: [],
197
- };
198
- // Empty $and should match everything
199
- expect(translator.translate(filter)).toEqual({ match_all: {} });
200
- });
201
-
202
- it('handles empty $or array', () => {
203
- const filter: OpenSearchVectorFilter = {
204
- $or: [],
205
- };
206
- // Empty $or should match nothing
207
- expect(translator.translate(filter)).toEqual({
208
- bool: {
209
- must_not: [{ match_all: {} }],
210
- },
211
- });
212
- });
213
-
214
- it('throws error for empty $not condition', () => {
215
- const filter = {
216
- $not: {},
217
- };
218
- expect(() => translator.translate(filter)).toThrow('not operator cannot be empty');
219
- });
220
-
221
- it('handles $not with comparison operators', () => {
222
- const filter: OpenSearchVectorFilter = {
223
- price: { $not: { $gt: 100 } },
224
- };
225
- expect(translator.translate(filter)).toEqual({
226
- bool: {
227
- must_not: [{ range: { 'metadata.price': { gt: 100 } } }],
228
- },
229
- });
230
- });
231
-
232
- it('handles nested $not with $or', () => {
233
- const filter: OpenSearchVectorFilter = {
234
- $not: { $or: [{ category: 'electronics' }, { category: 'books' }] },
235
- };
236
- expect(translator.translate(filter)).toEqual({
237
- bool: {
238
- must_not: [
239
- {
240
- bool: {
241
- should: [
242
- { term: { 'metadata.category.keyword': 'electronics' } },
243
- { term: { 'metadata.category.keyword': 'books' } },
244
- ],
245
- },
246
- },
247
- ],
248
- },
249
- });
250
- });
251
-
252
- it('handles $not with $not operator', () => {
253
- const filter: OpenSearchVectorFilter = {
254
- $not: { $not: { category: 'electronics' } },
255
- };
256
- expect(translator.translate(filter)).toEqual({
257
- bool: {
258
- must_not: [
259
- {
260
- bool: {
261
- must_not: [{ term: { 'metadata.category.keyword': 'electronics' } }],
262
- },
263
- },
264
- ],
265
- },
266
- });
267
- });
268
-
269
- it('handles nested logical operators', () => {
270
- const filter: OpenSearchVectorFilter = {
271
- $and: [
272
- { field1: 'value1' },
273
- {
274
- $or: [{ field2: 'value2' }, { field3: 'value3' }],
275
- },
276
- ],
277
- };
278
- expect(translator.translate(filter)).toEqual({
279
- bool: {
280
- must: [
281
- { term: { 'metadata.field1.keyword': 'value1' } },
282
- {
283
- bool: {
284
- should: [
285
- { term: { 'metadata.field2.keyword': 'value2' } },
286
- { term: { 'metadata.field3.keyword': 'value3' } },
287
- ],
288
- },
289
- },
290
- ],
291
- },
292
- });
293
- });
294
- });
295
-
296
- // Array Operators
297
- describe('array operators', () => {
298
- it('translates $in operator', () => {
299
- const filter: OpenSearchVectorFilter = { field: { $in: ['value1', 'value2'] } };
300
- expect(translator.translate(filter)).toEqual({
301
- terms: { 'metadata.field.keyword': ['value1', 'value2'] },
302
- });
303
- });
304
-
305
- it('translates $nin operator', () => {
306
- const filter: OpenSearchVectorFilter = { field: { $nin: ['value1', 'value2'] } };
307
- expect(translator.translate(filter)).toEqual({
308
- bool: {
309
- must_not: [{ terms: { 'metadata.field.keyword': ['value1', 'value2'] } }],
310
- },
311
- });
312
- });
313
-
314
- it('translates $all operator', () => {
315
- const filter: OpenSearchVectorFilter = { field: { $all: ['value1', 'value2'] } };
316
- expect(translator.translate(filter)).toEqual({
317
- bool: {
318
- must: [{ term: { 'metadata.field.keyword': 'value1' } }, { term: { 'metadata.field.keyword': 'value2' } }],
319
- },
320
- });
321
- });
322
-
323
- it('handles empty $in array', () => {
324
- const filter: OpenSearchVectorFilter = { field: { $in: [] } };
325
- // Empty $in should match nothing (empty terms)
326
- expect(translator.translate(filter)).toEqual({
327
- terms: { 'metadata.field.keyword': [] },
328
- });
329
- });
330
-
331
- it('handles empty $nin array', () => {
332
- const filter: OpenSearchVectorFilter = { field: { $nin: [] } };
333
- // Empty $nin should match everything
334
- expect(translator.translate(filter)).toEqual({
335
- match_all: {},
336
- });
337
- });
338
-
339
- it('handles empty $all array', () => {
340
- const filter: OpenSearchVectorFilter = { field: { $all: [] } };
341
- // Empty $all should match nothing
342
- expect(translator.translate(filter)).toEqual({
343
- bool: {
344
- must_not: [{ match_all: {} }],
345
- },
346
- });
347
- });
348
-
349
- it('handles $not with array operators', () => {
350
- const filter: OpenSearchVectorFilter = { tags: { $not: { $in: ['premium', 'new'] } } };
351
- expect(translator.translate(filter)).toEqual({
352
- bool: {
353
- must_not: [
354
- {
355
- terms: { 'metadata.tags.keyword': ['premium', 'new'] },
356
- },
357
- ],
358
- },
359
- });
360
- });
361
-
362
- it('handles $not with empty array operators', () => {
363
- const filter: OpenSearchVectorFilter = { tags: { $not: { $in: [] } } };
364
- expect(translator.translate(filter)).toEqual({
365
- bool: {
366
- must_not: [
367
- {
368
- terms: { 'metadata.tags.keyword': [] },
369
- },
370
- ],
371
- },
372
- });
373
- });
374
- });
375
-
376
- // Element Operators
377
- describe('element operators', () => {
378
- it('translates $exists operator', () => {
379
- const filter: OpenSearchVectorFilter = { field: { $exists: true } };
380
- expect(translator.translate(filter)).toEqual({
381
- exists: { field: 'metadata.field' },
382
- });
383
- });
384
-
385
- it('translates $exists operator with false', () => {
386
- const filter: OpenSearchVectorFilter = { field: { $exists: false } };
387
- expect(translator.translate(filter)).toEqual({
388
- bool: {
389
- must_not: [{ exists: { field: 'metadata.field' } }],
390
- },
391
- });
392
- });
393
- });
394
-
395
- // Regex Operators
396
- describe('regex operators', () => {
397
- it('translates $regex operator', () => {
398
- const filter: OpenSearchVectorFilter = { field: { $regex: 'pattern' } };
399
- expect(translator.translate(filter)).toEqual({
400
- regexp: { 'metadata.field': 'pattern' },
401
- });
402
- });
403
-
404
- it('handles $regex with start anchor', () => {
405
- const filter: OpenSearchVectorFilter = { category: { $regex: '^elect' } };
406
- // Should use wildcard for better anchor handling
407
- expect(translator.translate(filter)).toEqual({
408
- wildcard: { 'metadata.category': 'elect*' },
409
- });
410
- });
411
-
412
- it('handles $regex with end anchor', () => {
413
- const filter: OpenSearchVectorFilter = { category: { $regex: 'nics$' } };
414
- // Should use wildcard for better anchor handling
415
- expect(translator.translate(filter)).toEqual({
416
- wildcard: { 'metadata.category': '*nics' },
417
- });
418
- });
419
-
420
- it('handles $regex with both anchors', () => {
421
- const filter: OpenSearchVectorFilter = { category: { $regex: '^electronics$' } };
422
- // Should use exact match for both anchors
423
- expect(translator.translate(filter)).toEqual({
424
- wildcard: { 'metadata.category': 'electronics' },
425
- });
426
- });
427
-
428
- it('handles $not with $regex operator', () => {
429
- const filter: OpenSearchVectorFilter = { category: { $not: { $regex: '^elect' } } };
430
- expect(translator.translate(filter)).toEqual({
431
- bool: {
432
- must_not: [
433
- {
434
- wildcard: { 'metadata.category': 'elect*' },
435
- },
436
- ],
437
- },
438
- });
439
- });
440
- });
441
-
442
- // Complex Queries
443
- describe('complex queries', () => {
444
- it('translates numeric operators', () => {
445
- const filter: OpenSearchVectorFilter = { price: { $gt: 70, $lte: 100 } };
446
- expect(translator.translate(filter)).toEqual({
447
- range: { 'metadata.price': { gt: 70, lte: 100 } },
448
- });
449
- });
450
-
451
- it('translates multiple range operators on the same field', () => {
452
- const filter: OpenSearchVectorFilter = { price: { $gte: 50, $lt: 200 } };
453
- expect(translator.translate(filter)).toEqual({
454
- range: { 'metadata.price': { gte: 50, lt: 200 } },
455
- });
456
- });
457
-
458
- it('translates all four range operators combined', () => {
459
- // This is an edge case that would never occur in practice, but tests the implementation
460
- const filter: OpenSearchVectorFilter = { value: { $gt: 10, $gte: 20, $lt: 100, $lte: 90 } };
461
- expect(translator.translate(filter)).toEqual({
462
- range: { 'metadata.value': { gt: 10, gte: 20, lt: 100, lte: 90 } },
463
- });
464
- });
465
-
466
- it('translates mixed numeric and non-numeric operators', () => {
467
- const filter: OpenSearchVectorFilter = { price: { $gt: 50, $exists: true } };
468
- expect(translator.translate(filter)).toEqual({
469
- bool: {
470
- must: [{ range: { 'metadata.price': { gt: 50 } } }, { exists: { field: 'metadata.price' } }],
471
- },
472
- });
473
- });
474
- it('translates mixed operators', () => {
475
- const filter: OpenSearchVectorFilter = {
476
- $and: [{ field1: { $gt: 10 } }, { field2: { $in: ['value1', 'value2'] } }, { field3: { $exists: true } }],
477
- };
478
- expect(translator.translate(filter)).toEqual({
479
- bool: {
480
- must: [
481
- { range: { 'metadata.field1': { gt: 10 } } },
482
- { terms: { 'metadata.field2.keyword': ['value1', 'value2'] } },
483
- { exists: { field: 'metadata.field3' } },
484
- ],
485
- },
486
- });
487
- });
488
-
489
- it('translates complex nested queries', () => {
490
- const filter: OpenSearchVectorFilter = {
491
- $and: [
492
- { status: 'active' },
493
- {
494
- $or: [{ age: { $gt: 25 } }, { role: { $in: ['admin', 'manager'] } }],
495
- },
496
- {
497
- $not: {
498
- $and: [{ deleted: true }, { archived: true }],
499
- },
500
- },
501
- ],
502
- };
503
- expect(translator.translate(filter)).toEqual({
504
- bool: {
505
- must: [
506
- { term: { 'metadata.status.keyword': 'active' } },
507
- {
508
- bool: {
509
- should: [
510
- { range: { 'metadata.age': { gt: 25 } } },
511
- { terms: { 'metadata.role.keyword': ['admin', 'manager'] } },
512
- ],
513
- },
514
- },
515
- {
516
- bool: {
517
- must_not: [
518
- {
519
- bool: {
520
- must: [{ term: { 'metadata.deleted': true } }, { term: { 'metadata.archived': true } }],
521
- },
522
- },
523
- ],
524
- },
525
- },
526
- ],
527
- },
528
- });
529
- });
530
- });
531
-
532
- // Error Cases
533
- describe('error cases', () => {
534
- it('throws error for unsupported operators', () => {
535
- const filter = { field: { $unsupported: 'value' } };
536
- expect(() => translator.translate(filter)).toThrow(/Unsupported operator/);
537
- });
538
-
539
- it('throws error for invalid logical operator structure', () => {
540
- const filter = { $and: 'invalid' };
541
- expect(() => translator.translate(filter)).toThrow();
542
- });
543
-
544
- it('throws error for invalid array operator values', () => {
545
- const filter = { field: { $in: 'not-an-array' } };
546
- expect(() => translator.translate(filter as any)).toThrow();
547
- });
548
-
549
- it('throws error for nested invalid operators', () => {
550
- const filter = { user: { profile: { age: { $invalid: 25 } } } };
551
- expect(() => translator.translate(filter)).toThrow();
552
- });
553
- });
554
-
555
- describe('special values', () => {
556
- it('handles boolean values', () => {
557
- const filter: OpenSearchVectorFilter = { active: true, disabled: false };
558
- expect(translator.translate(filter)).toEqual({
559
- bool: {
560
- must: [{ term: { 'metadata.active': true } }, { term: { 'metadata.disabled': false } }],
561
- },
562
- });
563
- });
564
-
565
- it('handles null values', () => {
566
- const filter: OpenSearchVectorFilter = { field: null };
567
- expect(translator.translate(filter)).toEqual({
568
- term: { 'metadata.field': null },
569
- });
570
- });
571
- });
572
-
573
- describe('array handling', () => {
574
- it('translates array values to terms query', () => {
575
- const filter: OpenSearchVectorFilter = { tags: ['premium', 'new'] };
576
- expect(translator.translate(filter)).toEqual({
577
- terms: { 'metadata.tags.keyword': ['premium', 'new'] },
578
- });
579
- });
580
-
581
- it('translates numeric array values to terms query', () => {
582
- const filter: OpenSearchVectorFilter = { scores: [90, 95, 100] };
583
- expect(translator.translate(filter)).toEqual({
584
- terms: { 'metadata.scores': [90, 95, 100] },
585
- });
586
- });
587
-
588
- it('translates empty array values to empty terms query', () => {
589
- const filter: OpenSearchVectorFilter = { tags: [] };
590
- expect(translator.translate(filter)).toEqual({
591
- terms: { 'metadata.tags.keyword': [] },
592
- });
593
- });
594
-
595
- it('handles nested arrays in objects', () => {
596
- const filter = { user: { interests: ['sports', 'music'] } };
597
- expect(translator.translate(filter)).toEqual({
598
- bool: {
599
- must: [
600
- {
601
- term: {
602
- 'metadata.user.interests.keyword': ['sports', 'music'],
603
- },
604
- },
605
- ],
606
- },
607
- });
608
- });
609
- });
610
-
611
- describe('field type handling', () => {
612
- it('adds .keyword suffix for string fields', () => {
613
- const filter: OpenSearchVectorFilter = { field: 'value' };
614
- expect(translator.translate(filter)).toEqual({
615
- term: { 'metadata.field.keyword': 'value' },
616
- });
617
- });
618
-
619
- it('adds .keyword suffix for string array fields', () => {
620
- const filter: OpenSearchVectorFilter = { field: { $in: ['value1', 'value2'] } };
621
- expect(translator.translate(filter)).toEqual({
622
- terms: { 'metadata.field.keyword': ['value1', 'value2'] },
623
- });
624
- });
625
-
626
- it('does not add .keyword suffix for numeric fields', () => {
627
- const filter: OpenSearchVectorFilter = { field: 123 };
628
- expect(translator.translate(filter)).toEqual({
629
- term: { 'metadata.field': 123 },
630
- });
631
- });
632
-
633
- it('does not add .keyword suffix for numeric array fields', () => {
634
- const filter: OpenSearchVectorFilter = { field: { $in: [1, 2, 3] } };
635
- expect(translator.translate(filter)).toEqual({
636
- terms: { 'metadata.field': [1, 2, 3] },
637
- });
638
- });
639
-
640
- it('handles mixed field types in complex queries', () => {
641
- const filter: OpenSearchVectorFilter = {
642
- $and: [
643
- { textField: 'value' },
644
- { numericField: 123 },
645
- { arrayField: { $in: ['a', 'b'] } },
646
- { numericArray: { $in: [1, 2] } },
647
- ],
648
- };
649
- expect(translator.translate(filter)).toEqual({
650
- bool: {
651
- must: [
652
- { term: { 'metadata.textField.keyword': 'value' } },
653
- { term: { 'metadata.numericField': 123 } },
654
- { terms: { 'metadata.arrayField.keyword': ['a', 'b'] } },
655
- { terms: { 'metadata.numericArray': [1, 2] } },
656
- ],
657
- },
658
- });
659
- });
660
- });
661
- });