@functional-systems/lambdadb 0.3.4 → 0.4.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/README.md +42 -0
- package/dist/commonjs/compat/qdrant/client.d.ts +210 -0
- package/dist/commonjs/compat/qdrant/client.d.ts.map +1 -0
- package/dist/commonjs/compat/qdrant/client.js +573 -0
- package/dist/commonjs/compat/qdrant/client.js.map +1 -0
- package/dist/commonjs/compat/qdrant/conversions.d.ts +40 -0
- package/dist/commonjs/compat/qdrant/conversions.d.ts.map +1 -0
- package/dist/commonjs/compat/qdrant/conversions.js +386 -0
- package/dist/commonjs/compat/qdrant/conversions.js.map +1 -0
- package/dist/commonjs/compat/qdrant/errors.d.ts +10 -0
- package/dist/commonjs/compat/qdrant/errors.d.ts.map +1 -0
- package/dist/commonjs/compat/qdrant/errors.js +25 -0
- package/dist/commonjs/compat/qdrant/errors.js.map +1 -0
- package/dist/commonjs/compat/qdrant/filters.d.ts +10 -0
- package/dist/commonjs/compat/qdrant/filters.d.ts.map +1 -0
- package/dist/commonjs/compat/qdrant/filters.js +172 -0
- package/dist/commonjs/compat/qdrant/filters.js.map +1 -0
- package/dist/commonjs/compat/qdrant/models.d.ts +198 -0
- package/dist/commonjs/compat/qdrant/models.d.ts.map +1 -0
- package/dist/commonjs/compat/qdrant/models.js +205 -0
- package/dist/commonjs/compat/qdrant/models.js.map +1 -0
- package/dist/commonjs/compat/qdrant.d.ts +4 -0
- package/dist/commonjs/compat/qdrant.d.ts.map +1 -0
- package/dist/commonjs/compat/qdrant.js +45 -0
- package/dist/commonjs/compat/qdrant.js.map +1 -0
- package/dist/esm/compat/qdrant/client.d.ts +210 -0
- package/dist/esm/compat/qdrant/client.d.ts.map +1 -0
- package/dist/esm/compat/qdrant/client.js +536 -0
- package/dist/esm/compat/qdrant/client.js.map +1 -0
- package/dist/esm/compat/qdrant/conversions.d.ts +40 -0
- package/dist/esm/compat/qdrant/conversions.d.ts.map +1 -0
- package/dist/esm/compat/qdrant/conversions.js +336 -0
- package/dist/esm/compat/qdrant/conversions.js.map +1 -0
- package/dist/esm/compat/qdrant/errors.d.ts +10 -0
- package/dist/esm/compat/qdrant/errors.d.ts.map +1 -0
- package/dist/esm/compat/qdrant/errors.js +19 -0
- package/dist/esm/compat/qdrant/errors.js.map +1 -0
- package/dist/esm/compat/qdrant/filters.d.ts +10 -0
- package/dist/esm/compat/qdrant/filters.d.ts.map +1 -0
- package/dist/esm/compat/qdrant/filters.js +169 -0
- package/dist/esm/compat/qdrant/filters.js.map +1 -0
- package/dist/esm/compat/qdrant/models.d.ts +198 -0
- package/dist/esm/compat/qdrant/models.d.ts.map +1 -0
- package/dist/esm/compat/qdrant/models.js +185 -0
- package/dist/esm/compat/qdrant/models.js.map +1 -0
- package/dist/esm/compat/qdrant.d.ts +4 -0
- package/dist/esm/compat/qdrant.d.ts.map +1 -0
- package/dist/esm/compat/qdrant.js +4 -0
- package/dist/esm/compat/qdrant.js.map +1 -0
- package/jsr.json +3 -1
- package/package.json +32 -1
- package/src/compat/qdrant/client.ts +901 -0
- package/src/compat/qdrant/conversions.ts +422 -0
- package/src/compat/qdrant/errors.ts +20 -0
- package/src/compat/qdrant/filters.ts +213 -0
- package/src/compat/qdrant/models.ts +317 -0
- package/src/compat/qdrant.ts +3 -0
- package/test/integration/qdrant-compat-external.test.mjs +226 -0
- package/test/integration/qdrant-compat-live.test.mjs +122 -0
- package/test/qdrant-compat.test.mjs +402 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
QdrantClient,
|
|
6
|
+
QdrantCompatClient,
|
|
7
|
+
UnsupportedQdrantFeatureError,
|
|
8
|
+
models,
|
|
9
|
+
} from "../dist/esm/compat/qdrant.js";
|
|
10
|
+
import { PointStruct } from "../dist/esm/compat/qdrant/models.js";
|
|
11
|
+
|
|
12
|
+
class FakeDocs {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.upserts = [];
|
|
15
|
+
this.deletes = [];
|
|
16
|
+
this.fetches = [];
|
|
17
|
+
this.lists = [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async upsert(body) {
|
|
21
|
+
this.upserts.push(body);
|
|
22
|
+
return { message: "ok" };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async fetch(body) {
|
|
26
|
+
this.fetches.push(body);
|
|
27
|
+
return {
|
|
28
|
+
docs: [
|
|
29
|
+
{
|
|
30
|
+
collection: "docs",
|
|
31
|
+
doc: {
|
|
32
|
+
id: "1",
|
|
33
|
+
_qdrant_id: 1,
|
|
34
|
+
_qdrant_vector: [0.1, 0.2],
|
|
35
|
+
tenant: "acme",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async delete(body) {
|
|
43
|
+
this.deletes.push(body);
|
|
44
|
+
return { message: "ok" };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async list(body = {}) {
|
|
48
|
+
this.lists.push(body);
|
|
49
|
+
return {
|
|
50
|
+
docs: [
|
|
51
|
+
{
|
|
52
|
+
collection: "docs",
|
|
53
|
+
doc: { id: "1", _qdrant_id: 1, tenant: "acme" },
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
nextPageToken: undefined,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
class FakeCollection {
|
|
62
|
+
constructor(owner, name) {
|
|
63
|
+
this.owner = owner;
|
|
64
|
+
this.name = name;
|
|
65
|
+
this.docs = new FakeDocs();
|
|
66
|
+
this.queries = [];
|
|
67
|
+
this.updates = [];
|
|
68
|
+
this.deletes = [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async get() {
|
|
72
|
+
this.owner.gets.push({ collectionName: this.name });
|
|
73
|
+
return {
|
|
74
|
+
collection: {
|
|
75
|
+
collectionName: this.name,
|
|
76
|
+
indexConfigs: this.owner.indexConfigs,
|
|
77
|
+
numDocs: this.owner.numDocs,
|
|
78
|
+
collectionStatus: "ACTIVE",
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async update(body) {
|
|
84
|
+
this.updates.push(body);
|
|
85
|
+
this.owner.indexConfigs = body.indexConfigs;
|
|
86
|
+
return { collection: { collectionName: this.name, indexConfigs: body.indexConfigs } };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async delete() {
|
|
90
|
+
this.deletes.push({});
|
|
91
|
+
return { message: "ok" };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async query(body) {
|
|
95
|
+
this.queries.push(body);
|
|
96
|
+
return {
|
|
97
|
+
docs: [
|
|
98
|
+
{
|
|
99
|
+
collection: this.name,
|
|
100
|
+
score: 0.9,
|
|
101
|
+
doc: {
|
|
102
|
+
id: "1",
|
|
103
|
+
_qdrant_id: 1,
|
|
104
|
+
_qdrant_vector: [0.1, 0.2],
|
|
105
|
+
tenant: "acme",
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
class FakeLambdaDB {
|
|
114
|
+
constructor() {
|
|
115
|
+
this.created = [];
|
|
116
|
+
this.gets = [];
|
|
117
|
+
this.indexConfigs = {};
|
|
118
|
+
this.numDocs = 0;
|
|
119
|
+
this.collectionsByName = new Map();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async createCollection(body) {
|
|
123
|
+
this.created.push(body);
|
|
124
|
+
this.indexConfigs = body.indexConfigs ?? {};
|
|
125
|
+
return { collection: { collectionName: body.collectionName } };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
collection(name) {
|
|
129
|
+
if (!this.collectionsByName.has(name)) {
|
|
130
|
+
this.collectionsByName.set(name, new FakeCollection(this, name));
|
|
131
|
+
}
|
|
132
|
+
return this.collectionsByName.get(name);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
test("qdrant compatibility exports public client and models", () => {
|
|
137
|
+
assert.equal(QdrantClient, QdrantCompatClient);
|
|
138
|
+
assert.equal(models.PointStruct, PointStruct);
|
|
139
|
+
|
|
140
|
+
const point = new models.PointStruct({
|
|
141
|
+
id: 1,
|
|
142
|
+
vector: [0.1, 0.2],
|
|
143
|
+
payload: { tenant: "acme" },
|
|
144
|
+
});
|
|
145
|
+
assert.equal(point.id, 1);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("createCollection maps vector params and payload schema", async () => {
|
|
149
|
+
const fake = new FakeLambdaDB();
|
|
150
|
+
const client = new QdrantCompatClient(fake);
|
|
151
|
+
|
|
152
|
+
assert.equal(
|
|
153
|
+
await client.createCollection("docs", {
|
|
154
|
+
vectorsConfig: new models.VectorParams({
|
|
155
|
+
size: 3,
|
|
156
|
+
distance: models.Distance.DOT,
|
|
157
|
+
}),
|
|
158
|
+
payloadSchema: {
|
|
159
|
+
tenant: models.PayloadSchemaType.KEYWORD,
|
|
160
|
+
views: models.PayloadSchemaType.INTEGER,
|
|
161
|
+
score: models.PayloadSchemaType.FLOAT,
|
|
162
|
+
active: models.PayloadSchemaType.BOOL,
|
|
163
|
+
},
|
|
164
|
+
}),
|
|
165
|
+
true,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
assert.deepEqual(fake.created, [
|
|
169
|
+
{
|
|
170
|
+
collectionName: "docs",
|
|
171
|
+
indexConfigs: {
|
|
172
|
+
_qdrant_vector: {
|
|
173
|
+
type: "vector",
|
|
174
|
+
dimensions: 3,
|
|
175
|
+
similarity: "dot_product",
|
|
176
|
+
},
|
|
177
|
+
tenant: { type: "keyword" },
|
|
178
|
+
views: { type: "long" },
|
|
179
|
+
score: { type: "double" },
|
|
180
|
+
active: { type: "boolean" },
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("createPayloadIndex merges existing configs and rejects non-empty backfill", async () => {
|
|
187
|
+
const fake = new FakeLambdaDB();
|
|
188
|
+
fake.indexConfigs = {
|
|
189
|
+
_qdrant_vector: {
|
|
190
|
+
type: "vector",
|
|
191
|
+
dimensions: 3,
|
|
192
|
+
similarity: "cosine",
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
const client = new QdrantCompatClient(fake);
|
|
196
|
+
|
|
197
|
+
assert.equal(
|
|
198
|
+
await client.createPayloadIndex(
|
|
199
|
+
"docs",
|
|
200
|
+
"tenant",
|
|
201
|
+
models.PayloadSchemaType.KEYWORD,
|
|
202
|
+
),
|
|
203
|
+
true,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
assert.deepEqual(fake.collection("docs").updates, [
|
|
207
|
+
{
|
|
208
|
+
indexConfigs: {
|
|
209
|
+
_qdrant_vector: {
|
|
210
|
+
type: "vector",
|
|
211
|
+
dimensions: 3,
|
|
212
|
+
similarity: "cosine",
|
|
213
|
+
},
|
|
214
|
+
tenant: { type: "keyword" },
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
]);
|
|
218
|
+
|
|
219
|
+
fake.numDocs = 3;
|
|
220
|
+
await assert.rejects(
|
|
221
|
+
client.createPayloadIndex("docs", "views", models.PayloadSchemaType.INTEGER),
|
|
222
|
+
UnsupportedQdrantFeatureError,
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("upsert maps qdrant points to LambdaDB documents", async () => {
|
|
227
|
+
const fake = new FakeLambdaDB();
|
|
228
|
+
const client = new QdrantCompatClient(fake);
|
|
229
|
+
|
|
230
|
+
const result = await client.upsert("docs", {
|
|
231
|
+
points: [
|
|
232
|
+
new models.PointStruct({
|
|
233
|
+
id: 1,
|
|
234
|
+
vector: [0.1, 0.2],
|
|
235
|
+
payload: { tenant: "acme" },
|
|
236
|
+
}),
|
|
237
|
+
],
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
assert.equal(result.status, models.UpdateStatus.COMPLETED);
|
|
241
|
+
assert.deepEqual(fake.collection("docs").docs.upserts, [
|
|
242
|
+
{
|
|
243
|
+
docs: [
|
|
244
|
+
{
|
|
245
|
+
id: "1",
|
|
246
|
+
_qdrant_id: 1,
|
|
247
|
+
_qdrant_vector: [0.1, 0.2],
|
|
248
|
+
tenant: "acme",
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
]);
|
|
253
|
+
|
|
254
|
+
await assert.rejects(
|
|
255
|
+
client.upsert("docs", {
|
|
256
|
+
points: [{ id: 1, vector: [0.1], payload: { _qdrant_vector: [0.2] } }],
|
|
257
|
+
}),
|
|
258
|
+
/reserved prefix/,
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
await assert.rejects(
|
|
262
|
+
client.upsert("docs", {
|
|
263
|
+
points: [
|
|
264
|
+
{
|
|
265
|
+
id: 1,
|
|
266
|
+
vector: {
|
|
267
|
+
sparse: new models.SparseVector({ indices: [1], values: [0.9] }),
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
}),
|
|
272
|
+
UnsupportedQdrantFeatureError,
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("queryPoints maps dense vectors, filters, and responses", async () => {
|
|
277
|
+
const fake = new FakeLambdaDB();
|
|
278
|
+
const client = new QdrantCompatClient(fake);
|
|
279
|
+
|
|
280
|
+
const response = await client.queryPoints("docs", {
|
|
281
|
+
query: [0.1, 0.2],
|
|
282
|
+
queryFilter: new models.Filter({
|
|
283
|
+
must: [
|
|
284
|
+
new models.FieldCondition({
|
|
285
|
+
key: "tenant",
|
|
286
|
+
match: new models.MatchValue({ value: "acme" }),
|
|
287
|
+
}),
|
|
288
|
+
],
|
|
289
|
+
mustNot: [
|
|
290
|
+
new models.FieldCondition({
|
|
291
|
+
key: "status",
|
|
292
|
+
match: new models.MatchValue({ value: "deleted" }),
|
|
293
|
+
}),
|
|
294
|
+
],
|
|
295
|
+
}),
|
|
296
|
+
limit: 5,
|
|
297
|
+
withVectors: true,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
assert.equal(response.points[0].id, 1);
|
|
301
|
+
assert.equal(response.points[0].score, 0.9);
|
|
302
|
+
assert.deepEqual(response.points[0].payload, { tenant: "acme" });
|
|
303
|
+
assert.deepEqual(response.points[0].vector, [0.1, 0.2]);
|
|
304
|
+
assert.deepEqual(fake.collection("docs").queries, [
|
|
305
|
+
{
|
|
306
|
+
query: {
|
|
307
|
+
knn: {
|
|
308
|
+
field: "_qdrant_vector",
|
|
309
|
+
k: 5,
|
|
310
|
+
queryVector: [0.1, 0.2],
|
|
311
|
+
filter: {
|
|
312
|
+
bool: [
|
|
313
|
+
{ queryString: { query: "tenant:acme" }, occur: "filter" },
|
|
314
|
+
{ queryString: { query: "status:deleted" }, occur: "must_not" },
|
|
315
|
+
],
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
size: 5,
|
|
320
|
+
consistentRead: true,
|
|
321
|
+
includeVectors: true,
|
|
322
|
+
},
|
|
323
|
+
]);
|
|
324
|
+
|
|
325
|
+
await assert.rejects(
|
|
326
|
+
client.queryPoints("docs", { query: [0.1], offset: 1 }),
|
|
327
|
+
UnsupportedQdrantFeatureError,
|
|
328
|
+
);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("retrieve, delete, scroll, count, and getCollection return qdrant-style objects", async () => {
|
|
332
|
+
const fake = new FakeLambdaDB();
|
|
333
|
+
fake.indexConfigs = {
|
|
334
|
+
_qdrant_vector: {
|
|
335
|
+
type: "vector",
|
|
336
|
+
dimensions: 2,
|
|
337
|
+
similarity: "cosine",
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
fake.numDocs = 12;
|
|
341
|
+
const client = new QdrantCompatClient(fake);
|
|
342
|
+
|
|
343
|
+
const collection = await client.getCollection("docs");
|
|
344
|
+
assert.equal(collection.config.params.vectors.size, 2);
|
|
345
|
+
assert.equal(collection.config.params.vectors.distance, models.Distance.COSINE);
|
|
346
|
+
|
|
347
|
+
const records = await client.retrieve("docs", { ids: [1], withVectors: true });
|
|
348
|
+
assert.equal(records[0].id, 1);
|
|
349
|
+
assert.deepEqual(records[0].payload, { tenant: "acme" });
|
|
350
|
+
assert.deepEqual(records[0].vector, [0.1, 0.2]);
|
|
351
|
+
assert.deepEqual(fake.collection("docs").docs.fetches, [
|
|
352
|
+
{ ids: ["1"], consistentRead: true, includeVectors: true },
|
|
353
|
+
]);
|
|
354
|
+
|
|
355
|
+
const deleteResult = await client.delete("docs", { pointsSelector: [1] });
|
|
356
|
+
assert.equal(deleteResult.status, models.UpdateStatus.COMPLETED);
|
|
357
|
+
assert.deepEqual(fake.collection("docs").docs.deletes, [{ ids: ["1"] }]);
|
|
358
|
+
|
|
359
|
+
await client.delete("docs", {
|
|
360
|
+
filter: new models.Filter({
|
|
361
|
+
must: [
|
|
362
|
+
new models.FieldCondition({
|
|
363
|
+
key: "tenant",
|
|
364
|
+
match: new models.MatchValue({ value: "acme" }),
|
|
365
|
+
}),
|
|
366
|
+
],
|
|
367
|
+
}),
|
|
368
|
+
});
|
|
369
|
+
await client.delete("docs", {
|
|
370
|
+
pointsSelector: {
|
|
371
|
+
filter: {
|
|
372
|
+
must: [
|
|
373
|
+
{
|
|
374
|
+
key: "status",
|
|
375
|
+
match: { value: "archived" },
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
assert.deepEqual(fake.collection("docs").docs.deletes, [
|
|
382
|
+
{ ids: ["1"] },
|
|
383
|
+
{
|
|
384
|
+
filter: {
|
|
385
|
+
queryString: { query: "tenant:acme" },
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
filter: {
|
|
390
|
+
queryString: { query: "status:archived" },
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
]);
|
|
394
|
+
|
|
395
|
+
const [scrollRecords, nextOffset] = await client.scroll("docs", { limit: 3 });
|
|
396
|
+
assert.equal(scrollRecords[0].id, 1);
|
|
397
|
+
assert.equal(nextOffset, undefined);
|
|
398
|
+
assert.deepEqual(fake.collection("docs").docs.lists, [{ size: 3 }]);
|
|
399
|
+
|
|
400
|
+
const count = await client.count("docs");
|
|
401
|
+
assert.equal(count.count, 12);
|
|
402
|
+
});
|