@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.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { MastraError, ErrorCategory, ErrorDomain } from '@mastra/core/error';
2
2
  import { createVectorErrorId } from '@mastra/core/storage';
3
- import { MastraVector } from '@mastra/core/vector';
3
+ import { MastraVector, validateUpsertInput } from '@mastra/core/vector';
4
4
  import { QdrantClient } from '@qdrant/js-client-rest';
5
5
  import { BaseFilterTranslator } from '@mastra/core/vector/filter';
6
6
 
@@ -13,11 +13,15 @@ var QdrantFilterTranslator = class extends BaseFilterTranslator {
13
13
  return {
14
14
  ...BaseFilterTranslator.DEFAULT_OPERATORS,
15
15
  logical: ["$and", "$or", "$not"],
16
- array: ["$in", "$nin"],
16
+ array: ["$in", "$nin", "$all"],
17
17
  regex: ["$regex"],
18
+ element: ["$exists"],
18
19
  custom: ["$count", "$geo", "$nested", "$datetime", "$null", "$empty", "$hasId", "$hasVector"]
19
20
  };
20
21
  }
22
+ isOperator(key) {
23
+ return super.isOperator(key) || key === "$not";
24
+ }
21
25
  translate(filter) {
22
26
  if (this.isEmpty(filter)) return filter;
23
27
  this.validateFilter(filter);
@@ -44,7 +48,7 @@ var QdrantFilterTranslator = class extends BaseFilterTranslator {
44
48
  return node.length === 0 ? { is_empty: { key: fieldKey } } : this.createCondition("match", { any: this.normalizeArrayValues(node) }, fieldKey);
45
49
  }
46
50
  const entries = Object.entries(node);
47
- const logicalResult = this.handleLogicalOperators(entries, isNested);
51
+ const logicalResult = this.handleLogicalOperators(entries, isNested, fieldKey);
48
52
  if (logicalResult) {
49
53
  return logicalResult;
50
54
  }
@@ -66,8 +70,11 @@ var QdrantFilterTranslator = class extends BaseFilterTranslator {
66
70
  return { must: conditions };
67
71
  }
68
72
  }
