@functional-systems/lambdadb 0.3.4 → 0.4.1

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 (60) hide show
  1. package/README.md +42 -0
  2. package/dist/commonjs/compat/qdrant/client.d.ts +212 -0
  3. package/dist/commonjs/compat/qdrant/client.d.ts.map +1 -0
  4. package/dist/commonjs/compat/qdrant/client.js +618 -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 +399 -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/esm/compat/qdrant/client.d.ts +212 -0
  27. package/dist/esm/compat/qdrant/client.d.ts.map +1 -0
  28. package/dist/esm/compat/qdrant/client.js +581 -0
  29. package/dist/esm/compat/qdrant/client.js.map +1 -0
  30. package/dist/esm/compat/qdrant/conversions.d.ts +40 -0
  31. package/dist/esm/compat/qdrant/conversions.d.ts.map +1 -0
  32. package/dist/esm/compat/qdrant/conversions.js +349 -0
  33. package/dist/esm/compat/qdrant/conversions.js.map +1 -0
  34. package/dist/esm/compat/qdrant/errors.d.ts +10 -0
  35. package/dist/esm/compat/qdrant/errors.d.ts.map +1 -0
  36. package/dist/esm/compat/qdrant/errors.js +19 -0
  37. package/dist/esm/compat/qdrant/errors.js.map +1 -0
  38. package/dist/esm/compat/qdrant/filters.d.ts +10 -0
  39. package/dist/esm/compat/qdrant/filters.d.ts.map +1 -0
  40. package/dist/esm/compat/qdrant/filters.js +169 -0
  41. package/dist/esm/compat/qdrant/filters.js.map +1 -0
  42. package/dist/esm/compat/qdrant/models.d.ts +198 -0
  43. package/dist/esm/compat/qdrant/models.d.ts.map +1 -0
  44. package/dist/esm/compat/qdrant/models.js +185 -0
  45. package/dist/esm/compat/qdrant/models.js.map +1 -0
  46. package/dist/esm/compat/qdrant.d.ts +4 -0
  47. package/dist/esm/compat/qdrant.d.ts.map +1 -0
  48. package/dist/esm/compat/qdrant.js +4 -0
  49. package/dist/esm/compat/qdrant.js.map +1 -0
  50. package/jsr.json +3 -1
  51. package/package.json +32 -1
  52. package/src/compat/qdrant/client.ts +955 -0
  53. package/src/compat/qdrant/conversions.ts +440 -0
  54. package/src/compat/qdrant/errors.ts +20 -0
  55. package/src/compat/qdrant/filters.ts +213 -0
  56. package/src/compat/qdrant/models.ts +317 -0
  57. package/src/compat/qdrant.ts +3 -0
  58. package/test/integration/qdrant-compat-external.test.mjs +351 -0
  59. package/test/integration/qdrant-compat-live.test.mjs +122 -0
  60. package/test/qdrant-compat.test.mjs +455 -0
