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