@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,399 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
2
+
3
+ import { TurbopufferSearchIndex } from "../../search";
4
+
5
+ const TURBOPUFFER_API_KEY = process.env.TURBOPUFFER_API_KEY;
6
+ const TURBOPUFFER_REGION = process.env.TURBOPUFFER_REGION ?? "api";
7
+
8
+ describe("TurbopufferSearchIndex integration", () => {
9
+ if (!TURBOPUFFER_API_KEY) {
10
+ it.skip("requires TURBOPUFFER_API_KEY to be set", () => {});
11
+ return;
12
+ }
13
+
14
+ let tpuf: TurbopufferSearchIndex;
15
+ const testIndexId = `kernl-test-${Date.now()}`;
16
+
17
+ beforeAll(() => {
18
+ tpuf = new TurbopufferSearchIndex({
19
+ apiKey: TURBOPUFFER_API_KEY,
20
+ region: TURBOPUFFER_REGION,
21
+ });
22
+ });
23
+
24
+ afterAll(async () => {
25
+ // Clean up test index
26
+ try {
27
+ await tpuf.deleteIndex(testIndexId);
28
+ } catch {
29
+ // Ignore errors if index doesn't exist
30
+ }
31
+ });
32
+
33
+ describe("createIndex", () => {
34
+ it("creates a new index with schema", async () => {
35
+ await tpuf.createIndex({
36
+ id: testIndexId,
37
+ schema: {
38
+ content: { type: "string", fts: true },
39
+ vector: { type: "vector", dimensions: 384 },
40
+ category: { type: "string", filterable: true },
41
+ },
42
+ });
43
+
44
+ // Verify the index was created by describing it
45
+ const stats = await tpuf.describeIndex(testIndexId);
46
+ expect(stats.id).toBe(testIndexId);
47
+ expect(stats.status).toBe("ready");
48
+ });
49
+
50
+ it("throws if vector field is not named 'vector'", async () => {
51
+ await expect(
52
+ tpuf.createIndex({
53
+ id: `${testIndexId}-invalid`,
54
+ schema: {
55
+ embedding: { type: "vector", dimensions: 384 },
56
+ },
57
+ }),
58
+ ).rejects.toThrow(/requires vector fields to be named "vector"/);
59
+ });
60
+ });
61
+
62
+ describe("listIndexes", () => {
63
+ it("returns a CursorPage of indexes", async () => {
64
+ const page = await tpuf.listIndexes();
65
+
66
+ expect(page).toBeDefined();
67
+ expect(page.items).toBeDefined();
68
+ expect(Array.isArray(page.items)).toBe(true);
69
+
70
+ // Each item should have an id
71
+ for (const idx of page.items) {
72
+ expect(typeof idx.id).toBe("string");
73
+ }
74
+ });
75
+
76
+ it("supports pagination with limit", async () => {
77
+ const page = await tpuf.listIndexes({ limit: 2 });
78
+
79
+ expect(page.items.length).toBeLessThanOrEqual(2);
80
+ });
81
+
82
+ it("supports prefix filtering", async () => {
83
+ const page = await tpuf.listIndexes({ prefix: "kernl-test-" });
84
+
85
+ expect(page).toBeDefined();
86
+ expect(Array.isArray(page.items)).toBe(true);
87
+
88
+ // Should find our test index
89
+ const found = page.items.some((idx) => idx.id === testIndexId);
90
+ expect(found).toBe(true);
91
+
92
+ // All results should match prefix
93
+ for (const idx of page.items) {
94
+ expect(idx.id.startsWith("kernl-test-")).toBe(true);
95
+ }
96
+ });
97
+
98
+ it("supports async iteration via collect()", async () => {
99
+ const page = await tpuf.listIndexes({ limit: 5 });
100
+ const all = await page.collect();
101
+
102
+ expect(Array.isArray(all)).toBe(true);
103
+ expect(all.length).toBeGreaterThanOrEqual(page.items.length);
104
+ });
105
+
106
+ it("supports for-await iteration", async () => {
107
+ const page = await tpuf.listIndexes({ limit: 3 });
108
+ const collected: string[] = [];
109
+
110
+ for await (const idx of page) {
111
+ collected.push(idx.id);
112
+ if (collected.length >= 3) break;
113
+ }
114
+
115
+ expect(collected.length).toBeLessThanOrEqual(3);
116
+ });
117
+ });
118
+
119
+ describe("describeIndex", () => {
120
+ it("returns stats for the test index", async () => {
121
+ const stats = await tpuf.describeIndex(testIndexId);
122
+
123
+ expect(stats.id).toBe(testIndexId);
124
+ expect(typeof stats.count).toBe("number");
125
+ expect(stats.count).toBeGreaterThanOrEqual(0);
126
+ expect(stats.status).toBe("ready");
127
+
128
+ if (stats.sizeb !== undefined) {
129
+ expect(typeof stats.sizeb).toBe("number");
130
+ }
131
+ });
132
+
133
+ it("throws for non-existent index", async () => {
134
+ await expect(
135
+ tpuf.describeIndex("non-existent-index-12345"),
136
+ ).rejects.toThrow();
137
+ });
138
+ });
139
+
140
+ describe("deleteIndex", () => {
141
+ it("deletes an existing index", async () => {
142
+ // Create a temporary index to delete
143
+ const tempId = `kernl-test-delete-${Date.now()}`;
144
+
145
+ await tpuf.createIndex({
146
+ id: tempId,
147
+ schema: {
148
+ text: { type: "string" },
149
+ },
150
+ });
151
+
152
+ // Verify it exists
153
+ const stats = await tpuf.describeIndex(tempId);
154
+ expect(stats.id).toBe(tempId);
155
+
156
+ // Delete it
157
+ await tpuf.deleteIndex(tempId);
158
+
159
+ // Verify it's gone
160
+ await expect(tpuf.describeIndex(tempId)).rejects.toThrow();
161
+ });
162
+ });
163
+
164
+ describe("upsert", () => {
165
+ it("inserts a document with vector", async () => {
166
+ const vec = new Array(384).fill(0.1);
167
+ const index = tpuf.index(testIndexId);
168
+
169
+ await index.upsert({
170
+ id: "doc-1",
171
+ content: "Hello world",
172
+ vector: vec,
173
+ category: "greeting",
174
+ });
175
+
176
+ // Verify via query
177
+ const hits = await index.query({
178
+ query: [{ vector: vec }],
179
+ topK: 10,
180
+ include: ["category"],
181
+ });
182
+
183
+ expect(hits.length).toBeGreaterThanOrEqual(1);
184
+ const doc = hits.find((h) => h.id === "doc-1");
185
+ expect(doc).toBeDefined();
186
+ expect(doc?.document?.category).toBe("greeting");
187
+ });
188
+
189
+ it("updates an existing document", async () => {
190
+ const vec = new Array(384).fill(0.2);
191
+ const index = tpuf.index(testIndexId);
192
+
193
+ // Upsert same id with different content
194
+ await index.upsert({
195
+ id: "doc-1",
196
+ content: "Updated content",
197
+ vector: vec,
198
+ category: "updated",
199
+ });
200
+
201
+ // Verify via query
202
+ const hits = await index.query({
203
+ query: [{ vector: vec }],
204
+ topK: 10,
205
+ include: ["category"],
206
+ });
207
+
208
+ const doc = hits.find((h) => h.id === "doc-1");
209
+ expect(doc).toBeDefined();
210
+ expect(doc?.document?.category).toBe("updated");
211
+ });
212
+ });
213
+
214
+ describe("upsert (multiple)", () => {
215
+ it("inserts multiple documents", async () => {
216
+ const index = tpuf.index(testIndexId);
217
+
218
+ await index.upsert([
219
+ {
220
+ id: "doc-2",
221
+ content: "Second document",
222
+ vector: new Array(384).fill(0.3),
223
+ category: "test",
224
+ },
225
+ {
226
+ id: "doc-3",
227
+ content: "Third document",
228
+ vector: new Array(384).fill(0.4),
229
+ category: "test",
230
+ },
231
+ ]);
232
+
233
+ // Verify via query with filter
234
+ const hits = await index.query({
235
+ query: [{ vector: new Array(384).fill(0.3) }],
236
+ topK: 10,
237
+ filter: { category: "test" },
238
+ include: true,
239
+ });
240
+
241
+ expect(hits.length).toBeGreaterThanOrEqual(2);
242
+ const ids = hits.map((h) => h.id);
243
+ expect(ids).toContain("doc-2");
244
+ expect(ids).toContain("doc-3");
245
+ });
246
+
247
+ it("handles empty array", async () => {
248
+ const index = tpuf.index(testIndexId);
249
+ // Should not throw
250
+ await index.upsert([]);
251
+ });
252
+ });
253
+
254
+ describe("query", () => {
255
+ it("performs vector search", async () => {
256
+ const index = tpuf.index(testIndexId);
257
+
258
+ const hits = await index.query({
259
+ query: [{ vector: new Array(384).fill(0.1) }],
260
+ topK: 5,
261
+ });
262
+
263
+ expect(hits.length).toBeGreaterThan(0);
264
+ expect(hits[0]).toHaveProperty("id");
265
+ expect(hits[0]).toHaveProperty("score");
266
+ expect(hits[0]).toHaveProperty("index", testIndexId);
267
+ });
268
+
269
+ it("returns requested fields", async () => {
270
+ const index = tpuf.index(testIndexId);
271
+
272
+ const hits = await index.query({
273
+ query: [{ vector: new Array(384).fill(0.1) }],
274
+ topK: 5,
275
+ include: ["content", "category"],
276
+ });
277
+
278
+ expect(hits.length).toBeGreaterThan(0);
279
+ expect(hits[0].document).toBeDefined();
280
+ expect(hits[0].document).toHaveProperty("content");
281
+ expect(hits[0].document).toHaveProperty("category");
282
+ });
283
+
284
+ it("filters results", async () => {
285
+ const index = tpuf.index(testIndexId);
286
+
287
+ const hits = await index.query({
288
+ query: [{ vector: new Array(384).fill(0.3) }],
289
+ topK: 10,
290
+ filter: { category: "test" },
291
+ include: ["category"],
292
+ });
293
+
294
+ expect(hits.length).toBeGreaterThan(0);
295
+ for (const hit of hits) {
296
+ expect(hit.document?.category).toBe("test");
297
+ }
298
+ });
299
+
300
+ it("supports AND filters", async () => {
301
+ const index = tpuf.index(testIndexId);
302
+
303
+ const hits = await index.query({
304
+ query: [{ vector: new Array(384).fill(0.3) }],
305
+ topK: 10,
306
+ filter: {
307
+ $and: [
308
+ { category: "test" },
309
+ { id: { $in: ["doc-2", "doc-3"] } },
310
+ ],
311
+ },
312
+ include: ["category"],
313
+ });
314
+
315
+ expect(hits.length).toBeGreaterThanOrEqual(0);
316
+ for (const hit of hits) {
317
+ expect(hit.document?.category).toBe("test");
318
+ }
319
+ });
320
+ });
321
+
322
+ describe("delete", () => {
323
+ it("deletes a document by id", async () => {
324
+ const vec = new Array(384).fill(0.5);
325
+ const index = tpuf.index(testIndexId);
326
+
327
+ // Insert a document to delete
328
+ await index.upsert({
329
+ id: "doc-to-delete",
330
+ content: "Delete me",
331
+ vector: vec,
332
+ category: "deletable",
333
+ });
334
+
335
+ // Verify it exists
336
+ let hits = await index.query({
337
+ query: [{ vector: vec }],
338
+ topK: 10,
339
+ filter: { id: "doc-to-delete" },
340
+ });
341
+ expect(hits.some((h) => h.id === "doc-to-delete")).toBe(true);
342
+
343
+ // Delete it
344
+ await index.delete("doc-to-delete");
345
+
346
+ // Verify it's gone
347
+ hits = await index.query({
348
+ query: [{ vector: vec }],
349
+ topK: 10,
350
+ filter: { id: "doc-to-delete" },
351
+ });
352
+ expect(hits.some((h) => h.id === "doc-to-delete")).toBe(false);
353
+ });
354
+ });
355
+
356
+ describe("delete (multiple)", () => {
357
+ it("deletes multiple documents by ids", async () => {
358
+ const vec = new Array(384).fill(0.6);
359
+ const index = tpuf.index(testIndexId);
360
+
361
+ // Insert documents to delete
362
+ await index.upsert([
363
+ {
364
+ id: "bulk-del-1",
365
+ content: "Bulk delete 1",
366
+ vector: vec,
367
+ category: "bulk-delete",
368
+ },
369
+ {
370
+ id: "bulk-del-2",
371
+ content: "Bulk delete 2",
372
+ vector: vec,
373
+ category: "bulk-delete",
374
+ },
375
+ ]);
376
+
377
+ // Verify they exist
378
+ let hits = await index.query({
379
+ query: [{ vector: vec }],
380
+ topK: 10,
381
+ filter: { category: "bulk-delete" },
382
+ });
383
+ expect(hits.length).toBeGreaterThanOrEqual(2);
384
+
385
+ // Delete by ids
386
+ await index.delete(["bulk-del-1", "bulk-del-2"]);
387
+
388
+ // Verify they're gone
389
+ hits = await index.query({
390
+ query: [{ vector: vec }],
391
+ topK: 10,
392
+ filter: { category: "bulk-delete" },
393
+ });
394
+ expect(hits.some((h) => h.id === "bulk-del-1")).toBe(false);
395
+ expect(hits.some((h) => h.id === "bulk-del-2")).toBe(false);
396
+ });
397
+ });
398
+
399
+ });