@mastra/qdrant 1.1.1 → 1.1.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/index.cjs CHANGED
@@ -1,1060 +1,934 @@
1
- 'use strict';
2
-
3
- var error = require('@mastra/core/error');
4
- var storage = require('@mastra/core/storage');
5
- var vector = require('@mastra/core/vector');
6
- var jsClientRest = require('@qdrant/js-client-rest');
7
- var filter = require('@mastra/core/vector/filter');
8
-
9
- // src/vector/index.ts
10
- var QdrantFilterTranslator = class extends filter.BaseFilterTranslator {
11
- isLogicalOperator(key) {
12
- return super.isLogicalOperator(key) || key === "$hasId" || key === "$hasVector";
13
- }
14
- getSupportedOperators() {
15
- return {
16
- ...filter.BaseFilterTranslator.DEFAULT_OPERATORS,
17
- logical: ["$and", "$or", "$not"],
18
- array: ["$in", "$nin", "$all"],
19
- regex: ["$regex"],
20
- element: ["$exists"],
21
- custom: ["$count", "$geo", "$nested", "$datetime", "$null", "$empty", "$hasId", "$hasVector"]
22
- };
23
- }
24
- isOperator(key) {
25
- return super.isOperator(key) || key === "$not";
26
- }
27
- translate(filter) {
28
- if (this.isEmpty(filter)) return filter;
29
- this.validateFilter(filter);
30
- return this.translateNode(filter);
31
- }
32
- createCondition(type, value, fieldKey) {
33
- const condition = { [type]: value };
34
- return fieldKey ? { key: fieldKey, ...condition } : condition;
35
- }
36
- translateNode(node, isNested = false, fieldKey) {
37
- if (!this.isEmpty(node) && !!node && typeof node === "object" && "must" in node) {
38
- return node;
39
- }
40
- if (this.isPrimitive(node)) {
41
- if (node === null) {
42
- return { is_null: { key: fieldKey } };
43
- }
44
- return this.createCondition("match", { value: this.normalizeComparisonValue(node) }, fieldKey);
45
- }
46
- if (this.isRegex(node)) {
47
- throw new Error("Direct regex pattern format is not supported in Qdrant");
48
- }
49
- if (Array.isArray(node)) {
50
- return node.length === 0 ? { is_empty: { key: fieldKey } } : this.createCondition("match", { any: this.normalizeArrayValues(node) }, fieldKey);
51
- }
52
- const entries = Object.entries(node);
53
- const logicalResult = this.handleLogicalOperators(entries, isNested, fieldKey);
54
- if (logicalResult) {
55
- return logicalResult;
56
- }
57
- const { conditions, range, matchCondition } = this.handleFieldConditions(entries, fieldKey);
58
- if (Object.keys(range).length > 0) {
59
- conditions.push({ key: fieldKey, range });
60
- }
61
- if (matchCondition) {
62
- conditions.push({ key: fieldKey, match: matchCondition });
63
- }
64
- return this.buildFinalConditions(conditions, isNested);
65
- }
66
- buildFinalConditions(conditions, isNested) {
67
- if (conditions.length === 0) {
68
- return {};
69
- } else if (conditions.length === 1 && isNested) {
70
- return conditions[0];
71
- } else {
72
- return { must: conditions };
73
- }
74
- }
75
- handleLogicalOperators(entries, isNested, fieldKey) {
76
- const firstKey = entries[0]?.[0];
77
- if (firstKey === "$not" && fieldKey) {
78
- return null;
79
- }
80
- if (firstKey && this.isLogicalOperator(firstKey) && !this.isCustomOperator(firstKey)) {
81
- const [key, value] = entries[0];
82
- const qdrantOp = this.getQdrantLogicalOp(key);
83
- return {
84
- [qdrantOp]: Array.isArray(value) ? value.map((v) => this.translateNode(v, true)) : [this.translateNode(value, true)]
85
- };
86
- }
87
- if (entries.length > 1 && !isNested && entries.every(([key]) => !this.isOperator(key) && !this.isCustomOperator(key))) {
88
- return {
89
- must: entries.map(([key, value]) => this.translateNode(value, true, key))
90
- };
91
- }
92
- return null;
93
- }
94
- handleFieldConditions(entries, fieldKey) {
95
- const conditions = [];
96
- let range = {};
97
- let matchCondition = null;
98
- for (const [key, value] of entries) {
99
- if (this.isCustomOperator(key)) {
100
- const customOp = this.translateCustomOperator(key, value, fieldKey);
101
- conditions.push(customOp);
102
- } else if (this.isOperator(key)) {
103
- const opResult = this.translateOperatorValue(key, value);
104
- if (opResult._specialNull) {
105
- conditions.push({ is_null: { key: fieldKey } });
106
- } else if (opResult._specialNotNull) {
107
- conditions.push({ must_not: [{ is_null: { key: fieldKey } }] });
108
- } else if (opResult._specialNe) {
109
- conditions.push({
110
- must_not: [{ key: fieldKey, match: { value: opResult._specialNe } }]
111
- });
112
- } else if (opResult._specialNin) {
113
- conditions.push({
114
- must_not: [{ key: fieldKey, match: { any: opResult._specialNin } }]
115
- });
116
- } else if (opResult._specialAll) {
117
- for (const val of opResult._specialAll) {
118
- conditions.push({ key: fieldKey, match: { value: val } });
119
- }
120
- } else if (opResult._specialExists) {
121
- conditions.push({
122
- must_not: [{ is_null: { key: fieldKey } }, { is_empty: { key: fieldKey } }]
123
- });
124
- } else if (opResult._specialNotExists) {
125
- conditions.push({
126
- should: [{ is_null: { key: fieldKey } }, { is_empty: { key: fieldKey } }]
127
- });
128
- } else if (opResult._specialNot) {
129
- const innerResult = this.translateNode(opResult._specialNot, true, fieldKey);
130
- conditions.push({ must_not: [innerResult] });
131
- } else if (opResult.range) {
132
- Object.assign(range, opResult.range);
133
- } else {
134
- matchCondition = opResult;
135
- }
136
- } else {
137
- const nestedKey = fieldKey ? `${fieldKey}.${key}` : key;
138
- const nestedCondition = this.translateNode(value, true, nestedKey);
139
- if (nestedCondition.must) {
140
- conditions.push(...nestedCondition.must);
141
- } else if (!this.isEmpty(nestedCondition)) {
142
- conditions.push(nestedCondition);
143
- }
144
- }
145
- }
146
- return { conditions, range, matchCondition };
147
- }
148
- translateCustomOperator(op, value, fieldKey) {
149
- switch (op) {
150
- case "$count":
151
- const countConditions = Object.entries(value).reduce(
152
- (acc, [k, v]) => ({
153
- ...acc,
154
- [k.replace("$", "")]: v
155
- }),
156
- {}
157
- );
158
- return { key: fieldKey, values_count: countConditions };
159
- case "$geo":
160
- const geoOp = this.translateGeoFilter(value.type, value);
161
- return { key: fieldKey, ...geoOp };
162
- case "$hasId":
163
- return { has_id: Array.isArray(value) ? value : [value] };
164
- case "$nested":
165
- return {
166
- nested: {
167
- key: fieldKey,
168
- filter: this.translateNode(value)
169
- }
170
- };
171
- case "$hasVector":
172
- return { has_vector: value };
173
- case "$datetime":
174
- return {
175
- key: fieldKey,
176
- range: this.normalizeDatetimeRange(value.range)
177
- };
178
- case "$null":
179
- return { is_null: { key: fieldKey } };
180
- case "$empty":
181
- return { is_empty: { key: fieldKey } };
182
- default:
183
- throw new Error(`Unsupported custom operator: ${op}`);
184
- }
185
- }
186
- getQdrantLogicalOp(op) {
187
- switch (op) {
188
- case "$and":
189
- return "must";
190
- case "$or":
191
- return "should";
192
- case "$not":
193
- return "must_not";
194
- default:
195
- throw new Error(`Unsupported logical operator: ${op}`);
196
- }
197
- }
198
- translateOperatorValue(operator, value) {
199
- const normalizedValue = this.normalizeComparisonValue(value);
200
- switch (operator) {
201
- case "$eq":
202
- if (value === null) {
203
- return { _specialNull: true };
204
- }
205
- return { value: normalizedValue };
206
- case "$ne":
207
- if (value === null) {
208
- return { _specialNotNull: true };
209
- }
210
- return { _specialNe: normalizedValue };
211
- case "$gt":
212
- return { range: { gt: normalizedValue } };
213
- case "$gte":
214
- return { range: { gte: normalizedValue } };
215
- case "$lt":
216
- return { range: { lt: normalizedValue } };
217
- case "$lte":
218
- return { range: { lte: normalizedValue } };
219
- case "$in":
220
- return { any: this.normalizeArrayValues(value) };
221
- case "$nin":
222
- return { _specialNin: this.normalizeArrayValues(value) };
223
- case "$regex":
224
- return { text: value };
225
- case "$all":
226
- return { _specialAll: this.normalizeArrayValues(value) };
227
- case "$exists":
228
- return value ? { _specialExists: true } : { _specialNotExists: true };
229
- case "$not":
230
- return { _specialNot: value };
231
- default:
232
- throw new Error(`Unsupported operator: ${operator}`);
233
- }
234
- }
235
- translateGeoFilter(type, value) {
236
- switch (type) {
237
- case "box":
238
- return {
239
- geo_bounding_box: {
240
- top_left: value.top_left,
241
- bottom_right: value.bottom_right
242
- }
243
- };
244
- case "radius":
245
- return {
246
- geo_radius: {
247
- center: value.center,
248
- radius: value.radius
249
- }
250
- };
251
- case "polygon":
252
- return {
253
- geo_polygon: {
254
- exterior: value.exterior,
255
- interiors: value.interiors
256
- }
257
- };
258
- default:
259
- throw new Error(`Unsupported geo filter type: ${type}`);
260
- }
261
- }
262
- normalizeDatetimeRange(value) {
263
- const range = {};
264
- for (const [op, val] of Object.entries(value)) {
265
- if (val instanceof Date) {
266
- range[op] = val.toISOString();
267
- } else if (typeof val === "string") {
268
- range[op] = val;
269
- }
270
- }
271
- return range;
272
- }
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _mastra_core_error = require("@mastra/core/error");
3
+ let _mastra_core_storage = require("@mastra/core/storage");
4
+ let _mastra_core_vector = require("@mastra/core/vector");
5
+ let _qdrant_js_client_rest = require("@qdrant/js-client-rest");
6
+ let _mastra_core_vector_filter = require("@mastra/core/vector/filter");
7
+ //#region src/vector/filter.ts
8
+ /**
9
+ * Translates MongoDB-style filters to Qdrant compatible filters.
10
+ *
11
+ * Key transformations:
12
+ * - $and -> must
13
+ * - $or -> should
14
+ * - $not -> must_not
15
+ * - { field: { $op: value } } -> { key: field, match/range: { value/gt/lt: value } }
16
+ *
17
+ * Custom operators (Qdrant-specific):
18
+ * - $count -> values_count (array length/value count)
19
+ * - $geo -> geo filters (box, radius, polygon)
20
+ * - $hasId -> has_id filter
21
+ * - $nested -> nested object filters
22
+ * - $hasVector -> vector existence check
23
+ * - $datetime -> RFC 3339 datetime range
24
+ * - $null -> is_null check
25
+ * - $empty -> is_empty check
26
+ */
27
+ var QdrantFilterTranslator = class extends _mastra_core_vector_filter.BaseFilterTranslator {
28
+ isLogicalOperator(key) {
29
+ return super.isLogicalOperator(key) || key === "$hasId" || key === "$hasVector";
30
+ }
31
+ getSupportedOperators() {
32
+ return {
33
+ ..._mastra_core_vector_filter.BaseFilterTranslator.DEFAULT_OPERATORS,
34
+ logical: [
35
+ "$and",
36
+ "$or",
37
+ "$not"
38
+ ],
39
+ array: [
40
+ "$in",
41
+ "$nin",
42
+ "$all"
43
+ ],
44
+ regex: ["$regex"],
45
+ element: ["$exists"],
46
+ custom: [
47
+ "$count",
48
+ "$geo",
49
+ "$nested",
50
+ "$datetime",
51
+ "$null",
52
+ "$empty",
53
+ "$hasId",
54
+ "$hasVector"
55
+ ]
56
+ };
57
+ }
58
+ isOperator(key) {
59
+ return super.isOperator(key) || key === "$not";
60
+ }
61
+ translate(filter) {
62
+ if (this.isEmpty(filter)) return filter;
63
+ this.validateFilter(filter);
64
+ return this.translateNode(filter);
65
+ }
66
+ createCondition(type, value, fieldKey) {
67
+ const condition = { [type]: value };
68
+ return fieldKey ? {
69
+ key: fieldKey,
70
+ ...condition
71
+ } : condition;
72
+ }
73
+ translateNode(node, isNested = false, fieldKey) {
74
+ if (!this.isEmpty(node) && !!node && typeof node === "object" && "must" in node) return node;
75
+ if (this.isPrimitive(node)) {
76
+ if (node === null) return { is_null: { key: fieldKey } };
77
+ return this.createCondition("match", { value: this.normalizeComparisonValue(node) }, fieldKey);
78
+ }
79
+ if (this.isRegex(node)) throw new Error("Direct regex pattern format is not supported in Qdrant");
80
+ if (Array.isArray(node)) return node.length === 0 ? { is_empty: { key: fieldKey } } : this.createCondition("match", { any: this.normalizeArrayValues(node) }, fieldKey);
81
+ const entries = Object.entries(node);
82
+ const logicalResult = this.handleLogicalOperators(entries, isNested, fieldKey);
83
+ if (logicalResult) return logicalResult;
84
+ const { conditions, range, matchCondition } = this.handleFieldConditions(entries, fieldKey);
85
+ if (Object.keys(range).length > 0) conditions.push({
86
+ key: fieldKey,
87
+ range
88
+ });
89
+ if (matchCondition) conditions.push({
90
+ key: fieldKey,
91
+ match: matchCondition
92
+ });
93
+ return this.buildFinalConditions(conditions, isNested);
94
+ }
95
+ buildFinalConditions(conditions, isNested) {
96
+ if (conditions.length === 0) return {};
97
+ else if (conditions.length === 1 && isNested) return conditions[0];
98
+ else return { must: conditions };
99
+ }
100
+ handleLogicalOperators(entries, isNested, fieldKey) {
101
+ const firstKey = entries[0]?.[0];
102
+ if (firstKey === "$not" && fieldKey) return null;
103
+ if (firstKey && this.isLogicalOperator(firstKey) && !this.isCustomOperator(firstKey)) {
104
+ const [key, value] = entries[0];
105
+ return { [this.getQdrantLogicalOp(key)]: Array.isArray(value) ? value.map((v) => this.translateNode(v, true)) : [this.translateNode(value, true)] };
106
+ }
107
+ if (entries.length > 1 && !isNested && entries.every(([key]) => !this.isOperator(key) && !this.isCustomOperator(key))) return { must: entries.map(([key, value]) => this.translateNode(value, true, key)) };
108
+ return null;
109
+ }
110
+ handleFieldConditions(entries, fieldKey) {
111
+ const conditions = [];
112
+ let range = {};
113
+ let matchCondition = null;
114
+ for (const [key, value] of entries) if (this.isCustomOperator(key)) {
115
+ const customOp = this.translateCustomOperator(key, value, fieldKey);
116
+ conditions.push(customOp);
117
+ } else if (this.isOperator(key)) {
118
+ const opResult = this.translateOperatorValue(key, value);
119
+ if (opResult._specialNull) conditions.push({ is_null: { key: fieldKey } });
120
+ else if (opResult._specialNotNull) conditions.push({ must_not: [{ is_null: { key: fieldKey } }] });
121
+ else if (opResult._specialNe) conditions.push({ must_not: [{
122
+ key: fieldKey,
123
+ match: { value: opResult._specialNe }
124
+ }] });
125
+ else if (opResult._specialNin) conditions.push({ must_not: [{
126
+ key: fieldKey,
127
+ match: { any: opResult._specialNin }
128
+ }] });
129
+ else if (opResult._specialAll) for (const val of opResult._specialAll) conditions.push({
130
+ key: fieldKey,
131
+ match: { value: val }
132
+ });
133
+ else if (opResult._specialExists) conditions.push({ must_not: [{ is_null: { key: fieldKey } }, { is_empty: { key: fieldKey } }] });
134
+ else if (opResult._specialNotExists) conditions.push({ should: [{ is_null: { key: fieldKey } }, { is_empty: { key: fieldKey } }] });
135
+ else if (opResult._specialNot) {
136
+ const innerResult = this.translateNode(opResult._specialNot, true, fieldKey);
137
+ conditions.push({ must_not: [innerResult] });
138
+ } else if (opResult.range) Object.assign(range, opResult.range);
139
+ else matchCondition = opResult;
140
+ } else {
141
+ const nestedKey = fieldKey ? `${fieldKey}.${key}` : key;
142
+ const nestedCondition = this.translateNode(value, true, nestedKey);
143
+ if (nestedCondition.must) conditions.push(...nestedCondition.must);
144
+ else if (!this.isEmpty(nestedCondition)) conditions.push(nestedCondition);
145
+ }
146
+ return {
147
+ conditions,
148
+ range,
149
+ matchCondition
150
+ };
151
+ }
152
+ translateCustomOperator(op, value, fieldKey) {
153
+ switch (op) {
154
+ case "$count": return {
155
+ key: fieldKey,
156
+ values_count: Object.entries(value).reduce((acc, [k, v]) => ({
157
+ ...acc,
158
+ [k.replace("$", "")]: v
159
+ }), {})
160
+ };
161
+ case "$geo": return {
162
+ key: fieldKey,
163
+ ...this.translateGeoFilter(value.type, value)
164
+ };
165
+ case "$hasId": return { has_id: Array.isArray(value) ? value : [value] };
166
+ case "$nested": return { nested: {
167
+ key: fieldKey,
168
+ filter: this.translateNode(value)
169
+ } };
170
+ case "$hasVector": return { has_vector: value };
171
+ case "$datetime": return {
172
+ key: fieldKey,
173
+ range: this.normalizeDatetimeRange(value.range)
174
+ };
175
+ case "$null": return { is_null: { key: fieldKey } };
176
+ case "$empty": return { is_empty: { key: fieldKey } };
177
+ default: throw new Error(`Unsupported custom operator: ${op}`);
178
+ }
179
+ }
180
+ getQdrantLogicalOp(op) {
181
+ switch (op) {
182
+ case "$and": return "must";
183
+ case "$or": return "should";
184
+ case "$not": return "must_not";
185
+ default: throw new Error(`Unsupported logical operator: ${op}`);
186
+ }
187
+ }
188
+ translateOperatorValue(operator, value) {
189
+ const normalizedValue = this.normalizeComparisonValue(value);
190
+ switch (operator) {
191
+ case "$eq":
192
+ if (value === null) return { _specialNull: true };
193
+ return { value: normalizedValue };
194
+ case "$ne":
195
+ if (value === null) return { _specialNotNull: true };
196
+ return { _specialNe: normalizedValue };
197
+ case "$gt": return { range: { gt: normalizedValue } };
198
+ case "$gte": return { range: { gte: normalizedValue } };
199
+ case "$lt": return { range: { lt: normalizedValue } };
200
+ case "$lte": return { range: { lte: normalizedValue } };
201
+ case "$in": return { any: this.normalizeArrayValues(value) };
202
+ case "$nin": return { _specialNin: this.normalizeArrayValues(value) };
203
+ case "$regex": return { text: value };
204
+ case "$all": return { _specialAll: this.normalizeArrayValues(value) };
205
+ case "$exists": return value ? { _specialExists: true } : { _specialNotExists: true };
206
+ case "$not": return { _specialNot: value };
207
+ default: throw new Error(`Unsupported operator: ${operator}`);
208
+ }
209
+ }
210
+ translateGeoFilter(type, value) {
211
+ switch (type) {
212
+ case "box": return { geo_bounding_box: {
213
+ top_left: value.top_left,
214
+ bottom_right: value.bottom_right
215
+ } };
216
+ case "radius": return { geo_radius: {
217
+ center: value.center,
218
+ radius: value.radius
219
+ } };
220
+ case "polygon": return { geo_polygon: {
221
+ exterior: value.exterior,
222
+ interiors: value.interiors
223
+ } };
224
+ default: throw new Error(`Unsupported geo filter type: ${type}`);
225
+ }
226
+ }
227
+ normalizeDatetimeRange(value) {
228
+ const range = {};
229
+ for (const [op, val] of Object.entries(value)) if (val instanceof Date) range[op] = val.toISOString();
230
+ else if (typeof val === "string") range[op] = val;
231
+ return range;
232
+ }
273
233
  };
274
-
275
- // src/vector/index.ts
276
- var BATCH_SIZE = 256;
277
- var DISTANCE_MAPPING = {
278
- cosine: "Cosine",
279
- euclidean: "Euclid",
280
- dotproduct: "Dot"
234
+ //#endregion
235
+ //#region src/vector/index.ts
236
+ const BATCH_SIZE = 256;
237
+ const DISTANCE_MAPPING = {
238
+ cosine: "Cosine",
239
+ euclidean: "Euclid",
240
+ dotproduct: "Dot"
281
241
  };
282
- var QdrantVector = class extends vector.MastraVector {
283
- client;
284
- /**
285
- * Creates a new QdrantVector client.
286
- * @param id - The unique identifier for this vector store instance.
287
- * @param url - The URL of the Qdrant server.
288
- * @param apiKey - The API key for Qdrant.
289
- * @param https - Whether to use HTTPS.
290
- */
291
- constructor({ id, ...qdrantParams }) {
292
- super({ id });
293
- this.client = new jsClientRest.QdrantClient(qdrantParams);
294
- }
295
- /**
296
- * Validates that a named vector exists in the collection.
297
- * @param indexName - The name of the collection to check.
298
- * @param vectorName - The name of the vector space to validate.
299
- * @throws Error if the vector name doesn't exist in the collection.
300
- */
301
- async validateVectorName(indexName, vectorName) {
302
- const { config } = await this.client.getCollection(indexName);
303
- const vectorsConfig = config.params.vectors;
304
- const isNamedVectors = vectorsConfig && typeof vectorsConfig === "object" && !("size" in vectorsConfig);
305
- if (!isNamedVectors || !(vectorName in vectorsConfig)) {
306
- throw new Error(`Vector name "${vectorName}" does not exist in collection "${indexName}"`);
307
- }
308
- }
309
- /**
310
- * Upserts vectors into the index.
311
- * @param indexName - The name of the index to upsert into.
312
- * @param vectors - Array of embedding vectors.
313
- * @param metadata - Optional metadata for each vector.
314
- * @param ids - Optional vector IDs (auto-generated if not provided).
315
- * @param vectorName - Optional name of the vector space when using named vectors.
316
- */
317
- async upsert({ indexName, vectors, metadata, ids, vectorName }) {
318
- vector.validateUpsertInput("QDRANT", vectors, metadata, ids);
319
- const pointIds = ids ? ids.map((id) => this.parsePointId(id)) : vectors.map(() => crypto.randomUUID());
320
- if (vectorName) {
321
- try {
322
- await this.validateVectorName(indexName, vectorName);
323
- } catch (validationError) {
324
- throw new error.MastraError(
325
- {
326
- id: storage.createVectorErrorId("QDRANT", "UPSERT", "INVALID_VECTOR_NAME"),
327
- domain: error.ErrorDomain.STORAGE,
328
- category: error.ErrorCategory.USER,
329
- details: { indexName, vectorName }
330
- },
331
- validationError
332
- );
333
- }
334
- }
335
- const records = vectors.map((vector, i) => ({
336
- id: pointIds[i],
337
- vector: vectorName ? { [vectorName]: vector } : vector,
338
- payload: metadata?.[i] || {}
339
- }));
340
- try {
341
- for (let i = 0; i < records.length; i += BATCH_SIZE) {
342
- const batch = records.slice(i, i + BATCH_SIZE);
343
- await this.client.upsert(indexName, {
344
- points: batch,
345
- wait: true
346
- });
347
- }
348
- return pointIds.map(String);
349
- } catch (error$1) {
350
- throw new error.MastraError(
351
- {
352
- id: storage.createVectorErrorId("QDRANT", "UPSERT", "FAILED"),
353
- domain: error.ErrorDomain.STORAGE,
354
- category: error.ErrorCategory.THIRD_PARTY,
355
- details: { indexName, vectorCount: vectors.length, ...vectorName && { vectorName } }
356
- },
357
- error$1
358
- );
359
- }
360
- }
361
- /**
362
- * Creates a new index (collection) in Qdrant.
363
- * Supports both single vector and named vector configurations.
364
- *
365
- * @param indexName - The name of the collection to create.
366
- * @param dimension - Vector dimension (required for single vector mode).
367
- * @param metric - Distance metric (default: 'cosine').
368
- * @param namedVectors - Optional named vector configurations for multi-vector collections.
369
- *
370
- * @example
371
- * ```ts
372
- * // Single vector collection
373
- * await qdrant.createIndex({ indexName: 'docs', dimension: 768, metric: 'cosine' });
374
- *
375
- * // Named vectors collection
376
- * await qdrant.createIndex({
377
- * indexName: 'multi-modal',
378
- * dimension: 768, // Used as fallback, can be omitted with namedVectors
379
- * namedVectors: {
380
- * text: { size: 768, distance: 'cosine' },
381
- * image: { size: 512, distance: 'euclidean' },
382
- * },
383
- * });
384
- * ```
385
- */
386
- async createIndex({ indexName, dimension, metric = "cosine", namedVectors }) {
387
- try {
388
- if (namedVectors) {
389
- if (Object.keys(namedVectors).length === 0) {
390
- throw new Error("namedVectors must contain at least one named vector configuration");
391
- }
392
- for (const [name, config] of Object.entries(namedVectors)) {
393
- if (!Number.isInteger(config.size) || config.size <= 0) {
394
- throw new Error(`Named vector "${name}": size must be a positive integer`);
395
- }
396
- if (!DISTANCE_MAPPING[config.distance]) {
397
- throw new Error(
398
- `Named vector "${name}": invalid distance "${config.distance}". Must be one of: cosine, euclidean, dotproduct`
399
- );
400
- }
401
- }
402
- } else {
403
- if (!Number.isInteger(dimension) || dimension <= 0) {
404
- throw new Error("Dimension must be a positive integer");
405
- }
406
- if (!DISTANCE_MAPPING[metric]) {
407
- throw new Error(`Invalid metric: "${metric}". Must be one of: cosine, euclidean, dotproduct`);
408
- }
409
- }
410
- } catch (validationError) {
411
- throw new error.MastraError(
412
- {
413
- id: storage.createVectorErrorId("QDRANT", "CREATE_INDEX", "INVALID_ARGS"),
414
- domain: error.ErrorDomain.STORAGE,
415
- category: error.ErrorCategory.USER,
416
- details: {
417
- indexName,
418
- dimension,
419
- metric,
420
- ...namedVectors && { namedVectorNames: Object.keys(namedVectors).join(", ") }
421
- }
422
- },
423
- validationError
424
- );
425
- }
426
- try {
427
- if (namedVectors) {
428
- const namedVectorsConfig = Object.entries(namedVectors).reduce(
429
- (acc, [name, config]) => {
430
- acc[name] = {
431
- size: config.size,
432
- distance: DISTANCE_MAPPING[config.distance]
433
- };
434
- return acc;
435
- },
436
- {}
437
- );
438
- await this.client.createCollection(indexName, {
439
- vectors: namedVectorsConfig
440
- });
441
- } else {
442
- await this.client.createCollection(indexName, {
443
- vectors: {
444
- size: dimension,
445
- distance: DISTANCE_MAPPING[metric]
446
- }
447
- });
448
- }
449
- } catch (error$1) {
450
- const message = error$1?.message || error$1?.toString();
451
- if (error$1?.status === 409 || typeof message === "string" && message.toLowerCase().includes("exists")) {
452
- if (!namedVectors) {
453
- await this.validateExistingIndex(indexName, dimension, metric);
454
- } else {
455
- this.logger.info(
456
- `Collection "${indexName}" already exists. Skipping validation for named vectors configuration.`
457
- );
458
- }
459
- return;
460
- }
461
- throw new error.MastraError(
462
- {
463
- id: storage.createVectorErrorId("QDRANT", "CREATE_INDEX", "FAILED"),
464
- domain: error.ErrorDomain.STORAGE,
465
- category: error.ErrorCategory.THIRD_PARTY,
466
- details: { indexName, dimension, metric }
467
- },
468
- error$1
469
- );
470
- }
471
- }
472
- transformFilter(filter) {
473
- const translator = new QdrantFilterTranslator();
474
- return translator.translate(filter);
475
- }
476
- /**
477
- * Queries the index for similar vectors.
478
- *
479
- * @param indexName - The name of the index to query.
480
- * @param queryVector - The query vector to find similar vectors for.
481
- * @param topK - Number of results to return (default: 10).
482
- * @param filter - Optional metadata filter.
483
- * @param includeVector - Whether to include vectors in results (default: false).
484
- * @param using - Name of the vector space to query when using named vectors.
485
- */
486
- async query({
487
- indexName,
488
- queryVector,
489
- topK = 10,
490
- filter,
491
- includeVector = false,
492
- using
493
- }) {
494
- if (!queryVector) {
495
- throw new error.MastraError({
496
- id: storage.createVectorErrorId("QDRANT", "QUERY", "MISSING_VECTOR"),
497
- text: "queryVector is required for Qdrant queries. Metadata-only queries are not supported by this vector store.",
498
- domain: error.ErrorDomain.STORAGE,
499
- category: error.ErrorCategory.USER,
500
- details: { indexName }
501
- });
502
- }
503
- const translatedFilter = this.transformFilter(filter) ?? {};
504
- try {
505
- const results = (await this.client.query(indexName, {
506
- query: queryVector,
507
- limit: topK,
508
- filter: translatedFilter,
509
- with_payload: true,
510
- with_vector: includeVector,
511
- ...using ? { using } : {}
512
- })).points;
513
- return results.map((match) => {
514
- let vector = [];
515
- if (includeVector && match.vector != null) {
516
- if (Array.isArray(match.vector)) {
517
- vector = match.vector;
518
- } else if (typeof match.vector === "object" && match.vector !== null) {
519
- const namedVectors = match.vector;
520
- const sourceArray = using && Array.isArray(namedVectors[using]) ? namedVectors[using] : Object.values(namedVectors).find((v) => Array.isArray(v));
521
- if (sourceArray) {
522
- vector = sourceArray.filter((v) => typeof v === "number");
523
- }
524
- }
525
- }
526
- return {
527
- id: match.id,
528
- score: match.score || 0,
529
- metadata: match.payload,
530
- ...includeVector && { vector }
531
- };
532
- });
533
- } catch (error$1) {
534
- throw new error.MastraError(
535
- {
536
- id: storage.createVectorErrorId("QDRANT", "QUERY", "FAILED"),
537
- domain: error.ErrorDomain.STORAGE,
538
- category: error.ErrorCategory.THIRD_PARTY,
539
- details: { indexName, topK, ...using && { using } }
540
- },
541
- error$1
542
- );
543
- }
544
- }
545
- async listIndexes() {
546
- try {
547
- const response = await this.client.getCollections();
548
- return response.collections.map((collection) => collection.name) || [];
549
- } catch (error$1) {
550
- throw new error.MastraError(
551
- {
552
- id: storage.createVectorErrorId("QDRANT", "LIST_INDEXES", "FAILED"),
553
- domain: error.ErrorDomain.STORAGE,
554
- category: error.ErrorCategory.THIRD_PARTY
555
- },
556
- error$1
557
- );
558
- }
559
- }
560
- /**
561
- * Retrieves statistics about a vector index.
562
- *
563
- * @param {string} indexName - The name of the index to describe
564
- * @returns A promise that resolves to the index statistics including dimension, count and metric
565
- */
566
- async describeIndex({ indexName }) {
567
- try {
568
- const { config, points_count } = await this.client.getCollection(indexName);
569
- const distance = config.params.vectors?.distance;
570
- return {
571
- dimension: config.params.vectors?.size,
572
- count: points_count || 0,
573
- metric: Object.keys(DISTANCE_MAPPING).find(
574
- (key) => DISTANCE_MAPPING[key] === distance
575
- )
576
- };
577
- } catch (error$1) {
578
- throw new error.MastraError(
579
- {
580
- id: storage.createVectorErrorId("QDRANT", "DESCRIBE_INDEX", "FAILED"),
581
- domain: error.ErrorDomain.STORAGE,
582
- category: error.ErrorCategory.THIRD_PARTY,
583
- details: { indexName }
584
- },
585
- error$1
586
- );
587
- }
588
- }
589
- async deleteIndex({ indexName }) {
590
- try {
591
- await this.client.deleteCollection(indexName);
592
- } catch (error$1) {
593
- const errorMessage = error$1?.message || error$1?.toString() || "";
594
- if (error$1?.status === 404 || errorMessage.toLowerCase().includes("not found") || errorMessage.toLowerCase().includes("not exist")) {
595
- this.logger.info(`Collection ${indexName} does not exist, treating as already deleted`);
596
- return;
597
- }
598
- throw new error.MastraError(
599
- {
600
- id: storage.createVectorErrorId("QDRANT", "DELETE_INDEX", "FAILED"),
601
- domain: error.ErrorDomain.STORAGE,
602
- category: error.ErrorCategory.THIRD_PARTY,
603
- details: { indexName }
604
- },
605
- error$1
606
- );
607
- }
608
- }
609
- /**
610
- * Updates a vector by its ID or multiple vectors matching a filter.
611
- * @param indexName - The name of the index containing the vector(s).
612
- * @param id - The ID of the vector to update (mutually exclusive with filter).
613
- * @param filter - Filter to match multiple vectors to update (mutually exclusive with id).
614
- * @param update - An object containing the vector and/or metadata to update.
615
- * @param update.vector - An optional array of numbers representing the new vector.
616
- * @param update.metadata - An optional record containing the new metadata.
617
- * @returns A promise that resolves when the update is complete.
618
- * @throws Will throw an error if no updates are provided or if the update operation fails.
619
- */
620
- async updateVector({ indexName, id, filter, update }) {
621
- if (id && filter) {
622
- throw new error.MastraError({
623
- id: storage.createVectorErrorId("QDRANT", "UPDATE_VECTOR", "MUTUALLY_EXCLUSIVE"),
624
- text: "Cannot specify both id and filter - they are mutually exclusive",
625
- domain: error.ErrorDomain.STORAGE,
626
- category: error.ErrorCategory.USER,
627
- details: { indexName }
628
- });
629
- }
630
- if (!id && !filter) {
631
- throw new error.MastraError({
632
- id: storage.createVectorErrorId("QDRANT", "UPDATE_VECTOR", "NO_TARGET"),
633
- text: "Either id or filter must be provided",
634
- domain: error.ErrorDomain.STORAGE,
635
- category: error.ErrorCategory.USER,
636
- details: { indexName }
637
- });
638
- }
639
- if (!update.vector && !update.metadata) {
640
- throw new error.MastraError({
641
- id: storage.createVectorErrorId("QDRANT", "UPDATE_VECTOR", "NO_PAYLOAD"),
642
- text: "No updates provided",
643
- domain: error.ErrorDomain.STORAGE,
644
- category: error.ErrorCategory.USER,
645
- details: {
646
- indexName,
647
- ...id && { id }
648
- }
649
- });
650
- }
651
- if (filter && Object.keys(filter).length === 0) {
652
- throw new error.MastraError({
653
- id: storage.createVectorErrorId("QDRANT", "UPDATE_VECTOR", "EMPTY_FILTER"),
654
- text: "Filter cannot be an empty filter object",
655
- domain: error.ErrorDomain.STORAGE,
656
- category: error.ErrorCategory.USER,
657
- details: { indexName }
658
- });
659
- }
660
- try {
661
- if (id) {
662
- const pointId = this.parsePointId(id);
663
- if (update.metadata && !update.vector) {
664
- await this.client.setPayload(indexName, { payload: update.metadata, points: [pointId] });
665
- return;
666
- }
667
- if (update.vector && !update.metadata) {
668
- await this.client.updateVectors(indexName, {
669
- points: [
670
- {
671
- id: pointId,
672
- vector: update.vector
673
- }
674
- ]
675
- });
676
- return;
677
- }
678
- if (update.vector && update.metadata) {
679
- const point = {
680
- id: pointId,
681
- vector: update.vector,
682
- payload: update.metadata
683
- };
684
- await this.client.upsert(indexName, {
685
- points: [point]
686
- });
687
- return;
688
- }
689
- } else if (filter) {
690
- const translatedFilter = this.transformFilter(filter);
691
- const matchingPoints = [];
692
- let offset = void 0;
693
- do {
694
- const scrollResult = await this.client.scroll(indexName, {
695
- filter: translatedFilter,
696
- limit: 100,
697
- offset,
698
- with_payload: false,
699
- with_vector: update.vector ? false : true
700
- // Only fetch vectors if not updating them
701
- });
702
- matchingPoints.push(
703
- ...scrollResult.points.map((point) => ({
704
- id: point.id,
705
- vector: Array.isArray(point.vector) ? point.vector : void 0
706
- }))
707
- );
708
- const nextOffset = scrollResult.next_page_offset;
709
- offset = typeof nextOffset === "string" || typeof nextOffset === "number" ? nextOffset : void 0;
710
- } while (offset !== void 0);
711
- if (matchingPoints.length === 0) {
712
- return;
713
- }
714
- const pointIds = matchingPoints.map((p) => p.id);
715
- if (update.metadata && !update.vector) {
716
- await this.client.setPayload(indexName, { payload: update.metadata, points: pointIds });
717
- return;
718
- }
719
- if (update.vector) {
720
- const points = matchingPoints.map((p) => ({
721
- id: p.id,
722
- vector: update.vector,
723
- payload: update.metadata || {}
724
- }));
725
- for (let i = 0; i < points.length; i += BATCH_SIZE) {
726
- const batch = points.slice(i, i + BATCH_SIZE);
727
- await this.client.upsert(indexName, {
728
- points: batch,
729
- wait: true
730
- });
731
- }
732
- return;
733
- }
734
- }
735
- } catch (error$1) {
736
- if (error$1 instanceof error.MastraError) throw error$1;
737
- throw new error.MastraError(
738
- {
739
- id: storage.createVectorErrorId("QDRANT", "UPDATE_VECTOR", "FAILED"),
740
- domain: error.ErrorDomain.STORAGE,
741
- category: error.ErrorCategory.THIRD_PARTY,
742
- details: {
743
- indexName,
744
- ...id && { id },
745
- ...filter && { filter: JSON.stringify(filter) }
746
- }
747
- },
748
- error$1
749
- );
750
- }
751
- }
752
- /**
753
- * Deletes a vector by its ID.
754
- * @param indexName - The name of the index containing the vector.
755
- * @param id - The ID of the vector to delete.
756
- * @returns A promise that resolves when the deletion is complete.
757
- * @throws Will throw an error if the deletion operation fails.
758
- */
759
- async deleteVector({ indexName, id }) {
760
- try {
761
- const pointId = this.parsePointId(id);
762
- await this.client.delete(indexName, {
763
- points: [pointId]
764
- });
765
- } catch (error$1) {
766
- throw new error.MastraError(
767
- {
768
- id: storage.createVectorErrorId("QDRANT", "DELETE_VECTOR", "FAILED"),
769
- domain: error.ErrorDomain.STORAGE,
770
- category: error.ErrorCategory.THIRD_PARTY,
771
- details: {
772
- indexName,
773
- ...id && { id }
774
- }
775
- },
776
- error$1
777
- );
778
- }
779
- }
780
- /**
781
- * Parses and converts a string ID to the appropriate type (string or number) for Qdrant point operations.
782
- *
783
- * Qdrant supports both numeric and string IDs. This helper method ensures IDs are in the correct format
784
- * before sending them to the Qdrant client API.
785
- *
786
- * @param id - The ID string to parse
787
- * @returns The parsed ID as either a number (if string contains only digits) or the original string
788
- *
789
- * @example
790
- * // Numeric ID strings are converted to numbers
791
- * parsePointId("123") => 123
792
- * parsePointId("42") => 42
793
- * parsePointId("0") => 0
794
- *
795
- * // String IDs containing any non-digit characters remain as strings
796
- * parsePointId("doc-123") => "doc-123"
797
- * parsePointId("user_42") => "user_42"
798
- * parsePointId("abc123") => "abc123"
799
- * parsePointId("123abc") => "123abc"
800
- * parsePointId("") => ""
801
- * parsePointId("uuid-5678-xyz") => "uuid-5678-xyz"
802
- *
803
- * @remarks
804
- * - This conversion is important because Qdrant treats numeric and string IDs differently
805
- * - Only positive integers are converted to numbers (negative numbers with minus signs remain strings)
806
- * - The method uses base-10 parsing, so leading zeros will be dropped in numeric conversions
807
- * - reference: https://qdrant.tech/documentation/concepts/points/?q=qdrant+point+id#point-ids
808
- */
809
- parsePointId(id) {
810
- if (/^\d+$/.test(id)) {
811
- return parseInt(id, 10);
812
- }
813
- return id;
814
- }
815
- /**
816
- * Deletes multiple vectors by IDs or filter.
817
- * @param indexName - The name of the index containing the vectors.
818
- * @param ids - Array of vector IDs to delete (mutually exclusive with filter).
819
- * @param filter - Filter to match vectors to delete (mutually exclusive with ids).
820
- * @returns A promise that resolves when the deletion is complete.
821
- * @throws Will throw an error if both ids and filter are provided, or if neither is provided.
822
- */
823
- async deleteVectors({ indexName, filter, ids }) {
824
- if (ids && filter) {
825
- throw new error.MastraError({
826
- id: storage.createVectorErrorId("QDRANT", "DELETE_VECTORS", "MUTUALLY_EXCLUSIVE"),
827
- text: "Cannot specify both ids and filter - they are mutually exclusive",
828
- domain: error.ErrorDomain.STORAGE,
829
- category: error.ErrorCategory.USER,
830
- details: { indexName }
831
- });
832
- }
833
- if (!ids && !filter) {
834
- throw new error.MastraError({
835
- id: storage.createVectorErrorId("QDRANT", "DELETE_VECTORS", "NO_TARGET"),
836
- text: "Either filter or ids must be provided",
837
- domain: error.ErrorDomain.STORAGE,
838
- category: error.ErrorCategory.USER,
839
- details: { indexName }
840
- });
841
- }
842
- if (ids && ids.length === 0) {
843
- throw new error.MastraError({
844
- id: storage.createVectorErrorId("QDRANT", "DELETE_VECTORS", "EMPTY_IDS"),
845
- text: "Cannot delete with empty ids array",
846
- domain: error.ErrorDomain.STORAGE,
847
- category: error.ErrorCategory.USER,
848
- details: { indexName }
849
- });
850
- }
851
- if (filter && Object.keys(filter).length === 0) {
852
- throw new error.MastraError({
853
- id: storage.createVectorErrorId("QDRANT", "DELETE_VECTORS", "EMPTY_FILTER"),
854
- text: "Cannot delete with empty filter object",
855
- domain: error.ErrorDomain.STORAGE,
856
- category: error.ErrorCategory.USER,
857
- details: { indexName }
858
- });
859
- }
860
- try {
861
- if (ids) {
862
- const pointIds = ids.map((id) => this.parsePointId(id));
863
- try {
864
- await this.client.delete(indexName, {
865
- points: pointIds,
866
- wait: true
867
- });
868
- } catch (error) {
869
- const message = error?.message || error?.toString() || "";
870
- if (message.toLowerCase().includes("bad request")) {
871
- return;
872
- }
873
- throw error;
874
- }
875
- } else if (filter) {
876
- const translatedFilter = this.transformFilter(filter) ?? {};
877
- await this.client.delete(indexName, {
878
- filter: translatedFilter,
879
- wait: true
880
- });
881
- }
882
- } catch (error$1) {
883
- if (error$1 instanceof error.MastraError) throw error$1;
884
- throw new error.MastraError(
885
- {
886
- id: storage.createVectorErrorId("QDRANT", "DELETE_VECTORS", "FAILED"),
887
- domain: error.ErrorDomain.STORAGE,
888
- category: error.ErrorCategory.THIRD_PARTY,
889
- details: {
890
- indexName,
891
- ...filter && { filter: JSON.stringify(filter) },
892
- ...ids && { idsCount: ids.length }
893
- }
894
- },
895
- error$1
896
- );
897
- }
898
- }
899
- /**
900
- * Creates a payload index on a Qdrant collection to enable efficient filtering on metadata fields.
901
- *
902
- * This is required for Qdrant Cloud and any Qdrant instance with `strict_mode_config = true`,
903
- * where metadata (payload) fields must be explicitly indexed before they can be used for filtering.
904
- *
905
- * @param params - The parameters for creating the payload index.
906
- * @param params.indexName - The name of the collection (index) to create the payload index on.
907
- * @param params.fieldName - The name of the payload field to index.
908
- * @param params.fieldSchema - The schema type for the field (e.g., 'keyword', 'integer', 'text').
909
- * @param params.wait - Whether to wait for the operation to complete. Defaults to true.
910
- * @returns A promise that resolves when the index is created (idempotent if index already exists).
911
- * @throws Will throw a MastraError if arguments are invalid or if the operation fails.
912
- *
913
- * @example
914
- * ```ts
915
- * // Create a keyword index for filtering by source
916
- * await qdrant.createPayloadIndex({
917
- * indexName: 'my-collection',
918
- * fieldName: 'source',
919
- * fieldSchema: 'keyword',
920
- * });
921
- *
922
- * // Create an integer index for numeric filtering
923
- * await qdrant.createPayloadIndex({
924
- * indexName: 'my-collection',
925
- * fieldName: 'price',
926
- * fieldSchema: 'integer',
927
- * });
928
- * ```
929
- *
930
- * @see https://qdrant.tech/documentation/concepts/indexing/#payload-index
931
- */
932
- async createPayloadIndex({
933
- indexName,
934
- fieldName,
935
- fieldSchema,
936
- wait = true
937
- }) {
938
- const validSchemas = [
939
- "keyword",
940
- "integer",
941
- "float",
942
- "geo",
943
- "text",
944
- "bool",
945
- "datetime",
946
- "uuid"
947
- ];
948
- if (!indexName || typeof indexName !== "string" || indexName.trim() === "") {
949
- throw new error.MastraError({
950
- id: storage.createVectorErrorId("QDRANT", "CREATE_PAYLOAD_INDEX", "INVALID_ARGS"),
951
- text: "indexName must be a non-empty string",
952
- domain: error.ErrorDomain.STORAGE,
953
- category: error.ErrorCategory.USER,
954
- details: { indexName, fieldName, fieldSchema }
955
- });
956
- }
957
- if (!fieldName || typeof fieldName !== "string" || fieldName.trim() === "") {
958
- throw new error.MastraError({
959
- id: storage.createVectorErrorId("QDRANT", "CREATE_PAYLOAD_INDEX", "INVALID_ARGS"),
960
- text: "fieldName must be a non-empty string",
961
- domain: error.ErrorDomain.STORAGE,
962
- category: error.ErrorCategory.USER,
963
- details: { indexName, fieldName, fieldSchema }
964
- });
965
- }
966
- if (!validSchemas.includes(fieldSchema)) {
967
- throw new error.MastraError({
968
- id: storage.createVectorErrorId("QDRANT", "CREATE_PAYLOAD_INDEX", "INVALID_ARGS"),
969
- text: `fieldSchema must be one of: ${validSchemas.join(", ")}`,
970
- domain: error.ErrorDomain.STORAGE,
971
- category: error.ErrorCategory.USER,
972
- details: { indexName, fieldName, fieldSchema }
973
- });
974
- }
975
- try {
976
- await this.client.createPayloadIndex(indexName, {
977
- field_name: fieldName,
978
- field_schema: fieldSchema,
979
- wait
980
- });
981
- } catch (error$1) {
982
- const message = error$1?.message || error$1?.toString() || "";
983
- if (error$1?.status === 409 || message.toLowerCase().includes("exists")) {
984
- this.logger.info(`Payload index for field "${fieldName}" already exists on collection "${indexName}"`);
985
- return;
986
- }
987
- throw new error.MastraError(
988
- {
989
- id: storage.createVectorErrorId("QDRANT", "CREATE_PAYLOAD_INDEX", "FAILED"),
990
- domain: error.ErrorDomain.STORAGE,
991
- category: error.ErrorCategory.THIRD_PARTY,
992
- details: { indexName, fieldName, fieldSchema }
993
- },
994
- error$1
995
- );
996
- }
997
- }
998
- /**
999
- * Deletes a payload index from a Qdrant collection.
1000
- *
1001
- * @param params - The parameters for deleting the payload index.
1002
- * @param params.indexName - The name of the collection (index) to delete the payload index from.
1003
- * @param params.fieldName - The name of the payload field index to delete.
1004
- * @param params.wait - Whether to wait for the operation to complete. Defaults to true.
1005
- * @returns A promise that resolves when the index is deleted (idempotent if index doesn't exist).
1006
- * @throws Will throw a MastraError if the operation fails.
1007
- *
1008
- * @example
1009
- * ```ts
1010
- * await qdrant.deletePayloadIndex({
1011
- * indexName: 'my-collection',
1012
- * fieldName: 'source',
1013
- * });
1014
- * ```
1015
- */
1016
- async deletePayloadIndex({ indexName, fieldName, wait = true }) {
1017
- if (!indexName || typeof indexName !== "string" || indexName.trim() === "") {
1018
- throw new error.MastraError({
1019
- id: storage.createVectorErrorId("QDRANT", "DELETE_PAYLOAD_INDEX", "INVALID_ARGS"),
1020
- text: "indexName must be a non-empty string",
1021
- domain: error.ErrorDomain.STORAGE,
1022
- category: error.ErrorCategory.USER,
1023
- details: { indexName, fieldName }
1024
- });
1025
- }
1026
- if (!fieldName || typeof fieldName !== "string" || fieldName.trim() === "") {
1027
- throw new error.MastraError({
1028
- id: storage.createVectorErrorId("QDRANT", "DELETE_PAYLOAD_INDEX", "INVALID_ARGS"),
1029
- text: "fieldName must be a non-empty string",
1030
- domain: error.ErrorDomain.STORAGE,
1031
- category: error.ErrorCategory.USER,
1032
- details: { indexName, fieldName }
1033
- });
1034
- }
1035
- try {
1036
- await this.client.deletePayloadIndex(indexName, fieldName, { wait });
1037
- } catch (error$1) {
1038
- const message = error$1?.message || error$1?.toString() || "";
1039
- if (error$1?.status === 404 || message.toLowerCase().includes("not found") || message.toLowerCase().includes("not exist")) {
1040
- this.logger.info(`Payload index for field "${fieldName}" does not exist on collection "${indexName}"`);
1041
- return;
1042
- }
1043
- throw new error.MastraError(
1044
- {
1045
- id: storage.createVectorErrorId("QDRANT", "DELETE_PAYLOAD_INDEX", "FAILED"),
1046
- domain: error.ErrorDomain.STORAGE,
1047
- category: error.ErrorCategory.THIRD_PARTY,
1048
- details: { indexName, fieldName }
1049
- },
1050
- error$1
1051
- );
1052
- }
1053
- }
242
+ var QdrantVector = class extends _mastra_core_vector.MastraVector {
243
+ client;
244
+ /**
245
+ * Creates a new QdrantVector client.
246
+ * @param id - The unique identifier for this vector store instance.
247
+ * @param url - The URL of the Qdrant server.
248
+ * @param apiKey - The API key for Qdrant.
249
+ * @param https - Whether to use HTTPS.
250
+ */
251
+ constructor({ id, ...qdrantParams }) {
252
+ super({ id });
253
+ this.client = new _qdrant_js_client_rest.QdrantClient(qdrantParams);
254
+ }
255
+ /**
256
+ * Validates that a named vector exists in the collection.
257
+ * @param indexName - The name of the collection to check.
258
+ * @param vectorName - The name of the vector space to validate.
259
+ * @throws Error if the vector name doesn't exist in the collection.
260
+ */
261
+ async validateVectorName(indexName, vectorName) {
262
+ const { config } = await this.client.getCollection(indexName);
263
+ const vectorsConfig = config.params.vectors;
264
+ if (!(vectorsConfig && typeof vectorsConfig === "object" && !("size" in vectorsConfig)) || !(vectorName in vectorsConfig)) throw new Error(`Vector name "${vectorName}" does not exist in collection "${indexName}"`);
265
+ }
266
+ /**
267
+ * Upserts vectors into the index.
268
+ * @param indexName - The name of the index to upsert into.
269
+ * @param vectors - Array of embedding vectors.
270
+ * @param metadata - Optional metadata for each vector.
271
+ * @param ids - Optional vector IDs (auto-generated if not provided).
272
+ * @param vectorName - Optional name of the vector space when using named vectors.
273
+ */
274
+ async upsert({ indexName, vectors, metadata, ids, vectorName }) {
275
+ (0, _mastra_core_vector.validateUpsertInput)("QDRANT", vectors, metadata, ids);
276
+ const pointIds = ids ? ids.map((id) => this.parsePointId(id)) : vectors.map(() => crypto.randomUUID());
277
+ if (vectorName) try {
278
+ await this.validateVectorName(indexName, vectorName);
279
+ } catch (validationError) {
280
+ throw new _mastra_core_error.MastraError({
281
+ id: (0, _mastra_core_storage.createVectorErrorId)("QDRANT", "UPSERT", "INVALID_VECTOR_NAME"),
282
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
283
+ category: _mastra_core_error.ErrorCategory.USER,
284
+ details: {
285
+ indexName,
286
+ vectorName
287
+ }
288
+ }, validationError);
289
+ }
290
+ const records = vectors.map((vector, i) => ({
291
+ id: pointIds[i],
292
+ vector: vectorName ? { [vectorName]: vector } : vector,
293
+ payload: metadata?.[i] || {}
294
+ }));
295
+ try {
296
+ for (let i = 0; i < records.length; i += BATCH_SIZE) {
297
+ const batch = records.slice(i, i + BATCH_SIZE);
298
+ await this.client.upsert(indexName, {
299
+ points: batch,
300
+ wait: true
301
+ });
302
+ }
303
+ return pointIds.map(String);
304
+ } catch (error) {
305
+ throw new _mastra_core_error.MastraError({
306
+ id: (0, _mastra_core_storage.createVectorErrorId)("QDRANT", "UPSERT", "FAILED"),
307
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
308
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
309
+ details: {
310
+ indexName,
311
+ vectorCount: vectors.length,
312
+ ...vectorName && { vectorName }
313
+ }
314
+ }, error);
315
+ }
316
+ }
317
+ /**
318
+ * Creates a new index (collection) in Qdrant.
319
+ * Supports both single vector and named vector configurations.
320
+ *
321
+ * @param indexName - The name of the collection to create.
322
+ * @param dimension - Vector dimension (required for single vector mode).
323
+ * @param metric - Distance metric (default: 'cosine').
324
+ * @param namedVectors - Optional named vector configurations for multi-vector collections.
325
+ *
326
+ * @example
327
+ * ```ts
328
+ * // Single vector collection
329
+ * await qdrant.createIndex({ indexName: 'docs', dimension: 768, metric: 'cosine' });
330
+ *
331
+ * // Named vectors collection
332
+ * await qdrant.createIndex({
333
+ * indexName: 'multi-modal',
334
+ * dimension: 768, // Used as fallback, can be omitted with namedVectors
335
+ * namedVectors: {
336
+ * text: { size: 768, distance: 'cosine' },
337
+ * image: { size: 512, distance: 'euclidean' },
338
+ * },
339
+ * });
340
+ * ```
341
+ */
342
+ async createIndex({ indexName, dimension, metric = "cosine", namedVectors }) {
343
+ try {
344
+ if (namedVectors) {
345
+ if (Object.keys(namedVectors).length === 0) throw new Error("namedVectors must contain at least one named vector configuration");
346
+ for (const [name, config] of Object.entries(namedVectors)) {
347
+ if (!Number.isInteger(config.size) || config.size <= 0) throw new Error(`Named vector "${name}": size must be a positive integer`);
348
+ if (!DISTANCE_MAPPING[config.distance]) throw new Error(`Named vector "${name}": invalid distance "${config.distance}". Must be one of: cosine, euclidean, dotproduct`);
349
+ }
350
+ } else {
351
+ if (!Number.isInteger(dimension) || dimension <= 0) throw new Error("Dimension must be a positive integer");
352
+ if (!DISTANCE_MAPPING[metric]) throw new Error(`Invalid metric: "${metric}". Must be one of: cosine, euclidean, dotproduct`);
353
+ }
354
+ } catch (validationError) {
355
+ throw new _mastra_core_error.MastraError({
356
+ id: (0, _mastra_core_storage.createVectorErrorId)("QDRANT", "CREATE_INDEX", "INVALID_ARGS"),
357
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
358
+ category: _mastra_core_error.ErrorCategory.USER,
359
+ details: {
360
+ indexName,
361
+ dimension,
362
+ metric,
363
+ ...namedVectors && { namedVectorNames: Object.keys(namedVectors).join(", ") }
364
+ }
365
+ }, validationError);
366
+ }
367
+ try {
368
+ if (namedVectors) {
369
+ const namedVectorsConfig = Object.entries(namedVectors).reduce((acc, [name, config]) => {
370
+ acc[name] = {
371
+ size: config.size,
372
+ distance: DISTANCE_MAPPING[config.distance]
373
+ };
374
+ return acc;
375
+ }, {});
376
+ await this.client.createCollection(indexName, { vectors: namedVectorsConfig });
377
+ } else await this.client.createCollection(indexName, { vectors: {
378
+ size: dimension,
379
+ distance: DISTANCE_MAPPING[metric]
380
+ } });
381
+ } catch (error) {
382
+ const message = error?.message || error?.toString();
383
+ if (error?.status === 409 || typeof message === "string" && message.toLowerCase().includes("exists")) {
384
+ if (!namedVectors) await this.validateExistingIndex(indexName, dimension, metric);
385
+ else this.logger.info(`Collection "${indexName}" already exists. Skipping validation for named vectors configuration.`);
386
+ return;
387
+ }
388
+ throw new _mastra_core_error.MastraError({
389
+ id: (0, _mastra_core_storage.createVectorErrorId)("QDRANT", "CREATE_INDEX", "FAILED"),
390
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
391
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
392
+ details: {
393
+ indexName,
394
+ dimension,
395
+ metric
396
+ }
397
+ }, error);
398
+ }
399
+ }
400
+ transformFilter(filter) {
401
+ return new QdrantFilterTranslator().translate(filter);
402
+ }
403
+ /**
404
+ * Queries the index for similar vectors.
405
+ *
406
+ * @param indexName - The name of the index to query.
407
+ * @param queryVector - The query vector to find similar vectors for.
408
+ * @param topK - Number of results to return (default: 10).
409
+ * @param filter - Optional metadata filter.
410
+ * @param includeVector - Whether to include vectors in results (default: false).
411
+ * @param using - Name of the vector space to query when using named vectors.
412
+ */
413
+ async query({ indexName, queryVector, topK = 10, filter, includeVector = false, using }) {
414
+ if (!queryVector) throw new _mastra_core_error.MastraError({
415
+ id: (0, _mastra_core_storage.createVectorErrorId)("QDRANT", "QUERY", "MISSING_VECTOR"),
416
+ text: "queryVector is required for Qdrant queries. Metadata-only queries are not supported by this vector store.",
417
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
418
+ category: _mastra_core_error.ErrorCategory.USER,
419
+ details: { indexName }
420
+ });
421
+ const translatedFilter = this.transformFilter(filter) ?? {};
422
+ try {
423
+ return (await this.client.query(indexName, {
424
+ query: queryVector,
425
+ limit: topK,
426
+ filter: translatedFilter,
427
+ with_payload: true,
428
+ with_vector: includeVector,
429
+ ...using ? { using } : {}
430
+ })).points.map((match) => {
431
+ let vector = [];
432
+ if (includeVector && match.vector != null) {
433
+ if (Array.isArray(match.vector)) vector = match.vector;
434
+ else if (typeof match.vector === "object" && match.vector !== null) {
435
+ const namedVectors = match.vector;
436
+ const sourceArray = using && Array.isArray(namedVectors[using]) ? namedVectors[using] : Object.values(namedVectors).find((v) => Array.isArray(v));
437
+ if (sourceArray) vector = sourceArray.filter((v) => typeof v === "number");
438
+ }
439
+ }
440
+ return {
441
+ id: match.id,
442
+ score: match.score || 0,
443
+ metadata: match.payload,
444
+ ...includeVector && { vector }
445
+ };
446
+ });
447
+ } catch (error) {
448
+ throw new _mastra_core_error.MastraError({
449
+ id: (0, _mastra_core_storage.createVectorErrorId)("QDRANT", "QUERY", "FAILED"),
450
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
451
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
452
+ details: {
453
+ indexName,
454
+ topK,
455
+ ...using && { using }
456
+ }
457
+ }, error);
458
+ }
459
+ }
460
+ async listIndexes() {
461
+ try {
462
+ return (await this.client.getCollections()).collections.map((collection) => collection.name) || [];
463
+ } catch (error) {
464
+ throw new _mastra_core_error.MastraError({
465
+ id: (0, _mastra_core_storage.createVectorErrorId)("QDRANT", "LIST_INDEXES", "FAILED"),
466
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
467
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY
468
+ }, error);
469
+ }
470
+ }
471
+ /**
472
+ * Retrieves statistics about a vector index.
473
+ *
474
+ * @param {string} indexName - The name of the index to describe
475
+ * @returns A promise that resolves to the index statistics including dimension, count and metric
476
+ */
477
+ async describeIndex({ indexName }) {
478
+ try {
479
+ const { config, points_count } = await this.client.getCollection(indexName);
480
+ const distance = config.params.vectors?.distance;
481
+ return {
482
+ dimension: config.params.vectors?.size,
483
+ count: points_count || 0,
484
+ metric: Object.keys(DISTANCE_MAPPING).find((key) => DISTANCE_MAPPING[key] === distance)
485
+ };
486
+ } catch (error) {
487
+ throw new _mastra_core_error.MastraError({
488
+ id: (0, _mastra_core_storage.createVectorErrorId)("QDRANT", "DESCRIBE_INDEX", "FAILED"),
489
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
490
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
491
+ details: { indexName }
492
+ }, error);
493
+ }
494
+ }
495
+ async deleteIndex({ indexName }) {
496
+ try {
497
+ await this.client.deleteCollection(indexName);
498
+ } catch (error) {
499
+ const errorMessage = error?.message || error?.toString() || "";
500
+ if (error?.status === 404 || errorMessage.toLowerCase().includes("not found") || errorMessage.toLowerCase().includes("not exist")) {
501
+ this.logger.info(`Collection ${indexName} does not exist, treating as already deleted`);
502
+ return;
503
+ }
504
+ throw new _mastra_core_error.MastraError({
505
+ id: (0, _mastra_core_storage.createVectorErrorId)("QDRANT", "DELETE_INDEX", "FAILED"),
506
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
507
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
508
+ details: { indexName }
509
+ }, error);
510
+ }
511
+ }
512
+ /**
513
+ * Updates a vector by its ID or multiple vectors matching a filter.
514
+ * @param indexName - The name of the index containing the vector(s).
515
+ * @param id - The ID of the vector to update (mutually exclusive with filter).
516
+ * @param filter - Filter to match multiple vectors to update (mutually exclusive with id).
517
+ * @param update - An object containing the vector and/or metadata to update.
518
+ * @param update.vector - An optional array of numbers representing the new vector.
519
+ * @param update.metadata - An optional record containing the new metadata.
520
+ * @returns A promise that resolves when the update is complete.
521
+ * @throws Will throw an error if no updates are provided or if the update operation fails.
522
+ */
523
+ async updateVector({ indexName, id, filter, update }) {
524
+ if (id && filter) throw new _mastra_core_error.MastraError({
525
+ id: (0, _mastra_core_storage.createVectorErrorId)("QDRANT", "UPDATE_VECTOR", "MUTUALLY_EXCLUSIVE"),
526
+ text: "Cannot specify both id and filter - they are mutually exclusive",
527
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
528
+ category: _mastra_core_error.ErrorCategory.USER,
529
+ details: { indexName }
530
+ });
531
+ if (!id && !filter) throw new _mastra_core_error.MastraError({
532
+ id: (0, _mastra_core_storage.createVectorErrorId)("QDRANT", "UPDATE_VECTOR", "NO_TARGET"),
533
+ text: "Either id or filter must be provided",
534
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
535
+ category: _mastra_core_error.ErrorCategory.USER,
536
+ details: { indexName }
537
+ });
538
+ if (!update.vector && !update.metadata) throw new _mastra_core_error.MastraError({
539
+ id: (0, _mastra_core_storage.createVectorErrorId)("QDRANT", "UPDATE_VECTOR", "NO_PAYLOAD"),
540
+ text: "No updates provided",
541
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
542
+ category: _mastra_core_error.ErrorCategory.USER,
543
+ details: {
544
+ indexName,
545
+ ...id && { id }
546
+ }
547
+ });
548
+ if (filter && Object.keys(filter).length === 0) throw new _mastra_core_error.MastraError({
549
+ id: (0, _mastra_core_storage.createVectorErrorId)("QDRANT", "UPDATE_VECTOR", "EMPTY_FILTER"),
550
+ text: "Filter cannot be an empty filter object",
551
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
552
+ category: _mastra_core_error.ErrorCategory.USER,
553
+ details: { indexName }
554
+ });
555
+ try {
556
+ if (id) {
557
+ const pointId = this.parsePointId(id);
558
+ if (update.metadata && !update.vector) {
559
+ await this.client.setPayload(indexName, {
560
+ payload: update.metadata,
561
+ points: [pointId]
562
+ });
563
+ return;
564
+ }
565
+ if (update.vector && !update.metadata) {
566
+ await this.client.updateVectors(indexName, { points: [{
567
+ id: pointId,
568
+ vector: update.vector
569
+ }] });
570
+ return;
571
+ }
572
+ if (update.vector && update.metadata) {
573
+ const point = {
574
+ id: pointId,
575
+ vector: update.vector,
576
+ payload: update.metadata
577
+ };
578
+ await this.client.upsert(indexName, { points: [point] });
579
+ return;
580
+ }
581
+ } else if (filter) {
582
+ const translatedFilter = this.transformFilter(filter);
583
+ const matchingPoints = [];
584
+ let offset = void 0;
585
+ do {
586
+ const scrollResult = await this.client.scroll(indexName, {
587
+ filter: translatedFilter,
588
+ limit: 100,
589
+ offset,
590
+ with_payload: false,
591
+ with_vector: update.vector ? false : true
592
+ });
593
+ matchingPoints.push(...scrollResult.points.map((point) => ({
594
+ id: point.id,
595
+ vector: Array.isArray(point.vector) ? point.vector : void 0
596
+ })));
597
+ const nextOffset = scrollResult.next_page_offset;
598
+ offset = typeof nextOffset === "string" || typeof nextOffset === "number" ? nextOffset : void 0;
599
+ } while (offset !== void 0);
600
+ if (matchingPoints.length === 0) return;
601
+ const pointIds = matchingPoints.map((p) => p.id);
602
+ if (update.metadata && !update.vector) {
603
+ await this.client.setPayload(indexName, {
604
+ payload: update.metadata,
605
+ points: pointIds
606
+ });
607
+ return;
608
+ }
609
+ if (update.vector) {
610
+ const points = matchingPoints.map((p) => ({
611
+ id: p.id,
612
+ vector: update.vector,
613
+ payload: update.metadata || {}
614
+ }));
615
+ for (let i = 0; i < points.length; i += BATCH_SIZE) {
616
+ const batch = points.slice(i, i + BATCH_SIZE);
617
+ await this.client.upsert(indexName, {
618
+ points: batch,
619
+ wait: true
620
+ });
621
+ }
622
+ return;
623
+ }
624
+ }
625
+ } catch (error) {
626
+ if (error instanceof _mastra_core_error.MastraError) throw error;
627
+ throw new _mastra_core_error.MastraError({
628
+ id: (0, _mastra_core_storage.createVectorErrorId)("QDRANT", "UPDATE_VECTOR", "FAILED"),
629
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
630
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
631
+ details: {
632
+ indexName,
633
+ ...id && { id },
634
+ ...filter && { filter: JSON.stringify(filter) }
635
+ }
636
+ }, error);
637
+ }
638
+ }
639
+ /**
640
+ * Deletes a vector by its ID.
641
+ * @param indexName - The name of the index containing the vector.
642
+ * @param id - The ID of the vector to delete.
643
+ * @returns A promise that resolves when the deletion is complete.
644
+ * @throws Will throw an error if the deletion operation fails.
645
+ */
646
+ async deleteVector({ indexName, id }) {
647
+ try {
648
+ const pointId = this.parsePointId(id);
649
+ await this.client.delete(indexName, { points: [pointId] });
650
+ } catch (error) {
651
+ throw new _mastra_core_error.MastraError({
652
+ id: (0, _mastra_core_storage.createVectorErrorId)("QDRANT", "DELETE_VECTOR", "FAILED"),
653
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
654
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
655
+ details: {
656
+ indexName,
657
+ ...id && { id }
658
+ }
659
+ }, error);
660
+ }
661
+ }
662
+ /**
663
+ * Parses and converts a string ID to the appropriate type (string or number) for Qdrant point operations.
664
+ *
665
+ * Qdrant supports both numeric and string IDs. This helper method ensures IDs are in the correct format
666
+ * before sending them to the Qdrant client API.
667
+ *
668
+ * @param id - The ID string to parse
669
+ * @returns The parsed ID as either a number (if string contains only digits) or the original string
670
+ *
671
+ * @example
672
+ * // Numeric ID strings are converted to numbers
673
+ * parsePointId("123") => 123
674
+ * parsePointId("42") => 42
675
+ * parsePointId("0") => 0
676
+ *
677
+ * // String IDs containing any non-digit characters remain as strings
678
+ * parsePointId("doc-123") => "doc-123"
679
+ * parsePointId("user_42") => "user_42"
680
+ * parsePointId("abc123") => "abc123"
681
+ * parsePointId("123abc") => "123abc"
682
+ * parsePointId("") => ""
683
+ * parsePointId("uuid-5678-xyz") => "uuid-5678-xyz"
684
+ *
685
+ * @remarks
686
+ * - This conversion is important because Qdrant treats numeric and string IDs differently
687
+ * - Only positive integers are converted to numbers (negative numbers with minus signs remain strings)
688
+ * - The method uses base-10 parsing, so leading zeros will be dropped in numeric conversions
689
+ * - reference: https://qdrant.tech/documentation/concepts/points/?q=qdrant+point+id#point-ids
690
+ */
691
+ parsePointId(id) {
692
+ if (/^\d+$/.test(id)) return parseInt(id, 10);
693
+ return id;
694
+ }
695
+ /**
696
+ * Deletes multiple vectors by IDs or filter.
697
+ * @param indexName - The name of the index containing the vectors.
698
+ * @param ids - Array of vector IDs to delete (mutually exclusive with filter).
699
+ * @param filter - Filter to match vectors to delete (mutually exclusive with ids).
700
+ * @returns A promise that resolves when the deletion is complete.
701
+ * @throws Will throw an error if both ids and filter are provided, or if neither is provided.
702
+ */
703
+ async deleteVectors({ indexName, filter, ids }) {
704
+ if (ids && filter) throw new _mastra_core_error.MastraError({
705
+ id: (0, _mastra_core_storage.createVectorErrorId)("QDRANT", "DELETE_VECTORS", "MUTUALLY_EXCLUSIVE"),
706
+ text: "Cannot specify both ids and filter - they are mutually exclusive",
707
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
708
+ category: _mastra_core_error.ErrorCategory.USER,
709
+ details: { indexName }
710
+ });
711
+ if (!ids && !filter) throw new _mastra_core_error.MastraError({
712
+ id: (0, _mastra_core_storage.createVectorErrorId)("QDRANT", "DELETE_VECTORS", "NO_TARGET"),
713
+ text: "Either filter or ids must be provided",
714
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
715
+ category: _mastra_core_error.ErrorCategory.USER,
716
+ details: { indexName }
717
+ });
718
+ if (ids && ids.length === 0) throw new _mastra_core_error.MastraError({
719
+ id: (0, _mastra_core_storage.createVectorErrorId)("QDRANT", "DELETE_VECTORS", "EMPTY_IDS"),
720
+ text: "Cannot delete with empty ids array",
721
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
722
+ category: _mastra_core_error.ErrorCategory.USER,
723
+ details: { indexName }
724
+ });
725
+ if (filter && Object.keys(filter).length === 0) throw new _mastra_core_error.MastraError({
726
+ id: (0, _mastra_core_storage.createVectorErrorId)("QDRANT", "DELETE_VECTORS", "EMPTY_FILTER"),
727
+ text: "Cannot delete with empty filter object",
728
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
729
+ category: _mastra_core_error.ErrorCategory.USER,
730
+ details: { indexName }
731
+ });
732
+ try {
733
+ if (ids) {
734
+ const pointIds = ids.map((id) => this.parsePointId(id));
735
+ try {
736
+ await this.client.delete(indexName, {
737
+ points: pointIds,
738
+ wait: true
739
+ });
740
+ } catch (error) {
741
+ if ((error?.message || error?.toString() || "").toLowerCase().includes("bad request")) return;
742
+ throw error;
743
+ }
744
+ } else if (filter) {
745
+ const translatedFilter = this.transformFilter(filter) ?? {};
746
+ await this.client.delete(indexName, {
747
+ filter: translatedFilter,
748
+ wait: true
749
+ });
750
+ }
751
+ } catch (error) {
752
+ if (error instanceof _mastra_core_error.MastraError) throw error;
753
+ throw new _mastra_core_error.MastraError({
754
+ id: (0, _mastra_core_storage.createVectorErrorId)("QDRANT", "DELETE_VECTORS", "FAILED"),
755
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
756
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
757
+ details: {
758
+ indexName,
759
+ ...filter && { filter: JSON.stringify(filter) },
760
+ ...ids && { idsCount: ids.length }
761
+ }
762
+ }, error);
763
+ }
764
+ }
765
+ /**
766
+ * Creates a payload index on a Qdrant collection to enable efficient filtering on metadata fields.
767
+ *
768
+ * This is required for Qdrant Cloud and any Qdrant instance with `strict_mode_config = true`,
769
+ * where metadata (payload) fields must be explicitly indexed before they can be used for filtering.
770
+ *
771
+ * @param params - The parameters for creating the payload index.
772
+ * @param params.indexName - The name of the collection (index) to create the payload index on.
773
+ * @param params.fieldName - The name of the payload field to index.
774
+ * @param params.fieldSchema - The schema type for the field (e.g., 'keyword', 'integer', 'text').
775
+ * @param params.wait - Whether to wait for the operation to complete. Defaults to true.
776
+ * @returns A promise that resolves when the index is created (idempotent if index already exists).
777
+ * @throws Will throw a MastraError if arguments are invalid or if the operation fails.
778
+ *
779
+ * @example
780
+ * ```ts
781
+ * // Create a keyword index for filtering by source
782
+ * await qdrant.createPayloadIndex({
783
+ * indexName: 'my-collection',
784
+ * fieldName: 'source',
785
+ * fieldSchema: 'keyword',
786
+ * });
787
+ *
788
+ * // Create an integer index for numeric filtering
789
+ * await qdrant.createPayloadIndex({
790
+ * indexName: 'my-collection',
791
+ * fieldName: 'price',
792
+ * fieldSchema: 'integer',
793
+ * });
794
+ * ```
795
+ *
796
+ * @see https://qdrant.tech/documentation/concepts/indexing/#payload-index
797
+ */
798
+ async createPayloadIndex({ indexName, fieldName, fieldSchema, wait = true }) {
799
+ const validSchemas = [
800
+ "keyword",
801
+ "integer",
802
+ "float",
803
+ "geo",
804
+ "text",
805
+ "bool",
806
+ "datetime",
807
+ "uuid"
808
+ ];
809
+ if (!indexName || typeof indexName !== "string" || indexName.trim() === "") throw new _mastra_core_error.MastraError({
810
+ id: (0, _mastra_core_storage.createVectorErrorId)("QDRANT", "CREATE_PAYLOAD_INDEX", "INVALID_ARGS"),
811
+ text: "indexName must be a non-empty string",
812
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
813
+ category: _mastra_core_error.ErrorCategory.USER,
814
+ details: {
815
+ indexName,
816
+ fieldName,
817
+ fieldSchema
818
+ }
819
+ });
820
+ if (!fieldName || typeof fieldName !== "string" || fieldName.trim() === "") throw new _mastra_core_error.MastraError({
821
+ id: (0, _mastra_core_storage.createVectorErrorId)("QDRANT", "CREATE_PAYLOAD_INDEX", "INVALID_ARGS"),
822
+ text: "fieldName must be a non-empty string",
823
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
824
+ category: _mastra_core_error.ErrorCategory.USER,
825
+ details: {
826
+ indexName,
827
+ fieldName,
828
+ fieldSchema
829
+ }
830
+ });
831
+ if (!validSchemas.includes(fieldSchema)) throw new _mastra_core_error.MastraError({
832
+ id: (0, _mastra_core_storage.createVectorErrorId)("QDRANT", "CREATE_PAYLOAD_INDEX", "INVALID_ARGS"),
833
+ text: `fieldSchema must be one of: ${validSchemas.join(", ")}`,
834
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
835
+ category: _mastra_core_error.ErrorCategory.USER,
836
+ details: {
837
+ indexName,
838
+ fieldName,
839
+ fieldSchema
840
+ }
841
+ });
842
+ try {
843
+ await this.client.createPayloadIndex(indexName, {
844
+ field_name: fieldName,
845
+ field_schema: fieldSchema,
846
+ wait
847
+ });
848
+ } catch (error) {
849
+ const message = error?.message || error?.toString() || "";
850
+ if (error?.status === 409 || message.toLowerCase().includes("exists")) {
851
+ this.logger.info(`Payload index for field "${fieldName}" already exists on collection "${indexName}"`);
852
+ return;
853
+ }
854
+ throw new _mastra_core_error.MastraError({
855
+ id: (0, _mastra_core_storage.createVectorErrorId)("QDRANT", "CREATE_PAYLOAD_INDEX", "FAILED"),
856
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
857
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
858
+ details: {
859
+ indexName,
860
+ fieldName,
861
+ fieldSchema
862
+ }
863
+ }, error);
864
+ }
865
+ }
866
+ /**
867
+ * Deletes a payload index from a Qdrant collection.
868
+ *
869
+ * @param params - The parameters for deleting the payload index.
870
+ * @param params.indexName - The name of the collection (index) to delete the payload index from.
871
+ * @param params.fieldName - The name of the payload field index to delete.
872
+ * @param params.wait - Whether to wait for the operation to complete. Defaults to true.
873
+ * @returns A promise that resolves when the index is deleted (idempotent if index doesn't exist).
874
+ * @throws Will throw a MastraError if the operation fails.
875
+ *
876
+ * @example
877
+ * ```ts
878
+ * await qdrant.deletePayloadIndex({
879
+ * indexName: 'my-collection',
880
+ * fieldName: 'source',
881
+ * });
882
+ * ```
883
+ */
884
+ async deletePayloadIndex({ indexName, fieldName, wait = true }) {
885
+ if (!indexName || typeof indexName !== "string" || indexName.trim() === "") throw new _mastra_core_error.MastraError({
886
+ id: (0, _mastra_core_storage.createVectorErrorId)("QDRANT", "DELETE_PAYLOAD_INDEX", "INVALID_ARGS"),
887
+ text: "indexName must be a non-empty string",
888
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
889
+ category: _mastra_core_error.ErrorCategory.USER,
890
+ details: {
891
+ indexName,
892
+ fieldName
893
+ }
894
+ });
895
+ if (!fieldName || typeof fieldName !== "string" || fieldName.trim() === "") throw new _mastra_core_error.MastraError({
896
+ id: (0, _mastra_core_storage.createVectorErrorId)("QDRANT", "DELETE_PAYLOAD_INDEX", "INVALID_ARGS"),
897
+ text: "fieldName must be a non-empty string",
898
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
899
+ category: _mastra_core_error.ErrorCategory.USER,
900
+ details: {
901
+ indexName,
902
+ fieldName
903
+ }
904
+ });
905
+ try {
906
+ await this.client.deletePayloadIndex(indexName, fieldName, { wait });
907
+ } catch (error) {
908
+ const message = error?.message || error?.toString() || "";
909
+ if (error?.status === 404 || message.toLowerCase().includes("not found") || message.toLowerCase().includes("not exist")) {
910
+ this.logger.info(`Payload index for field "${fieldName}" does not exist on collection "${indexName}"`);
911
+ return;
912
+ }
913
+ throw new _mastra_core_error.MastraError({
914
+ id: (0, _mastra_core_storage.createVectorErrorId)("QDRANT", "DELETE_PAYLOAD_INDEX", "FAILED"),
915
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
916
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
917
+ details: {
918
+ indexName,
919
+ fieldName
920
+ }
921
+ }, error);
922
+ }
923
+ }
1054
924
  };
1055
-
1056
- // src/vector/prompt.ts
1057
- var QDRANT_PROMPT = `When querying Qdrant, you can ONLY use the operators listed below. Any other operators will be rejected.
925
+ //#endregion
926
+ //#region src/vector/prompt.ts
927
+ /**
928
+ * Vector store specific prompt that details supported operators and examples.
929
+ * This prompt helps users construct valid filters for Qdrant Vector.
930
+ */
931
+ const QDRANT_PROMPT = `When querying Qdrant, you can ONLY use the operators listed below. Any other operators will be rejected.
1058
932
  Important: Don't explain how to construct the filter - use the specified operators and fields to search the content and return relevant results.
1059
933
  If a user tries to give an explicit operator that is not supported, reject the filter entirely and let them know that the operator is not supported.
1060
934
 
@@ -1135,8 +1009,8 @@ Example Complex Query:
1135
1009
  { "$not": { "status": "discontinued" } }
1136
1010
  ]
1137
1011
  }`;
1138
-
1012
+ //#endregion
1139
1013
  exports.QDRANT_PROMPT = QDRANT_PROMPT;
1140
1014
  exports.QdrantVector = QdrantVector;
1141
- //# sourceMappingURL=index.cjs.map
1015
+
1142
1016
  //# sourceMappingURL=index.cjs.map