@kernl-sdk/turbopuffer 0.1.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.
Files changed (91) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-check-types.log +60 -0
  3. package/CHANGELOG.md +33 -0
  4. package/LICENSE +201 -0
  5. package/README.md +60 -0
  6. package/dist/__tests__/convert.test.d.ts +2 -0
  7. package/dist/__tests__/convert.test.d.ts.map +1 -0
  8. package/dist/__tests__/convert.test.js +346 -0
  9. package/dist/__tests__/filter.test.d.ts +8 -0
  10. package/dist/__tests__/filter.test.d.ts.map +1 -0
  11. package/dist/__tests__/filter.test.js +649 -0
  12. package/dist/__tests__/filters.integration.test.d.ts +8 -0
  13. package/dist/__tests__/filters.integration.test.d.ts.map +1 -0
  14. package/dist/__tests__/filters.integration.test.js +502 -0
  15. package/dist/__tests__/integration/filters.integration.test.d.ts +8 -0
  16. package/dist/__tests__/integration/filters.integration.test.d.ts.map +1 -0
  17. package/dist/__tests__/integration/filters.integration.test.js +475 -0
  18. package/dist/__tests__/integration/integration.test.d.ts +2 -0
  19. package/dist/__tests__/integration/integration.test.d.ts.map +1 -0
  20. package/dist/__tests__/integration/integration.test.js +329 -0
  21. package/dist/__tests__/integration/lifecycle.integration.test.d.ts +8 -0
  22. package/dist/__tests__/integration/lifecycle.integration.test.d.ts.map +1 -0
  23. package/dist/__tests__/integration/lifecycle.integration.test.js +370 -0
  24. package/dist/__tests__/integration/memory.integration.test.d.ts +2 -0
  25. package/dist/__tests__/integration/memory.integration.test.d.ts.map +1 -0
  26. package/dist/__tests__/integration/memory.integration.test.js +287 -0
  27. package/dist/__tests__/integration/query.integration.test.d.ts +8 -0
  28. package/dist/__tests__/integration/query.integration.test.d.ts.map +1 -0
  29. package/dist/__tests__/integration/query.integration.test.js +385 -0
  30. package/dist/__tests__/integration.test.d.ts +2 -0
  31. package/dist/__tests__/integration.test.d.ts.map +1 -0
  32. package/dist/__tests__/integration.test.js +343 -0
  33. package/dist/__tests__/lifecycle.integration.test.d.ts +8 -0
  34. package/dist/__tests__/lifecycle.integration.test.d.ts.map +1 -0
  35. package/dist/__tests__/lifecycle.integration.test.js +385 -0
  36. package/dist/__tests__/query.integration.test.d.ts +8 -0
  37. package/dist/__tests__/query.integration.test.d.ts.map +1 -0
  38. package/dist/__tests__/query.integration.test.js +423 -0
  39. package/dist/__tests__/query.test.d.ts +8 -0
  40. package/dist/__tests__/query.test.d.ts.map +1 -0
  41. package/dist/__tests__/query.test.js +472 -0
  42. package/dist/convert/document.d.ts +20 -0
  43. package/dist/convert/document.d.ts.map +1 -0
  44. package/dist/convert/document.js +72 -0
  45. package/dist/convert/filter.d.ts +15 -0
  46. package/dist/convert/filter.d.ts.map +1 -0
  47. package/dist/convert/filter.js +109 -0
  48. package/dist/convert/index.d.ts +8 -0
  49. package/dist/convert/index.d.ts.map +1 -0
  50. package/dist/convert/index.js +7 -0
  51. package/dist/convert/query.d.ts +22 -0
  52. package/dist/convert/query.d.ts.map +1 -0
  53. package/dist/convert/query.js +111 -0
  54. package/dist/convert/schema.d.ts +39 -0
  55. package/dist/convert/schema.d.ts.map +1 -0
  56. package/dist/convert/schema.js +124 -0
  57. package/dist/convert.d.ts +68 -0
  58. package/dist/convert.d.ts.map +1 -0
  59. package/dist/convert.js +333 -0
  60. package/dist/handle.d.ts +34 -0
  61. package/dist/handle.d.ts.map +1 -0
  62. package/dist/handle.js +72 -0
  63. package/dist/index.d.ts +27 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +26 -0
  66. package/dist/search.d.ts +85 -0
  67. package/dist/search.d.ts.map +1 -0
  68. package/dist/search.js +167 -0
  69. package/dist/types.d.ts +14 -0
  70. package/dist/types.d.ts.map +1 -0
  71. package/dist/types.js +1 -0
  72. package/package.json +57 -0
  73. package/src/__tests__/convert.test.ts +425 -0
  74. package/src/__tests__/filter.test.ts +730 -0
  75. package/src/__tests__/integration/filters.integration.test.ts +558 -0
  76. package/src/__tests__/integration/integration.test.ts +399 -0
  77. package/src/__tests__/integration/lifecycle.integration.test.ts +464 -0
  78. package/src/__tests__/integration/memory.integration.test.ts +353 -0
  79. package/src/__tests__/integration/query.integration.test.ts +471 -0
  80. package/src/__tests__/query.test.ts +636 -0
  81. package/src/convert/document.ts +95 -0
  82. package/src/convert/filter.ts +123 -0
  83. package/src/convert/index.ts +8 -0
  84. package/src/convert/query.ts +151 -0
  85. package/src/convert/schema.ts +163 -0
  86. package/src/handle.ts +104 -0
  87. package/src/index.ts +31 -0
  88. package/src/search.ts +207 -0
  89. package/src/types.ts +14 -0
  90. package/tsconfig.json +13 -0
  91. package/vitest.config.ts +15 -0
