@kernl-sdk/turbopuffer 0.1.2 → 0.1.4

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