@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.
- package/dist/commonjs/compat/qdrant/client.d.ts +18 -16
- package/dist/commonjs/compat/qdrant/client.d.ts.map +1 -1
- package/dist/commonjs/compat/qdrant/client.js +65 -23
- package/dist/commonjs/compat/qdrant/client.js.map +1 -1
- package/dist/commonjs/compat/qdrant/conversions.d.ts +4 -4
- package/dist/commonjs/compat/qdrant/conversions.d.ts.map +1 -1
- package/dist/commonjs/compat/qdrant/conversions.js +23 -10
- package/dist/commonjs/compat/qdrant/conversions.js.map +1 -1
- package/dist/esm/compat/qdrant/client.d.ts +18 -16
- package/dist/esm/compat/qdrant/client.d.ts.map +1 -1
- package/dist/esm/compat/qdrant/client.js +66 -24
- package/dist/esm/compat/qdrant/client.js.map +1 -1
- package/dist/esm/compat/qdrant/conversions.d.ts +4 -4
- package/dist/esm/compat/qdrant/conversions.d.ts.map +1 -1
- package/dist/esm/compat/qdrant/conversions.js +23 -10
- package/dist/esm/compat/qdrant/conversions.js.map +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/compat/qdrant/client.ts +89 -38
- package/src/compat/qdrant/conversions.ts +30 -12
- package/test/integration/qdrant-compat-external.test.mjs +130 -5
- package/test/qdrant-compat.test.mjs +67 -2
|
@@ -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?:
|
|
86
|
-
with_payload?:
|
|
87
|
-
withVectors?:
|
|
88
|
-
with_vectors?:
|
|
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?:
|
|
403
|
-
with_payload?:
|
|
404
|
-
withVectors?:
|
|
405
|
-
with_vectors?:
|
|
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
|
|
419
|
-
const
|
|
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:
|
|
426
|
-
withVectors:
|
|
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
|
|
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
|
|
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:
|
|
534
|
-
withVectors:
|
|
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?:
|
|
553
|
-
withPayload?:
|
|
554
|
-
with_vector?:
|
|
555
|
-
withVectors?:
|
|
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 =
|
|
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?:
|
|
607
|
-
with_payload?:
|
|
608
|
-
withVectors?:
|
|
609
|
-
with_vectors?:
|
|
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
|
-
|
|
628
|
-
|
|
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:
|
|
634
|
-
withVectors:
|
|
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
|
|
855
|
-
if (
|
|
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
|
|
105
|
-
vector: vectorFromDoc(doc, options.withVectors
|
|
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
|
|
121
|
-
vector: vectorFromDoc(doc, options.withVectors
|
|
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 (
|
|
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,
|
|
370
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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: {
|
|
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", {
|
|
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
|
|