@@ -0,0 +1,558 @@
1
+ /**
2
+ * Comprehensive filter integration tests.
3
+ *
4
+ * Tests all filter operators against real Turbopuffer API with a
5
+ * deterministic dataset.
6
+ */
7
+
8
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
9
+
10
+ import { TurbopufferSearchIndex } from "../../search";
11
+ import type { IndexHandle } from "@kernl-sdk/retrieval";
12
+ import type { Filter } from "@kernl-sdk/retrieval";
13
+
14
+ const TURBOPUFFER_API_KEY = process.env.TURBOPUFFER_API_KEY;
15
+ const TURBOPUFFER_REGION = process.env.TURBOPUFFER_REGION ?? "api";
16
+
17
+ /**
18
+ * Test document type for results.
19
+ */
20
+ interface TestDoc {
21
+ id: string;
22
+ num: number;
23
+ flag: boolean;
24
+ status: string;
25
+ tags: string[];
26
+ name: string;
27
+ optionalField?: string | null;
28
+ vector: number[];
29
+ }
30
+
31
+ /**
32
+ * Deterministic test dataset.
33
+ *
34
+ * 10 documents with varied field values for comprehensive filter testing.
35
+ */
36
+ const TEST_DOCS: TestDoc[] = [
37
+ {
38
+ id: "doc-01",
39
+ num: 10,
40
+ flag: true,
41
+ status: "active",
42
+ tags: ["important", "urgent"],
43
+ name: "alice_smith",
44
+ optionalField: "present",
45
+ vector: [0.1, 0.0, 0.0, 0.0],
46
+ },
47
+ {
48
+ id: "doc-02",
49
+ num: 20,
50
+ flag: false,
51
+ status: "active",
52
+ tags: ["normal"],
53
+ name: "bob_jones",
54
+ optionalField: null,
55
+ vector: [0.0, 0.1, 0.0, 0.0],
56
+ },
57
+ {
58
+ id: "doc-03",
59
+ num: 30,
60
+ flag: true,
61
+ status: "pending",
62
+ tags: ["important"],
63
+ name: "charlie_smith",
64
+ optionalField: "also_present",
65
+ vector: [0.0, 0.0, 0.1, 0.0],
66
+ },
67
+ {
68
+ id: "doc-04",
69
+ num: 40,
70
+ flag: false,
71
+ status: "pending",
72
+ tags: ["normal", "review"],
73
+ name: "diana_brown",
74
+ optionalField: null,
75
+ vector: [0.0, 0.0, 0.0, 0.1],
76
+ },
77
+ {
78
+ id: "doc-05",
79
+ num: 50,
80
+ flag: true,
81
+ status: "deleted",
82
+ tags: ["archived"],
83
+ name: "eve_johnson",
84
+ optionalField: "value",
85
+ vector: [0.1, 0.1, 0.0, 0.0],
86
+ },
87
+ {
88
+ id: "doc-06",
89
+ num: 0,
90
+ flag: false,
91
+ status: "active",
92
+ tags: [],
93
+ name: "frank_miller",
94
+ optionalField: null,
95
+ vector: [0.0, 0.1, 0.1, 0.0],
96
+ },
97
+ {
98
+ id: "doc-07",
99
+ num: -10,
100
+ flag: true,
101
+ status: "active",
102
+ tags: ["urgent", "critical"],
103
+ name: "grace_wilson",
104
+ optionalField: "exists",
105
+ vector: [0.0, 0.0, 0.1, 0.1],
106
+ },
107
+ {
108
+ id: "doc-08",
109
+ num: 100,
110
+ flag: false,
111
+ status: "deleted",
112
+ tags: ["archived", "old"],
113
+ name: "henry_davis",
114
+ vector: [0.1, 0.0, 0.1, 0.0],
115
+ },
116
+ {
117
+ id: "doc-09",
118
+ num: 25,
119
+ flag: true,
120
+ status: "pending",
121
+ tags: ["important", "review"],
122
+ name: "ivy_taylor",
123
+ optionalField: "set",
124
+ vector: [0.0, 0.1, 0.0, 0.1],
125
+ },
126
+ {
127
+ id: "doc-10",
128
+ num: 35,
129
+ flag: false,
130
+ status: "active",
131
+ tags: ["normal"],
132
+ name: "jack_anderson",
133
+ optionalField: null,
134
+ vector: [0.1, 0.0, 0.0, 0.1],
135
+ },
136
+ ];
137
+
138
+ // Standard query vector for all filter tests
139
+ const QUERY_VECTOR = [0.1, 0.1, 0.1, 0.1];
140
+
141
+ describe("Filter integration tests", () => {
142
+ if (!TURBOPUFFER_API_KEY) {
143
+ it.skip("requires TURBOPUFFER_API_KEY to be set", () => {});
144
+ return;
145
+ }
146
+
147
+ let tpuf: TurbopufferSearchIndex;
148
+ let ns: IndexHandle<TestDoc>;
149
+ const testIndexId = `kernl-filter-test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
150
+
151
+ beforeAll(async () => {
152
+ tpuf = new TurbopufferSearchIndex({
153
+ apiKey: TURBOPUFFER_API_KEY,
154
+ region: TURBOPUFFER_REGION,
155
+ });
156
+
157
+ // Create index with schema
158
+ await tpuf.createIndex({
159
+ id: testIndexId,
160
+ schema: {
161
+ num: { type: "int", filterable: true },
162
+ flag: { type: "boolean", filterable: true },
163
+ status: { type: "string", filterable: true },
164
+ tags: { type: "string[]", filterable: true },
165
+ name: { type: "string", filterable: true },
166
+ optionalField: { type: "string", filterable: true },
167
+ vector: { type: "vector", dimensions: 4 },
168
+ },
169
+ });
170
+
171
+ ns = tpuf.index<TestDoc>(testIndexId);
172
+
173
+ // Insert test documents
174
+ await ns.upsert(TEST_DOCS);
175
+
176
+ // Wait for indexing
177
+ await new Promise((r) => setTimeout(r, 1000));
178
+ }, 30000);
179
+
180
+ afterAll(async () => {
181
+ try {
182
+ await tpuf.deleteIndex(testIndexId);
183
+ } catch {
184
+ // Ignore cleanup errors
185
+ }
186
+ });
187
+
188
+ /**
189
+ * Helper to query and return sorted IDs.
190
+ */
191
+ async function queryIds(filter: Filter): Promise<string[]> {
192
+ const hits = await ns.query({
193
+ query: [{ vector: QUERY_VECTOR }],
194
+ topK: 100,
195
+ filter,
196
+ });
197
+ return hits.map((h) => h.id).sort();
198
+ }
199
+
200
+ // ============================================================
201
+ // EQUALITY OPERATORS
202
+ // ============================================================
203
+
204
+ describe("equality operators", () => {
205
+ it("$eq on string field", async () => {
206
+ const ids = await queryIds({ status: "active" });
207
+ expect(ids).toEqual(["doc-01", "doc-02", "doc-06", "doc-07", "doc-10"]);
208
+ });
209
+
210
+ it("$eq on number field", async () => {
211
+ const ids = await queryIds({ num: 30 });
212
+ expect(ids).toEqual(["doc-03"]);
213
+ });
214
+
215
+ it("$eq on boolean true", async () => {
216
+ const ids = await queryIds({ flag: true });
217
+ expect(ids).toEqual(["doc-01", "doc-03", "doc-05", "doc-07", "doc-09"]);
218
+ });
219
+
220
+ it("$eq on boolean false", async () => {
221
+ const ids = await queryIds({ flag: false });
222
+ expect(ids).toEqual(["doc-02", "doc-04", "doc-06", "doc-08", "doc-10"]);
223
+ });
224
+
225
+ it("$eq on zero", async () => {
226
+ const ids = await queryIds({ num: 0 });
227
+ expect(ids).toEqual(["doc-06"]);
228
+ });
229
+
230
+ it("$eq on negative number", async () => {
231
+ const ids = await queryIds({ num: -10 });
232
+ expect(ids).toEqual(["doc-07"]);
233
+ });
234
+
235
+ it("$neq on string field", async () => {
236
+ const ids = await queryIds({ status: { $neq: "active" } });
237
+ expect(ids).toEqual(["doc-03", "doc-04", "doc-05", "doc-08", "doc-09"]);
238
+ });
239
+
240
+ it("$neq on boolean", async () => {
241
+ const ids = await queryIds({ flag: { $neq: true } });
242
+ expect(ids).toEqual(["doc-02", "doc-04", "doc-06", "doc-08", "doc-10"]);
243
+ });
244
+ });
245
+
246
+ // ============================================================
247
+ // COMPARISON OPERATORS
248
+ // ============================================================
249
+
250
+ describe("comparison operators", () => {
251
+ it("$gt on number", async () => {
252
+ const ids = await queryIds({ num: { $gt: 30 } });
253
+ expect(ids).toEqual(["doc-04", "doc-05", "doc-08", "doc-10"]);
254
+ });
255
+
256
+ it("$gte on number", async () => {
257
+ const ids = await queryIds({ num: { $gte: 30 } });
258
+ expect(ids).toEqual(["doc-03", "doc-04", "doc-05", "doc-08", "doc-10"]);
259
+ });
260
+
261
+ it("$lt on number", async () => {
262
+ const ids = await queryIds({ num: { $lt: 20 } });
263
+ expect(ids).toEqual(["doc-01", "doc-06", "doc-07"]);
264
+ });
265
+
266
+ it("$lte on number", async () => {
267
+ const ids = await queryIds({ num: { $lte: 20 } });
268
+ expect(ids).toEqual(["doc-01", "doc-02", "doc-06", "doc-07"]);
269
+ });
270
+
271
+ it("$gt with negative number", async () => {
272
+ const ids = await queryIds({ num: { $gt: -10 } });
273
+ expect(ids).toEqual([
274
+ "doc-01",
275
+ "doc-02",
276
+ "doc-03",
277
+ "doc-04",
278
+ "doc-05",
279
+ "doc-06",
280
+ "doc-08",
281
+ "doc-09",
282
+ "doc-10",
283
+ ]);
284
+ });
285
+
286
+ it("$lt with zero", async () => {
287
+ const ids = await queryIds({ num: { $lt: 0 } });
288
+ expect(ids).toEqual(["doc-07"]);
289
+ });
290
+
291
+ it("range filter (gt + lt)", async () => {
292
+ const ids = await queryIds({ num: { $gt: 20, $lt: 40 } });
293
+ expect(ids).toEqual(["doc-03", "doc-09", "doc-10"]);
294
+ });
295
+
296
+ it("inclusive range (gte + lte)", async () => {
297
+ const ids = await queryIds({ num: { $gte: 20, $lte: 40 } });
298
+ expect(ids).toEqual(["doc-02", "doc-03", "doc-04", "doc-09", "doc-10"]);
299
+ });
300
+ });
301
+
302
+ // ============================================================
303
+ // SET MEMBERSHIP OPERATORS
304
+ // ============================================================
305
+
306
+ describe("set membership operators", () => {
307
+ it("$in with string values", async () => {
308
+ const ids = await queryIds({ status: { $in: ["active", "pending"] } });
309
+ expect(ids).toEqual([
310
+ "doc-01",
311
+ "doc-02",
312
+ "doc-03",
313
+ "doc-04",
314
+ "doc-06",
315
+ "doc-07",
316
+ "doc-09",
317
+ "doc-10",
318
+ ]);
319
+ });
320
+
321
+ it("$in with single value", async () => {
322
+ const ids = await queryIds({ status: { $in: ["deleted"] } });
323
+ expect(ids).toEqual(["doc-05", "doc-08"]);
324
+ });
325
+
326
+ it("$in with number values", async () => {
327
+ const ids = await queryIds({ num: { $in: [10, 20, 30] } });
328
+ expect(ids).toEqual(["doc-01", "doc-02", "doc-03"]);
329
+ });
330
+
331
+ it("$nin with string values", async () => {
332
+ const ids = await queryIds({ status: { $nin: ["deleted"] } });
333
+ expect(ids).toEqual([
334
+ "doc-01",
335
+ "doc-02",
336
+ "doc-03",
337
+ "doc-04",
338
+ "doc-06",
339
+ "doc-07",
340
+ "doc-09",
341
+ "doc-10",
342
+ ]);
343
+ });
344
+
345
+ it("$nin excludes multiple values", async () => {
346
+ const ids = await queryIds({ status: { $nin: ["active", "deleted"] } });
347
+ expect(ids).toEqual(["doc-03", "doc-04", "doc-09"]);
348
+ });
349
+ });
350
+
351
+ // ============================================================
352
+ // ARRAY OPERATORS
353
+ // ============================================================
354
+
355
+ describe("array operators", () => {
356
+ it("$contains on array field", async () => {
357
+ const ids = await queryIds({ tags: { $contains: "important" } });
358
+ expect(ids).toEqual(["doc-01", "doc-03", "doc-09"]);
359
+ });
360
+
361
+ it("$contains with different value", async () => {
362
+ const ids = await queryIds({ tags: { $contains: "urgent" } });
363
+ expect(ids).toEqual(["doc-01", "doc-07"]);
364
+ });
365
+
366
+ it("$contains with value not in any doc", async () => {
367
+ const ids = await queryIds({ tags: { $contains: "nonexistent" } });
368
+ expect(ids).toEqual([]);
369
+ });
370
+ });
371
+
372
+ // ============================================================
373
+ // STRING PATTERN OPERATORS
374
+ // ============================================================
375
+
376
+ describe("string pattern operators", () => {
377
+ it("$startsWith matches prefix", async () => {
378
+ const ids = await queryIds({ name: { $startsWith: "alice" } });
379
+ expect(ids).toEqual(["doc-01"]);
380
+ });
381
+
382
+ it("$startsWith with common prefix", async () => {
383
+ // All names ending with _smith or starting with common letters
384
+ const ids = await queryIds({ name: { $startsWith: "j" } });
385
+ expect(ids).toEqual(["doc-10"]); // jack_anderson
386
+ });
387
+
388
+ it("$endsWith matches suffix", async () => {
389
+ const ids = await queryIds({ name: { $endsWith: "_smith" } });
390
+ expect(ids).toEqual(["doc-01", "doc-03"]); // alice_smith, charlie_smith
391
+ });
392
+
393
+ it("$endsWith with different suffix", async () => {
394
+ const ids = await queryIds({ name: { $endsWith: "_jones" } });
395
+ expect(ids).toEqual(["doc-02"]); // bob_jones
396
+ });
397
+ });
398
+
399
+ // ============================================================
400
+ // EXISTENCE OPERATORS
401
+ // ============================================================
402
+
403
+ describe("existence operators", () => {
404
+ it("$exists: true finds docs with non-null field", async () => {
405
+ const ids = await queryIds({ optionalField: { $exists: true } });
406
+ // docs with optionalField set to a string value (not null, not missing)
407
+ expect(ids).toEqual(["doc-01", "doc-03", "doc-05", "doc-07", "doc-09"]);
408
+ });
409
+
410
+ it("$exists: false finds docs with null or missing field", async () => {
411
+ const ids = await queryIds({ optionalField: { $exists: false } });
412
+ // docs where optionalField is null or missing
413
+ expect(ids).toEqual(["doc-02", "doc-04", "doc-06", "doc-08", "doc-10"]);
414
+ });
415
+ });
416
+
417
+ // ============================================================
418
+ // LOGICAL OPERATORS
419
+ // ============================================================
420
+
421
+ describe("logical operators", () => {
422
+ it("implicit AND with multiple fields", async () => {
423
+ const ids = await queryIds({ status: "active", flag: true });
424
+ expect(ids).toEqual(["doc-01", "doc-07"]);
425
+ });
426
+
427
+ it("$and with two conditions", async () => {
428
+ const ids = await queryIds({
429
+ $and: [{ status: "active" }, { num: { $gte: 0 } }],
430
+ });
431
+ expect(ids).toEqual(["doc-01", "doc-02", "doc-06", "doc-10"]);
432
+ });
433
+
434
+ it("$and with three conditions", async () => {
435
+ const ids = await queryIds({
436
+ $and: [{ status: "active" }, { flag: true }, { num: { $gt: 0 } }],
437
+ });
438
+ expect(ids).toEqual(["doc-01"]);
439
+ });
440
+
441
+ it("$or with two conditions", async () => {
442
+ const ids = await queryIds({
443
+ $or: [{ status: "deleted" }, { num: { $lt: 0 } }],
444
+ });
445
+ expect(ids).toEqual(["doc-05", "doc-07", "doc-08"]);
446
+ });
447
+
448
+ it("$or with equality on same field", async () => {
449
+ const ids = await queryIds({
450
+ $or: [{ num: 10 }, { num: 20 }, { num: 30 }],
451
+ });
452
+ expect(ids).toEqual(["doc-01", "doc-02", "doc-03"]);
453
+ });
454
+
455
+ it("$not with simple condition", async () => {
456
+ const ids = await queryIds({
457
+ $not: { status: "active" },
458
+ });
459
+ expect(ids).toEqual(["doc-03", "doc-04", "doc-05", "doc-08", "doc-09"]);
460
+ });
461
+
462
+ it("$not with comparison", async () => {
463
+ const ids = await queryIds({
464
+ $not: { num: { $gte: 30 } },
465
+ });
466
+ expect(ids).toEqual(["doc-01", "doc-02", "doc-06", "doc-07", "doc-09"]);
467
+ });
468
+
469
+ it("AND of ORs", async () => {
470
+ const ids = await queryIds({
471
+ $and: [
472
+ { $or: [{ status: "active" }, { status: "pending" }] },
473
+ { $or: [{ flag: true }] },
474
+ ],
475
+ });
476
+ expect(ids).toEqual(["doc-01", "doc-03", "doc-07", "doc-09"]);
477
+ });
478
+
479
+ it("OR of ANDs", async () => {
480
+ const ids = await queryIds({
481
+ $or: [
482
+ { status: "active", flag: true },
483
+ { status: "deleted", flag: false },
484
+ ],
485
+ });
486
+ expect(ids).toEqual(["doc-01", "doc-07", "doc-08"]);
487
+ });
488
+
489
+ it("deeply nested filter", async () => {
490
+ const ids = await queryIds({
491
+ $and: [
492
+ { num: { $gte: 0 } },
493
+ {
494
+ $or: [
495
+ { status: "active", flag: true },
496
+ {
497
+ $and: [
498
+ { status: "pending" },
499
+ { tags: { $contains: "review" } },
500
+ ],
501
+ },
502
+ ],
503
+ },
504
+ ],
505
+ });
506
+ // doc-01: active + flag=true
507
+ // doc-04: pending + tags contains "review"
508
+ // doc-09: pending + tags contains "review"
509
+ expect(ids).toEqual(["doc-01", "doc-04", "doc-09"]);
510
+ });
511
+ });
512
+
513
+ // ============================================================
514
+ // COMBINED FILTER + FIELD ASSERTIONS
515
+ // ============================================================
516
+
517
+ describe("filter result validation", () => {
518
+ it("filtered results have correct field values", async () => {
519
+ const hits = await ns.query({
520
+ query: [{ vector: QUERY_VECTOR }],
521
+ topK: 100,
522
+ filter: { status: "pending" },
523
+ include: ["status", "flag", "num"],
524
+ });
525
+
526
+ expect(hits.length).toBe(3);
527
+ for (const hit of hits) {
528
+ expect(hit.document?.status).toBe("pending");
529
+ }
530
+
531
+ const nums = hits
532
+ .map((h) => h.document?.num)
533
+ .sort((a, b) => (a ?? 0) - (b ?? 0));
534
+ expect(nums).toEqual([25, 30, 40]);
535
+ });
536
+
537
+ it("complex filter returns expected documents with correct data", async () => {
538
+ const hits = await ns.query({
539
+ query: [{ vector: QUERY_VECTOR }],
540
+ topK: 100,
541
+ filter: {
542
+ $and: [{ num: { $gte: 20, $lte: 50 } }, { flag: true }],
543
+ },
544
+ include: ["num", "flag", "name"],
545
+ });
546
+
547
+ expect(hits.length).toBe(3);
548
+ const names = hits.map((h) => h.document?.name).sort();
549
+ expect(names).toEqual(["charlie_smith", "eve_johnson", "ivy_taylor"]);
550
+
551
+ for (const hit of hits) {
552
+ expect(hit.document?.flag).toBe(true);
553
+ expect(hit.document?.num).toBeGreaterThanOrEqual(20);
554
+ expect(hit.document?.num).toBeLessThanOrEqual(50);
555
+ }
556
+ });
557
+ });
558
+ });