@functional-systems/lambdadb 0.4.0 → 0.4.2

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.
@@ -2,6 +2,7 @@ import { LambdaDBClient, type LambdaDBClientOptions } from "../../client.js";
2
2
  import { ResourceNotFoundError } from "../../models/errors/index.js";
3
3
  import type { CreateCollectionInput } from "../../types/public.js";
4
4
  import {
5
+ ORIGINAL_ID_FIELD,
5
6
  mergeIndexConfigs,
6
7
  payloadSchemaToIndexConfigs,
7
8
  plainIndexConfigs,
@@ -10,6 +11,7 @@ import {
10
11
  queryVectorAndField,
11
12
  resultToScoredPoint,
12
13
  docToRecord,
14
+ vectorFieldName,
13
15
  vectorConfigToIndexConfigs,
14
16
  type IndexConfigs,
15
17
  } from "./conversions.js";
@@ -82,10 +84,10 @@ type QueryPointsOptions = {
82
84
  queryFilter?: models.Filter | JsonObject | null;
83
85
  query_filter?: models.Filter | JsonObject | null;
84
86
  limit?: number | undefined;
85
- withPayload?: boolean | undefined;
86
- with_payload?: boolean | undefined;
87
- withVectors?: boolean | undefined;
88
- with_vectors?: boolean | undefined;
87
+ withPayload?: PayloadSelector | undefined;
88
+ with_payload?: PayloadSelector | undefined;
89
+ withVectors?: VectorSelector | undefined;
90
+ with_vectors?: VectorSelector | undefined;
89
91
  using?: string | undefined;
90
92
  searchParams?: models.SearchParams | JsonObject | null;
91
93
  search_params?: models.SearchParams | JsonObject | null;
@@ -98,6 +100,9 @@ type QueryPointsOptions = {
98
100
  [key: string]: unknown;
99
101
  };
100
102
 
103
+ type PayloadSelector = boolean | string[];
104
+ type VectorSelector = boolean | string[];
105
+
101
106
  export class QdrantCompatClient {
102
107
  private readonly client: LambdaDBLike;
103
108
 
@@ -399,10 +404,10 @@ export class QdrantCompatClient {
399
404
  collectionName: string,
400
405
  options: {
401
406
  ids: models.PointId[];
402
- withPayload?: boolean | undefined;
403
- with_payload?: boolean | undefined;
404
- withVectors?: boolean | undefined;
405
- with_vectors?: boolean | undefined;
407
+ withPayload?: PayloadSelector | undefined;
408
+ with_payload?: PayloadSelector | undefined;
409
+ withVectors?: VectorSelector | undefined;
410
+ with_vectors?: VectorSelector | undefined;
406
411
  [key: string]: unknown;
407
412
  },
408
413
  ): Promise<models.Record[]> {
@@ -415,15 +420,19 @@ export class QdrantCompatClient {
415
420
  ...rest
416
421
  } = options;
417
422
  warnIgnored(rest);
418
- const includeVectors = withVectors ?? withVectorsSnake ?? false;
419
- const response = await this.client.collection(collectionName).docs.fetch({
423
+ const payloadSelector = normalizePayloadSelector(withPayload ?? withPayloadSnake ?? true);
424
+ const vectorSelector = normalizeVectorSelector(withVectors ?? withVectorsSnake ?? false);
425
+ const fetchBody: JsonObject = {
420
426
  ids: ids.map(String),
421
427
  consistentRead: true,
422
- includeVectors,
423
- });
428
+ includeVectors: vectorSelector !== false,
429
+ };
430
+ const fields = fieldsForSelectors(payloadSelector, vectorSelector);
431
+ if (fields !== undefined) fetchBody["fields"] = fields;
432
+ const response = await this.client.collection(collectionName).docs.fetch(fetchBody);
424
433
  return response.docs.map((doc) => docToRecord(doc, {
425
- withPayload: withPayload ?? withPayloadSnake ?? true,
426
- withVectors: includeVectors,
434
+ withPayload: payloadSelector,
435
+ withVectors: vectorSelector,
427
436
  }));
428
437
  }
429
438
 
@@ -512,7 +521,8 @@ export class QdrantCompatClient {
512
521
  warn("Qdrant searchParams are ignored by the LambdaDB compatibility client");
513
522
  }
514
523
 
515
- const includeVectors = withVectors ?? withVectorsSnake ?? false;
524
+ const payloadSelector = normalizePayloadSelector(withPayload ?? withPayloadSnake ?? true);
525
+ const vectorSelector = normalizeVectorSelector(withVectors ?? withVectorsSnake ?? false);
516
526
  const { vector, field } = queryVectorAndField(query, using);
517
527
  const knn: JsonObject = {
518
528
  field,
@@ -522,16 +532,19 @@ export class QdrantCompatClient {
522
532
  const convertedFilter = filterToLambdaDB(queryFilter ?? queryFilterSnake);
523
533
  if (Object.keys(convertedFilter).length > 0) knn["filter"] = convertedFilter;
524
534
 
525
- const response = await this.client.collection(collectionName).query({
535
+ const queryBody: JsonObject = {
526
536
  query: { knn },
527
537
  size: limit,
528
538
  consistentRead: true,
529
- includeVectors,
530
- });
539
+ includeVectors: vectorSelector !== false,
540
+ };
541
+ const fields = fieldsForSelectors(payloadSelector, vectorSelector);
542
+ if (fields !== undefined) queryBody["fields"] = fields;
543
+ const response = await this.client.collection(collectionName).query(queryBody);
531
544
  return new models.QueryResponse({
532
545
  points: response.docs.map((result) => resultToScoredPoint(result, {
533
- withPayload: withPayload ?? withPayloadSnake ?? true,
534
- withVectors: includeVectors,
546
+ withPayload: payloadSelector,
547
+ withVectors: vectorSelector,
535
548
  })),
536
549
  });
537
550
  }
@@ -549,10 +562,10 @@ export class QdrantCompatClient {
549
562
  query: unknown;
550
563
  limit?: number | undefined;
551
564
  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;
565
+ with_payload?: PayloadSelector | undefined;
566
+ withPayload?: PayloadSelector | undefined;
567
+ with_vector?: VectorSelector | undefined;
568
+ withVectors?: VectorSelector | undefined;
556
569
  params?: unknown;
557
570
  [key: string]: unknown;
558
571
  },
@@ -574,9 +587,9 @@ export class QdrantCompatClient {
574
587
  };
575
588
  if (filter !== undefined) queryOptions.queryFilter = filter;
576
589
  if (limit !== undefined) queryOptions.limit = limit;
577
- const payloadSelector = payloadSelectorToBoolean(withPayload ?? withPayloadSnake);
590
+ const payloadSelector = normalizeOptionalPayloadSelector(withPayload ?? withPayloadSnake);
578
591
  if (payloadSelector !== undefined) queryOptions.withPayload = payloadSelector;
579
- const vectorSelector = withVectors ?? withVectorSnake;
592
+ const vectorSelector = normalizeOptionalVectorSelector(withVectors ?? withVectorSnake);
580
593
  if (vectorSelector !== undefined) queryOptions.withVectors = vectorSelector;
581
594
  if (isObject(params)) queryOptions.searchParams = params;
582
595
  return this.queryPoints(collectionName, queryOptions);
@@ -603,10 +616,10 @@ export class QdrantCompatClient {
603
616
  scrollFilter?: models.Filter | JsonObject | null;
604
617
  scroll_filter?: models.Filter | JsonObject | null;
605
618
  limit?: number | undefined;
606
- withPayload?: boolean | undefined;
607
- with_payload?: boolean | undefined;
608
- withVectors?: boolean | undefined;
609
- with_vectors?: boolean | undefined;
619
+ withPayload?: PayloadSelector | undefined;
620
+ with_payload?: PayloadSelector | undefined;
621
+ withVectors?: VectorSelector | undefined;
622
+ with_vectors?: VectorSelector | undefined;
610
623
  [key: string]: unknown;
611
624
  } = {},
612
625
  ): Promise<[models.Record[], string | undefined]> {
@@ -624,14 +637,13 @@ export class QdrantCompatClient {
624
637
  if (scrollFilter !== undefined || scrollFilterSnake !== undefined) {
625
638
  throw new UnsupportedQdrantFeatureError("Filtered scroll is not supported in v1");
626
639
  }
627
- if (withVectors === true || withVectorsSnake === true) {
628
- throw new UnsupportedQdrantFeatureError("Scroll with vectors is not supported in v1");
629
- }
640
+ const payloadSelector = normalizePayloadSelector(withPayload ?? withPayloadSnake ?? true);
641
+ const vectorSelector = normalizeVectorSelector(withVectors ?? withVectorsSnake ?? false);
630
642
  const response = await this.client.collection(collectionName).docs.list({ size: limit });
631
643
  return [
632
644
  response.docs.map((doc) => docToRecord(doc, {
633
- withPayload: withPayload ?? withPayloadSnake ?? true,
634
- withVectors: false,
645
+ withPayload: payloadSelector,
646
+ withVectors: vectorSelector,
635
647
  })),
636
648
  response.nextPageToken,
637
649
  ];
@@ -851,9 +863,48 @@ function hasSameIndexConfigs(existing: IndexConfigs, expected: IndexConfigs): bo
851
863
  );
852
864
  }
853
865
 
854
- function payloadSelectorToBoolean(selector: boolean | unknown[] | undefined): boolean | undefined {
855
- if (Array.isArray(selector)) return true;
856
- return selector;
866
+ function normalizeOptionalPayloadSelector(selector: unknown): PayloadSelector | undefined {
867
+ if (selector === undefined) return undefined;
868
+ return normalizePayloadSelector(selector);
869
+ }
870
+
871
+ function normalizePayloadSelector(selector: unknown): PayloadSelector {
872
+ if (selector === true || selector === false) return selector;
873
+ if (Array.isArray(selector) && selector.every((item) => typeof item === "string")) {
874
+ return selector;
875
+ }
876
+ throw new QdrantCompatValidationError(
877
+ "withPayload/with_payload must be a boolean or a string field list",
878
+ );
879
+ }
880
+
881
+ function normalizeOptionalVectorSelector(selector: unknown): VectorSelector | undefined {
882
+ if (selector === undefined) return undefined;
883
+ return normalizeVectorSelector(selector);
884
+ }
885
+
886
+ function normalizeVectorSelector(selector: unknown): VectorSelector {
887
+ if (selector === true || selector === false) return selector;
888
+ if (Array.isArray(selector) && selector.every((item) => typeof item === "string")) {
889
+ return selector;
890
+ }
891
+ throw new QdrantCompatValidationError(
892
+ "withVectors/with_vector must be a boolean or a string vector-name list",
893
+ );
894
+ }
895
+
896
+ function fieldsForSelectors(
897
+ payloadSelector: PayloadSelector,
898
+ vectorSelector: VectorSelector,
899
+ ): { include: string[] } | undefined {
900
+ if (!Array.isArray(payloadSelector)) return undefined;
901
+ if (vectorSelector === true) return undefined;
902
+
903
+ const include = new Set<string>([ORIGINAL_ID_FIELD, ...payloadSelector]);
904
+ if (Array.isArray(vectorSelector)) {
905
+ for (const name of vectorSelector) include.add(vectorFieldName(name));
906
+ }
907
+ return { include: Array.from(include) };
857
908
  }
858
909
 
859
910
  function isLambdaDBLike(value: unknown): value is LambdaDBLike {
@@ -96,19 +96,19 @@ export function pointToDoc(point: unknown): JsonObject {
96
96
 
97
97
  export function docToRecord(
98
98
  input: unknown,
99
- options: { withPayload?: boolean; withVectors?: boolean } = {},
99
+ options: { withPayload?: boolean | string[]; withVectors?: boolean | string[] } = {},
100
100
  ): models.Record {
101
101
  const doc = unwrapDoc(input);
102
102
  return new models.Record({
103
103
  id: docId(doc),
104
- payload: options.withPayload === false ? undefined : payloadFromDoc(doc),
105
- vector: vectorFromDoc(doc, options.withVectors === true),
104
+ payload: payloadFromDoc(doc, options.withPayload ?? true),
105
+ vector: vectorFromDoc(doc, options.withVectors ?? false),
106
106
  });
107
107
  }
108
108
 
109
109
  export function resultToScoredPoint(
110
110
  result: unknown,
111
- options: { withPayload?: boolean; withVectors?: boolean } = {},
111
+ options: { withPayload?: boolean | string[]; withVectors?: boolean | string[] } = {},
112
112
  ): models.ScoredPoint {
113
113
  const doc = unwrapDoc(result);
114
114
  const score = isObject(result) && typeof result["score"] === "number"
@@ -117,8 +117,8 @@ export function resultToScoredPoint(
117
117
  return new models.ScoredPoint({
118
118
  id: docId(doc),
119
119
  score,
120
- payload: options.withPayload === false ? undefined : payloadFromDoc(doc),
121
- vector: vectorFromDoc(doc, options.withVectors === true),
120
+ payload: payloadFromDoc(doc, options.withPayload ?? true),
121
+ vector: vectorFromDoc(doc, options.withVectors ?? false),
122
122
  });
123
123
  }
124
124
 
@@ -356,26 +356,34 @@ function validatePayload(payload: JsonObject): void {
356
356
  }
357
357
  }
358
358
 
359
- function payloadFromDoc(doc: JsonObject): JsonObject {
359
+ function payloadFromDoc(doc: JsonObject, selector: boolean | string[]): JsonObject | undefined {
360
+ if (selector === false) return undefined;
361
+ const selectedFields = Array.isArray(selector) ? new Set(selector) : undefined;
360
362
  const payload: JsonObject = {};
361
363
  for (const [key, value] of Object.entries(doc)) {
362
- if (key !== ID_FIELD && !key.startsWith(RESERVED_PREFIX)) {
364
+ if (
365
+ key !== ID_FIELD &&
366
+ !key.startsWith(RESERVED_PREFIX) &&
367
+ (selectedFields === undefined || selectedFields.has(key))
368
+ ) {
363
369
  payload[key] = value;
364
370
  }
365
371
  }
366
372
  return payload;
367
373
  }
368
374
 
369
- function vectorFromDoc(doc: JsonObject, withVectors: boolean): unknown {
370
- if (!withVectors) return undefined;
375
+ function vectorFromDoc(doc: JsonObject, selector: boolean | string[]): unknown {
376
+ if (selector === false) return undefined;
377
+ const selectedNames = Array.isArray(selector) ? new Set(selector) : undefined;
371
378
 
372
379
  const named: JsonObject = {};
373
380
  let defaultVector: unknown;
374
381
  for (const [key, value] of Object.entries(doc)) {
375
- if (key === DEFAULT_VECTOR_NAME) {
382
+ if (key === DEFAULT_VECTOR_NAME && shouldIncludeVectorName("", key, selectedNames)) {
376
383
  defaultVector = value;
377
384
  } else if (key.startsWith(`${DEFAULT_VECTOR_NAME}_`)) {
378
- named[key.slice(DEFAULT_VECTOR_NAME.length + 1)] = value;
385
+ const name = key.slice(DEFAULT_VECTOR_NAME.length + 1);
386
+ if (shouldIncludeVectorName(name, key, selectedNames)) named[name] = value;
379
387
  }
380
388
  }
381
389
 
@@ -386,6 +394,16 @@ function vectorFromDoc(doc: JsonObject, withVectors: boolean): unknown {
386
394
  return defaultVector;
387
395
  }
388
396
 
397
+ function shouldIncludeVectorName(
398
+ qdrantName: string,
399
+ internalName: string,
400
+ selectedNames: Set<string> | undefined,
401
+ ): boolean {
402
+ return selectedNames === undefined ||
403
+ selectedNames.has(qdrantName) ||
404
+ selectedNames.has(internalName);
405
+ }
406
+
389
407
  function docId(doc: JsonObject): PointId {
390
408
  const originalId = doc[ORIGINAL_ID_FIELD];
391
409
  if (typeof originalId === "string" || typeof originalId === "number") {
@@ -38,7 +38,7 @@ class InMemoryDocs {
38
38
  docs: body.ids
39
39
  .map((id) => this.collection.docs.get(String(id)))
40
40
  .filter(Boolean)
41
- .map((doc) => ({ collection: this.collection.name, doc })),
41
+ .map((doc) => ({ collection: this.collection.name, doc: projectDoc(doc, body.fields) })),
42
42
  total: body.ids.length,
43
43
  took: 0,
44
44
  isDocsInline: true,
@@ -46,8 +46,15 @@ class InMemoryDocs {
46
46
  }
47
47
 
48
48
  async delete(body) {
49
- for (const id of body.ids) {
50
- this.collection.docs.delete(String(id));
49
+ if (body.ids) {
50
+ for (const id of body.ids) {
51
+ this.collection.docs.delete(String(id));
52
+ }
53
+ }
54
+ if (body.filter) {
55
+ for (const [id, doc] of this.collection.docs.entries()) {
56
+ if (matchesLambdaDBFilter(doc, body.filter)) this.collection.docs.delete(id);
57
+ }
51
58
  }
52
59
  return { message: "ok" };
53
60
  }
@@ -56,7 +63,7 @@ class InMemoryDocs {
56
63
  return {
57
64
  docs: Array.from(this.collection.docs.values())
58
65
  .slice(0, body.size ?? 10)
59
- .map((doc) => ({ collection: this.collection.name, doc })),
66
+ .map((doc) => ({ collection: this.collection.name, doc: projectDoc(doc, body.fields) })),
60
67
  isDocsInline: true,
61
68
  total: this.collection.docs.size,
62
69
  };
@@ -95,9 +102,10 @@ class InMemoryCollection {
95
102
  const knn = body.query.knn;
96
103
  const queryVector = knn.queryVector;
97
104
  const docs = Array.from(this.docs.values())
105
+ .filter((doc) => matchesLambdaDBFilter(doc, knn.filter))
98
106
  .map((doc) => ({
99
107
  collection: this.name,
100
- doc,
108
+ doc: projectDoc(doc, body.fields),
101
109
  score: dot(queryVector, doc[knn.field]),
102
110
  }))
103
111
  .sort((a, b) => b.score - a.score)
@@ -179,6 +187,48 @@ test("langchain qdrant vector store works with QdrantCompatClient", {
179
187
  assert.equal(results[0][0].metadata.tenant, "acme");
180
188
  });
181
189
 
190
+ test("langchain qdrant vector store supports filter search and delete", {
191
+ skip: shouldRun ? false : "Set LAMBDADB_RUN_EXTERNAL_INTEGRATION_TESTS=1",
192
+ }, async () => {
193
+ const client = new QdrantCompatClient(new InMemoryLambdaDB());
194
+ const embeddings = new StaticEmbeddings();
195
+
196
+ const store = await LangChainQdrantVectorStore.fromDocuments(
197
+ [
198
+ new LangChainDocument({
199
+ pageContent: "alpha",
200
+ metadata: { tenant: "acme", group: "keep" },
201
+ }),
202
+ new LangChainDocument({
203
+ pageContent: "beta",
204
+ metadata: { tenant: "other", group: "remove" },
205
+ }),
206
+ ],
207
+ embeddings,
208
+ {
209
+ client,
210
+ collectionName: "langchain-filter-docs",
211
+ },
212
+ );
213
+
214
+ const filteredResults = await store.similaritySearchVectorWithScore([1, 0, 0], 2, {
215
+ must: [{ key: "content", match: { value: "alpha" } }],
216
+ });
217
+
218
+ assert.equal(filteredResults.length, 1);
219
+ assert.equal(filteredResults[0][0].pageContent, "alpha");
220
+
221
+ await store.delete({
222
+ filter: {
223
+ must: [{ key: "content", match: { value: "beta" } }],
224
+ },
225
+ });
226
+
227
+ const remainingResults = await store.similaritySearchVectorWithScore([0, 1, 0], 2);
228
+ assert.equal(remainingResults.length, 1);
229
+ assert.equal(remainingResults[0][0].pageContent, "alpha");
230
+ });
231
+
182
232
  test("llamaindex qdrant vector store works with QdrantCompatClient", {
183
233
  skip: shouldRun ? false : "Set LAMBDADB_RUN_EXTERNAL_INTEGRATION_TESTS=1",
184
234
  }, async () => {
@@ -214,6 +264,42 @@ test("llamaindex qdrant vector store works with QdrantCompatClient", {
214
264
  assert.equal(results.nodes[0].getContent(), "alpha");
215
265
  });
216
266
 
267
+ test("llamaindex qdrant vector store supports delete by filter", {
268
+ skip: shouldRun ? false : "Set LAMBDADB_RUN_EXTERNAL_INTEGRATION_TESTS=1",
269
+ }, async () => {
270
+ const client = new QdrantCompatClient(new InMemoryLambdaDB());
271
+ const store = new LlamaIndexQdrantVectorStore({
272
+ client,
273
+ collectionName: "llamaindex-delete-docs",
274
+ });
275
+
276
+ const alpha = new TextNode({
277
+ text: "alpha",
278
+ metadata: { tenant: "acme" },
279
+ relationships: { SOURCE: { nodeId: "alpha-ref" } },
280
+ });
281
+ alpha.id_ = "alpha";
282
+ alpha.embedding = [1, 0, 0];
283
+
284
+ const beta = new TextNode({
285
+ text: "beta",
286
+ metadata: { tenant: "acme" },
287
+ relationships: { SOURCE: { nodeId: "beta-ref" } },
288
+ });
289
+ beta.id_ = "beta";
290
+ beta.embedding = [0, 1, 0];
291
+
292
+ await store.add([alpha, beta]);
293
+ await store.delete("alpha-ref");
294
+
295
+ const results = await store.query({
296
+ queryEmbedding: [1, 0, 0],
297
+ similarityTopK: 2,
298
+ });
299
+
300
+ assert.deepEqual(results.ids, ["beta"]);
301
+ });
302
+
217
303
  function vectorForText(text) {
218
304
  if (text.includes("alpha")) return [1, 0, 0];
219
305
  if (text.includes("beta")) return [0, 1, 0];
@@ -224,3 +310,42 @@ function dot(left, right) {
224
310
  if (!Array.isArray(left) || !Array.isArray(right)) return 0;
225
311
  return left.reduce((sum, value, index) => sum + value * (right[index] ?? 0), 0);
226
312
  }
313
+
314
+ function projectDoc(doc, fields) {
315
+ const include = fields?.include;
316
+ if (!Array.isArray(include)) return doc;
317
+ const projected = { id: doc.id };
318
+ for (const field of include) {
319
+ if (field in doc) projected[field] = doc[field];
320
+ }
321
+ return projected;
322
+ }
323
+
324
+ function matchesLambdaDBFilter(doc, filter) {
325
+ if (!filter || Object.keys(filter).length === 0) return true;
326
+ if (filter.queryString) return matchesQueryString(doc, filter.queryString.query);
327
+ if (Array.isArray(filter.bool)) return matchesBoolClauses(doc, filter.bool);
328
+ if (filter.bool?.clauses) return matchesBoolClauses(doc, filter.bool.clauses);
329
+ return true;
330
+ }
331
+
332
+ function matchesBoolClauses(doc, clauses) {
333
+ const shouldClauses = [];
334
+ for (const clause of clauses) {
335
+ const matched = matchesLambdaDBFilter(doc, clause);
336
+ if (clause.occur === "must_not") {
337
+ if (matched) return false;
338
+ } else if (clause.occur === "should") {
339
+ shouldClauses.push(matched);
340
+ } else if (!matched) {
341
+ return false;
342
+ }
343
+ }
344
+ return shouldClauses.length === 0 || shouldClauses.some(Boolean);
345
+ }
346
+
347
+ function matchesQueryString(doc, query) {
348
+ const [field, ...rest] = String(query).split(":");
349
+ const expected = rest.join(":");
350
+ return String(doc[field]) === expected;
351
+ }
@@ -50,7 +50,13 @@ class FakeDocs {
50
50
  docs: [
51
51
  {
52
52
  collection: "docs",
53
- doc: { id: "1", _qdrant_id: 1, tenant: "acme" },
53
+ doc: {
54
+ id: "1",
55
+ _qdrant_id: 1,
56
+ _qdrant_vector: [0.1, 0.2],
57
+ _qdrant_vector_title: [0.3, 0.4],
58
+ tenant: "acme",
59
+ },
54
60
  },
55
61
  ],
56
62
  nextPageToken: undefined,
@@ -328,6 +334,59 @@ test("queryPoints maps dense vectors, filters, and responses", async () => {
328
334
  );
329
335
  });
330
336
 
337
+ test("payload and vector selectors map to LambdaDB fields and qdrant responses", async () => {
338
+ const fake = new FakeLambdaDB();
339
+ const collection = fake.collection("docs");
340
+ collection.query = async function query(body) {
341
+ this.queries.push(body);
342
+ return {
343
+ docs: [
344
+ {
345
+ collection: this.name,
346
+ score: 0.9,
347
+ doc: {
348
+ id: "1",
349
+ _qdrant_id: 1,
350
+ _qdrant_vector: [0.1, 0.2],
351
+ _qdrant_vector_title: [0.3, 0.4],
352
+ tenant: "acme",
353
+ hidden: "value",
354
+ },
355
+ },
356
+ ],
357
+ };
358
+ };
359
+ const client = new QdrantCompatClient(fake);
360
+
361
+ const response = await client.queryPoints("docs", {
362
+ query: [0.1, 0.2],
363
+ limit: 1,
364
+ withPayload: ["tenant"],
365
+ withVectors: ["title"],
366
+ });
367
+
368
+ assert.deepEqual(response.points[0].payload, { tenant: "acme" });
369
+ assert.deepEqual(response.points[0].vector, { title: [0.3, 0.4] });
370
+ assert.deepEqual(collection.queries[0].fields, {
371
+ include: ["_qdrant_id", "tenant", "_qdrant_vector_title"],
372
+ });
373
+ assert.equal(collection.queries[0].includeVectors, true);
374
+
375
+ const records = await client.retrieve("docs", {
376
+ ids: [1],
377
+ withPayload: ["tenant"],
378
+ withVectors: false,
379
+ });
380
+ assert.deepEqual(records[0].payload, { tenant: "acme" });
381
+ assert.equal(records[0].vector, undefined);
382
+ assert.deepEqual(collection.docs.fetches.at(-1), {
383
+ ids: ["1"],
384
+ consistentRead: true,
385
+ includeVectors: false,
386
+ fields: { include: ["_qdrant_id", "tenant"] },
387
+ });
388
+ });
389
+
331
390
  test("retrieve, delete, scroll, count, and getCollection return qdrant-style objects", async () => {
332
391
  const fake = new FakeLambdaDB();
333
392
  fake.indexConfigs = {
@@ -392,8 +451,14 @@ test("retrieve, delete, scroll, count, and getCollection return qdrant-style obj
392
451
  },
393
452
  ]);
394
453
 
395
- const [scrollRecords, nextOffset] = await client.scroll("docs", { limit: 3 });
454
+ const [scrollRecords, nextOffset] = await client.scroll("docs", {
455
+ limit: 3,
456
+ withPayload: ["tenant"],
457
+ withVectors: ["title"],
458
+ });
396
459
  assert.equal(scrollRecords[0].id, 1);
460
+ assert.deepEqual(scrollRecords[0].payload, { tenant: "acme" });
461
+ assert.deepEqual(scrollRecords[0].vector, { title: [0.3, 0.4] });
397
462
  assert.equal(nextOffset, undefined);
398
463
  assert.deepEqual(fake.collection("docs").docs.lists, [{ size: 3 }]);
399
464