@mastra/qdrant 1.0.0-beta.4 → 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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @mastra/qdrant
2
2
 
3
+ ## 1.0.0-beta.5
4
+
5
+ ### Minor Changes
6
+
7
+ - Add full support for Qdrant named vectors, allowing collections with multiple vector spaces. ([#11905](https://github.com/mastra-ai/mastra/pull/11905))
8
+ - Added `namedVectors` parameter to `createIndex()` for creating multi-vector collections
9
+ - Added `vectorName` parameter to `upsert()` for inserting into specific vector spaces
10
+ - Added `using` parameter to `query()` for querying specific vector spaces
11
+ - Changed `client` from `private` to `protected` to enable subclass extension
12
+ - Added vector name validation when upserting to named vector collections
13
+
14
+ ### Patch Changes
15
+
16
+ - Updated dependencies [[`1dbd8c7`](https://github.com/mastra-ai/mastra/commit/1dbd8c729fb6536ec52f00064d76b80253d346e9), [`c59e13c`](https://github.com/mastra-ai/mastra/commit/c59e13c7688284bd96b2baee3e314335003548de), [`f9a2509`](https://github.com/mastra-ai/mastra/commit/f9a25093ea72d210a5e52cfcb3bcc8b5e02dc25c), [`7a010c5`](https://github.com/mastra-ai/mastra/commit/7a010c56b846a313a49ae42fccd3d8de2b9f292d)]:
17
+ - @mastra/core@1.0.0-beta.24
18
+
3
19
  ## 1.0.0-beta.4
4
20
 
5
21
  ### Minor Changes
package/README.md CHANGED
@@ -13,11 +13,11 @@ pnpm add @mastra/qdrant
13
13
  ```typescript
14
14
  import { QdrantVector } from '@mastra/qdrant';
15
15
 
16
- const vectorStore = new QdrantVector(
17
- 'http://localhost:6333', // url
18
- 'optional-api-key', // optional
19
- false // https (optional)
20
- );
16
+ const vectorStore = new QdrantVector({
17
+ id: 'my-qdrant',
18
+ url: 'http://localhost:6333',
19
+ apiKey: 'optional-api-key', // optional
20
+ });
21
21
 
22
22
  // Create a new collection
23
23
  await vectorStore.createIndex({ indexName: 'myCollection', dimension: 1536, metric: 'cosine' });
@@ -31,9 +31,61 @@ const ids = await vectorStore.upsert({ indexName: 'myCollection', vectors, metad
31
31
  const results = await vectorStore.query({
32
32
  indexName: 'myCollection',
33
33
  queryVector: [0.1, 0.2, ...],
34
- topK: 10, // topK
34
+ topK: 10,
35
35
  filter: { text: { $eq: 'doc1' } }, // optional filter
36
- includeVector: false // includeVector
36
+ includeVector: false,
37
+ });
38
+
39
+ // Query with named vectors (for collections with multiple vector fields)
40
+ const namedResults = await vectorStore.query({
41
+ indexName: 'myCollection',
42
+ queryVector: [0.1, 0.2, ...],
43
+ topK: 10,
44
+ using: 'title_embedding', // specify which named vector to query
45
+ });
46
+ ```
47
+
48
+ ## Named Vectors
49
+
50
+ Qdrant supports [named vectors](https://qdrant.tech/documentation/concepts/vectors/#named-vectors), allowing multiple vector fields per collection. This is useful for multi-modal data (text + images) or different embedding models.
51
+
52
+ ```typescript
53
+ // Create a collection with multiple named vector spaces
54
+ await vectorStore.createIndex({
55
+ indexName: 'multi_modal',
56
+ dimension: 768, // fallback
57
+ namedVectors: {
58
+ text: { size: 768, distance: 'cosine' },
59
+ image: { size: 512, distance: 'euclidean' },
60
+ },
61
+ });
62
+
63
+ // Upsert into specific vector spaces
64
+ await vectorStore.upsert({
65
+ indexName: 'multi_modal',
66
+ vectors: textEmbeddings,
67
+ metadata: [{ type: 'text' }],
68
+ vectorName: 'text', // target the text vector space
69
+ });
70
+
71
+ await vectorStore.upsert({
72
+ indexName: 'multi_modal',
73
+ vectors: imageEmbeddings,
74
+ metadata: [{ type: 'image' }],
75
+ vectorName: 'image', // target the image vector space
76
+ });
77
+
78
+ // Query specific vector spaces
79
+ const textResults = await vectorStore.query({
80
+ indexName: 'multi_modal',
81
+ queryVector: textQuery,
82
+ using: 'text',
83
+ });
84
+
85
+ const imageResults = await vectorStore.query({
86
+ indexName: 'multi_modal',
87
+ queryVector: imageQuery,
88
+ using: 'image',
37
89
  });
38
90
  ```
39
91
 
@@ -41,6 +93,7 @@ const results = await vectorStore.query({
41
93
 
42
94
  Required:
43
95
 
96
+ - `id`: Unique identifier for this vector store instance
44
97
  - `url`: URL of your Qdrant instance
45
98
 
46
99
  Optional:
@@ -51,6 +104,7 @@ Optional:
51
104
  ## Features
52
105
 
53
106
  - Vector similarity search with Cosine, Euclidean, and Dot Product metrics
107
+ - [Named vectors](https://qdrant.tech/documentation/concepts/vectors/#named-vectors) support for collections with multiple vector fields
54
108
  - Automatic batching for large upserts (256 vectors per batch)
55
109
  - Built-in telemetry support
56
110
  - Metadata filtering
@@ -69,12 +123,14 @@ The following distance metrics are supported:
69
123
 
70
124
  ## Methods
71
125
 
72
- - `createIndex({ indexName, dimension, metric? })`: Create a new collection
73
- - `upsert({ indexName, vectors, metadata?, ids? })`: Add or update vectors
74
- - `query({ indexName, queryVector, topK?, filter?, includeVector? })`: Search for similar vectors
126
+ - `createIndex({ indexName, dimension, metric?, namedVectors? })`: Create a new collection (supports named vectors)
127
+ - `upsert({ indexName, vectors, metadata?, ids?, vectorName? })`: Add or update vectors (supports named vectors)
128
+ - `query({ indexName, queryVector, topK?, filter?, includeVector?, using? })`: Search for similar vectors
75
129
  - `updateVector({ indexName, id?, filter?, update })`: Update a single vector by ID or metadata filter
76
130
  - `deleteVector({ indexName, id })`: Delete a single vector by ID
77
131
  - `deleteVectors({ indexName, ids?, filter? })`: Delete multiple vectors by IDs or metadata filter
132
+ - `createPayloadIndex({ indexName, fieldName, fieldSchema, wait? })`: Create a payload index for filtering
133
+ - `deletePayloadIndex({ indexName, fieldName, wait? })`: Delete a payload index
78
134
  - `listIndexes()`: List all collections
79
135
  - `describeIndex(indexName)`: Get collection statistics
80
136
  - `deleteIndex(indexName)`: Delete a collection
@@ -29,4 +29,4 @@ docs/
29
29
  ## Version
30
30
 
31
31
  Package: @mastra/qdrant
32
- Version: 1.0.0-beta.4
32
+ Version: 1.0.0-beta.5
@@ -5,7 +5,7 @@ description: Documentation for @mastra/qdrant. Includes links to type definition
5
5
 
6
6
  # @mastra/qdrant Documentation
7
7
 
8
- > **Version**: 1.0.0-beta.4
8
+ > **Version**: 1.0.0-beta.5
9
9
  > **Package**: @mastra/qdrant
10
10
 
11
11
  ## Quick Navigation
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.0-beta.4",
2
+ "version": "1.0.0-beta.5",
3
3
  "package": "@mastra/qdrant",
4
4
  "exports": {},
5
5
  "modules": {}
@@ -27,7 +27,7 @@ await store.upsert({
27
27
  });
28
28
  ```
29
29
 
30
- ### Using MongoDB Atlas Vector search
30
+ <h3>Using MongoDB Atlas Vector search</h3>
31
31
 
32
32
  For detailed setup instructions and best practices, see the [official MongoDB Atlas Vector Search documentation](https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-overview/?utm_campaign=devrel&utm_source=third-party-content&utm_medium=cta&utm_content=mastra-docs).
33
33
 
@@ -55,7 +55,7 @@ await store.upsert({
55
55
  });
56
56
  ```
57
57
 
58
- ### Using PostgreSQL with pgvector
58
+ <h3>Using PostgreSQL with pgvector</h3>
59
59
 
60
60
  PostgreSQL with the pgvector extension is a good solution for teams already using PostgreSQL who want to minimize infrastructure complexity.
61
61
  For detailed setup instructions and best practices, see the [official pgvector repository](https://github.com/pgvector/pgvector).
@@ -323,7 +323,7 @@ await store.upsert({
323
323
  });
324
324
  ```
325
325
 
326
- ### Using LanceDB
326
+ <h3>Using LanceDB</h3>
327
327
 
328
328
  LanceDB is an embedded vector database built on the Lance columnar format, suitable for local development or cloud deployment.
329
329
  For detailed setup instructions and best practices, see the [official LanceDB documentation](https://lancedb.github.io/lancedb/).
@@ -18,10 +18,57 @@ It provides a production-ready service with a convenient API to store, search, a
18
18
 
19
19
  ### createIndex()
20
20
 
21
+ #### Creating a Named Vectors Collection
22
+
23
+ ```typescript
24
+ // Create a collection with multiple named vector spaces
25
+ await store.createIndex({
26
+ indexName: "multi_modal",
27
+ dimension: 768, // fallback
28
+ namedVectors: {
29
+ text: { size: 768, distance: "cosine" },
30
+ image: { size: 512, distance: "euclidean" },
31
+ },
32
+ });
33
+ ```
34
+
21
35
  ### upsert()
22
36
 
37
+ #### Upserting into Named Vector Spaces
38
+
39
+ ```typescript
40
+ // Upsert into the "text" vector space
41
+ await store.upsert({
42
+ indexName: "multi_modal",
43
+ vectors: textEmbeddings,
44
+ metadata: textMetadata,
45
+ vectorName: "text",
46
+ });
47
+
48
+ // Upsert into the "image" vector space
49
+ await store.upsert({
50
+ indexName: "multi_modal",
51
+ vectors: imageEmbeddings,
52
+ metadata: imageMetadata,
53
+ vectorName: "image",
54
+ });
55
+ ```
56
+
23
57
  ### query()
24
58
 
59
+ #### Named Vectors
60
+
61
+ Qdrant supports [named vectors](https://qdrant.tech/documentation/concepts/vectors/#named-vectors), allowing multiple vector fields per collection. Use the `using` parameter to specify which named vector to query against:
62
+
63
+ ```typescript
64
+ const results = await store.query({
65
+ indexName: "my_index",
66
+ queryVector: embedding,
67
+ topK: 10,
68
+ using: "title_embedding", // Query against a specific named vector
69
+ });
70
+ ```
71
+
25
72
  ### listIndexes()
26
73
 
27
74
  Returns an array of index names as strings.
package/dist/index.cjs CHANGED
@@ -252,18 +252,54 @@ var QdrantVector = class extends vector.MastraVector {
252
252
  super({ id });
253
253
  this.client = new jsClientRest.QdrantClient(qdrantParams);
254
254
  }
255
- async upsert({ indexName, vectors, metadata, ids }) {
255
+ /**
256
+ * Validates that a named vector exists in the collection.
257
+ * @param indexName - The name of the collection to check.
258
+ * @param vectorName - The name of the vector space to validate.
259
+ * @throws Error if the vector name doesn't exist in the collection.
260
+ */
261
+ async validateVectorName(indexName, vectorName) {
262
+ const { config } = await this.client.getCollection(indexName);
263
+ const vectorsConfig = config.params.vectors;
264
+ const isNamedVectors = vectorsConfig && typeof vectorsConfig === "object" && !("size" in vectorsConfig);
265
+ if (!isNamedVectors || !(vectorName in vectorsConfig)) {
266
+ throw new Error(`Vector name "${vectorName}" does not exist in collection "${indexName}"`);
267
+ }
268
+ }
269
+ /**
270
+ * Upserts vectors into the index.
271
+ * @param indexName - The name of the index to upsert into.
272
+ * @param vectors - Array of embedding vectors.
273
+ * @param metadata - Optional metadata for each vector.
274
+ * @param ids - Optional vector IDs (auto-generated if not provided).
275
+ * @param vectorName - Optional name of the vector space when using named vectors.
276
+ */
277
+ async upsert({ indexName, vectors, metadata, ids, vectorName }) {
256
278
  const pointIds = ids || vectors.map(() => crypto.randomUUID());
279
+ if (vectorName) {
280
+ try {
281
+ await this.validateVectorName(indexName, vectorName);
282
+ } catch (validationError) {
283
+ throw new error.MastraError(
284
+ {
285
+ id: storage.createVectorErrorId("QDRANT", "UPSERT", "INVALID_VECTOR_NAME"),
286
+ domain: error.ErrorDomain.STORAGE,
287
+ category: error.ErrorCategory.USER,
288
+ details: { indexName, vectorName }
289
+ },
290
+ validationError
291
+ );
292
+ }
293
+ }
257
294
  const records = vectors.map((vector, i) => ({
258
295
  id: pointIds[i],
259
- vector,
296
+ vector: vectorName ? { [vectorName]: vector } : vector,
260
297
  payload: metadata?.[i] || {}
261
298
  }));
262
299
  try {
263
300
  for (let i = 0; i < records.length; i += BATCH_SIZE) {
264
301
  const batch = records.slice(i, i + BATCH_SIZE);
265
302
  await this.client.upsert(indexName, {
266
- // @ts-expect-error
267
303
  points: batch,
268
304
  wait: true
269
305
  });
@@ -275,19 +311,60 @@ var QdrantVector = class extends vector.MastraVector {
275
311
  id: storage.createVectorErrorId("QDRANT", "UPSERT", "FAILED"),
276
312
  domain: error.ErrorDomain.STORAGE,
277
313
  category: error.ErrorCategory.THIRD_PARTY,
278
- details: { indexName, vectorCount: vectors.length }
314
+ details: { indexName, vectorCount: vectors.length, ...vectorName && { vectorName } }
279
315
  },
280
316
  error$1
281
317
  );
282
318
  }
283
319
  }
284
- async createIndex({ indexName, dimension, metric = "cosine" }) {
320
+ /**
321
+ * Creates a new index (collection) in Qdrant.
322
+ * Supports both single vector and named vector configurations.
323
+ *
324
+ * @param indexName - The name of the collection to create.
325
+ * @param dimension - Vector dimension (required for single vector mode).
326
+ * @param metric - Distance metric (default: 'cosine').
327
+ * @param namedVectors - Optional named vector configurations for multi-vector collections.
328
+ *
329
+ * @example
330
+ * ```ts
331
+ * // Single vector collection
332
+ * await qdrant.createIndex({ indexName: 'docs', dimension: 768, metric: 'cosine' });
333
+ *
334
+ * // Named vectors collection
335
+ * await qdrant.createIndex({
336
+ * indexName: 'multi-modal',
337
+ * dimension: 768, // Used as fallback, can be omitted with namedVectors
338
+ * namedVectors: {
339
+ * text: { size: 768, distance: 'cosine' },
340
+ * image: { size: 512, distance: 'euclidean' },
341
+ * },
342
+ * });
343
+ * ```
344
+ */
345
+ async createIndex({ indexName, dimension, metric = "cosine", namedVectors }) {
285
346
  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`);
347
+ if (namedVectors) {
348
+ if (Object.keys(namedVectors).length === 0) {
349
+ throw new Error("namedVectors must contain at least one named vector configuration");
350
+ }
351
+ for (const [name, config] of Object.entries(namedVectors)) {
352
+ if (!Number.isInteger(config.size) || config.size <= 0) {
353
+ throw new Error(`Named vector "${name}": size must be a positive integer`);
354
+ }
355
+ if (!DISTANCE_MAPPING[config.distance]) {
356
+ throw new Error(
357
+ `Named vector "${name}": invalid distance "${config.distance}". Must be one of: cosine, euclidean, dotproduct`
358
+ );
359
+ }
360
+ }
361
+ } else {
362
+ if (!Number.isInteger(dimension) || dimension <= 0) {
363
+ throw new Error("Dimension must be a positive integer");
364
+ }
365
+ if (!DISTANCE_MAPPING[metric]) {
366
+ throw new Error(`Invalid metric: "${metric}". Must be one of: cosine, euclidean, dotproduct`);
367
+ }
291
368
  }
292
369
  } catch (validationError) {
293
370
  throw new error.MastraError(
@@ -295,22 +372,49 @@ var QdrantVector = class extends vector.MastraVector {
295
372
  id: storage.createVectorErrorId("QDRANT", "CREATE_INDEX", "INVALID_ARGS"),
296
373
  domain: error.ErrorDomain.STORAGE,
297
374
  category: error.ErrorCategory.USER,
298
- details: { indexName, dimension, metric }
375
+ details: {
376
+ indexName,
377
+ dimension,
378
+ metric,
379
+ ...namedVectors && { namedVectorNames: Object.keys(namedVectors).join(", ") }
380
+ }
299
381
  },
300
382
  validationError
301
383
  );
302
384
  }
303
385
  try {
304
- await this.client.createCollection(indexName, {
305
- vectors: {
306
- size: dimension,
307
- distance: DISTANCE_MAPPING[metric]
308
- }
309
- });
386
+ if (namedVectors) {
387
+ const namedVectorsConfig = Object.entries(namedVectors).reduce(
388
+ (acc, [name, config]) => {
389
+ acc[name] = {
390
+ size: config.size,
391
+ distance: DISTANCE_MAPPING[config.distance]
392
+ };
393
+ return acc;
394
+ },
395
+ {}
396
+ );
397
+ await this.client.createCollection(indexName, {
398
+ vectors: namedVectorsConfig
399
+ });
400
+ } else {
401
+ await this.client.createCollection(indexName, {
402
+ vectors: {
403
+ size: dimension,
404
+ distance: DISTANCE_MAPPING[metric]
405
+ }
406
+ });
407
+ }
310
408
  } catch (error$1) {
311
409
  const message = error$1?.message || error$1?.toString();
312
410
  if (error$1?.status === 409 || typeof message === "string" && message.toLowerCase().includes("exists")) {
313
- await this.validateExistingIndex(indexName, dimension, metric);
411
+ if (!namedVectors) {
412
+ await this.validateExistingIndex(indexName, dimension, metric);
413
+ } else {
414
+ this.logger.info(
415
+ `Collection "${indexName}" already exists. Skipping validation for named vectors configuration.`
416
+ );
417
+ }
314
418
  return;
315
419
  }
316
420
  throw new error.MastraError(
@@ -328,12 +432,23 @@ var QdrantVector = class extends vector.MastraVector {
328
432
  const translator = new QdrantFilterTranslator();
329
433
  return translator.translate(filter);
330
434
  }
435
+ /**
436
+ * Queries the index for similar vectors.
437
+ *
438
+ * @param indexName - The name of the index to query.
439
+ * @param queryVector - The query vector to find similar vectors for.
440
+ * @param topK - Number of results to return (default: 10).
441
+ * @param filter - Optional metadata filter.
442
+ * @param includeVector - Whether to include vectors in results (default: false).
443
+ * @param using - Name of the vector space to query when using named vectors.
444
+ */
331
445
  async query({
332
446
  indexName,
333
447
  queryVector,
334
448
  topK = 10,
335
449
  filter,
336
- includeVector = false
450
+ includeVector = false,
451
+ using
337
452
  }) {
338
453
  const translatedFilter = this.transformFilter(filter) ?? {};
339
454
  try {
@@ -342,15 +457,20 @@ var QdrantVector = class extends vector.MastraVector {
342
457
  limit: topK,
343
458
  filter: translatedFilter,
344
459
  with_payload: true,
345
- with_vector: includeVector
460
+ with_vector: includeVector,
461
+ ...using ? { using } : {}
346
462
  })).points;
347
463
  return results.map((match) => {
348
464
  let vector = [];
349
- if (includeVector) {
465
+ if (includeVector && match.vector != null) {
350
466
  if (Array.isArray(match.vector)) {
351
467
  vector = match.vector;
352
468
  } else if (typeof match.vector === "object" && match.vector !== null) {
353
- vector = Object.values(match.vector).filter((v) => typeof v === "number");
469
+ const namedVectors = match.vector;
470
+ const sourceArray = using && Array.isArray(namedVectors[using]) ? namedVectors[using] : Object.values(namedVectors).find((v) => Array.isArray(v));
471
+ if (sourceArray) {
472
+ vector = sourceArray.filter((v) => typeof v === "number");
473
+ }
354
474
  }
355
475
  }
356
476
  return {
@@ -366,7 +486,7 @@ var QdrantVector = class extends vector.MastraVector {
366
486
  id: storage.createVectorErrorId("QDRANT", "QUERY", "FAILED"),
367
487
  domain: error.ErrorDomain.STORAGE,
368
488
  category: error.ErrorCategory.THIRD_PARTY,
369
- details: { indexName, topK }
489
+ details: { indexName, topK, ...using && { using } }
370
490
  },
371
491
  error$1
372
492
  );