69
- handleLogicalOperators(entries, isNested) {
73
+ handleLogicalOperators(entries, isNested, fieldKey) {
70
74
  const firstKey = entries[0]?.[0];
75
+ if (firstKey === "$not" && fieldKey) {
76
+ return null;
77
+ }
71
78
  if (firstKey && this.isLogicalOperator(firstKey) && !this.isCustomOperator(firstKey)) {
72
79
  const [key, value] = entries[0];
73
80
  const qdrantOp = this.getQdrantLogicalOp(key);
@@ -92,7 +99,34 @@ var QdrantFilterTranslator = class extends BaseFilterTranslator {
92
99
  conditions.push(customOp);
93
100
  } else if (this.isOperator(key)) {
94
101
  const opResult = this.translateOperatorValue(key, value);
95
- if (opResult.range) {
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) {
96
130
  Object.assign(range, opResult.range);
97
131
  } else {
98
132
  matchCondition = opResult;
@@ -163,9 +197,15 @@ var QdrantFilterTranslator = class extends BaseFilterTranslator {
163
197
  const normalizedValue = this.normalizeComparisonValue(value);
164
198
  switch (operator) {
165
199
  case "$eq":
200
+ if (value === null) {
201
+ return { _specialNull: true };
202
+ }
166
203
  return { value: normalizedValue };
167
204
  case "$ne":
168
- return { except: [normalizedValue] };
205
+ if (value === null) {
206
+ return { _specialNotNull: true };
207
+ }
208
+ return { _specialNe: normalizedValue };
169
209
  case "$gt":
170
210
  return { range: { gt: normalizedValue } };
171
211
  case "$gte":
@@ -177,15 +217,15 @@ var QdrantFilterTranslator = class extends BaseFilterTranslator {
177
217
  case "$in":
178
218
  return { any: this.normalizeArrayValues(value) };
179
219
  case "$nin":
180
- return { except: this.normalizeArrayValues(value) };
220
+ return { _specialNin: this.normalizeArrayValues(value) };
181
221
  case "$regex":
182
222
  return { text: value };
183
- case "exists":
184
- return value ? {
185
- must_not: [{ is_null: { key: value } }, { is_empty: { key: value } }]
186
- } : {
187
- is_empty: { key: value }
188
- };
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 };
189
229
  default:
190
230
  throw new Error(`Unsupported operator: ${operator}`);
191
231
  }
@@ -250,18 +290,55 @@ var QdrantVector = class extends MastraVector {
250
290
  super({ id });
251
291
  this.client = new QdrantClient(qdrantParams);
252
292
  }
253
- async upsert({ indexName, vectors, metadata, ids }) {
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);
254
317
  const pointIds = ids || 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
+ }
255
333
  const records = vectors.map((vector, i) => ({
256
334
  id: pointIds[i],
257
- vector,
335
+ vector: vectorName ? { [vectorName]: vector } : vector,
258
336
  payload: metadata?.[i] || {}
259
337
  }));
260
338
  try {
261
339
  for (let i = 0; i < records.length; i += BATCH_SIZE) {
262
340
  const batch = records.slice(i, i + BATCH_SIZE);
263
341
  await this.client.upsert(indexName, {
264
- // @ts-expect-error
265
342
  points: batch,
266
343
  wait: true
267
344
  });
@@ -273,19 +350,60 @@ var QdrantVector = class extends MastraVector {
273
350
  id: createVectorErrorId("QDRANT", "UPSERT", "FAILED"),
274
351
  domain: ErrorDomain.STORAGE,
275
352
  category: ErrorCategory.THIRD_PARTY,
276
- details: { indexName, vectorCount: vectors.length }
353
+ details: { indexName, vectorCount: vectors.length, ...vectorName && { vectorName } }
277
354
  },
278
355
  error
279
356
  );
280
357
  }
281
358
  }
282
- async createIndex({ indexName, dimension, metric = "cosine" }) {
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 }) {
283
385
  try {
284
- if (!Number.isInteger(dimension) || dimension <= 0) {
285
- throw new Error("Dimension must be a positive integer");
286
- }
287
- if (!DISTANCE_MAPPING[metric]) {
288
- throw new Error(`Invalid metric: "${metric}". Must be one of: cosine, euclidean, dotproduct`);
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
+ }
289
407
  }
290
408
  } catch (validationError) {
291
409
  throw new MastraError(
@@ -293,22 +411,49 @@ var QdrantVector = class extends MastraVector {
293
411
  id: createVectorErrorId("QDRANT", "CREATE_INDEX", "INVALID_ARGS"),
294
412
  domain: ErrorDomain.STORAGE,
295
413
  category: ErrorCategory.USER,
296
- details: { indexName, dimension, metric }
414
+ details: {
415
+ indexName,
416
+ dimension,
417
+ metric,
418
+ ...namedVectors && { namedVectorNames: Object.keys(namedVectors).join(", ") }
419
+ }
297
420
  },
298
421
  validationError
299
422
  );
300
423
  }
301
424
  try {
302
- await this.client.createCollection(indexName, {
303
- vectors: {
304
- size: dimension,
305
- distance: DISTANCE_MAPPING[metric]
306
- }
307
- });
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
+ }
308
447
  } catch (error) {
309
448
  const message = error?.message || error?.toString();
310
449
  if (error?.status === 409 || typeof message === "string" && message.toLowerCase().includes("exists")) {
311
- await this.validateExistingIndex(indexName, dimension, metric);
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
+ }
312
457
  return;
313
458
  }
314
459
  throw new MastraError(
@@ -326,12 +471,23 @@ var QdrantVector = class extends MastraVector {
326
471
  const translator = new QdrantFilterTranslator();
327
472
  return translator.translate(filter);
328
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
+ */
329
484
  async query({
330
485
  indexName,
331
486
  queryVector,
332
487
  topK = 10,
333
488
  filter,
334
- includeVector = false
489
+ includeVector = false,
490
+ using
335
491
  }) {
336
492
  const translatedFilter = this.transformFilter(filter) ?? {};
337
493
  try {
@@ -340,15 +496,20 @@ var QdrantVector = class extends MastraVector {
340
496
  limit: topK,
341
497
  filter: translatedFilter,
342
498
  with_payload: true,
343
- with_vector: includeVector
499
+ with_vector: includeVector,
500
+ ...using ? { using } : {}
344
501
  })).points;
345
502
  return results.map((match) => {
346
503
  let vector = [];
347
- if (includeVector) {
504
+ if (includeVector && match.vector != null) {
348
505
  if (Array.isArray(match.vector)) {
349
506
  vector = match.vector;
350
507
  } else if (typeof match.vector === "object" && match.vector !== null) {
351
- vector = Object.values(match.vector).filter((v) => typeof v === "number");
508
+ const namedVectors = match.vector;
509
+ const sourceArray = using && Array.isArray(namedVectors[using]) ? namedVectors[using] : Object.values(namedVectors).find((v) => Array.isArray(v));
510
+ if (sourceArray) {
511
+ vector = sourceArray.filter((v) => typeof v === "number");
512
+ }
352
513
  }
353
514
  }
354
515
  return {
@@ -364,7 +525,7 @@ var QdrantVector = class extends MastraVector {
364
525
  id: createVectorErrorId("QDRANT", "QUERY", "FAILED"),
365
526
  domain: ErrorDomain.STORAGE,
366
527
  category: ErrorCategory.THIRD_PARTY,
367
- details: { indexName, topK }
528
+ details: { indexName, topK, ...using && { using } }
368
529
  },
369
530
  error
370
531
  );