@cocreate/qdrant 1.0.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.
package/src/index.js ADDED
@@ -0,0 +1,936 @@
1
+ /********************************************************************************
2
+ * Copyright (C) 2026 CoCreate and Contributors.
3
+ *
4
+ * This program is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU Affero General Public License as published
6
+ * by the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU Affero General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU Affero General Public License
15
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ ********************************************************************************/
17
+
18
+ import { QdrantClient } from "@qdrant/js-client-rest";
19
+ import {
20
+ dotNotationToObject,
21
+ queryData,
22
+ searchData,
23
+ sortData,
24
+ isValidDate
25
+ } from "@cocreate/utils";
26
+
27
+ const organizations = {};
28
+
29
+ /**
30
+ * Dynamically resolves or caches a long-lived Qdrant Vector Client connection.
31
+ * Caches the connection promise to prevent concurrent, duplicate handshakes.
32
+ * Supports direct connection URLs as well as credential objects containing API keys.
33
+ */
34
+ async function dbClient(data) {
35
+ const storageUrl = data.storageUrl || data.url;
36
+
37
+ if (storageUrl) {
38
+ if (!organizations[data.organization_id]) {
39
+ organizations[data.organization_id] = {};
40
+ }
41
+ try {
42
+ if (!organizations[data.organization_id][storageUrl]) {
43
+ const connectionPromise = (async () => {
44
+ let config = {};
45
+
46
+ // Case A: Structured credentials object containing apiKey and endpoint
47
+ if (typeof storageUrl === "object" && storageUrl !== null) {
48
+ config = {
49
+ url: storageUrl.url || storageUrl.endpoint || "http://localhost:6333",
50
+ apiKey: storageUrl.apiKey || storageUrl.api_key || undefined
51
+ };
52
+ }
53
+ // Case B: Standard connection string URL
54
+ else if (typeof storageUrl === "string") {
55
+ config = { url: storageUrl };
56
+ }
57
+
58
+ const client = new QdrantClient(config);
59
+
60
+ // Test the client connection viability
61
+ await client.api("collections").getCollections();
62
+ return client;
63
+ })();
64
+
65
+ organizations[data.organization_id][storageUrl] = connectionPromise;
66
+ }
67
+
68
+ return await organizations[data.organization_id][storageUrl];
69
+ } catch (error) {
70
+ console.error(`${data.organization_id}: Qdrant failed to connect`);
71
+ errorHandler(data, error);
72
+ return { status: false };
73
+ }
74
+ }
75
+
76
+ errorHandler(data, "missing Qdrant Connection URL/Credentials");
77
+ return;
78
+ }
79
+
80
+ /**
81
+ * Global Event Listener: orgDeleted
82
+ * Cleans up active Qdrant instances and destroys connection configurations gracefully.
83
+ */
84
+ process.on("orgDeleted", (organization_id) => {
85
+ const orgPools = organizations[organization_id];
86
+ if (orgPools) {
87
+ console.log(`[Qdrant Utilities] Cleanup Scheduled: Initiating 5-second grace period for Org: ${organization_id}`);
88
+ setTimeout(async () => {
89
+ const currentPools = organizations[organization_id];
90
+ if (!currentPools) return;
91
+
92
+ console.log(`[Qdrant Utilities] Cleanup Executing: Closing clients for Org: ${organization_id}`);
93
+ // Qdrant client uses standard http/https fetch underneath, so we simply delete the cached connections
94
+ delete organizations[organization_id];
95
+ console.log(`[Qdrant Utilities] Memory Cleared: Registry freed for Org: ${organization_id}`);
96
+ }, 5000);
97
+ }
98
+ });
99
+
100
+ /**
101
+ * Universal router matching the standard CoCreate pipeline signature.
102
+ */
103
+ export function send(data) {
104
+ let [type, method] = data.method.split(".");
105
+ if (type === "database") return database(method, data);
106
+ if (type === "array") return array(method, data);
107
+ if (type === "object") return object(method, data);
108
+ }
109
+
110
+ /**
111
+ * "database" operations. Since Qdrant is catalog-less and runs globally,
112
+ * we emulate isolated databases by appending a prefix or treating the
113
+ * active tenant name as a partition namespace.
114
+ */
115
+ function database(method, data) {
116
+ const storageName = data.storageName || data.provider || "qdrant";
117
+ return new Promise(async (resolve) => {
118
+ let type = "database";
119
+ let databaseArray = [];
120
+
121
+ try {
122
+ const db = await dbClient(data);
123
+ if (!db || db.status === false) return resolve(data);
124
+
125
+ if (method === "read") {
126
+ const dbName = data.database || "default";
127
+ const dbObj = { name: dbName };
128
+
129
+ process.emit("usage", {
130
+ type: "egress",
131
+ data: { api: "listDatabases", context: dbName },
132
+ organization_id: data.organization_id
133
+ });
134
+
135
+ if (data.$filter && data.$filter.query) {
136
+ let isFilter = queryData(dbObj, data.$filter.query);
137
+ if (isFilter) {
138
+ databaseArray.push({ database: dbObj, storage: storageName });
139
+ }
140
+ } else {
141
+ databaseArray.push({ database: dbObj, storage: storageName });
142
+ }
143
+ resolve(createData(data, databaseArray, type));
144
+ }
145
+ if (method === "delete") {
146
+ // Emulate database partition teardown by dropping all collections with our organization prefix
147
+ const listRes = await db.api("collections").getCollections();
148
+ const prefix = `${data.organization_id}_`;
149
+
150
+ for (let col of listRes.result.collections || []) {
151
+ if (col.name.startsWith(prefix)) {
152
+ process.emit("usage", {
153
+ type: "egress",
154
+ data: { api: "deleteCollection", collection: col.name },
155
+ organization_id: data.organization_id
156
+ });
157
+ await db.deleteCollection(col.name);
158
+ }
159
+ }
160
+ resolve({ status: true });
161
+ }
162
+ } catch (error) {
163
+ errorHandler(data, error);
164
+ console.log(method, "error", error);
165
+ resolve(data);
166
+ }
167
+ });
168
+ }
169
+
170
+ /**
171
+ * "array" operations mapped directly to Qdrant Collections.
172
+ * We isolate multi-tenant structures cleanly by prefixing collections: "orgId_collectionName"
173
+ */
174
+ function array(method, data) {
175
+ const storageName = data.storageName || data.provider || "qdrant";
176
+ return new Promise(async (resolve) => {
177
+ let type = "array";
178
+ let arrayArray = [];
179
+
180
+ try {
181
+ const db = await dbClient(data);
182
+ if (!db || db.status === false) return resolve(data);
183
+
184
+ if (data.request) data.array = data.request;
185
+
186
+ let databases = data.database;
187
+ if (!Array.isArray(databases)) databases = [databases];
188
+
189
+ let databasesLength = databases.length;
190
+ for (let database of databases) {
191
+ const prefix = `${data.organization_id}_`;
192
+
193
+ if (method === "read") {
194
+ process.emit("usage", {
195
+ type: "egress",
196
+ data: { api: "getCollections" },
197
+ organization_id: data.organization_id
198
+ });
199
+
200
+ const listRes = await db.api("collections").getCollections();
201
+ for (let col of listRes.result.collections || []) {
202
+ if (col.name.startsWith(prefix)) {
203
+ const cleanName = col.name.substring(prefix.length);
204
+ const tableObj = { name: cleanName };
205
+
206
+ if (data.$filter && data.$filter.query) {
207
+ let isFilter = queryData(tableObj, data.$filter.query);
208
+ if (isFilter) {
209
+ arrayArray.push({ name: cleanName, database, storage: storageName });
210
+ }
211
+ } else {
212
+ arrayArray.push({ name: cleanName, database, storage: storageName });
213
+ }
214
+ }
215
+ }
216
+
217
+ databasesLength -= 1;
218
+ if (!databasesLength) {
219
+ data = createData(data, arrayArray, type);
220
+ resolve(data);
221
+ }
222
+ } else {
223
+ let arrays = data.array;
224
+ if (method === "update") arrays = Object.entries(data.array);
225
+ if (!Array.isArray(arrays)) arrays = [arrays];
226
+
227
+ let arraysLength = arrays.length;
228
+ for (let array of arrays) {
229
+ if (method === "create") {
230
+ const realTableName = `${prefix}${array}`;
231
+
232
+ process.emit("usage", {
233
+ type: "egress",
234
+ data: { api: "createCollection", collection: realTableName },
235
+ organization_id: data.organization_id
236
+ });
237
+
238
+ // Create a vector index collection. Qdrant requires vector configurations.
239
+ // Defaulting to 1536 dimensions (standard OpenAI text-embedding-3-small size) and Cosine metric.
240
+ const vectorSize = data.vectorSize || data.vector_size || 1536;
241
+ const distanceMetric = data.distanceMetric || data.distance_metric || "Cosine";
242
+
243
+ await db.createCollection(realTableName, {
244
+ vectors: {
245
+ size: vectorSize,
246
+ distance: distanceMetric
247
+ }
248
+ });
249
+
250
+ arrayArray.push({ name: array, database, storage: storageName });
251
+
252
+ arraysLength -= 1;
253
+ if (!arraysLength) databasesLength -= 1;
254
+ if (!databasesLength && !arraysLength) {
255
+ data = createData(data, arrayArray, type);
256
+ resolve(data);
257
+ }
258
+ } else {
259
+ if (method === "update") {
260
+ let [oldName, newName] = array;
261
+ const oldRealName = `${prefix}${oldName}`;
262
+ const newRealName = `${prefix}${newName}`;
263
+
264
+ process.emit("usage", {
265
+ type: "egress",
266
+ data: { api: "renameCollectionEmulation", from: oldRealName, to: newRealName },
267
+ organization_id: data.organization_id
268
+ });
269
+
270
+ // Qdrant does not natively support renaming collections. We emulate by creating
271
+ // the target collection and migrating all points.
272
+ const colInfo = await db.getCollection(oldRealName);
273
+ const vectorParams = colInfo.config.params.vectors;
274
+
275
+ await db.createCollection(newRealName, {
276
+ vectors: vectorParams
277
+ });
278
+
279
+ // Scroll and migrate points in batches
280
+ let scrollRes = await db.scroll(oldRealName, { limit: 100, with_payload: true, with_vector: true });
281
+ while (scrollRes.points && scrollRes.points.length > 0) {
282
+ await db.upsert(newRealName, {
283
+ wait: true,
284
+ points: scrollRes.points
285
+ });
286
+ if (!scrollRes.next_page_offset) break;
287
+ scrollRes = await db.scroll(oldRealName, {
288
+ limit: 100,
289
+ offset: scrollRes.next_page_offset,
290
+ with_payload: true,
291
+ with_vector: true
292
+ });
293
+ }
294
+
295
+ // Drop old collection
296
+ await db.deleteCollection(oldRealName);
297
+
298
+ arrayArray.push({ name: newName, oldName: oldName, database, storage: storageName });
299
+
300
+ arraysLength -= 1;
301
+ if (!arraysLength) databasesLength -= 1;
302
+ if (!databasesLength && !arraysLength) {
303
+ data = createData(data, arrayArray, type);
304
+ resolve(data);
305
+ }
306
+ }
307
+
308
+ if (method === "delete") {
309
+ const realTableName = `${prefix}${array}`;
310
+ process.emit("usage", {
311
+ type: "egress",
312
+ data: { api: "deleteCollection", collection: realTableName },
313
+ organization_id: data.organization_id
314
+ });
315
+
316
+ await db.deleteCollection(realTableName);
317
+
318
+ arrayArray.push({ name: array, database, storage: storageName });
319
+
320
+ arraysLength -= 1;
321
+ if (!arraysLength) databasesLength -= 1;
322
+ if (!databasesLength && !arraysLength) {
323
+ data = createData(data, arrayArray, type);
324
+ resolve(data);
325
+ }
326
+ }
327
+ }
328
+ }
329
+ }
330
+ }
331
+ } catch (error) {
332
+ errorHandler(data, error);
333
+ console.log(method, "error", error);
334
+ resolve(data);
335
+ }
336
+ });
337
+ }
338
+
339
+ /**
340
+ * "object" operations mapped to Qdrant Collection Points (Vector + Metadata Payload).
341
+ * Documents in Qdrant must contain a "vector" key (an array of floats representing embeddings).
342
+ */
343
+ function object(method, data) {
344
+ const storageName = data.storageName || data.provider || "qdrant";
345
+ return new Promise(async (resolve) => {
346
+ try {
347
+ const db = await dbClient(data);
348
+ if (!db || db.status === false) return resolve(data);
349
+
350
+ let type = "object";
351
+ let documents = [];
352
+
353
+ if (!data["timeStamp"]) {
354
+ data["timeStamp"] = new Date().toISOString();
355
+ }
356
+
357
+ let databases = data.database;
358
+ if (!Array.isArray(databases)) databases = [databases];
359
+
360
+ for (let database of databases) {
361
+ let arrays = data.array;
362
+ if (!Array.isArray(arrays)) arrays = [arrays];
363
+
364
+ for (let array of arrays) {
365
+ const realTableName = `${data.organization_id}_${array}`;
366
+ const reference = {
367
+ $storage: storageName,
368
+ $database: database,
369
+ $array: array
370
+ };
371
+
372
+ if (!data[type]) data[type] = [];
373
+ else if (typeof data[type] === "string")
374
+ data[type] = [{ _id: data[type] }];
375
+ else if (!Array.isArray(data[type]))
376
+ data[type] = [data[type]];
377
+
378
+ let isFilter = !!data.$filter;
379
+ if ((isFilter && !data[type].length) || data.isFilter) {
380
+ data[type].splice(0, 0, { isFilter: "isEmptyObjectFilter" });
381
+ }
382
+
383
+ for (let i = 0; i < data[type].length; i++) {
384
+ let $storage = data[type][i].$storage || [];
385
+ let $database = data[type][i].$database || [];
386
+ let $array = data[type][i].$array || [];
387
+
388
+ if (!Array.isArray($storage)) $storage = [data[type][i].$storage];
389
+ if (!Array.isArray($database)) $database = [data[type][i].$database];
390
+ if (!Array.isArray($array)) $array = [data[type][i].$array];
391
+
392
+ if (!$storage.includes(storageName)) $storage.push(storageName);
393
+ if (!$database.includes(database)) $database.push(database);
394
+ if (!$array.includes(array)) $array.push(array);
395
+
396
+ delete data[type][i].$storage;
397
+ delete data[type][i].$database;
398
+ delete data[type][i].$array;
399
+
400
+ let queryFilter = isFilter ? data.$filter : (data[type][i].$filter || null);
401
+
402
+ if (method === "create") {
403
+ data[type][i] = replaceArray(data[type][i]);
404
+ data[type][i] = dotNotationToObject(data[type][i]);
405
+
406
+ // Qdrant point IDs must be UUIDs or 64-bit integers.
407
+ const id = data[type][i]._id || generateUUID();
408
+ const orgId = data.organization_id;
409
+ const created = {
410
+ on: new Date(data.timeStamp).toISOString(),
411
+ by: data.user_id || data.clientId
412
+ };
413
+
414
+ // Extract the vector data. If none is passed, we default to a mock zero-vector.
415
+ const vector = data[type][i].vector || data[type][i].embeddings || new Array(1536).fill(0.0);
416
+
417
+ const recordPayload = { ...data[type][i] };
418
+ delete recordPayload._id;
419
+ delete recordPayload.organization_id;
420
+ delete recordPayload.vector;
421
+ delete recordPayload.embeddings;
422
+
423
+ const finalPayload = {
424
+ _id: id,
425
+ organization_id: orgId,
426
+ created,
427
+ modified: null,
428
+ ...recordPayload
429
+ };
430
+
431
+ process.emit("usage", {
432
+ type: "egress",
433
+ data: { api: "upsertPoint", collection: realTableName, id },
434
+ organization_id: data.organization_id
435
+ });
436
+
437
+ await db.upsert(realTableName, {
438
+ wait: true,
439
+ points: [
440
+ {
441
+ id: id,
442
+ vector: vector,
443
+ payload: finalPayload
444
+ }
445
+ ]
446
+ });
447
+
448
+ data[type][i] = { ...finalPayload, vector, $storage, $database, $array };
449
+ }
450
+ else if (method === "read") {
451
+ if (data[type][i]._id && !isFilter) {
452
+ process.emit("usage", {
453
+ type: "egress",
454
+ data: { api: "retrievePoint", collection: realTableName, id: data[type][i]._id },
455
+ organization_id: data.organization_id
456
+ });
457
+
458
+ const retrieveRes = await db.retrieve(realTableName, {
459
+ ids: [data[type][i]._id],
460
+ with_payload: true,
461
+ with_vector: true
462
+ });
463
+
464
+ if (retrieveRes.length > 0) {
465
+ let dbRecord = retrieveRes[0].payload;
466
+ let vector = retrieveRes[0].vector;
467
+
468
+ if ($storage.length && data[type][i].modified && data[type][i].modified.on) {
469
+ let clientTime = new Date(data[type][i].modified.on);
470
+ let dbTime = new Date(dbRecord.modified ? dbRecord.modified.on : 0);
471
+
472
+ if (clientTime > dbTime) {
473
+ const updatedRecord = { ...dbRecord, ...data[type][i] };
474
+ const savedRecord = await executeUpdatePoint(db, realTableName, data[type][i]._id, updatedRecord, data);
475
+ data[type][i] = { ...savedRecord, $storage, $database, $array };
476
+ } else {
477
+ data[type][i] = { ...data[type][i], ...dbRecord, vector, $storage, $database, $array };
478
+ }
479
+ } else {
480
+ data[type][i] = { ...data[type][i], ...dbRecord, vector, $storage, $database, $array };
481
+ }
482
+ } else {
483
+ data[type].splice(i, 1);
484
+ i--;
485
+ }
486
+ } else {
487
+ process.emit("usage", {
488
+ type: "egress",
489
+ data: { api: "scrollPoints", collection: realTableName },
490
+ organization_id: data.organization_id
491
+ });
492
+
493
+ let queryResults = [];
494
+
495
+ if (data.$filter && (data.$filter.vector || data.$filter.embeddings)) {
496
+ const queryVector = data.$filter.vector || data.$filter.embeddings;
497
+ const filterParams = compileFilterToQdrant(queryFilter, data.organization_id);
498
+
499
+ process.emit("usage", {
500
+ type: "egress",
501
+ data: { api: "searchPoints", collection: realTableName },
502
+ organization_id: data.organization_id
503
+ });
504
+
505
+ // Execute high-performance cosine similarity nearest-neighbor semantic search
506
+ const searchRes = await db.search(realTableName, {
507
+ vector: queryVector,
508
+ filter: filterParams,
509
+ limit: data.$filter.limit || 10,
510
+ with_payload: true,
511
+ with_vector: true
512
+ });
513
+
514
+ for (let match of searchRes) {
515
+ queryResults.push({
516
+ ...match.payload,
517
+ _score: match.score,
518
+ vector: match.vector,
519
+ ...reference
520
+ });
521
+ }
522
+ } else {
523
+ const filterParams = compileFilterToQdrant(queryFilter, data.organization_id);
524
+
525
+ process.emit("usage", {
526
+ type: "egress",
527
+ data: { api: "scrollPoints", collection: realTableName },
528
+ organization_id: data.organization_id
529
+ });
530
+
531
+ const scrollRes = await db.scroll(realTableName, {
532
+ filter: filterParams,
533
+ limit: data.$filter?.limit || 100,
534
+ offset: data.$filter?.index || undefined,
535
+ with_payload: true,
536
+ with_vector: true
537
+ });
538
+
539
+ for (let pt of scrollRes.points || []) {
540
+ queryResults.push({
541
+ ...pt.payload,
542
+ vector: pt.vector,
543
+ ...reference
544
+ });
545
+ }
546
+ }
547
+
548
+ // In-memory sort fallback
549
+ if (data.$filter && data.$filter.sort) {
550
+ queryResults = sortData(queryResults, data.$filter.sort);
551
+ }
552
+
553
+ for (let record of queryResults) {
554
+ if (data.$filter && data.$filter.search) {
555
+ let isMatch = searchData(record, data.$filter.search);
556
+ if (!isMatch) continue;
557
+ }
558
+ documents.push(record);
559
+ }
560
+ }
561
+ }
562
+ else if (method === "update") {
563
+ const updatePayload = { ...data[type][i] };
564
+ delete updatePayload._id;
565
+ delete updatePayload.$filter;
566
+
567
+ if (data[type][i]._id) {
568
+ const updatedDoc = await executeUpdatePoint(db, realTableName, data[type][i]._id, updatePayload, data);
569
+ if (updatedDoc) {
570
+ data[type][i] = { ...updatedDoc, $storage, $database, $array };
571
+ }
572
+ } else if (isFilter) {
573
+ const filterParams = compileFilterToQdrant(queryFilter, data.organization_id);
574
+ const scrollRes = await db.scroll(realTableName, {
575
+ filter: filterParams,
576
+ with_payload: true
577
+ });
578
+
579
+ for (let pt of scrollRes.points || []) {
580
+ const updatedDoc = await executeUpdatePoint(db, realTableName, pt.id, updatePayload, data);
581
+ if (updatedDoc) {
582
+ documents.push({ ...updatedDoc, ...reference });
583
+ }
584
+ }
585
+ }
586
+ }
587
+ else if (method === "delete") {
588
+ if (data[type][i]._id) {
589
+ process.emit("usage", {
590
+ type: "egress",
591
+ data: { api: "deletePoints", collection: realTableName, id: data[type][i]._id },
592
+ organization_id: data.organization_id
593
+ });
594
+ await db.delete(realTableName, {
595
+ points: [data[type][i]._id]
596
+ });
597
+ data[type][i] = { _id: data[type][i]._id, $storage, $database, $array };
598
+ } else if (isFilter) {
599
+ const filterParams = compileFilterToQdrant(queryFilter, data.organization_id);
600
+ process.emit("usage", {
601
+ type: "egress",
602
+ data: { api: "deletePointsByFilter", collection: realTableName },
603
+ organization_id: data.organization_id
604
+ });
605
+
606
+ // Get the point IDs to delete
607
+ const scrollRes = await db.scroll(realTableName, {
608
+ filter: filterParams,
609
+ with_payload: false
610
+ });
611
+
612
+ if (scrollRes.points && scrollRes.points.length > 0) {
613
+ const idsToDelete = scrollRes.points.map(pt => pt.id);
614
+ await db.delete(realTableName, {
615
+ points: idsToDelete
616
+ });
617
+ for (let id of idsToDelete) {
618
+ documents.push({ _id: id, ...reference });
619
+ }
620
+ }
621
+ }
622
+ }
623
+ }
624
+ }
625
+ }
626
+
627
+ data = createData(data, documents, type);
628
+ resolve(data);
629
+ } catch (error) {
630
+ errorHandler(data, error);
631
+ console.log(method, "error", error);
632
+ resolve(data);
633
+ }
634
+ });
635
+ }
636
+
637
+ /**
638
+ * Simulates transactional updates inside Qdrant by performing a Retrieve-Modify-Upsert lock pattern.
639
+ */
640
+ async function executeUpdatePoint(db, realTableName, pointId, rawUpdateInput, requestData) {
641
+ try {
642
+ const retrieveRes = await db.retrieve(realTableName, {
643
+ ids: [pointId],
644
+ with_payload: true,
645
+ with_vector: true
646
+ });
647
+
648
+ let currentPoint = retrieveRes.length > 0 ? retrieveRes[0] : null;
649
+ let currentRecord = currentPoint ? currentPoint.payload : null;
650
+ let currentVector = currentPoint ? currentPoint.vector : (rawUpdateInput.vector || rawUpdateInput.embeddings || new Array(1536).fill(0.0));
651
+
652
+ if (!currentRecord) {
653
+ if (requestData.upsert || rawUpdateInput.upsert || rawUpdateInput.$upsert) {
654
+ currentRecord = { _id: pointId, organization_id: requestData.organization_id };
655
+ } else {
656
+ return null;
657
+ }
658
+ }
659
+
660
+ const modified = {
661
+ on: new Date(requestData.timeStamp).toISOString(),
662
+ by: requestData.user_id || requestData.clientId
663
+ };
664
+
665
+ // Run deep nested JSON modifier aligning with Mongo semantics
666
+ const updatedPayload = applyMongoUpdate(currentRecord, rawUpdateInput);
667
+
668
+ // If a new vector was passed in the update payload, update the vectors coordinates
669
+ if (rawUpdateInput.vector || rawUpdateInput.embeddings) {
670
+ currentVector = rawUpdateInput.vector || rawUpdateInput.embeddings;
671
+ }
672
+
673
+ const savePayload = { ...updatedPayload };
674
+ delete savePayload._id;
675
+ delete savePayload.organization_id;
676
+ delete savePayload.created;
677
+ delete savePayload.modified;
678
+ delete savePayload.vector;
679
+ delete savePayload.embeddings;
680
+
681
+ const recordToWrite = {
682
+ _id: pointId,
683
+ organization_id: requestData.organization_id,
684
+ created: currentRecord.created || modified,
685
+ modified,
686
+ ...savePayload
687
+ };
688
+
689
+ process.emit("usage", {
690
+ type: "egress",
691
+ data: { api: "upsertPoint", collection: realTableName, id: pointId },
692
+ organization_id: requestData.organization_id
693
+ });
694
+
695
+ await db.upsert(realTableName, {
696
+ wait: true,
697
+ points: [
698
+ {
699
+ id: pointId,
700
+ vector: currentVector,
701
+ payload: recordToWrite
702
+ }
703
+ ]
704
+ });
705
+
706
+ return { ...recordToWrite, vector: currentVector };
707
+ } catch (error) {
708
+ throw error;
709
+ }
710
+ }
711
+
712
+ /**
713
+ * Applies MongoDB update operators recursively on payloads before index storage.
714
+ */
715
+ function applyMongoUpdate(record, updatePayload) {
716
+ let target = { ...record };
717
+
718
+ Object.keys(updatePayload).forEach((rawKey) => {
719
+ if (rawKey.startsWith("$")) return;
720
+ let cleanKey = rawKey.replace(/\[(\d+)\]/g, ".$1");
721
+ setNestedValue(target, cleanKey, updatePayload[rawKey]);
722
+ });
723
+
724
+ if (updatePayload.$set) {
725
+ Object.keys(updatePayload.$set).forEach((key) => {
726
+ let cleanKey = key.replace(/\[(\d+)\]/g, ".$1");
727
+ setNestedValue(target, cleanKey, updatePayload.$set[key]);
728
+ });
729
+ }
730
+
731
+ if (updatePayload.$unset) {
732
+ Object.keys(updatePayload.$unset).forEach((key) => {
733
+ let cleanKey = key.replace(/\[(\d+)\]/g, ".$1");
734
+ deleteNestedValue(target, cleanKey);
735
+ });
736
+ }
737
+
738
+ if (updatePayload.$inc) {
739
+ Object.keys(updatePayload.$inc).forEach((key) => {
740
+ let cleanKey = key.replace(/\[(\d+)\]/g, ".$1");
741
+ let currentVal = getNestedValue(target, cleanKey) || 0;
742
+ setNestedValue(target, cleanKey, Number(currentVal) + Number(updatePayload.$inc[key]));
743
+ });
744
+ }
745
+
746
+ if (updatePayload.$push) {
747
+ Object.keys(updatePayload.$push).forEach((key) => {
748
+ let cleanKey = key.replace(/\[(\d+)\]/g, ".$1");
749
+ let arr = getNestedValue(target, cleanKey);
750
+ if (!Array.isArray(arr)) arr = [];
751
+
752
+ const pushValue = updatePayload.$push[key];
753
+ if (pushValue && pushValue.$each) {
754
+ let valuesToPush = pushValue.$each;
755
+ let position = typeof pushValue.$position === "number" ? pushValue.$position : arr.length;
756
+ arr.splice(position, 0, ...valuesToPush);
757
+ } else {
758
+ arr.push(pushValue);
759
+ }
760
+ setNestedValue(target, cleanKey, arr);
761
+ });
762
+ }
763
+
764
+ if (updatePayload.$pull) {
765
+ Object.keys(updatePayload.$pull).forEach((key) => {
766
+ let cleanKey = key.replace(/\[(\d+)\]/g, ".$1");
767
+ let arr = getNestedValue(target, cleanKey);
768
+ if (Array.isArray(arr)) {
769
+ const filterVal = updatePayload.$pull[key];
770
+ arr = arr.filter(item => {
771
+ if (typeof filterVal === "object" && filterVal !== null) {
772
+ return !queryData(item, filterVal);
773
+ }
774
+ return item !== filterVal;
775
+ });
776
+ setNestedValue(target, cleanKey, arr);
777
+ }
778
+ });
779
+ }
780
+
781
+ if (updatePayload.$addToSet) {
782
+ Object.keys(updatePayload.$addToSet).forEach((key) => {
783
+ let cleanKey = key.replace(/\[(\d+)\]/g, ".$1");
784
+ let arr = getNestedValue(target, cleanKey);
785
+ if (!Array.isArray(arr)) arr = [];
786
+
787
+ const addValue = updatePayload.$addToSet[key];
788
+ if (addValue && addValue.$each) {
789
+ addValue.$each.forEach(item => {
790
+ if (!arr.includes(item)) arr.push(item);
791
+ });
792
+ } else {
793
+ if (!arr.includes(addValue)) arr.push(addValue);
794
+ }
795
+ setNestedValue(target, cleanKey, arr);
796
+ });
797
+ }
798
+
799
+ return target;
800
+ }
801
+
802
+ /**
803
+ * Normalizes and translates Mongo-style schema filters dynamically to Qdrant Payload Filter syntax.
804
+ */
805
+ function compileFilterToQdrant(filter, organizationId) {
806
+ let qdrantFilter = {
807
+ must: [
808
+ {
809
+ key: "organization_id",
810
+ match: { value: organizationId }
811
+ }
812
+ ]
813
+ };
814
+
815
+ if (filter && filter.query) {
816
+ for (let key in filter.query) {
817
+ if (["$or", "$and", "organization_id"].includes(key)) continue;
818
+
819
+ let value = filter.query[key];
820
+
821
+ if (value && typeof value === "object" && !Array.isArray(value)) {
822
+ for (let op in value) {
823
+ let operand = value[op];
824
+ if (op === "$gt") qdrantFilter.must.push({ key, range: { gt: operand } });
825
+ else if (op === "$gte") qdrantFilter.must.push({ key, range: { gte: operand } });
826
+ else if (op === "$lt") qdrantFilter.must.push({ key, range: { lt: operand } });
827
+ else if (op === "$lte") qdrantFilter.must.push({ key, range: { lte: operand } });
828
+ else if (op === "$ne") {
829
+ if (!qdrantFilter.must_not) qdrantFilter.must_not = [];
830
+ qdrantFilter.must_not.push({ key, match: { value: operand } });
831
+ }
832
+ else if (op === "$in") {
833
+ qdrantFilter.must.push({ key, match: { any: operand } });
834
+ }
835
+ }
836
+ } else {
837
+ qdrantFilter.must.push({
838
+ key: key,
839
+ match: { value: value }
840
+ });
841
+ }
842
+ }
843
+ }
844
+
845
+ return qdrantFilter;
846
+ }
847
+
848
+ /**
849
+ * UUID generator utility used to produce fully compliant UUID-v4 point IDs for Qdrant.
850
+ */
851
+ function generateUUID() {
852
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
853
+ const r = (Math.random() * 16) | 0,
854
+ v = c == "x" ? r : (r & 0x3) | 0x8;
855
+ return v.toString(16);
856
+ });
857
+ }
858
+
859
+ function replaceArray(data = {}) {
860
+ let object = {};
861
+ Object.keys(data).forEach((key) => {
862
+ object[key.replace(/\[(\d+)\]/g, ".$1")] = data[key];
863
+ });
864
+ return object;
865
+ }
866
+
867
+ function getNestedValue(obj, path) {
868
+ return path.split(".").reduce((acc, part) => acc && acc[part], obj);
869
+ }
870
+
871
+ function setNestedValue(obj, path, value) {
872
+ const parts = path.split(".");
873
+ const last = parts.pop();
874
+ const target = parts.reduce((acc, part) => acc[part] = acc[part] || {}, obj);
875
+ target[last] = value;
876
+ }
877
+
878
+ function deleteNestedValue(obj, path) {
879
+ const parts = path.split(".");
880
+ const last = parts.pop();
881
+ const target = parts.reduce((acc, part) => acc && acc[part], obj);
882
+ if (target) delete target[last];
883
+ }
884
+
885
+ function createData(data, array, type) {
886
+ if (data[type] && data[type][0] && data[type][0].isFilter === "isEmptyObjectFilter") {
887
+ data[type].shift();
888
+ data.isFilter = true;
889
+ }
890
+
891
+ let key = type !== "object" ? "name" : "_id";
892
+
893
+ if (!Array.isArray(data[type])) {
894
+ console.log("data[type] is not an array", type);
895
+ } else {
896
+ for (let i = 0; i < array.length; i++) {
897
+ const matchIndex = data[type].findIndex((item) => item[key] === array[i][key]);
898
+ if (matchIndex !== -1) {
899
+ for (let $type of ["$storage", "$database", "$array"]) {
900
+ if (!data[type][matchIndex][$type])
901
+ data[type][matchIndex][$type] = [];
902
+ if (!Array.isArray(data[type][matchIndex][$type])) {
903
+ data[type][matchIndex][$type] = [data[type][matchIndex][$type]];
904
+ }
905
+ if (!data[type][matchIndex][$type].includes(array[i][$type])) {
906
+ data[type][matchIndex][$type].push(array[i][$type]);
907
+ }
908
+ delete array[i][$type];
909
+ }
910
+ data[type][matchIndex] = { ...data[type][matchIndex], ...array[i] };
911
+ } else {
912
+ data[type].push(array[i]);
913
+ }
914
+ }
915
+ }
916
+ return data;
917
+ }
918
+
919
+ function errorHandler(data, error, database, array) {
920
+ let errorMessage = typeof error === "object" && error.message ? error.message : error;
921
+ let errorObject = {
922
+ message: errorMessage,
923
+ storage: "qdrant"
924
+ };
925
+
926
+ if (database) errorObject.database = database;
927
+ if (array) errorObject.array = array;
928
+
929
+ if (Array.isArray(data.error)) {
930
+ data.error.push(errorObject);
931
+ } else {
932
+ data.error = [errorObject];
933
+ }
934
+ }
935
+
936
+ export default { send };