@mastra/qdrant 1.0.0-beta.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -15,11 +15,15 @@ var QdrantFilterTranslator = class extends filter.BaseFilterTranslator {
15
15
  return {
16
16
  ...filter.BaseFilterTranslator.DEFAULT_OPERATORS,
17
17
  logical: ["$and", "$or", "$not"],
18
- array: ["$in", "$nin"],
18
+ array: ["$in", "$nin", "$all"],
19
19
  regex: ["$regex"],
20
+ element: ["$exists"],
20
21
  custom: ["$count", "$geo", "$nested", "$datetime", "$null", "$empty", "$hasId", "$hasVector"]
21
22
  };
22
23
  }
24
+ isOperator(key) {
25
+ return super.isOperator(key) || key === "$not";
26
+ }
23
27
  translate(filter) {
24
28
  if (this.isEmpty(filter)) return filter;
25
29
  this.validateFilter(filter);
@@ -46,7 +50,7 @@ var QdrantFilterTranslator = class extends filter.BaseFilterTranslator {
46
50
  return node.length === 0 ? { is_empty: { key: fieldKey } } : this.createCondition("match", { any: this.normalizeArrayValues(node) }, fieldKey);
47
51
  }
48
52
  const entries = Object.entries(node);
49
- const logicalResult = this.handleLogicalOperators(entries, isNested);
53
+ const logicalResult = this.handleLogicalOperators(entries, isNested, fieldKey);
50
54
  if (logicalResult) {
51
55
  return logicalResult;
52
56
  }
@@ -68,8 +72,11 @@ var QdrantFilterTranslator = class extends filter.BaseFilterTranslator {
68
72
  return { must: conditions };
69
73
  }
70
74
  }
