@functional-systems/lambdadb 0.3.4-dev.0 → 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.
Files changed (70) hide show
  1. package/README.md +50 -7
  2. package/dist/commonjs/compat/qdrant/client.d.ts +210 -0
  3. package/dist/commonjs/compat/qdrant/client.d.ts.map +1 -0
  4. package/dist/commonjs/compat/qdrant/client.js +573 -0
  5. package/dist/commonjs/compat/qdrant/client.js.map +1 -0
  6. package/dist/commonjs/compat/qdrant/conversions.d.ts +40 -0
  7. package/dist/commonjs/compat/qdrant/conversions.d.ts.map +1 -0
  8. package/dist/commonjs/compat/qdrant/conversions.js +386 -0
  9. package/dist/commonjs/compat/qdrant/conversions.js.map +1 -0
  10. package/dist/commonjs/compat/qdrant/errors.d.ts +10 -0
  11. package/dist/commonjs/compat/qdrant/errors.d.ts.map +1 -0
  12. package/dist/commonjs/compat/qdrant/errors.js +25 -0
  13. package/dist/commonjs/compat/qdrant/errors.js.map +1 -0
  14. package/dist/commonjs/compat/qdrant/filters.d.ts +10 -0
  15. package/dist/commonjs/compat/qdrant/filters.d.ts.map +1 -0
  16. package/dist/commonjs/compat/qdrant/filters.js +172 -0
  17. package/dist/commonjs/compat/qdrant/filters.js.map +1 -0
  18. package/dist/commonjs/compat/qdrant/models.d.ts +198 -0
  19. package/dist/commonjs/compat/qdrant/models.d.ts.map +1 -0
  20. package/dist/commonjs/compat/qdrant/models.js +205 -0
  21. package/dist/commonjs/compat/qdrant/models.js.map +1 -0
  22. package/dist/commonjs/compat/qdrant.d.ts +4 -0
  23. package/dist/commonjs/compat/qdrant.d.ts.map +1 -0
  24. package/dist/commonjs/compat/qdrant.js +45 -0
  25. package/dist/commonjs/compat/qdrant.js.map +1 -0
  26. package/dist/commonjs/models/indexconfigsunion.d.ts +2 -2
  27. package/dist/commonjs/models/indexconfigsunion.d.ts.map +1 -1
  28. package/dist/commonjs/models/indexconfigsunion.js +4 -4
  29. package/dist/commonjs/models/indexconfigsunion.js.map +1 -1
  30. package/dist/esm/compat/qdrant/client.d.ts +210 -0
  31. package/dist/esm/compat/qdrant/client.d.ts.map +1 -0
  32. package/dist/esm/compat/qdrant/client.js +536 -0
  33. package/dist/esm/compat/qdrant/client.js.map +1 -0
  34. package/dist/esm/compat/qdrant/conversions.d.ts +40 -0
  35. package/dist/esm/compat/qdrant/conversions.d.ts.map +1 -0
  36. package/dist/esm/compat/qdrant/conversions.js +336 -0
  37. package/dist/esm/compat/qdrant/conversions.js.map +1 -0
  38. package/dist/esm/compat/qdrant/errors.d.ts +10 -0
  39. package/dist/esm/compat/qdrant/errors.d.ts.map +1 -0
  40. package/dist/esm/compat/qdrant/errors.js +19 -0
  41. package/dist/esm/compat/qdrant/errors.js.map +1 -0
  42. package/dist/esm/compat/qdrant/filters.d.ts +10 -0
  43. package/dist/esm/compat/qdrant/filters.d.ts.map +1 -0
  44. package/dist/esm/compat/qdrant/filters.js +169 -0
  45. package/dist/esm/compat/qdrant/filters.js.map +1 -0
  46. package/dist/esm/compat/qdrant/models.d.ts +198 -0
  47. package/dist/esm/compat/qdrant/models.d.ts.map +1 -0
  48. package/dist/esm/compat/qdrant/models.js +185 -0
  49. package/dist/esm/compat/qdrant/models.js.map +1 -0
  50. package/dist/esm/compat/qdrant.d.ts +4 -0
  51. package/dist/esm/compat/qdrant.d.ts.map +1 -0
  52. package/dist/esm/compat/qdrant.js +4 -0
  53. package/dist/esm/compat/qdrant.js.map +1 -0
  54. package/dist/esm/models/indexconfigsunion.d.ts +2 -2
  55. package/dist/esm/models/indexconfigsunion.d.ts.map +1 -1
  56. package/dist/esm/models/indexconfigsunion.js +4 -4
  57. package/dist/esm/models/indexconfigsunion.js.map +1 -1
  58. package/jsr.json +3 -1
  59. package/package.json +32 -1
  60. package/src/compat/qdrant/client.ts +901 -0
  61. package/src/compat/qdrant/conversions.ts +422 -0
  62. package/src/compat/qdrant/errors.ts +20 -0
  63. package/src/compat/qdrant/filters.ts +213 -0
  64. package/src/compat/qdrant/models.ts +317 -0
  65. package/src/compat/qdrant.ts +3 -0
  66. package/src/models/indexconfigsunion.ts +6 -6
  67. package/test/integration/qdrant-compat-external.test.mjs +226 -0
  68. package/test/integration/qdrant-compat-live.test.mjs +122 -0
  69. package/test/public-api.test.mjs +145 -4
  70. package/test/qdrant-compat.test.mjs +402 -0