@@ -0,0 +1,317 @@
1
+ type JsonObject = { [key: string]: unknown };
2
+
3
+ export const Distance = {
4
+ COSINE: "Cosine",
5
+ EUCLID: "Euclid",
6
+ DOT: "Dot",
7
+ MANHATTAN: "Manhattan",
8
+ Cosine: "Cosine",
9
+ Euclid: "Euclid",
10
+ Dot: "Dot",
11
+ Manhattan: "Manhattan",
12
+ } as const;
13
+ export type Distance = (typeof Distance)[keyof typeof Distance];
14
+
15
+ export const PayloadSchemaType = {
16
+ KEYWORD: "keyword",
17
+ INTEGER: "integer",
18
+ FLOAT: "float",
19
+ BOOL: "bool",
20
+ DATETIME: "datetime",
21
+ TEXT: "text",
22
+ UUID: "uuid",
23
+ GEO: "geo",
24
+ Keyword: "keyword",
25
+ Integer: "integer",
26
+ Float: "float",
27
+ Bool: "bool",
28
+ Datetime: "datetime",
29
+ Text: "text",
30
+ Uuid: "uuid",
31
+ Geo: "geo",
32
+ } as const;
33
+ export type PayloadSchemaType =
34
+ (typeof PayloadSchemaType)[keyof typeof PayloadSchemaType];
35
+
36
+ export const UpdateStatus = {
37
+ ACKNOWLEDGED: "acknowledged",
38
+ COMPLETED: "completed",
39
+ } as const;
40
+ export type UpdateStatus = (typeof UpdateStatus)[keyof typeof UpdateStatus];
41
+
42
+ export type PointId = string | number;
43
+ export type DenseVector = number[];
44
+ export type NamedVector = {
45
+ [name: string]: DenseVector | SparseVector | JsonObject;
46
+ };
47
+ export type PointVector = DenseVector | NamedVector;
48
+
49
+ function assignExtras<T extends object>(target: T, init: JsonObject): T {
50
+ for (const [key, value] of Object.entries(init)) {
51
+ if (!(key in target)) {
52
+ Object.assign(target, { [key]: value });
53
+ }
54
+ }
55
+ return target;
56
+ }
57
+
58
+ export class VectorParams {
59
+ size: number;
60
+ distance: Distance;
61
+
62
+ constructor(init: { size: number; distance?: Distance | string | undefined }) {
63
+ this.size = init.size;
64
+ this.distance = normalizeDistance(init.distance);
65
+ assignExtras(this, init as JsonObject);
66
+ }
67
+ }
68
+
69
+ export class SparseVector {
70
+ indices: number[];
71
+ values: number[];
72
+
73
+ constructor(init: { indices: number[]; values: number[] }) {
74
+ this.indices = init.indices;
75
+ this.values = init.values;
76
+ assignExtras(this, init as JsonObject);
77
+ }
78
+ }
79
+
80
+ export class PointStruct {
81
+ id: PointId;
82
+ vector: PointVector;
83
+ payload: JsonObject | undefined;
84
+
85
+ constructor(init: {
86
+ id: PointId;
87
+ vector: PointVector;
88
+ payload?: JsonObject | null | undefined;
89
+ }) {
90
+ this.id = init.id;
91
+ this.vector = init.vector;
92
+ this.payload = init.payload ?? undefined;
93
+ assignExtras(this, init as JsonObject);
94
+ }
95
+ }
96
+
97
+ export class MatchValue {
98
+ value: unknown;
99
+
100
+ constructor(init: { value: unknown }) {
101
+ this.value = init.value;
102
+ assignExtras(this, init as JsonObject);
103
+ }
104
+ }
105
+
106
+ export class MatchAny {
107
+ any: unknown[];
108
+
109
+ constructor(init: { any: unknown[] }) {
110
+ this.any = init.any;
111
+ assignExtras(this, init as JsonObject);
112
+ }
113
+ }
114
+
115
+ export class MatchExcept {
116
+ except: unknown[];
117
+ except_: unknown[];
118
+
119
+ constructor(init: { except?: unknown[] | undefined; except_?: unknown[] | undefined }) {
120
+ this.except = init.except ?? init.except_ ?? [];
121
+ this.except_ = this.except;
122
+ assignExtras(this, init as JsonObject);
123
+ }
124
+ }
125
+
126
+ export class MatchText {
127
+ text: string;
128
+
129
+ constructor(init: { text: string }) {
130
+ this.text = init.text;
131
+ assignExtras(this, init as JsonObject);
132
+ }
133
+ }
134
+
135
+ export class Range {
136
+ gt: unknown;
137
+ gte: unknown;
138
+ lt: unknown;
139
+ lte: unknown;
140
+
141
+ constructor(init: {
142
+ gt?: unknown;
143
+ gte?: unknown;
144
+ lt?: unknown;
145
+ lte?: unknown;
146
+ }) {
147
+ this.gt = init.gt;
148
+ this.gte = init.gte;
149
+ this.lt = init.lt;
150
+ this.lte = init.lte;
151
+ assignExtras(this, init as JsonObject);
152
+ }
153
+ }
154
+
155
+ export type MatchCondition =
156
+ | MatchValue
157
+ | MatchAny
158
+ | MatchExcept
159
+ | MatchText
160
+ | JsonObject;
161
+
162
+ export class FieldCondition {
163
+ key: string;
164
+ match: MatchCondition | undefined;
165
+ range: Range | JsonObject | undefined;
166
+
167
+ constructor(init: {
168
+ key: string;
169
+ match?: MatchCondition | null | undefined;
170
+ range?: Range | JsonObject | null | undefined;
171
+ }) {
172
+ this.key = init.key;
173
+ this.match = init.match ?? undefined;
174
+ this.range = init.range ?? undefined;
175
+ assignExtras(this, init as JsonObject);
176
+ }
177
+ }
178
+
179
+ export class HasIdCondition {
180
+ hasId: PointId[];
181
+ has_id: PointId[];
182
+
183
+ constructor(init: { hasId?: PointId[] | undefined; has_id?: PointId[] | undefined }) {
184
+ this.hasId = init.hasId ?? init.has_id ?? [];
185
+ this.has_id = this.hasId;
186
+ assignExtras(this, init as JsonObject);
187
+ }
188
+ }
189
+
190
+ export type Condition = FieldCondition | HasIdCondition | JsonObject;
191
+
192
+ export class Filter {
193
+ must: Condition[] | undefined;
194
+ should: Condition[] | undefined;
195
+ mustNot: Condition[] | undefined;
196
+ must_not: Condition[] | undefined;
197
+
198
+ constructor(init: {
199
+ must?: Condition[] | null | undefined;
200
+ should?: Condition[] | null | undefined;
201
+ mustNot?: Condition[] | null | undefined;
202
+ must_not?: Condition[] | null | undefined;
203
+ } = {}) {
204
+ this.must = init.must ?? undefined;
205
+ this.should = init.should ?? undefined;
206
+ this.mustNot = init.mustNot ?? init.must_not ?? undefined;
207
+ this.must_not = this.mustNot;
208
+ assignExtras(this, init as JsonObject);
209
+ }
210
+ }
211
+
212
+ export class SearchParams {
213
+ hnswEf: number | undefined;
214
+ hnsw_ef: number | undefined;
215
+ exact: boolean | undefined;
216
+
217
+ constructor(init: {
218
+ hnswEf?: number | undefined;
219
+ hnsw_ef?: number | undefined;
220
+ exact?: boolean | undefined;
221
+ } = {}) {
222
+ this.hnswEf = init.hnswEf ?? init.hnsw_ef;
223
+ this.hnsw_ef = this.hnswEf;
224
+ this.exact = init.exact;
225
+ assignExtras(this, init as JsonObject);
226
+ }
227
+ }
228
+
229
+ export class ScoredPoint {
230
+ id: PointId;
231
+ version: number | undefined;
232
+ score: number | undefined;
233
+ payload: JsonObject | undefined;
234
+ vector: unknown;
235
+
236
+ constructor(init: {
237
+ id: PointId;
238
+ version?: number | undefined;
239
+ score?: number | undefined;
240
+ payload?: JsonObject | null | undefined;
241
+ vector?: unknown;
242
+ }) {
243
+ this.id = init.id;
244
+ this.version = init.version;
245
+ this.score = init.score;
246
+ this.payload = init.payload ?? undefined;
247
+ this.vector = init.vector;
248
+ assignExtras(this, init as JsonObject);
249
+ }
250
+ }
251
+
252
+ export class Record {
253
+ id: PointId;
254
+ payload: JsonObject | undefined;
255
+ vector: unknown;
256
+
257
+ constructor(init: {
258
+ id: PointId;
259
+ payload?: JsonObject | null | undefined;
260
+ vector?: unknown;
261
+ }) {
262
+ this.id = init.id;
263
+ this.payload = init.payload ?? undefined;
264
+ this.vector = init.vector;
265
+ assignExtras(this, init as JsonObject);
266
+ }
267
+ }
268
+
269
+ export class QueryResponse {
270
+ points: ScoredPoint[];
271
+
272
+ constructor(init: { points: ScoredPoint[] }) {
273
+ this.points = init.points;
274
+ assignExtras(this, init as JsonObject);
275
+ }
276
+ }
277
+
278
+ export class UpdateResult {
279
+ operationId: PointId | undefined;
280
+ operation_id: PointId | undefined;
281
+ status: UpdateStatus;
282
+
283
+ constructor(init: {
284
+ operationId?: PointId | undefined;
285
+ operation_id?: PointId | undefined;
286
+ status?: UpdateStatus | string | undefined;
287
+ } = {}) {
288
+ this.operationId = init.operationId ?? init.operation_id;
289
+ this.operation_id = this.operationId;
290
+ this.status = normalizeUpdateStatus(init.status);
291
+ assignExtras(this, init as JsonObject);
292
+ }
293
+ }
294
+
295
+ export class CountResult {
296
+ count: number;
297
+
298
+ constructor(init: { count: number }) {
299
+ this.count = init.count;
300
+ assignExtras(this, init as JsonObject);
301
+ }
302
+ }
303
+
304
+ function normalizeDistance(distance: Distance | string | undefined): Distance {
305
+ const value = distance ?? Distance.COSINE;
306
+ if (value === "Cosine" || value === "cosine") return Distance.COSINE;
307
+ if (value === "Euclid" || value === "euclidean") return Distance.EUCLID;
308
+ if (value === "Dot" || value === "dot_product") return Distance.DOT;
309
+ if (value === "Manhattan" || value === "manhattan") return Distance.MANHATTAN;
310
+ return value as Distance;
311
+ }
312
+
313
+ function normalizeUpdateStatus(status: UpdateStatus | string | undefined): UpdateStatus {
314
+ const value = status ?? UpdateStatus.COMPLETED;
315
+ if (value === UpdateStatus.ACKNOWLEDGED) return UpdateStatus.ACKNOWLEDGED;
316
+ return UpdateStatus.COMPLETED;
317
+ }
@@ -0,0 +1,3 @@
1
+ export { QdrantCompatClient, QdrantClient } from "./qdrant/client.js";
2
+ export * as models from "./qdrant/models.js";
3
+ export * from "./qdrant/errors.js";
@@ -0,0 +1,351 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { Document as LangChainDocument } from "@langchain/core/documents";
5
+ import { QdrantVectorStore as LangChainQdrantVectorStore } from "@langchain/qdrant";
6
+ import { Settings } from "@llamaindex/core/global";
7
+ import { TextNode } from "@llamaindex/core/schema";
8
+ import { QdrantVectorStore as LlamaIndexQdrantVectorStore } from "@llamaindex/qdrant";
9
+
10
+ import { QdrantCompatClient } from "../../dist/esm/compat/qdrant.js";
11
+
12
+ const shouldRun = process.env.LAMBDADB_RUN_EXTERNAL_INTEGRATION_TESTS === "1";
13
+
14
+ class StaticEmbeddings {
15
+ async embedDocuments(texts) {
16
+ return texts.map(vectorForText);
17
+ }
18
+
19
+ async embedQuery(text) {
20
+ return vectorForText(text);
21
+ }
22
+ }
23
+
24
+ class InMemoryDocs {
25
+ constructor(collection) {
26
+ this.collection = collection;
27
+ }
28
+
29
+ async upsert(body) {
30
+ for (const doc of body.docs) {
31
+ this.collection.docs.set(String(doc.id), doc);
32
+ }
33
+ return { message: "ok" };
34
+ }
35
+
36
+ async fetch(body) {
37
+ return {
38
+ docs: body.ids
39
+ .map((id) => this.collection.docs.get(String(id)))
40
+ .filter(Boolean)
41
+ .map((doc) => ({ collection: this.collection.name, doc: projectDoc(doc, body.fields) })),
42
+ total: body.ids.length,
43
+ took: 0,
44
+ isDocsInline: true,
45
+ };
46
+ }
47
+
48
+ async delete(body) {
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
+ }
58
+ }
59
+ return { message: "ok" };
60
+ }
61
+
62
+ async list(body = {}) {
63
+ return {
64
+ docs: Array.from(this.collection.docs.values())
65
+ .slice(0, body.size ?? 10)
66
+ .map((doc) => ({ collection: this.collection.name, doc: projectDoc(doc, body.fields) })),
67
+ isDocsInline: true,
68
+ total: this.collection.docs.size,
69
+ };
70
+ }
71
+ }
72
+
73
+ class InMemoryCollection {
74
+ constructor(owner, name) {
75
+ this.owner = owner;
76
+ this.name = name;
77
+ this.docs = new Map();
78
+ this.docApi = new InMemoryDocs(this);
79
+ this.indexConfigs = {};
80
+ }
81
+
82
+ handle() {
83
+ return {
84
+ docs: this.docApi,
85
+ get: async () => ({
86
+ collection: {
87
+ collectionName: this.name,
88
+ indexConfigs: this.indexConfigs,
89
+ numDocs: this.docs.size,
90
+ collectionStatus: "ACTIVE",
91
+ },
92
+ }),
93
+ update: async (body) => {
94
+ this.indexConfigs = body.indexConfigs;
95
+ return { collection: { collectionName: this.name, indexConfigs: this.indexConfigs } };
96
+ },
97
+ delete: async () => {
98
+ this.owner.collections.delete(this.name);
99
+ return { message: "ok" };
100
+ },
101
+ query: async (body) => {
102
+ const knn = body.query.knn;
103
+ const queryVector = knn.queryVector;
104
+ const docs = Array.from(this.docs.values())
105
+ .filter((doc) => matchesLambdaDBFilter(doc, knn.filter))
106
+ .map((doc) => ({
107
+ collection: this.name,
108
+ doc: projectDoc(doc, body.fields),
109
+ score: dot(queryVector, doc[knn.field]),
110
+ }))
111
+ .sort((a, b) => b.score - a.score)
112
+ .slice(0, body.size ?? knn.k ?? 10);
113
+ return {
114
+ docs,
115
+ total: docs.length,
116
+ took: 0,
117
+ isDocsInline: true,
118
+ };
119
+ },
120
+ };
121
+ }
122
+ }
123
+
124
+ class InMemoryLambdaDB {
125
+ constructor() {
126
+ this.collections = new Map();
127
+ }
128
+
129
+ async listCollections() {
130
+ return {
131
+ collections: Array.from(this.collections.keys()).map((collectionName) => ({
132
+ collectionName,
133
+ })),
134
+ };
135
+ }
136
+
137
+ async createCollection(body) {
138
+ const collection = this.ensureCollection(body.collectionName);
139
+ collection.indexConfigs = body.indexConfigs ?? {};
140
+ return { collection: { collectionName: body.collectionName } };
141
+ }
142
+
143
+ collection(name) {
144
+ return this.ensureCollection(name).handle();
145
+ }
146
+
147
+ ensureCollection(name) {
148
+ if (!this.collections.has(name)) {
149
+ this.collections.set(name, new InMemoryCollection(this, name));
150
+ }
151
+ return this.collections.get(name);
152
+ }
153
+ }
154
+
155
+ test("langchain qdrant vector store works with QdrantCompatClient", {
156
+ skip: shouldRun ? false : "Set LAMBDADB_RUN_EXTERNAL_INTEGRATION_TESTS=1",
157
+ }, async () => {
158
+ Settings.embedModel = {
159
+ getTextEmbedding: async (text) => vectorForText(text),
160
+ getQueryEmbedding: async (text) => vectorForText(text),
161
+ };
162
+ const client = new QdrantCompatClient(new InMemoryLambdaDB());
163
+ const embeddings = new StaticEmbeddings();
164
+
165
+ const store = await LangChainQdrantVectorStore.fromDocuments(
166
+ [
167
+ new LangChainDocument({
168
+ pageContent: "alpha",
169
+ metadata: { tenant: "acme" },
170
+ }),
171
+ new LangChainDocument({
172
+ pageContent: "beta",
173
+ metadata: { tenant: "acme" },
174
+ }),
175
+ ],
176
+ embeddings,
177
+ {
178
+ client,
179
+ collectionName: "langchain-docs",
180
+ },
181
+ );
182
+
183
+ const results = await store.similaritySearchVectorWithScore([1, 0, 0], 1);
184
+
185
+ assert.equal(results.length, 1);
186
+ assert.equal(results[0][0].pageContent, "alpha");
187
+ assert.equal(results[0][0].metadata.tenant, "acme");
188
+ });
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
+
232
+ test("llamaindex qdrant vector store works with QdrantCompatClient", {
233
+ skip: shouldRun ? false : "Set LAMBDADB_RUN_EXTERNAL_INTEGRATION_TESTS=1",
234
+ }, async () => {
235
+ const client = new QdrantCompatClient(new InMemoryLambdaDB());
236
+ const store = new LlamaIndexQdrantVectorStore({
237
+ client,
238
+ collectionName: "llamaindex-docs",
239
+ });
240
+
241
+ const alpha = new TextNode({
242
+ text: "alpha",
243
+ metadata: { tenant: "acme" },
244
+ });
245
+ alpha.id_ = "alpha";
246
+ alpha.embedding = [1, 0, 0];
247
+
248
+ const beta = new TextNode({
249
+ text: "beta",
250
+ metadata: { tenant: "acme" },
251
+ });
252
+ beta.id_ = "beta";
253
+ beta.embedding = [0, 1, 0];
254
+
255
+ const ids = await store.add([alpha, beta]);
256
+ assert.deepEqual(ids, ["alpha", "beta"]);
257
+
258
+ const results = await store.query({
259
+ queryEmbedding: [1, 0, 0],
260
+ similarityTopK: 1,
261
+ });
262
+
263
+ assert.equal(results.ids[0], "alpha");
264
+ assert.equal(results.nodes[0].getContent(), "alpha");
265
+ });
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
+
303
+ function vectorForText(text) {
304
+ if (text.includes("alpha")) return [1, 0, 0];
305
+ if (text.includes("beta")) return [0, 1, 0];
306
+ return [0, 0, 1];
307
+ }
308
+
309
+ function dot(left, right) {
310
+ if (!Array.isArray(left) || !Array.isArray(right)) return 0;
311
+ return left.reduce((sum, value, index) => sum + value * (right[index] ?? 0), 0);
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
+ }