@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.
- package/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-check-types.log +60 -0
- package/CHANGELOG.md +33 -0
- package/LICENSE +201 -0
- package/README.md +60 -0
- package/dist/__tests__/convert.test.d.ts +2 -0
- package/dist/__tests__/convert.test.d.ts.map +1 -0
- package/dist/__tests__/convert.test.js +346 -0
- package/dist/__tests__/filter.test.d.ts +8 -0
- package/dist/__tests__/filter.test.d.ts.map +1 -0
- package/dist/__tests__/filter.test.js +649 -0
- package/dist/__tests__/filters.integration.test.d.ts +8 -0
- package/dist/__tests__/filters.integration.test.d.ts.map +1 -0
- package/dist/__tests__/filters.integration.test.js +502 -0
- package/dist/__tests__/integration/filters.integration.test.d.ts +8 -0
- package/dist/__tests__/integration/filters.integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration/filters.integration.test.js +475 -0
- package/dist/__tests__/integration/integration.test.d.ts +2 -0
- package/dist/__tests__/integration/integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration/integration.test.js +329 -0
- package/dist/__tests__/integration/lifecycle.integration.test.d.ts +8 -0
- package/dist/__tests__/integration/lifecycle.integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration/lifecycle.integration.test.js +370 -0
- package/dist/__tests__/integration/memory.integration.test.d.ts +2 -0
- package/dist/__tests__/integration/memory.integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration/memory.integration.test.js +287 -0
- package/dist/__tests__/integration/query.integration.test.d.ts +8 -0
- package/dist/__tests__/integration/query.integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration/query.integration.test.js +385 -0
- package/dist/__tests__/integration.test.d.ts +2 -0
- package/dist/__tests__/integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration.test.js +343 -0
- package/dist/__tests__/lifecycle.integration.test.d.ts +8 -0
- package/dist/__tests__/lifecycle.integration.test.d.ts.map +1 -0
- package/dist/__tests__/lifecycle.integration.test.js +385 -0
- package/dist/__tests__/query.integration.test.d.ts +8 -0
- package/dist/__tests__/query.integration.test.d.ts.map +1 -0
- package/dist/__tests__/query.integration.test.js +423 -0
- package/dist/__tests__/query.test.d.ts +8 -0
- package/dist/__tests__/query.test.d.ts.map +1 -0
- package/dist/__tests__/query.test.js +472 -0
- package/dist/convert/document.d.ts +20 -0
- package/dist/convert/document.d.ts.map +1 -0
- package/dist/convert/document.js +72 -0
- package/dist/convert/filter.d.ts +15 -0
- package/dist/convert/filter.d.ts.map +1 -0
- package/dist/convert/filter.js +109 -0
- package/dist/convert/index.d.ts +8 -0
- package/dist/convert/index.d.ts.map +1 -0
- package/dist/convert/index.js +7 -0
- package/dist/convert/query.d.ts +22 -0
- package/dist/convert/query.d.ts.map +1 -0
- package/dist/convert/query.js +111 -0
- package/dist/convert/schema.d.ts +39 -0
- package/dist/convert/schema.d.ts.map +1 -0
- package/dist/convert/schema.js +124 -0
- package/dist/convert.d.ts +68 -0
- package/dist/convert.d.ts.map +1 -0
- package/dist/convert.js +333 -0
- package/dist/handle.d.ts +34 -0
- package/dist/handle.d.ts.map +1 -0
- package/dist/handle.js +72 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/search.d.ts +85 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +167 -0
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +57 -0
- package/src/__tests__/convert.test.ts +425 -0
- package/src/__tests__/filter.test.ts +730 -0
- package/src/__tests__/integration/filters.integration.test.ts +558 -0
- package/src/__tests__/integration/integration.test.ts +399 -0
- package/src/__tests__/integration/lifecycle.integration.test.ts +464 -0
- package/src/__tests__/integration/memory.integration.test.ts +353 -0
- package/src/__tests__/integration/query.integration.test.ts +471 -0
- package/src/__tests__/query.test.ts +636 -0
- package/src/convert/document.ts +95 -0
- package/src/convert/filter.ts +123 -0
- package/src/convert/index.ts +8 -0
- package/src/convert/query.ts +151 -0
- package/src/convert/schema.ts +163 -0
- package/src/handle.ts +104 -0
- package/src/index.ts +31 -0
- package/src/search.ts +207 -0
- package/src/types.ts +14 -0
- package/tsconfig.json +13 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { DOCUMENT, PATCH } from "../convert/document.js";
|
|
3
|
+
import { FILTER } from "../convert/filter.js";
|
|
4
|
+
import { QUERY, SEARCH_HIT } from "../convert/query.js";
|
|
5
|
+
import { SCALAR_TYPE, SIMILARITY, INDEX_SCHEMA } from "../convert/schema.js";
|
|
6
|
+
describe("convert", () => {
|
|
7
|
+
describe("SCALAR_TYPE", () => {
|
|
8
|
+
it("encodes kernl types to turbopuffer types", () => {
|
|
9
|
+
expect(SCALAR_TYPE.encode("string")).toBe("string");
|
|
10
|
+
expect(SCALAR_TYPE.encode("int")).toBe("int");
|
|
11
|
+
expect(SCALAR_TYPE.encode("float")).toBe("int"); // tpuf doesn't have float
|
|
12
|
+
expect(SCALAR_TYPE.encode("boolean")).toBe("bool");
|
|
13
|
+
expect(SCALAR_TYPE.encode("date")).toBe("datetime");
|
|
14
|
+
expect(SCALAR_TYPE.encode("string[]")).toBe("[]string");
|
|
15
|
+
expect(SCALAR_TYPE.encode("int[]")).toBe("[]int");
|
|
16
|
+
});
|
|
17
|
+
it("decodes turbopuffer types to kernl types", () => {
|
|
18
|
+
expect(SCALAR_TYPE.decode("string")).toBe("string");
|
|
19
|
+
expect(SCALAR_TYPE.decode("int")).toBe("int");
|
|
20
|
+
expect(SCALAR_TYPE.decode("bool")).toBe("boolean");
|
|
21
|
+
expect(SCALAR_TYPE.decode("datetime")).toBe("date");
|
|
22
|
+
expect(SCALAR_TYPE.decode("[]string")).toBe("string[]");
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
describe("SIMILARITY", () => {
|
|
26
|
+
it("encodes similarity to distance metric", () => {
|
|
27
|
+
expect(SIMILARITY.encode("cosine")).toBe("cosine_distance");
|
|
28
|
+
expect(SIMILARITY.encode("euclidean")).toBe("euclidean_squared");
|
|
29
|
+
expect(SIMILARITY.encode("dot_product")).toBe("cosine_distance");
|
|
30
|
+
expect(SIMILARITY.encode(undefined)).toBe("cosine_distance");
|
|
31
|
+
});
|
|
32
|
+
it("decodes distance metric to similarity", () => {
|
|
33
|
+
expect(SIMILARITY.decode("cosine_distance")).toBe("cosine");
|
|
34
|
+
expect(SIMILARITY.decode("euclidean_squared")).toBe("euclidean");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe("INDEX_SCHEMA", () => {
|
|
38
|
+
it("encodes a schema with scalar fields", () => {
|
|
39
|
+
const result = INDEX_SCHEMA.encode({
|
|
40
|
+
title: { type: "string", filterable: true },
|
|
41
|
+
count: { type: "int" },
|
|
42
|
+
});
|
|
43
|
+
expect(result.title).toEqual({ type: "string", filterable: true });
|
|
44
|
+
expect(result.count).toEqual({ type: "int" });
|
|
45
|
+
});
|
|
46
|
+
it("encodes a schema with vector field", () => {
|
|
47
|
+
const result = INDEX_SCHEMA.encode({
|
|
48
|
+
vector: { type: "vector", dimensions: 384 },
|
|
49
|
+
});
|
|
50
|
+
expect(result.vector).toEqual({ type: "[384]f32", ann: true });
|
|
51
|
+
});
|
|
52
|
+
it("throws if vector field is not named vector", () => {
|
|
53
|
+
expect(() => INDEX_SCHEMA.encode({
|
|
54
|
+
embedding: { type: "vector", dimensions: 384 },
|
|
55
|
+
})).toThrow(/requires vector fields to be named "vector"/);
|
|
56
|
+
});
|
|
57
|
+
it("encodes fts fields", () => {
|
|
58
|
+
const result = INDEX_SCHEMA.encode({
|
|
59
|
+
content: { type: "string", fts: true },
|
|
60
|
+
});
|
|
61
|
+
expect(result.content).toEqual({
|
|
62
|
+
type: "string",
|
|
63
|
+
full_text_search: true,
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
describe("DOCUMENT", () => {
|
|
68
|
+
it("encodes flat document with scalar fields", () => {
|
|
69
|
+
const result = DOCUMENT.encode({
|
|
70
|
+
id: "doc-1",
|
|
71
|
+
title: "Hello",
|
|
72
|
+
count: 42,
|
|
73
|
+
active: true,
|
|
74
|
+
});
|
|
75
|
+
expect(result).toEqual({
|
|
76
|
+
id: "doc-1",
|
|
77
|
+
title: "Hello",
|
|
78
|
+
count: 42,
|
|
79
|
+
active: true,
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
it("encodes flat document with vector field", () => {
|
|
83
|
+
const result = DOCUMENT.encode({
|
|
84
|
+
id: "doc-2",
|
|
85
|
+
vector: [0.1, 0.2, 0.3],
|
|
86
|
+
});
|
|
87
|
+
expect(result).toEqual({
|
|
88
|
+
id: "doc-2",
|
|
89
|
+
vector: [0.1, 0.2, 0.3],
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
it("throws if id is missing", () => {
|
|
93
|
+
expect(() => DOCUMENT.encode({ title: "No ID" })).toThrow('Document must have a string "id" field');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe("PATCH", () => {
|
|
97
|
+
it("encodes flat patch with field updates", () => {
|
|
98
|
+
const result = PATCH.encode({
|
|
99
|
+
id: "doc-1",
|
|
100
|
+
title: "Updated",
|
|
101
|
+
count: 100,
|
|
102
|
+
});
|
|
103
|
+
expect(result).toEqual({
|
|
104
|
+
id: "doc-1",
|
|
105
|
+
title: "Updated",
|
|
106
|
+
count: 100,
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
it("encodes null values to unset fields", () => {
|
|
110
|
+
const result = PATCH.encode({
|
|
111
|
+
id: "doc-1",
|
|
112
|
+
title: "Keep this",
|
|
113
|
+
description: null,
|
|
114
|
+
});
|
|
115
|
+
expect(result).toEqual({
|
|
116
|
+
id: "doc-1",
|
|
117
|
+
title: "Keep this",
|
|
118
|
+
description: null,
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
it("encodes vector updates", () => {
|
|
122
|
+
const result = PATCH.encode({
|
|
123
|
+
id: "doc-1",
|
|
124
|
+
vector: [0.5, 0.6],
|
|
125
|
+
});
|
|
126
|
+
expect(result).toEqual({
|
|
127
|
+
id: "doc-1",
|
|
128
|
+
vector: [0.5, 0.6],
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
it("throws if id is missing", () => {
|
|
132
|
+
expect(() => PATCH.encode({ title: "No ID" })).toThrow('Patch must have a string "id" field');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
describe("FILTER", () => {
|
|
136
|
+
// Simple equality (shorthand)
|
|
137
|
+
it("encodes simple equality filter", () => {
|
|
138
|
+
expect(FILTER.encode({ f: "x" })).toEqual(["f", "Eq", "x"]);
|
|
139
|
+
expect(FILTER.encode({ f: 42 })).toEqual(["f", "Eq", 42]);
|
|
140
|
+
expect(FILTER.encode({ f: true })).toEqual(["f", "Eq", true]);
|
|
141
|
+
expect(FILTER.encode({ f: null })).toEqual(["f", "Eq", null]);
|
|
142
|
+
});
|
|
143
|
+
// Field operators
|
|
144
|
+
it("encodes $eq operator", () => {
|
|
145
|
+
expect(FILTER.encode({ f: { $eq: "x" } })).toEqual(["f", "Eq", "x"]);
|
|
146
|
+
});
|
|
147
|
+
it("encodes $neq operator", () => {
|
|
148
|
+
expect(FILTER.encode({ f: { $neq: "x" } })).toEqual(["f", "NotEq", "x"]);
|
|
149
|
+
});
|
|
150
|
+
it("encodes comparison operators", () => {
|
|
151
|
+
expect(FILTER.encode({ f: { $gt: 10 } })).toEqual(["f", "Gt", 10]);
|
|
152
|
+
expect(FILTER.encode({ f: { $gte: 10 } })).toEqual(["f", "Gte", 10]);
|
|
153
|
+
expect(FILTER.encode({ f: { $lt: 10 } })).toEqual(["f", "Lt", 10]);
|
|
154
|
+
expect(FILTER.encode({ f: { $lte: 10 } })).toEqual(["f", "Lte", 10]);
|
|
155
|
+
});
|
|
156
|
+
it("encodes set membership operators", () => {
|
|
157
|
+
expect(FILTER.encode({ f: { $in: ["a", "b"] } })).toEqual(["f", "In", ["a", "b"]]);
|
|
158
|
+
expect(FILTER.encode({ f: { $nin: ["a", "b"] } })).toEqual(["f", "NotIn", ["a", "b"]]);
|
|
159
|
+
});
|
|
160
|
+
it("encodes $contains operator", () => {
|
|
161
|
+
expect(FILTER.encode({ f: { $contains: "x" } })).toEqual(["f", "Contains", "x"]);
|
|
162
|
+
});
|
|
163
|
+
it("encodes string pattern operators as Glob", () => {
|
|
164
|
+
expect(FILTER.encode({ f: { $startsWith: "pre" } })).toEqual(["f", "Glob", "pre*"]);
|
|
165
|
+
expect(FILTER.encode({ f: { $endsWith: "suf" } })).toEqual(["f", "Glob", "*suf"]);
|
|
166
|
+
});
|
|
167
|
+
it("encodes $exists operator", () => {
|
|
168
|
+
expect(FILTER.encode({ f: { $exists: true } })).toEqual(["f", "NotEq", null]);
|
|
169
|
+
expect(FILTER.encode({ f: { $exists: false } })).toEqual(["f", "Eq", null]);
|
|
170
|
+
});
|
|
171
|
+
// Logical operators
|
|
172
|
+
it("encodes $and filter", () => {
|
|
173
|
+
const result = FILTER.encode({
|
|
174
|
+
$and: [{ a: 1 }, { b: 2 }],
|
|
175
|
+
});
|
|
176
|
+
expect(result).toEqual([
|
|
177
|
+
"And",
|
|
178
|
+
[
|
|
179
|
+
["a", "Eq", 1],
|
|
180
|
+
["b", "Eq", 2],
|
|
181
|
+
],
|
|
182
|
+
]);
|
|
183
|
+
});
|
|
184
|
+
it("encodes $or filter", () => {
|
|
185
|
+
const result = FILTER.encode({
|
|
186
|
+
$or: [{ status: "active" }, { status: "pending" }],
|
|
187
|
+
});
|
|
188
|
+
expect(result).toEqual([
|
|
189
|
+
"Or",
|
|
190
|
+
[
|
|
191
|
+
["status", "Eq", "active"],
|
|
192
|
+
["status", "Eq", "pending"],
|
|
193
|
+
],
|
|
194
|
+
]);
|
|
195
|
+
});
|
|
196
|
+
it("encodes $not filter", () => {
|
|
197
|
+
const result = FILTER.encode({
|
|
198
|
+
$not: { deleted: true },
|
|
199
|
+
});
|
|
200
|
+
expect(result).toEqual(["Not", ["deleted", "Eq", true]]);
|
|
201
|
+
});
|
|
202
|
+
it("encodes deeply nested filters", () => {
|
|
203
|
+
const result = FILTER.encode({
|
|
204
|
+
$and: [
|
|
205
|
+
{ active: true },
|
|
206
|
+
{
|
|
207
|
+
$or: [
|
|
208
|
+
{ role: "admin" },
|
|
209
|
+
{
|
|
210
|
+
$and: [{ role: "user" }, { verified: true }],
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
});
|
|
216
|
+
expect(result).toEqual([
|
|
217
|
+
"And",
|
|
218
|
+
[
|
|
219
|
+
["active", "Eq", true],
|
|
220
|
+
[
|
|
221
|
+
"Or",
|
|
222
|
+
[
|
|
223
|
+
["role", "Eq", "admin"],
|
|
224
|
+
[
|
|
225
|
+
"And",
|
|
226
|
+
[
|
|
227
|
+
["role", "Eq", "user"],
|
|
228
|
+
["verified", "Eq", true],
|
|
229
|
+
],
|
|
230
|
+
],
|
|
231
|
+
],
|
|
232
|
+
],
|
|
233
|
+
],
|
|
234
|
+
]);
|
|
235
|
+
});
|
|
236
|
+
it("encodes multiple fields as implicit AND", () => {
|
|
237
|
+
const result = FILTER.encode({ a: 1, b: 2 });
|
|
238
|
+
expect(result).toEqual([
|
|
239
|
+
"And",
|
|
240
|
+
[
|
|
241
|
+
["a", "Eq", 1],
|
|
242
|
+
["b", "Eq", 2],
|
|
243
|
+
],
|
|
244
|
+
]);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
describe("QUERY", () => {
|
|
248
|
+
it("encodes vector search query", () => {
|
|
249
|
+
const result = QUERY.encode({
|
|
250
|
+
query: [{ vector: [0.1, 0.2, 0.3] }],
|
|
251
|
+
topK: 10,
|
|
252
|
+
});
|
|
253
|
+
expect(result.rank_by).toEqual(["vector", "ANN", [0.1, 0.2, 0.3]]);
|
|
254
|
+
expect(result.top_k).toBe(10);
|
|
255
|
+
});
|
|
256
|
+
it("encodes text search query", () => {
|
|
257
|
+
const result = QUERY.encode({
|
|
258
|
+
query: [{ content: "hello world" }],
|
|
259
|
+
topK: 5,
|
|
260
|
+
});
|
|
261
|
+
expect(result.rank_by).toEqual(["content", "BM25", "hello world"]);
|
|
262
|
+
expect(result.top_k).toBe(5);
|
|
263
|
+
});
|
|
264
|
+
it("encodes multi-field text search as Sum", () => {
|
|
265
|
+
const result = QUERY.encode({
|
|
266
|
+
query: [{ title: "search term" }, { body: "search term" }],
|
|
267
|
+
topK: 10,
|
|
268
|
+
});
|
|
269
|
+
expect(result.rank_by).toEqual([
|
|
270
|
+
"Sum",
|
|
271
|
+
[
|
|
272
|
+
["title", "BM25", "search term"],
|
|
273
|
+
["body", "BM25", "search term"],
|
|
274
|
+
],
|
|
275
|
+
]);
|
|
276
|
+
});
|
|
277
|
+
it("encodes filter", () => {
|
|
278
|
+
const result = QUERY.encode({
|
|
279
|
+
query: [{ vector: [0.1] }],
|
|
280
|
+
topK: 5,
|
|
281
|
+
filter: { category: { $eq: "news" } },
|
|
282
|
+
});
|
|
283
|
+
expect(result.filters).toEqual(["category", "Eq", "news"]);
|
|
284
|
+
});
|
|
285
|
+
it("encodes include fields array", () => {
|
|
286
|
+
const result = QUERY.encode({
|
|
287
|
+
query: [{ vector: [0.1] }],
|
|
288
|
+
topK: 5,
|
|
289
|
+
include: ["title", "content"],
|
|
290
|
+
});
|
|
291
|
+
expect(result.include_attributes).toEqual(["title", "content"]);
|
|
292
|
+
});
|
|
293
|
+
it("encodes include as boolean", () => {
|
|
294
|
+
const result = QUERY.encode({
|
|
295
|
+
query: [{ vector: [0.1] }],
|
|
296
|
+
topK: 5,
|
|
297
|
+
include: true,
|
|
298
|
+
});
|
|
299
|
+
expect(result.include_attributes).toBe(true);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
describe("SEARCH_HIT", () => {
|
|
303
|
+
it("decodes row to search hit", () => {
|
|
304
|
+
const result = SEARCH_HIT.decode({
|
|
305
|
+
id: "doc-1",
|
|
306
|
+
$dist: 0.5,
|
|
307
|
+
title: "Test",
|
|
308
|
+
category: "news",
|
|
309
|
+
}, "my-index");
|
|
310
|
+
expect(result).toEqual({
|
|
311
|
+
id: "doc-1",
|
|
312
|
+
index: "my-index",
|
|
313
|
+
score: -0.5, // negated: distance → similarity
|
|
314
|
+
document: {
|
|
315
|
+
id: "doc-1",
|
|
316
|
+
title: "Test",
|
|
317
|
+
category: "news",
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
it("decodes row with vector", () => {
|
|
322
|
+
const result = SEARCH_HIT.decode({
|
|
323
|
+
id: "doc-2",
|
|
324
|
+
$dist: 0.1,
|
|
325
|
+
vector: [0.1, 0.2, 0.3],
|
|
326
|
+
}, "my-index");
|
|
327
|
+
expect(result.document?.id).toBe("doc-2");
|
|
328
|
+
expect(result.document?.vector).toEqual([0.1, 0.2, 0.3]);
|
|
329
|
+
});
|
|
330
|
+
it("handles missing score", () => {
|
|
331
|
+
const result = SEARCH_HIT.decode({
|
|
332
|
+
id: "doc-3",
|
|
333
|
+
}, "my-index");
|
|
334
|
+
expect(result.score).toBe(0);
|
|
335
|
+
expect(result.document).toEqual({ id: "doc-3" });
|
|
336
|
+
});
|
|
337
|
+
it("converts numeric id to string", () => {
|
|
338
|
+
const result = SEARCH_HIT.decode({
|
|
339
|
+
id: 123,
|
|
340
|
+
$dist: 0.1,
|
|
341
|
+
}, "my-index");
|
|
342
|
+
expect(result.id).toBe("123");
|
|
343
|
+
expect(result.document?.id).toBe(123); // document keeps original type
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/filter.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
|