71
- handleLogicalOperators(entries, isNested) {
75
+ handleLogicalOperators(entries, isNested, fieldKey) {
72
76
  const firstKey = entries[0]?.[0];
77
+ if (firstKey === "$not" && fieldKey) {
78
+ return null;
79
+ }
73
80
  if (firstKey && this.isLogicalOperator(firstKey) && !this.isCustomOperator(firstKey)) {
74
81
  const [key, value] = entries[0];
75
82
  const qdrantOp = this.getQdrantLogicalOp(key);
@@ -94,7 +101,34 @@ var QdrantFilterTranslator = class extends filter.BaseFilterTranslator {
94
101
  conditions.push(customOp);
95
102
  } else if (this.isOperator(key)) {
96
103
  const opResult = this.translateOperatorValue(key, value);
97
- if (opResult.range) {
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) {
98
132
  Object.assign(range, opResult.range);
99
133
  } else {
100
134
  matchCondition = opResult;
@@ -165,9 +199,15 @@ var QdrantFilterTranslator = class extends filter.BaseFilterTranslator {
165
199
  const normalizedValue = this.normalizeComparisonValue(value);
166
200
  switch (operator) {
167
201
  case "$eq":
202
+ if (value === null) {
203
+ return { _specialNull: true };
204
+ }
168
205
  return { value: normalizedValue };
169
206
  case "$ne":
170
- return { except: [normalizedValue] };
207
+ if (value === null) {
208
+ return { _specialNotNull: true };
209
+ }
210
+ return { _specialNe: normalizedValue };
171
211
  case "$gt":
172
212
  return { range: { gt: normalizedValue } };
173
213
  case "$gte":
@@ -179,15 +219,15 @@ var QdrantFilterTranslator = class extends filter.BaseFilterTranslator {
179
219
  case "$in":
180
220
  return { any: this.normalizeArrayValues(value) };
181
221
  case "$nin":
182
- return { except: this.normalizeArrayValues(value) };
222
+ return { _specialNin: this.normalizeArrayValues(value) };
183
223
  case "$regex":
184
224
  return { text: value };
185
- case "exists":
186
- return value ? {
187
- must_not: [{ is_null: { key: value } }, { is_empty: { key: value } }]
188
- } : {
189
- is_empty: { key: value }
190
- };
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 };
191
231
  default:
192
232
  throw new Error(`Unsupported operator: ${operator}`);
193
233
  }
@@ -252,18 +292,55 @@ var QdrantVector = class extends vector.MastraVector {
252
292
  super({ id });
253
293
  this.client = new jsClientRest.QdrantClient(qdrantParams);
254
294
  }
255
- async upsert({ indexName, vectors, metadata, ids }) {
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);
256
319
  const pointIds = ids || 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
+ }
257
335
  const records = vectors.map((vector, i) => ({
258
336
  id: pointIds[i],
259
- vector,
337
+ vector: vectorName ? { [vectorName]: vector } : vector,
260
338
  payload: metadata?.[i] || {}
261
339
  }));
262
340
  try {
263
341
  for (let i = 0; i < records.length; i += BATCH_SIZE) {
264
342
  const batch = records.slice(i, i + BATCH_SIZE);
265
343
  await this.client.upsert(indexName, {
266
- // @ts-expect-error
267
344
  points: batch,
268
345
  wait: true
269
346
  });
@@ -275,19 +352,60 @@ var QdrantVector = class extends vector.MastraVector {
275
352
  id: storage.createVectorErrorId("QDRANT", "UPSERT", "FAILED"),
276
353
  domain: error.ErrorDomain.STORAGE,
277
354
  category: error.ErrorCategory.THIRD_PARTY,
278
- details: { indexName, vectorCount: vectors.length }
355
+ details: { indexName, vectorCount: vectors.length, ...vectorName && { vectorName } }
279
356
  },
280
357
  error$1
281
358
  );
282
359
  }
283
360
  }
284
- async createIndex({ indexName, dimension, metric = "cosine" }) {
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 }) {
285
387
  try {
286
- if (!Number.isInteger(dimension) || dimension <= 0) {
287
- throw new Error("Dimension must be a positive integer");
288
- }
289
- if (!DISTANCE_MAPPING[metric]) {
290
- throw new Error(`Invalid metric: "${metric}". Must be one of: cosine, euclidean, dotproduct`);
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
+ }
291
409
  }
292
410
  } catch (validationError) {
293
411
  throw new error.MastraError(
@@ -295,22 +413,49 @@ var QdrantVector = class extends vector.MastraVector {
295
413
  id: storage.createVectorErrorId("QDRANT", "CREATE_INDEX", "INVALID_ARGS"),
296
414
  domain: error.ErrorDomain.STORAGE,
297
415
  category: error.ErrorCategory.USER,
298
- details: { indexName, dimension, metric }
416
+ details: {
417
+ indexName,
418
+ dimension,
419
+ metric,
420
+ ...namedVectors && { namedVectorNames: Object.keys(namedVectors).join(", ") }
421
+ }
299
422
  },
300
423
  validationError
301
424
  );
302
425
  }
303
426
  try {
304
- await this.client.createCollection(indexName, {
305
- vectors: {
306
- size: dimension,
307
- distance: DISTANCE_MAPPING[metric]
308
- }
309
- });
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
+ }
310
449
  } catch (error$1) {
311
450
  const message = error$1?.message || error$1?.toString();
312
451
  if (error$1?.status === 409 || typeof message === "string" && message.toLowerCase().includes("exists")) {
313
- await this.validateExistingIndex(indexName, dimension, metric);
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
+ }
314
459
  return;
315
460
  }
316
461
  throw new error.MastraError(
@@ -328,12 +473,23 @@ var QdrantVector = class extends vector.MastraVector {
328
473
  const translator = new QdrantFilterTranslator();
329
474
  return translator.translate(filter);
330
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
+ */
331
486
  async query({
332
487
  indexName,
333
488
  queryVector,
334
489
  topK = 10,
335
490
  filter,
336
- includeVector = false
491
+ includeVector = false,
492
+ using
337
493
  }) {
338
494
  const translatedFilter = this.transformFilter(filter) ?? {};
339
495
  try {
@@ -342,15 +498,20 @@ var QdrantVector = class extends vector.MastraVector {
342
498
  limit: topK,
343
499
  filter: translatedFilter,
344
500
  with_payload: true,
345
- with_vector: includeVector
501
+ with_vector: includeVector,
502
+ ...using ? { using } : {}
346
503
  })).points;
347
504
  return results.map((match) => {
348
505
  let vector = [];
349
- if (includeVector) {
506
+ if (includeVector && match.vector != null) {
350
507
  if (Array.isArray(match.vector)) {
351
508
  vector = match.vector;
352
509
  } else if (typeof match.vector === "object" && match.vector !== null) {
353
- vector = Object.values(match.vector).filter((v) => typeof v === "number");
510
+ const namedVectors = match.vector;
511
+ const sourceArray = using && Array.isArray(namedVectors[using]) ? namedVectors[using] : Object.values(namedVectors).find((v) => Array.isArray(v));
512
+ if (sourceArray) {
513
+ vector = sourceArray.filter((v) => typeof v === "number");
514
+ }
354
515
  }
355
516
  }
356
517
  return {
@@ -366,7 +527,7 @@ var QdrantVector = class extends vector.MastraVector {
366
527
  id: storage.createVectorErrorId("QDRANT", "QUERY", "FAILED"),
367
528
  domain: error.ErrorDomain.STORAGE,
368
529
  category: error.ErrorCategory.THIRD_PARTY,
369
- details: { indexName, topK }
530
+ details: { indexName, topK, ...using && { using } }
370
531
  },
371
532
  error$1
372
533
  );