@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
@@ -0,0 +1,901 @@
1
+ import { LambdaDBClient, type LambdaDBClientOptions } from "../../client.js";
2
+ import { ResourceNotFoundError } from "../../models/errors/index.js";
3
+ import type { CreateCollectionInput } from "../../types/public.js";
4
+ import {
5
+ mergeIndexConfigs,
6
+ payloadSchemaToIndexConfigs,
7
+ plainIndexConfigs,
8
+ pointsToDocs,
9
+ qdrantVectorsConfig,
10
+ queryVectorAndField,
11
+ resultToScoredPoint,
12
+ docToRecord,
13
+ vectorConfigToIndexConfigs,
14
+ type IndexConfigs,
15
+ } from "./conversions.js";
16
+ import { filterToLambdaDB } from "./filters.js";
17
+ import * as models from "./models.js";
18
+ import {
19
+ QdrantCompatError,
20
+ QdrantCompatValidationError,
21
+ UnsupportedQdrantFeatureError,
22
+ } from "./errors.js";
23
+
24
+ type JsonObject = { [key: string]: unknown };
25
+
26
+ type CollectionLike = {
27
+ get(): Promise<{ collection: JsonObject }>;
28
+ update(body: { indexConfigs: IndexConfigs }): Promise<unknown>;
29
+ delete(): Promise<unknown>;
30
+ query(body: JsonObject): Promise<{ docs: unknown[] }>;
31
+ docs: {
32
+ upsert(body: { docs: JsonObject[] }): Promise<unknown>;
33
+ fetch(body: JsonObject): Promise<{ docs: unknown[] }>;
34
+ delete(body: { ids?: string[]; filter?: JsonObject }): Promise<unknown>;
35
+ list(body?: { size?: number; pageToken?: string }): Promise<{
36
+ docs: unknown[];
37
+ nextPageToken?: string | undefined;
38
+ }>;
39
+ };
40
+ };
41
+
42
+ type LambdaDBLike = {
43
+ createCollection(request: CreateCollectionInput): Promise<unknown>;
44
+ listCollections?(): Promise<{ collections: Array<{ collectionName?: string; name?: string }> }>;
45
+ collection(name: string): CollectionLike;
46
+ };
47
+
48
+ type CompatConstructorOptions = LambdaDBClientOptions & {
49
+ project_api_key?: string | undefined;
50
+ apiKey?: string | undefined;
51
+ api_key?: string | undefined;
52
+ base_url?: string | undefined;
53
+ project_name?: string | undefined;
54
+ url?: string | undefined;
55
+ timeout?: number | undefined;
56
+ path?: string | undefined;
57
+ location?: string | undefined;
58
+ host?: string | undefined;
59
+ port?: number | undefined;
60
+ https?: boolean | undefined;
61
+ prefix?: string | undefined;
62
+ [key: string]: unknown;
63
+ };
64
+
65
+ type CreateCollectionOptions = {
66
+ vectorsConfig?: unknown;
67
+ vectors_config?: unknown;
68
+ payloadSchema?: unknown;
69
+ payload_schema?: unknown;
70
+ payloadIndexes?: unknown;
71
+ payload_indexes?: unknown;
72
+ payloadIndexConfigs?: unknown;
73
+ payload_index_configs?: unknown;
74
+ timeout?: number | undefined;
75
+ initFrom?: unknown;
76
+ init_from?: unknown;
77
+ [key: string]: unknown;
78
+ };
79
+
80
+ type QueryPointsOptions = {
81
+ query: unknown;
82
+ queryFilter?: models.Filter | JsonObject | null;
83
+ query_filter?: models.Filter | JsonObject | null;
84
+ limit?: number | undefined;
85
+ withPayload?: boolean | undefined;
86
+ with_payload?: boolean | undefined;
87
+ withVectors?: boolean | undefined;
88
+ with_vectors?: boolean | undefined;
89
+ using?: string | undefined;
90
+ searchParams?: models.SearchParams | JsonObject | null;
91
+ search_params?: models.SearchParams | JsonObject | null;
92
+ offset?: number | undefined;
93
+ scoreThreshold?: number | undefined;
94
+ score_threshold?: number | undefined;
95
+ consistency?: unknown;
96
+ shardKeySelector?: unknown;
97
+ shard_key_selector?: unknown;
98
+ [key: string]: unknown;
99
+ };
100
+
101
+ export class QdrantCompatClient {
102
+ private readonly client: LambdaDBLike;
103
+
104
+ constructor(clientOrOptions?: LambdaDBLike | CompatConstructorOptions) {
105
+ if (isLambdaDBLike(clientOrOptions)) {
106
+ this.client = clientOrOptions;
107
+ return;
108
+ }
109
+ this.client = new LambdaDBClient(normalizeConstructorOptions(clientOrOptions));
110
+ }
111
+
112
+ async collectionExists(collectionName: string): Promise<boolean> {
113
+ try {
114
+ await this.client.collection(collectionName).get();
115
+ return true;
116
+ } catch (error) {
117
+ if (isNotFoundError(error)) return false;
118
+ throw error;
119
+ }
120
+ }
121
+
122
+ async collection_exists(collectionName: string): Promise<boolean> {
123
+ return this.collectionExists(collectionName);
124
+ }
125
+
126
+ async getCollection(collectionName: string): Promise<JsonObject> {
127
+ const collection = await this.currentCollection(collectionName);
128
+ const indexConfigs = plainIndexConfigs(collection["indexConfigs"]);
129
+ return {
130
+ config: {
131
+ params: {
132
+ vectors: qdrantVectorsConfig(indexConfigs),
133
+ sparseVectors: {},
134
+ sparse_vectors: {},
135
+ },
136
+ },
137
+ collectionName,
138
+ collection_name: collectionName,
139
+ };
140
+ }
141
+
142
+ async get_collection(collectionName: string): Promise<JsonObject> {
143
+ return this.getCollection(collectionName);
144
+ }
145
+
146
+ async getCollections(): Promise<{ collections: Array<{ name: string }> }> {
147
+ if (typeof this.client.listCollections !== "function") {
148
+ return { collections: [] };
149
+ }
150
+ const response = await this.client.listCollections();
151
+ return {
152
+ collections: response.collections
153
+ .map((collection) => collection.name ?? collection.collectionName)
154
+ .filter((name): name is string => typeof name === "string")
155
+ .map((name) => ({ name })),
156
+ };
157
+ }
158
+
159
+ async createCollection(
160
+ collectionNameOrInput: string | (CreateCollectionOptions & {
161
+ collectionName?: string;
162
+ collection_name?: string;
163
+ }),
164
+ options: CreateCollectionOptions = {},
165
+ ): Promise<boolean> {
166
+ const { collectionName, body } = normalizeCollectionArgs(collectionNameOrInput, options);
167
+ const payloadSchema = popPayloadSchema(body);
168
+ rejectResultChanging(body, new Set(["timeout", "initFrom", "init_from"]));
169
+ const vectorsConfig = body.vectorsConfig ?? body.vectors_config ?? body["vectors"];
170
+ if (vectorsConfig === undefined) {
171
+ throw new QdrantCompatValidationError("vectorsConfig is required");
172
+ }
173
+ const indexConfigs = mergeIndexConfigs(
174
+ vectorConfigToIndexConfigs(vectorsConfig),
175
+ payloadSchemaToIndexConfigs(payloadSchema),
176
+ );
177
+ await this.client.createCollection({
178
+ collectionName,
179
+ indexConfigs: indexConfigs as CreateCollectionInput["indexConfigs"],
180
+ });
181
+ await this.waitForCollectionActive(collectionName, body.timeout);
182
+ return true;
183
+ }
184
+
185
+ async create_collection(
186
+ collectionNameOrInput: Parameters<QdrantCompatClient["createCollection"]>[0],
187
+ options?: Parameters<QdrantCompatClient["createCollection"]>[1],
188
+ ): Promise<boolean> {
189
+ return this.createCollection(collectionNameOrInput, options);
190
+ }
191
+
192
+ async recreateCollection(
193
+ collectionNameOrInput: string | (CreateCollectionOptions & {
194
+ collectionName?: string;
195
+ collection_name?: string;
196
+ }),
197
+ options: CreateCollectionOptions = {},
198
+ ): Promise<boolean> {
199
+ const { collectionName } = normalizeCollectionArgs(collectionNameOrInput, options);
200
+ if (await this.collectionExists(collectionName)) {
201
+ await this.deleteCollection(collectionName);
202
+ }
203
+ return this.createCollection(collectionNameOrInput, options);
204
+ }
205
+
206
+ async recreate_collection(
207
+ collectionNameOrInput: Parameters<QdrantCompatClient["recreateCollection"]>[0],
208
+ options?: Parameters<QdrantCompatClient["recreateCollection"]>[1],
209
+ ): Promise<boolean> {
210
+ return this.recreateCollection(collectionNameOrInput, options);
211
+ }
212
+
213
+ async deleteCollection(collectionName: string): Promise<boolean> {
214
+ await this.client.collection(collectionName).delete();
215
+ return true;
216
+ }
217
+
218
+ async delete_collection(collectionName: string): Promise<boolean> {
219
+ return this.deleteCollection(collectionName);
220
+ }
221
+
222
+ async createPayloadIndex(
223
+ collectionName: string,
224
+ fieldName: string,
225
+ fieldSchema: unknown,
226
+ options: {
227
+ wait?: boolean | undefined;
228
+ timeout?: number | undefined;
229
+ [key: string]: unknown;
230
+ } = {},
231
+ ): Promise<boolean> {
232
+ const { timeout, wait, ...rest } = options;
233
+ if (wait === false) {
234
+ warn("wait=false is accepted but LambdaDB index updates follow LambdaDB semantics");
235
+ }
236
+ warnIgnored(rest);
237
+
238
+ const collection = await this.currentCollection(collectionName);
239
+ const existingIndexConfigs = plainIndexConfigs(collection["indexConfigs"]);
240
+ const payloadIndexConfig = payloadSchemaToIndexConfigs({ [fieldName]: fieldSchema });
241
+ if (hasSameIndexConfigs(existingIndexConfigs, payloadIndexConfig)) return true;
242
+
243
+ const numDocs = typeof collection["numDocs"] === "number" ? collection["numDocs"] : 0;
244
+ if (numDocs > 0) {
245
+ throw new UnsupportedQdrantFeatureError(
246
+ "createPayloadIndex is only supported for empty LambdaDB collections. " +
247
+ "Declare payloadSchema during createCollection or reingest documents after adding the index.",
248
+ );
249
+ }
250
+
251
+ await this.client.collection(collectionName).update({
252
+ indexConfigs: mergeIndexConfigs(existingIndexConfigs, payloadIndexConfig),
253
+ });
254
+ await this.waitForCollectionActive(collectionName, timeout);
255
+ return true;
256
+ }
257
+
258
+ async create_payload_index(
259
+ collectionName: string,
260
+ fieldName: string,
261
+ fieldSchema: unknown,
262
+ options?: Parameters<QdrantCompatClient["createPayloadIndex"]>[3],
263
+ ): Promise<boolean> {
264
+ return this.createPayloadIndex(collectionName, fieldName, fieldSchema, options);
265
+ }
266
+
267
+ async upsert(
268
+ collectionName: string,
269
+ options: {
270
+ points: Iterable<unknown>;
271
+ wait?: boolean | undefined;
272
+ [key: string]: unknown;
273
+ },
274
+ ): Promise<models.UpdateResult> {
275
+ const { points, wait, ...rest } = options;
276
+ warnIgnored(rest);
277
+ if (wait === false) {
278
+ warn("wait=false is accepted but LambdaDB write visibility follows LambdaDB semantics");
279
+ }
280
+ await this.client.collection(collectionName).docs.upsert({
281
+ docs: pointsToDocs(points),
282
+ });
283
+ return new models.UpdateResult({ status: models.UpdateStatus.COMPLETED });
284
+ }
285
+
286
+ async uploadPoints(
287
+ collectionName: string,
288
+ options: {
289
+ points: Iterable<unknown>;
290
+ batchSize?: number | undefined;
291
+ batch_size?: number | undefined;
292
+ wait?: boolean | undefined;
293
+ parallel?: number | undefined;
294
+ maxRetries?: number | undefined;
295
+ max_retries?: number | undefined;
296
+ shardKeySelector?: unknown;
297
+ shard_key_selector?: unknown;
298
+ [key: string]: unknown;
299
+ },
300
+ ): Promise<void> {
301
+ const {
302
+ points,
303
+ batchSize,
304
+ batch_size: batchSizeSnake,
305
+ wait,
306
+ shardKeySelector,
307
+ shard_key_selector: shardKeySelectorSnake,
308
+ ...rest
309
+ } = options;
310
+ if (shardKeySelector !== undefined || shardKeySelectorSnake !== undefined) {
311
+ throw new UnsupportedQdrantFeatureError(
312
+ "Qdrant shard key routing is not supported in v1",
313
+ );
314
+ }
315
+ warnIgnored(rest);
316
+ const effectiveBatchSize = batchSize ?? batchSizeSnake ?? 64;
317
+ let batch: unknown[] = [];
318
+ for (const point of points) {
319
+ batch.push(point);
320
+ if (batch.length >= effectiveBatchSize) {
321
+ const upsertOptions: { points: Iterable<unknown>; wait?: boolean | undefined } = {
322
+ points: batch,
323
+ };
324
+ if (wait !== undefined) upsertOptions.wait = wait;
325
+ await this.upsert(collectionName, upsertOptions);
326
+ batch = [];
327
+ }
328
+ }
329
+ if (batch.length > 0) {
330
+ const upsertOptions: { points: Iterable<unknown>; wait?: boolean | undefined } = {
331
+ points: batch,
332
+ };
333
+ if (wait !== undefined) upsertOptions.wait = wait;
334
+ await this.upsert(collectionName, upsertOptions);
335
+ }
336
+ }
337
+
338
+ async upload_points(
339
+ collectionName: string,
340
+ options: Parameters<QdrantCompatClient["uploadPoints"]>[1],
341
+ ): Promise<void> {
342
+ return this.uploadPoints(collectionName, options);
343
+ }
344
+
345
+ async uploadCollection(
346
+ collectionName: string,
347
+ options: {
348
+ vectors: Iterable<unknown>;
349
+ ids?: Iterable<models.PointId> | undefined;
350
+ payload?: Iterable<JsonObject | null | undefined> | undefined;
351
+ batchSize?: number | undefined;
352
+ batch_size?: number | undefined;
353
+ [key: string]: unknown;
354
+ },
355
+ ): Promise<void> {
356
+ const { vectors, ids, payload, batchSize, batch_size: batchSizeSnake, ...rest } = options;
357
+ warnIgnored(rest);
358
+ const vectorList = Array.from(vectors);
359
+ const idList = ids ? Array.from(ids) : vectorList.map((_, index) => index);
360
+ const payloadList = payload
361
+ ? Array.from(payload)
362
+ : vectorList.map(() => undefined);
363
+ if (vectorList.length !== idList.length || vectorList.length !== payloadList.length) {
364
+ throw new QdrantCompatValidationError(
365
+ "vectors, ids, and payload must have the same length",
366
+ );
367
+ }
368
+ const points = vectorList.map((vector, index) => {
369
+ const pointInit: {
370
+ id: models.PointId;
371
+ vector: models.PointVector;
372
+ payload?: JsonObject | null | undefined;
373
+ } = {
374
+ id: idList[index] ?? index,
375
+ vector: vector as models.PointVector,
376
+ };
377
+ const pointPayload = payloadList[index];
378
+ if (pointPayload !== undefined && pointPayload !== null) {
379
+ pointInit.payload = pointPayload;
380
+ }
381
+ return new models.PointStruct(pointInit);
382
+ });
383
+ const uploadOptions: Parameters<QdrantCompatClient["uploadPoints"]>[1] = {
384
+ points,
385
+ };
386
+ const effectiveBatchSize = batchSize ?? batchSizeSnake;
387
+ if (effectiveBatchSize !== undefined) uploadOptions.batchSize = effectiveBatchSize;
388
+ await this.uploadPoints(collectionName, uploadOptions);
389
+ }
390
+
391
+ async upload_collection(
392
+ collectionName: string,
393
+ options: Parameters<QdrantCompatClient["uploadCollection"]>[1],
394
+ ): Promise<void> {
395
+ return this.uploadCollection(collectionName, options);
396
+ }
397
+
398
+ async retrieve(
399
+ collectionName: string,
400
+ options: {
401
+ ids: models.PointId[];
402
+ withPayload?: boolean | undefined;
403
+ with_payload?: boolean | undefined;
404
+ withVectors?: boolean | undefined;
405
+ with_vectors?: boolean | undefined;
406
+ [key: string]: unknown;
407
+ },
408
+ ): Promise<models.Record[]> {
409
+ const {
410
+ ids,
411
+ withPayload,
412
+ with_payload: withPayloadSnake,
413
+ withVectors,
414
+ with_vectors: withVectorsSnake,
415
+ ...rest
416
+ } = options;
417
+ warnIgnored(rest);
418
+ const includeVectors = withVectors ?? withVectorsSnake ?? false;
419
+ const response = await this.client.collection(collectionName).docs.fetch({
420
+ ids: ids.map(String),
421
+ consistentRead: true,
422
+ includeVectors,
423
+ });
424
+ return response.docs.map((doc) => docToRecord(doc, {
425
+ withPayload: withPayload ?? withPayloadSnake ?? true,
426
+ withVectors: includeVectors,
427
+ }));
428
+ }
429
+
430
+ async delete(
431
+ collectionName: string,
432
+ options: {
433
+ pointsSelector?: unknown;
434
+ points_selector?: unknown;
435
+ points?: unknown;
436
+ ids?: unknown;
437
+ filter?: models.Filter | JsonObject | null;
438
+ wait?: boolean | undefined;
439
+ [key: string]: unknown;
440
+ } = {},
441
+ ): Promise<models.UpdateResult> {
442
+ const {
443
+ pointsSelector,
444
+ points_selector: pointsSelectorSnake,
445
+ points,
446
+ ids: idsOption,
447
+ filter,
448
+ wait,
449
+ ...rest
450
+ } = options;
451
+ warnIgnored(rest);
452
+ const selector = pointsSelector ?? pointsSelectorSnake ?? points ?? idsOption;
453
+ const ids = idsFromSelector(selector);
454
+ const convertedFilter = filterToLambdaDB(filterFromSelector(selector) ?? filter);
455
+ if (ids === undefined && Object.keys(convertedFilter).length === 0) {
456
+ throw new UnsupportedQdrantFeatureError(
457
+ "delete requires point IDs or a supported Qdrant filter",
458
+ );
459
+ }
460
+ if (wait === false) {
461
+ warn("wait=false is accepted but LambdaDB write visibility follows LambdaDB semantics");
462
+ }
463
+ const body: { ids?: string[]; filter?: JsonObject } = {};
464
+ if (ids !== undefined) body.ids = ids.map(String);
465
+ if (Object.keys(convertedFilter).length > 0) body.filter = convertedFilter;
466
+ await this.client.collection(collectionName).docs.delete(body);
467
+ return new models.UpdateResult({ status: models.UpdateStatus.COMPLETED });
468
+ }
469
+
470
+ async queryPoints(
471
+ collectionName: string,
472
+ options: QueryPointsOptions,
473
+ ): Promise<models.QueryResponse> {
474
+ const {
475
+ query,
476
+ queryFilter,
477
+ query_filter: queryFilterSnake,
478
+ limit = 10,
479
+ withPayload,
480
+ with_payload: withPayloadSnake,
481
+ withVectors,
482
+ with_vectors: withVectorsSnake,
483
+ using,
484
+ searchParams,
485
+ search_params: searchParamsSnake,
486
+ offset,
487
+ scoreThreshold,
488
+ score_threshold: scoreThresholdSnake,
489
+ consistency,
490
+ shardKeySelector,
491
+ shard_key_selector: shardKeySelectorSnake,
492
+ ...rest
493
+ } = options;
494
+ warnIgnored(rest);
495
+ if (offset !== undefined && offset !== 0) {
496
+ throw new UnsupportedQdrantFeatureError("Query offset is not supported in v1");
497
+ }
498
+ if (scoreThreshold !== undefined || scoreThresholdSnake !== undefined) {
499
+ throw new UnsupportedQdrantFeatureError(
500
+ "Query scoreThreshold is not supported in v1",
501
+ );
502
+ }
503
+ if (shardKeySelector !== undefined || shardKeySelectorSnake !== undefined) {
504
+ throw new UnsupportedQdrantFeatureError(
505
+ "Qdrant shard key routing is not supported in v1",
506
+ );
507
+ }
508
+ if (consistency !== undefined) {
509
+ warn("Qdrant consistency is ignored; LambdaDB queryPoints uses consistentRead=true");
510
+ }
511
+ if (searchParams !== undefined || searchParamsSnake !== undefined) {
512
+ warn("Qdrant searchParams are ignored by the LambdaDB compatibility client");
513
+ }
514
+
515
+ const includeVectors = withVectors ?? withVectorsSnake ?? false;
516
+ const { vector, field } = queryVectorAndField(query, using);
517
+ const knn: JsonObject = {
518
+ field,
519
+ k: limit,
520
+ queryVector: vector,
521
+ };
522
+ const convertedFilter = filterToLambdaDB(queryFilter ?? queryFilterSnake);
523
+ if (Object.keys(convertedFilter).length > 0) knn["filter"] = convertedFilter;
524
+
525
+ const response = await this.client.collection(collectionName).query({
526
+ query: { knn },
527
+ size: limit,
528
+ consistentRead: true,
529
+ includeVectors,
530
+ });
531
+ return new models.QueryResponse({
532
+ points: response.docs.map((result) => resultToScoredPoint(result, {
533
+ withPayload: withPayload ?? withPayloadSnake ?? true,
534
+ withVectors: includeVectors,
535
+ })),
536
+ });
537
+ }
538
+
539
+ async query_points(
540
+ collectionName: string,
541
+ options: Parameters<QdrantCompatClient["queryPoints"]>[1],
542
+ ): Promise<models.QueryResponse> {
543
+ return this.queryPoints(collectionName, options);
544
+ }
545
+
546
+ async query(
547
+ collectionName: string,
548
+ options: {
549
+ query: unknown;
550
+ limit?: number | undefined;
551
+ filter?: models.Filter | JsonObject | null;
552
+ with_payload?: boolean | unknown[] | undefined;
553
+ withPayload?: boolean | unknown[] | undefined;
554
+ with_vector?: boolean | undefined;
555
+ withVectors?: boolean | undefined;
556
+ params?: unknown;
557
+ [key: string]: unknown;
558
+ },
559
+ ): Promise<models.QueryResponse> {
560
+ const {
561
+ query,
562
+ limit,
563
+ filter,
564
+ with_payload: withPayloadSnake,
565
+ withPayload,
566
+ with_vector: withVectorSnake,
567
+ withVectors,
568
+ params,
569
+ ...rest
570
+ } = options;
571
+ warnIgnored(rest);
572
+ const queryOptions: QueryPointsOptions = {
573
+ query,
574
+ };
575
+ if (filter !== undefined) queryOptions.queryFilter = filter;
576
+ if (limit !== undefined) queryOptions.limit = limit;
577
+ const payloadSelector = payloadSelectorToBoolean(withPayload ?? withPayloadSnake);
578
+ if (payloadSelector !== undefined) queryOptions.withPayload = payloadSelector;
579
+ const vectorSelector = withVectors ?? withVectorSnake;
580
+ if (vectorSelector !== undefined) queryOptions.withVectors = vectorSelector;
581
+ if (isObject(params)) queryOptions.searchParams = params;
582
+ return this.queryPoints(collectionName, queryOptions);
583
+ }
584
+
585
+ async search(
586
+ collectionName: string,
587
+ options: Omit<QueryPointsOptions, "query"> & {
588
+ queryVector?: unknown;
589
+ query_vector?: unknown;
590
+ },
591
+ ): Promise<models.ScoredPoint[]> {
592
+ const query = options.queryVector ?? options.query_vector;
593
+ if (query === undefined) {
594
+ throw new QdrantCompatValidationError("queryVector is required");
595
+ }
596
+ const response = await this.queryPoints(collectionName, { ...options, query });
597
+ return response.points;
598
+ }
599
+
600
+ async scroll(
601
+ collectionName: string,
602
+ options: {
603
+ scrollFilter?: models.Filter | JsonObject | null;
604
+ scroll_filter?: models.Filter | JsonObject | null;
605
+ limit?: number | undefined;
606
+ withPayload?: boolean | undefined;
607
+ with_payload?: boolean | undefined;
608
+ withVectors?: boolean | undefined;
609
+ with_vectors?: boolean | undefined;
610
+ [key: string]: unknown;
611
+ } = {},
612
+ ): Promise<[models.Record[], string | undefined]> {
613
+ const {
614
+ scrollFilter,
615
+ scroll_filter: scrollFilterSnake,
616
+ limit = 10,
617
+ withPayload,
618
+ with_payload: withPayloadSnake,
619
+ withVectors,
620
+ with_vectors: withVectorsSnake,
621
+ ...rest
622
+ } = options;
623
+ warnIgnored(rest);
624
+ if (scrollFilter !== undefined || scrollFilterSnake !== undefined) {
625
+ throw new UnsupportedQdrantFeatureError("Filtered scroll is not supported in v1");
626
+ }
627
+ if (withVectors === true || withVectorsSnake === true) {
628
+ throw new UnsupportedQdrantFeatureError("Scroll with vectors is not supported in v1");
629
+ }
630
+ const response = await this.client.collection(collectionName).docs.list({ size: limit });
631
+ return [
632
+ response.docs.map((doc) => docToRecord(doc, {
633
+ withPayload: withPayload ?? withPayloadSnake ?? true,
634
+ withVectors: false,
635
+ })),
636
+ response.nextPageToken,
637
+ ];
638
+ }
639
+
640
+ async count(
641
+ collectionName: string,
642
+ options: {
643
+ countFilter?: models.Filter | JsonObject | null;
644
+ count_filter?: models.Filter | JsonObject | null;
645
+ [key: string]: unknown;
646
+ } = {},
647
+ ): Promise<models.CountResult> {
648
+ const { countFilter, count_filter: countFilterSnake, ...rest } = options;
649
+ warnIgnored(rest);
650
+ if (countFilter !== undefined || countFilterSnake !== undefined) {
651
+ throw new UnsupportedQdrantFeatureError("Filtered count is not supported in v1");
652
+ }
653
+ const collection = await this.currentCollection(collectionName);
654
+ return new models.CountResult({
655
+ count: typeof collection["numDocs"] === "number" ? collection["numDocs"] : 0,
656
+ });
657
+ }
658
+
659
+ private async currentCollection(collectionName: string): Promise<JsonObject> {
660
+ const response = await this.client.collection(collectionName).get();
661
+ if (!isObject(response.collection)) {
662
+ throw new QdrantCompatError("LambdaDB collection response is missing collection metadata");
663
+ }
664
+ return response.collection;
665
+ }
666
+
667
+ private async waitForCollectionActive(
668
+ collectionName: string,
669
+ timeoutSeconds: unknown,
670
+ ): Promise<void> {
671
+ const timeout = typeof timeoutSeconds === "number" ? timeoutSeconds : 60;
672
+ const deadline = Date.now() + timeout * 1000;
673
+ let lastStatus = "unknown";
674
+ while (true) {
675
+ const collection = await this.currentCollection(collectionName);
676
+ const status = collection["collectionStatus"];
677
+ if (status === undefined || status === "ACTIVE") return;
678
+ lastStatus = String(status);
679
+ if (Date.now() >= deadline) {
680
+ throw new QdrantCompatError(
681
+ `Collection ${collectionName} did not become ACTIVE within ${timeout}s; last status=${lastStatus}`,
682
+ );
683
+ }
684
+ await sleep(500);
685
+ }
686
+ }
687
+ }
688
+
689
+ export const QdrantClient = QdrantCompatClient;
690
+
691
+ function normalizeConstructorOptions(
692
+ options: CompatConstructorOptions = {},
693
+ ): LambdaDBClientOptions {
694
+ if (options.path !== undefined || options.location === ":memory:") {
695
+ throw new UnsupportedQdrantFeatureError(
696
+ "Qdrant local mode is not supported by LambdaDB compatibility client",
697
+ );
698
+ }
699
+
700
+ const known = new Set([
701
+ "projectApiKey",
702
+ "project_api_key",
703
+ "apiKey",
704
+ "api_key",
705
+ "baseUrl",
706
+ "base_url",
707
+ "url",
708
+ "projectName",
709
+ "project_name",
710
+ "timeout",
711
+ "timeoutMs",
712
+ "retryConfig",
713
+ "httpClient",
714
+ "serverURL",
715
+ "projectHost",
716
+ "path",
717
+ "location",
718
+ "host",
719
+ "port",
720
+ "https",
721
+ "prefix",
722
+ ]);
723
+ warnIgnored(unknownEntries(options, known));
724
+
725
+ let baseUrl = options.baseUrl ?? options.base_url ?? options.url;
726
+ if (baseUrl === undefined && options.host !== undefined) {
727
+ const scheme = options.https === false ? "http" : "https";
728
+ baseUrl = `${scheme}://${options.host}`;
729
+ if (options.port !== undefined) baseUrl = `${baseUrl}:${options.port}`;
730
+ if (options.prefix !== undefined) {
731
+ baseUrl = `${baseUrl.replace(/\/+$/, "")}/${options.prefix.replace(/^\/+/, "")}`;
732
+ }
733
+ }
734
+ if (
735
+ typeof baseUrl === "string" &&
736
+ (baseUrl.includes("localhost") || baseUrl.includes("127.0.0.1"))
737
+ ) {
738
+ warn(
739
+ "The Qdrant compatibility client interprets url as LambdaDB baseUrl, not a Qdrant server URL.",
740
+ );
741
+ }
742
+
743
+ const timeoutMs = options.timeoutMs ??
744
+ (typeof options.timeout === "number" ? Math.trunc(options.timeout * 1000) : undefined);
745
+
746
+ const normalized: LambdaDBClientOptions = {};
747
+ const projectApiKey = options.projectApiKey ?? options.project_api_key ??
748
+ options.apiKey ?? options.api_key;
749
+ if (projectApiKey !== undefined) normalized.projectApiKey = projectApiKey;
750
+ if (typeof baseUrl === "string") normalized.baseUrl = baseUrl;
751
+ const projectName = options.projectName ?? options.project_name;
752
+ if (projectName !== undefined) normalized.projectName = projectName;
753
+ if (timeoutMs !== undefined) normalized.timeoutMs = timeoutMs;
754
+ if (options.retryConfig !== undefined) normalized.retryConfig = options.retryConfig;
755
+ if (options.httpClient !== undefined) normalized.httpClient = options.httpClient;
756
+ if (options.serverURL !== undefined) normalized.serverURL = options.serverURL;
757
+ if (options.projectHost !== undefined) normalized.projectHost = options.projectHost;
758
+ return normalized;
759
+ }
760
+
761
+ function normalizeCollectionArgs(
762
+ collectionNameOrInput: string | (CreateCollectionOptions & {
763
+ collectionName?: string;
764
+ collection_name?: string;
765
+ }),
766
+ options: CreateCollectionOptions,
767
+ ): {
768
+ collectionName: string;
769
+ body: CreateCollectionOptions;
770
+ } {
771
+ if (typeof collectionNameOrInput === "string") {
772
+ return { collectionName: collectionNameOrInput, body: { ...options } };
773
+ }
774
+ const collectionName = collectionNameOrInput.collectionName ??
775
+ collectionNameOrInput.collection_name;
776
+ if (collectionName === undefined) {
777
+ throw new QdrantCompatValidationError("collectionName is required");
778
+ }
779
+ return { collectionName, body: { ...collectionNameOrInput, ...options } };
780
+ }
781
+
782
+ function popPayloadSchema(body: CreateCollectionOptions): unknown {
783
+ const keys = [
784
+ "payloadSchema",
785
+ "payload_schema",
786
+ "payloadIndexes",
787
+ "payload_indexes",
788
+ "payloadIndexConfigs",
789
+ "payload_index_configs",
790
+ ] as const;
791
+ const present = keys.filter((key) => body[key] !== undefined && body[key] !== null);
792
+ if (present.length > 1) {
793
+ throw new QdrantCompatValidationError(
794
+ `Use only one payload schema option, got: ${present.join(", ")}`,
795
+ );
796
+ }
797
+ const [firstKey] = present;
798
+ return firstKey === undefined ? undefined : body[firstKey];
799
+ }
800
+
801
+ function rejectResultChanging(body: JsonObject, allowed: Set<string>): void {
802
+ const ignored = Object.entries(body)
803
+ .filter(([key, value]) =>
804
+ value !== undefined &&
805
+ value !== null &&
806
+ !allowed.has(key) &&
807
+ ![
808
+ "collectionName",
809
+ "collection_name",
810
+ "vectorsConfig",
811
+ "vectors_config",
812
+ "vectors",
813
+ "payloadSchema",
814
+ "payload_schema",
815
+ "payloadIndexes",
816
+ "payload_indexes",
817
+ "payloadIndexConfigs",
818
+ "payload_index_configs",
819
+ ].includes(key)
820
+ )
821
+ .map(([key]) => key);
822
+ if (ignored.length > 0) {
823
+ throw new UnsupportedQdrantFeatureError(
824
+ `Unsupported Qdrant collection options: ${ignored.join(", ")}`,
825
+ );
826
+ }
827
+ }
828
+
829
+ function idsFromSelector(selector: unknown): models.PointId[] | undefined {
830
+ if (selector === undefined || selector === null) return undefined;
831
+ if (Array.isArray(selector)) return selector as models.PointId[];
832
+ if (isObject(selector)) {
833
+ const points = selector["points"];
834
+ const ids = selector["ids"];
835
+ if (Array.isArray(points)) return points as models.PointId[];
836
+ if (Array.isArray(ids)) return ids as models.PointId[];
837
+ }
838
+ return undefined;
839
+ }
840
+
841
+ function filterFromSelector(selector: unknown): models.Filter | JsonObject | null | undefined {
842
+ if (isObject(selector) && "filter" in selector) {
843
+ return selector["filter"] as models.Filter | JsonObject | null | undefined;
844
+ }
845
+ return undefined;
846
+ }
847
+
848
+ function hasSameIndexConfigs(existing: IndexConfigs, expected: IndexConfigs): boolean {
849
+ return Object.entries(expected).every(([fieldName, indexConfig]) =>
850
+ JSON.stringify(existing[fieldName]) === JSON.stringify(indexConfig),
851
+ );
852
+ }
853
+
854
+ function payloadSelectorToBoolean(selector: boolean | unknown[] | undefined): boolean | undefined {
855
+ if (Array.isArray(selector)) return true;
856
+ return selector;
857
+ }
858
+
859
+ function isLambdaDBLike(value: unknown): value is LambdaDBLike {
860
+ return isObject(value) &&
861
+ typeof value["createCollection"] === "function" &&
862
+ typeof value["collection"] === "function";
863
+ }
864
+
865
+ function isNotFoundError(error: unknown): boolean {
866
+ return error instanceof ResourceNotFoundError ||
867
+ (isObject(error) && error["name"] === "ResourceNotFoundError");
868
+ }
869
+
870
+ function unknownEntries(
871
+ options: CompatConstructorOptions,
872
+ known: Set<string>,
873
+ ): JsonObject {
874
+ const result: JsonObject = {};
875
+ for (const [key, value] of Object.entries(options)) {
876
+ if (!known.has(key)) result[key] = value;
877
+ }
878
+ return result;
879
+ }
880
+
881
+ function warnIgnored(options: JsonObject): void {
882
+ const keys = Object.entries(options)
883
+ .filter(([, value]) => value !== undefined && value !== null)
884
+ .map(([key]) => key)
885
+ .sort();
886
+ if (keys.length > 0) {
887
+ warn(`Ignoring unsupported Qdrant options: ${keys.join(", ")}`);
888
+ }
889
+ }
890
+
891
+ function warn(message: string): void {
892
+ console.warn(message);
893
+ }
894
+
895
+ function sleep(ms: number): Promise<void> {
896
+ return new Promise((resolve) => setTimeout(resolve, ms));
897
+ }
898
+
899
+ function isObject(value: unknown): value is JsonObject {
900
+ return typeof value === "object" && value !== null && !Array.isArray(value);
901
+ }