@mastra/qdrant 1.0.0-beta.3 → 1.0.0-beta.5

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
@@ -250,18 +250,54 @@ var QdrantVector = class extends MastraVector {
250
250
  super({ id });
251
251
  this.client = new QdrantClient(qdrantParams);
252
252
  }
253
- async upsert({ indexName, vectors, metadata, ids }) {
253
+ /**
254
+ * Validates that a named vector exists in the collection.
255
+ * @param indexName - The name of the collection to check.
256
+ * @param vectorName - The name of the vector space to validate.
257
+ * @throws Error if the vector name doesn't exist in the collection.
258
+ */
259
+ async validateVectorName(indexName, vectorName) {
260
+ const { config } = await this.client.getCollection(indexName);
261
+ const vectorsConfig = config.params.vectors;
262
+ const isNamedVectors = vectorsConfig && typeof vectorsConfig === "object" && !("size" in vectorsConfig);
263
+ if (!isNamedVectors || !(vectorName in vectorsConfig)) {
264
+ throw new Error(`Vector name "${vectorName}" does not exist in collection "${indexName}"`);
265
+ }
266
+ }
267
+ /**
268
+ * Upserts vectors into the index.
269
+ * @param indexName - The name of the index to upsert into.
270
+ * @param vectors - Array of embedding vectors.
271
+ * @param metadata - Optional metadata for each vector.
272
+ * @param ids - Optional vector IDs (auto-generated if not provided).
273
+ * @param vectorName - Optional name of the vector space when using named vectors.
274
+ */
275
+ async upsert({ indexName, vectors, metadata, ids, vectorName }) {
254
276
  const pointIds = ids || vectors.map(() => crypto.randomUUID());
277
+ if (vectorName) {
278
+ try {
279
+ await this.validateVectorName(indexName, vectorName);
280
+ } catch (validationError) {
281
+ throw new MastraError(
282
+ {
283
+ id: createVectorErrorId("QDRANT", "UPSERT", "INVALID_VECTOR_NAME"),
284
+ domain: ErrorDomain.STORAGE,
285
+ category: ErrorCategory.USER,
286
+ details: { indexName, vectorName }
287
+ },
288
+ validationError
289
+ );
290
+ }
291
+ }
255
292
  const records = vectors.map((vector, i) => ({
256
293
  id: pointIds[i],
257
- vector,
294
+ vector: vectorName ? { [vectorName]: vector } : vector,
258
295
  payload: metadata?.[i] || {}
259
296
  }));
260
297
  try {
261
298
  for (let i = 0; i < records.length; i += BATCH_SIZE) {
262
299
  const batch = records.slice(i, i + BATCH_SIZE);
263
300
  await this.client.upsert(indexName, {
264
- // @ts-expect-error
265
301
  points: batch,
266
302
  wait: true
267
303
  });
@@ -273,19 +309,60 @@ var QdrantVector = class extends MastraVector {
273
309
  id: createVectorErrorId("QDRANT", "UPSERT", "FAILED"),
274
310
  domain: ErrorDomain.STORAGE,
275
311
  category: ErrorCategory.THIRD_PARTY,
276
- details: { indexName, vectorCount: vectors.length }
312
+ details: { indexName, vectorCount: vectors.length, ...vectorName && { vectorName } }
277
313
  },
278
314
  error
279
315
  );
280
316
  }
281
317
  }
282
- async createIndex({ indexName, dimension, metric = "cosine" }) {
318
+ /**
319
+ * Creates a new index (collection) in Qdrant.
320
+ * Supports both single vector and named vector configurations.
321
+ *
322
+ * @param indexName - The name of the collection to create.
323
+ * @param dimension - Vector dimension (required for single vector mode).
324
+ * @param metric - Distance metric (default: 'cosine').
325
+ * @param namedVectors - Optional named vector configurations for multi-vector collections.
326
+ *
327
+ * @example
328
+ * ```ts
329
+ * // Single vector collection
330
+ * await qdrant.createIndex({ indexName: 'docs', dimension: 768, metric: 'cosine' });
331
+ *
332
+ * // Named vectors collection
333
+ * await qdrant.createIndex({
334
+ * indexName: 'multi-modal',
335
+ * dimension: 768, // Used as fallback, can be omitted with namedVectors
336
+ * namedVectors: {
337
+ * text: { size: 768, distance: 'cosine' },
338
+ * image: { size: 512, distance: 'euclidean' },
339
+ * },
340
+ * });
341
+ * ```
342
+ */
343
+ async createIndex({ indexName, dimension, metric = "cosine", namedVectors }) {
283
344
  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`);
345
+ if (namedVectors) {
346
+ if (Object.keys(namedVectors).length === 0) {
347
+ throw new Error("namedVectors must contain at least one named vector configuration");
348
+ }
349
+ for (const [name, config] of Object.entries(namedVectors)) {
350
+ if (!Number.isInteger(config.size) || config.size <= 0) {
351
+ throw new Error(`Named vector "${name}": size must be a positive integer`);
352
+ }
353
+ if (!DISTANCE_MAPPING[config.distance]) {
354
+ throw new Error(
355
+ `Named vector "${name}": invalid distance "${config.distance}". Must be one of: cosine, euclidean, dotproduct`
356
+ );
357
+ }
358
+ }
359
+ } else {
360
+ if (!Number.isInteger(dimension) || dimension <= 0) {
361
+ throw new Error("Dimension must be a positive integer");
362
+ }
363
+ if (!DISTANCE_MAPPING[metric]) {
364
+ throw new Error(`Invalid metric: "${metric}". Must be one of: cosine, euclidean, dotproduct`);
365
+ }
289
366
  }
290
367
  } catch (validationError) {
291
368
  throw new MastraError(
@@ -293,22 +370,49 @@ var QdrantVector = class extends MastraVector {
293
370
  id: createVectorErrorId("QDRANT", "CREATE_INDEX", "INVALID_ARGS"),
294
371
  domain: ErrorDomain.STORAGE,
295
372
  category: ErrorCategory.USER,
296
- details: { indexName, dimension, metric }
373
+ details: {
374
+ indexName,
375
+ dimension,
376
+ metric,
377
+ ...namedVectors && { namedVectorNames: Object.keys(namedVectors).join(", ") }
378
+ }
297
379
  },
298
380
  validationError
299
381
  );
300
382
  }
301
383
  try {
302
- await this.client.createCollection(indexName, {
303
- vectors: {
304
- size: dimension,
305
- distance: DISTANCE_MAPPING[metric]
306
- }
307
- });
384
+ if (namedVectors) {
385
+ const namedVectorsConfig = Object.entries(namedVectors).reduce(
386
+ (acc, [name, config]) => {
387
+ acc[name] = {
388
+ size: config.size,
389
+ distance: DISTANCE_MAPPING[config.distance]
390
+ };
391
+ return acc;
392
+ },
393
+ {}
394
+ );
395
+ await this.client.createCollection(indexName, {
396
+ vectors: namedVectorsConfig
397
+ });
398
+ } else {
399
+ await this.client.createCollection(indexName, {
400
+ vectors: {
401
+ size: dimension,
402
+ distance: DISTANCE_MAPPING[metric]
403
+ }
404
+ });
405
+ }
308
406
  } catch (error) {
309
407
  const message = error?.message || error?.toString();
310
408
  if (error?.status === 409 || typeof message === "string" && message.toLowerCase().includes("exists")) {
311
- await this.validateExistingIndex(indexName, dimension, metric);
409
+ if (!namedVectors) {
410
+ await this.validateExistingIndex(indexName, dimension, metric);
411
+ } else {
412
+ this.logger.info(
413
+ `Collection "${indexName}" already exists. Skipping validation for named vectors configuration.`
414
+ );
415
+ }
312
416
  return;
313
417
  }
314
418
  throw new MastraError(
@@ -326,12 +430,23 @@ var QdrantVector = class extends MastraVector {
326
430
  const translator = new QdrantFilterTranslator();
327
431
  return translator.translate(filter);
328
432
  }
433
+ /**
434
+ * Queries the index for similar vectors.
435
+ *
436
+ * @param indexName - The name of the index to query.
437
+ * @param queryVector - The query vector to find similar vectors for.
438
+ * @param topK - Number of results to return (default: 10).
439
+ * @param filter - Optional metadata filter.
440
+ * @param includeVector - Whether to include vectors in results (default: false).
441
+ * @param using - Name of the vector space to query when using named vectors.
442
+ */
329
443
  async query({
330
444
  indexName,
331
445
  queryVector,
332
446
  topK = 10,
333
447
  filter,
334
- includeVector = false
448
+ includeVector = false,
449
+ using
335
450
  }) {
336
451
  const translatedFilter = this.transformFilter(filter) ?? {};
337
452
  try {
@@ -340,15 +455,20 @@ var QdrantVector = class extends MastraVector {
340
455
  limit: topK,
341
456
  filter: translatedFilter,
342
457
  with_payload: true,
343
- with_vector: includeVector
458
+ with_vector: includeVector,
459
+ ...using ? { using } : {}
344
460
  })).points;
345
461
  return results.map((match) => {
346
462
  let vector = [];
347
- if (includeVector) {
463
+ if (includeVector && match.vector != null) {
348
464
  if (Array.isArray(match.vector)) {
349
465
  vector = match.vector;
350
466
  } else if (typeof match.vector === "object" && match.vector !== null) {
351
- vector = Object.values(match.vector).filter((v) => typeof v === "number");
467
+ const namedVectors = match.vector;
468
+ const sourceArray = using && Array.isArray(namedVectors[using]) ? namedVectors[using] : Object.values(namedVectors).find((v) => Array.isArray(v));
469
+ if (sourceArray) {
470
+ vector = sourceArray.filter((v) => typeof v === "number");
471
+ }
352
472
  }
353
473
  }
354
474
  return {
@@ -364,7 +484,7 @@ var QdrantVector = class extends MastraVector {
364
484
  id: createVectorErrorId("QDRANT", "QUERY", "FAILED"),
365
485
  domain: ErrorDomain.STORAGE,
366
486
  category: ErrorCategory.THIRD_PARTY,
367
- details: { indexName, topK }
487
+ details: { indexName, topK, ...using && { using } }
368
488
  },
369
489
  error
370
490
  );
@@ -723,6 +843,161 @@ var QdrantVector = class extends MastraVector {
723
843
  );
724
844
  }
725
845
  }
846
+ /**
847
+ * Creates a payload index on a Qdrant collection to enable efficient filtering on metadata fields.
848
+ *
849
+ * This is required for Qdrant Cloud and any Qdrant instance with `strict_mode_config = true`,
850
+ * where metadata (payload) fields must be explicitly indexed before they can be used for filtering.
851
+ *
852
+ * @param params - The parameters for creating the payload index.
853
+ * @param params.indexName - The name of the collection (index) to create the payload index on.
854
+ * @param params.fieldName - The name of the payload field to index.
855
+ * @param params.fieldSchema - The schema type for the field (e.g., 'keyword', 'integer', 'text').
856
+ * @param params.wait - Whether to wait for the operation to complete. Defaults to true.
857
+ * @returns A promise that resolves when the index is created (idempotent if index already exists).
858
+ * @throws Will throw a MastraError if arguments are invalid or if the operation fails.
859
+ *
860
+ * @example
861
+ * ```ts
862
+ * // Create a keyword index for filtering by source
863
+ * await qdrant.createPayloadIndex({
864
+ * indexName: 'my-collection',
865
+ * fieldName: 'source',
866
+ * fieldSchema: 'keyword',
867
+ * });
868
+ *
869
+ * // Create an integer index for numeric filtering
870
+ * await qdrant.createPayloadIndex({
871
+ * indexName: 'my-collection',
872
+ * fieldName: 'price',
873
+ * fieldSchema: 'integer',
874
+ * });
875
+ * ```
876
+ *
877
+ * @see https://qdrant.tech/documentation/concepts/indexing/#payload-index
878
+ */
879
+ async createPayloadIndex({
880
+ indexName,
881
+ fieldName,
882
+ fieldSchema,
883
+ wait = true
884
+ }) {
885
+ const validSchemas = [
886
+ "keyword",
887
+ "integer",
888
+ "float",
889
+ "geo",
890
+ "text",
891
+ "bool",
892
+ "datetime",
893
+ "uuid"
894
+ ];
895
+ if (!indexName || typeof indexName !== "string" || indexName.trim() === "") {
896
+ throw new MastraError({
897
+ id: createVectorErrorId("QDRANT", "CREATE_PAYLOAD_INDEX", "INVALID_ARGS"),
898
+ text: "indexName must be a non-empty string",
899
+ domain: ErrorDomain.STORAGE,
900
+ category: ErrorCategory.USER,
901
+ details: { indexName, fieldName, fieldSchema }
902
+ });
903
+ }
904
+ if (!fieldName || typeof fieldName !== "string" || fieldName.trim() === "") {
905
+ throw new MastraError({
906
+ id: createVectorErrorId("QDRANT", "CREATE_PAYLOAD_INDEX", "INVALID_ARGS"),
907
+ text: "fieldName must be a non-empty string",
908
+ domain: ErrorDomain.STORAGE,
909
+ category: ErrorCategory.USER,
910
+ details: { indexName, fieldName, fieldSchema }
911
+ });
912
+ }
913
+ if (!validSchemas.includes(fieldSchema)) {
914
+ throw new MastraError({
915
+ id: createVectorErrorId("QDRANT", "CREATE_PAYLOAD_INDEX", "INVALID_ARGS"),
916
+ text: `fieldSchema must be one of: ${validSchemas.join(", ")}`,
917
+ domain: ErrorDomain.STORAGE,
918
+ category: ErrorCategory.USER,
919
+ details: { indexName, fieldName, fieldSchema }
920
+ });
921
+ }
922
+ try {
923
+ await this.client.createPayloadIndex(indexName, {
924
+ field_name: fieldName,
925
+ field_schema: fieldSchema,
926
+ wait
927
+ });
928
+ } catch (error) {
929
+ const message = error?.message || error?.toString() || "";
930
+ if (error?.status === 409 || message.toLowerCase().includes("exists")) {
931
+ this.logger.info(`Payload index for field "${fieldName}" already exists on collection "${indexName}"`);
932
+ return;
933
+ }
934
+ throw new MastraError(
935
+ {
936
+ id: createVectorErrorId("QDRANT", "CREATE_PAYLOAD_INDEX", "FAILED"),
937
+ domain: ErrorDomain.STORAGE,
938
+ category: ErrorCategory.THIRD_PARTY,
939
+ details: { indexName, fieldName, fieldSchema }
940
+ },
941
+ error
942
+ );
943
+ }
944
+ }
945
+ /**
946
+ * Deletes a payload index from a Qdrant collection.
947
+ *
948
+ * @param params - The parameters for deleting the payload index.
949
+ * @param params.indexName - The name of the collection (index) to delete the payload index from.
950
+ * @param params.fieldName - The name of the payload field index to delete.
951
+ * @param params.wait - Whether to wait for the operation to complete. Defaults to true.
952
+ * @returns A promise that resolves when the index is deleted (idempotent if index doesn't exist).
953
+ * @throws Will throw a MastraError if the operation fails.
954
+ *
955
+ * @example
956
+ * ```ts
957
+ * await qdrant.deletePayloadIndex({
958
+ * indexName: 'my-collection',
959
+ * fieldName: 'source',
960
+ * });
961
+ * ```
962
+ */
963
+ async deletePayloadIndex({ indexName, fieldName, wait = true }) {
964
+ if (!indexName || typeof indexName !== "string" || indexName.trim() === "") {
965
+ throw new MastraError({
966
+ id: createVectorErrorId("QDRANT", "DELETE_PAYLOAD_INDEX", "INVALID_ARGS"),
967
+ text: "indexName must be a non-empty string",
968
+ domain: ErrorDomain.STORAGE,
969
+ category: ErrorCategory.USER,
970
+ details: { indexName, fieldName }
971
+ });
972
+ }
973
+ if (!fieldName || typeof fieldName !== "string" || fieldName.trim() === "") {
974
+ throw new MastraError({
975
+ id: createVectorErrorId("QDRANT", "DELETE_PAYLOAD_INDEX", "INVALID_ARGS"),
976
+ text: "fieldName must be a non-empty string",
977
+ domain: ErrorDomain.STORAGE,
978
+ category: ErrorCategory.USER,
979
+ details: { indexName, fieldName }
980
+ });
981
+ }
982
+ try {
983
+ await this.client.deletePayloadIndex(indexName, fieldName, { wait });
984
+ } catch (error) {
985
+ const message = error?.message || error?.toString() || "";
986
+ if (error?.status === 404 || message.toLowerCase().includes("not found") || message.toLowerCase().includes("not exist")) {
987
+ this.logger.info(`Payload index for field "${fieldName}" does not exist on collection "${indexName}"`);
988
+ return;
989
+ }
990
+ throw new MastraError(
991
+ {
992
+ id: createVectorErrorId("QDRANT", "DELETE_PAYLOAD_INDEX", "FAILED"),
993
+ domain: ErrorDomain.STORAGE,
994
+ category: ErrorCategory.THIRD_PARTY,
995
+ details: { indexName, fieldName }
996
+ },
997
+ error
998
+ );
999
+ }
1000
+ }
726
1001
  };
727
1002
 
728
1003
  // src/vector/prompt.ts