@@ -5,6 +5,7 @@ import {
5
5
  BadRequestError,
6
6
  HTTPClient,
7
7
  LambdaDBClient,
8
+ SDKValidationError,
8
9
  } from "../dist/esm/index.js";
9
10
 
10
11
  const BASE_URL = "https://api.test";
@@ -147,15 +148,155 @@ test("public client supports managed embedding vector index configs", async () =
147
148
  indexConfigs: managedIndexConfigs,
148
149
  });
149
150
 
151
+ assert.equal(calls.length, 1);
152
+ assert.deepEqual(
153
+ result.collection.indexConfigs.bodyEmbedding.embedding,
154
+ {
155
+ ...managedIndexConfigs.bodyEmbedding.embedding,
156
+ dimensions: 1536,
157
+ similarity: "cosine",
158
+ },
159
+ );
160
+ });
161
+
162
+ test("managed embedding vector config supports optional embedding dimensions and similarity", async () => {
163
+ const { calls, client } = createClient((call) => {
164
+ assert.equal(call.method, "POST");
165
+ assert.deepEqual(JSON.parse(call.body), {
166
+ collectionName: "semantic-items",
167
+ indexConfigs: {
168
+ bodyEmbedding: {
169
+ type: "vector",
170
+ managedEmbedding: true,
171
+ embedding: {
172
+ provider: "openai",
173
+ model: "text-embedding-3-small",
174
+ sourceField: "body",
175
+ dimensions: 1536,
176
+ similarity: "cosine",
177
+ },
178
+ },
179
+ },
180
+ });
181
+
182
+ return jsonResponse(
183
+ {
184
+ collection: collectionFixture("semantic-items", {
185
+ indexConfigs: {
186
+ bodyEmbedding: {
187
+ type: "vector",
188
+ managedEmbedding: true,
189
+ embedding: {
190
+ provider: "openai",
191
+ model: "text-embedding-3-small",
192
+ sourceField: "body",
193
+ dimensions: 1536,
194
+ similarity: "cosine",
195
+ },
196
+ },
197
+ },
198
+ }),
199
+ },
200
+ { status: 202 },
201
+ );
202
+ });
203
+
204
+ const result = await client.createCollection({
205
+ collectionName: "semantic-items",
206
+ indexConfigs: {
207
+ bodyEmbedding: {
208
+ type: "vector",
209
+ managedEmbedding: true,
210
+ embedding: {
211
+ provider: "openai",
212
+ model: "text-embedding-3-small",
213
+ sourceField: "body",
214
+ dimensions: 1536,
215
+ similarity: "cosine",
216
+ },
217
+ },
218
+ },
219
+ });
220
+
150
221
  assert.equal(calls.length, 1);
151
222
  assert.equal(
152
223
  result.collection.indexConfigs.bodyEmbedding.embedding.dimensions,
153
224
  1536,
154
225
  );
155
- assert.equal(
156
- result.collection.indexConfigs.bodyEmbedding.embedding.similarity,
157
- "cosine",
158
- );
226
+ });
227
+
228
+ test("managed embedding vector config rejects invalid Java contract shapes", async () => {
229
+ const { calls, client } = createClient(() => {
230
+ throw new Error("request should not be sent for invalid index configs");
231
+ });
232
+
233
+ const invalidIndexConfigs = [
234
+ {
235
+ type: "vector",
236
+ dimensions: 1536,
237
+ embedding: {
238
+ provider: "openai",
239
+ model: "text-embedding-3-small",
240
+ sourceField: "body",
241
+ },
242
+ },
243
+ {
244
+ type: "vector",
245
+ managedEmbedding: false,
246
+ dimensions: 1536,
247
+ embedding: {
248
+ provider: "openai",
249
+ model: "text-embedding-3-small",
250
+ sourceField: "body",
251
+ },
252
+ },
253
+ {
254
+ type: "vector",
255
+ managedEmbedding: true,
256
+ dimensions: 1536,
257
+ embedding: {
258
+ provider: "openai",
259
+ model: "text-embedding-3-small",
260
+ sourceField: "body",
261
+ },
262
+ },
263
+ {
264
+ type: "vector",
265
+ managedEmbedding: true,
266
+ similarity: "cosine",
267
+ embedding: {
268
+ provider: "openai",
269
+ model: "text-embedding-3-small",
270
+ sourceField: "body",
271
+ },
272
+ },
273
+ {
274
+ type: "vector",
275
+ managedEmbedding: true,
276
+ },
277
+ {
278
+ type: "vector",
279
+ managedEmbedding: true,
280
+ embedding: {
281
+ provider: "openai",
282
+ model: "text-embedding-3-small",
283
+ sourceField: "body",
284
+ unsupported: true,
285
+ },
286
+ },
287
+ ];
288
+
289
+ for (const indexConfig of invalidIndexConfigs) {
290
+ const result = await client.createCollectionSafe({
291
+ collectionName: "invalid-vector",
292
+ indexConfigs: { bodyEmbedding: indexConfig },
293
+ });
294
+
295
+ assert.equal(result.ok, false);
296
+ assert.ok(result.error instanceof SDKValidationError);
297
+ }
298
+
299
+ assert.equal(calls.length, 0);
159
300
  });
160
301
 
161
302
  test("collection query supports managed embedding queryText KNN payloads", async () => {
@@ -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
+ });