@mastra/qdrant 0.11.8 → 0.11.9-alpha.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.
@@ -1,396 +0,0 @@
1
- import { MastraError, ErrorDomain, ErrorCategory } from '@mastra/core/error';
2
- import { MastraVector } from '@mastra/core/vector';
3
- import type {
4
- QueryResult,
5
- IndexStats,
6
- CreateIndexParams,
7
- UpsertVectorParams,
8
- QueryVectorParams,
9
- DescribeIndexParams,
10
- DeleteIndexParams,
11
- DeleteVectorParams,
12
- UpdateVectorParams,
13
- } from '@mastra/core/vector';
14
- import { QdrantClient } from '@qdrant/js-client-rest';
15
- import type { Schemas } from '@qdrant/js-client-rest';
16
-
17
- import { QdrantFilterTranslator } from './filter';
18
- import type { QdrantVectorFilter } from './filter';
19
-
20
- const BATCH_SIZE = 256;
21
- const DISTANCE_MAPPING: Record<string, Schemas['Distance']> = {
22
- cosine: 'Cosine',
23
- euclidean: 'Euclid',
24
- dotproduct: 'Dot',
25
- };
26
-
27
- type QdrantQueryVectorParams = QueryVectorParams<QdrantVectorFilter>;
28
-
29
- export class QdrantVector extends MastraVector {
30
- private client: QdrantClient;
31
-
32
- /**
33
- * Creates a new QdrantVector client.
34
- * @param url - The URL of the Qdrant server.
35
- * @param apiKey - The API key for Qdrant.
36
- * @param https - Whether to use HTTPS.
37
- */
38
- constructor({ url, apiKey, https }: { url: string; apiKey?: string; https?: boolean }) {
39
- super();
40
- const baseClient = new QdrantClient({
41
- url,
42
- apiKey,
43
- https,
44
- });
45
- const telemetry = this.__getTelemetry();
46
- this.client =
47
- telemetry?.traceClass(baseClient, {
48
- spanNamePrefix: 'qdrant-vector',
49
- attributes: {
50
- 'vector.type': 'qdrant',
51
- },
52
- }) ?? baseClient;
53
- }
54
-
55
- async upsert({ indexName, vectors, metadata, ids }: UpsertVectorParams): Promise<string[]> {
56
- const pointIds = ids || vectors.map(() => crypto.randomUUID());
57
-
58
- const records = vectors.map((vector, i) => ({
59
- id: pointIds[i],
60
- vector: vector,
61
- payload: metadata?.[i] || {},
62
- }));
63
-
64
- try {
65
- for (let i = 0; i < records.length; i += BATCH_SIZE) {
66
- const batch = records.slice(i, i + BATCH_SIZE);
67
- await this.client.upsert(indexName, {
68
- // @ts-expect-error
69
- points: batch,
70
- wait: true,
71
- });
72
- }
73
-
74
- return pointIds;
75
- } catch (error) {
76
- throw new MastraError(
77
- {
78
- id: 'STORAGE_QDRANT_VECTOR_UPSERT_FAILED',
79
- domain: ErrorDomain.STORAGE,
80
- category: ErrorCategory.THIRD_PARTY,
81
- details: { indexName, vectorCount: vectors.length },
82
- },
83
- error,
84
- );
85
- }
86
- }
87
-
88
- async createIndex({ indexName, dimension, metric = 'cosine' }: CreateIndexParams): Promise<void> {
89
- try {
90
- if (!Number.isInteger(dimension) || dimension <= 0) {
91
- throw new Error('Dimension must be a positive integer');
92
- }
93
- if (!DISTANCE_MAPPING[metric]) {
94
- throw new Error(`Invalid metric: "${metric}". Must be one of: cosine, euclidean, dotproduct`);
95
- }
96
- } catch (validationError) {
97
- throw new MastraError(
98
- {
99
- id: 'STORAGE_QDRANT_VECTOR_CREATE_INDEX_INVALID_ARGS',
100
- domain: ErrorDomain.STORAGE,
101
- category: ErrorCategory.USER,
102
- details: { indexName, dimension, metric },
103
- },
104
- validationError,
105
- );
106
- }
107
-
108
- try {
109
- await this.client.createCollection(indexName, {
110
- vectors: {
111
- size: dimension,
112
- distance: DISTANCE_MAPPING[metric],
113
- },
114
- });
115
- } catch (error: any) {
116
- const message = error?.message || error?.toString();
117
- // Qdrant typically returns 409 for existing collection
118
- if (error?.status === 409 || (typeof message === 'string' && message.toLowerCase().includes('exists'))) {
119
- // Fetch collection info and check dimension
120
- await this.validateExistingIndex(indexName, dimension, metric);
121
- return;
122
- }
123
-
124
- throw new MastraError(
125
- {
126
- id: 'STORAGE_QDRANT_VECTOR_CREATE_INDEX_FAILED',
127
- domain: ErrorDomain.STORAGE,
128
- category: ErrorCategory.THIRD_PARTY,
129
- details: { indexName, dimension, metric },
130
- },
131
- error,
132
- );
133
- }
134
- }
135
-
136
- transformFilter(filter?: QdrantVectorFilter) {
137
- const translator = new QdrantFilterTranslator();
138
- return translator.translate(filter);
139
- }
140
-
141
- async query({
142
- indexName,
143
- queryVector,
144
- topK = 10,
145
- filter,
146
- includeVector = false,
147
- }: QdrantQueryVectorParams): Promise<QueryResult[]> {
148
- const translatedFilter = this.transformFilter(filter) ?? {};
149
-
150
- try {
151
- const results = (
152
- await this.client.query(indexName, {
153
- query: queryVector,
154
- limit: topK,
155
- filter: translatedFilter,
156
- with_payload: true,
157
- with_vector: includeVector,
158
- })
159
- ).points;
160
-
161
- return results.map(match => {
162
- let vector: number[] = [];
163
- if (includeVector) {
164
- if (Array.isArray(match.vector)) {
165
- // If it's already an array of numbers
166
- vector = match.vector as number[];
167
- } else if (typeof match.vector === 'object' && match.vector !== null) {
168
- // If it's an object with vector data
169
- vector = Object.values(match.vector).filter(v => typeof v === 'number');
170
- }
171
- }
172
-
173
- return {
174
- id: match.id as string,
175
- score: match.score || 0,
176
- metadata: match.payload as Record<string, any>,
177
- ...(includeVector && { vector }),
178
- };
179
- });
180
- } catch (error) {
181
- throw new MastraError(
182
- {
183
- id: 'STORAGE_QDRANT_VECTOR_QUERY_FAILED',
184
- domain: ErrorDomain.STORAGE,
185
- category: ErrorCategory.THIRD_PARTY,
186
- details: { indexName, topK },
187
- },
188
- error,
189
- );
190
- }
191
- }
192
-
193
- async listIndexes(): Promise<string[]> {
194
- try {
195
- const response = await this.client.getCollections();
196
- return response.collections.map(collection => collection.name) || [];
197
- } catch (error) {
198
- throw new MastraError(
199
- {
200
- id: 'STORAGE_QDRANT_VECTOR_LIST_INDEXES_FAILED',
201
- domain: ErrorDomain.STORAGE,
202
- category: ErrorCategory.THIRD_PARTY,
203
- },
204
- error,
205
- );
206
- }
207
- }
208
-
209
- /**
210
- * Retrieves statistics about a vector index.
211
- *
212
- * @param {string} indexName - The name of the index to describe
213
- * @returns A promise that resolves to the index statistics including dimension, count and metric
214
- */
215
- async describeIndex({ indexName }: DescribeIndexParams): Promise<IndexStats> {
216
- try {
217
- const { config, points_count } = await this.client.getCollection(indexName);
218
-
219
- const distance = config.params.vectors?.distance as Schemas['Distance'];
220
- return {
221
- dimension: config.params.vectors?.size as number,
222
- count: points_count || 0,
223
- // @ts-expect-error
224
- metric: Object.keys(DISTANCE_MAPPING).find(key => DISTANCE_MAPPING[key] === distance),
225
- };
226
- } catch (error) {
227
- throw new MastraError(
228
- {
229
- id: 'STORAGE_QDRANT_VECTOR_DESCRIBE_INDEX_FAILED',
230
- domain: ErrorDomain.STORAGE,
231
- category: ErrorCategory.THIRD_PARTY,
232
- details: { indexName },
233
- },
234
- error,
235
- );
236
- }
237
- }
238
-
239
- async deleteIndex({ indexName }: DeleteIndexParams): Promise<void> {
240
- try {
241
- await this.client.deleteCollection(indexName);
242
- } catch (error) {
243
- throw new MastraError(
244
- {
245
- id: 'STORAGE_QDRANT_VECTOR_DELETE_INDEX_FAILED',
246
- domain: ErrorDomain.STORAGE,
247
- category: ErrorCategory.THIRD_PARTY,
248
- details: { indexName },
249
- },
250
- error,
251
- );
252
- }
253
- }
254
-
255
- /**
256
- * Updates a vector by its ID with the provided vector and/or metadata.
257
- * @param indexName - The name of the index containing the vector.
258
- * @param id - The ID of the vector to update.
259
- * @param update - An object containing the vector and/or metadata to update.
260
- * @param update.vector - An optional array of numbers representing the new vector.
261
- * @param update.metadata - An optional record containing the new metadata.
262
- * @returns A promise that resolves when the update is complete.
263
- * @throws Will throw an error if no updates are provided or if the update operation fails.
264
- */
265
- async updateVector({ indexName, id, update }: UpdateVectorParams): Promise<void> {
266
- try {
267
- if (!update.vector && !update.metadata) {
268
- throw new Error('No updates provided');
269
- }
270
- } catch (validationError) {
271
- throw new MastraError(
272
- {
273
- id: 'STORAGE_QDRANT_VECTOR_UPDATE_VECTOR_INVALID_ARGS',
274
- domain: ErrorDomain.STORAGE,
275
- category: ErrorCategory.USER,
276
- details: { indexName, id },
277
- },
278
- validationError,
279
- );
280
- }
281
-
282
- const pointId = this.parsePointId(id);
283
-
284
- try {
285
- // Handle metadata-only update
286
- if (update.metadata && !update.vector) {
287
- // For metadata-only updates, use the setPayload method
288
- await this.client.setPayload(indexName, { payload: update.metadata, points: [pointId] });
289
- return;
290
- }
291
-
292
- // Handle vector-only update
293
- if (update.vector && !update.metadata) {
294
- await this.client.updateVectors(indexName, {
295
- points: [
296
- {
297
- id: pointId,
298
- vector: update.vector,
299
- },
300
- ],
301
- });
302
- return;
303
- }
304
-
305
- // Handle both vector and metadata update
306
- if (update.vector && update.metadata) {
307
- const point = {
308
- id: pointId,
309
- vector: update.vector,
310
- payload: update.metadata,
311
- };
312
-
313
- await this.client.upsert(indexName, {
314
- points: [point],
315
- });
316
- return;
317
- }
318
- } catch (error) {
319
- throw new MastraError(
320
- {
321
- id: 'STORAGE_QDRANT_VECTOR_UPDATE_VECTOR_FAILED',
322
- domain: ErrorDomain.STORAGE,
323
- category: ErrorCategory.THIRD_PARTY,
324
- details: { indexName, id },
325
- },
326
- error,
327
- );
328
- }
329
- }
330
-
331
- /**
332
- * Deletes a vector by its ID.
333
- * @param indexName - The name of the index containing the vector.
334
- * @param id - The ID of the vector to delete.
335
- * @returns A promise that resolves when the deletion is complete.
336
- * @throws Will throw an error if the deletion operation fails.
337
- */
338
- async deleteVector({ indexName, id }: DeleteVectorParams): Promise<void> {
339
- try {
340
- // Parse the ID - Qdrant supports both string and numeric IDs
341
- const pointId = this.parsePointId(id);
342
-
343
- // Use the Qdrant client to delete the point from the collection
344
- await this.client.delete(indexName, {
345
- points: [pointId],
346
- });
347
- } catch (error) {
348
- throw new MastraError(
349
- {
350
- id: 'STORAGE_QDRANT_VECTOR_DELETE_VECTOR_FAILED',
351
- domain: ErrorDomain.STORAGE,
352
- category: ErrorCategory.THIRD_PARTY,
353
- details: { indexName, id },
354
- },
355
- error,
356
- );
357
- }
358
- }
359
-
360
- /**
361
- * Parses and converts a string ID to the appropriate type (string or number) for Qdrant point operations.
362
- *
363
- * Qdrant supports both numeric and string IDs. This helper method ensures IDs are in the correct format
364
- * before sending them to the Qdrant client API.
365
- *
366
- * @param id - The ID string to parse
367
- * @returns The parsed ID as either a number (if string contains only digits) or the original string
368
- *
369
- * @example
370
- * // Numeric ID strings are converted to numbers
371
- * parsePointId("123") => 123
372
- * parsePointId("42") => 42
373
- * parsePointId("0") => 0
374
- *
375
- * // String IDs containing any non-digit characters remain as strings
376
- * parsePointId("doc-123") => "doc-123"
377
- * parsePointId("user_42") => "user_42"
378
- * parsePointId("abc123") => "abc123"
379
- * parsePointId("123abc") => "123abc"
380
- * parsePointId("") => ""
381
- * parsePointId("uuid-5678-xyz") => "uuid-5678-xyz"
382
- *
383
- * @remarks
384
- * - This conversion is important because Qdrant treats numeric and string IDs differently
385
- * - Only positive integers are converted to numbers (negative numbers with minus signs remain strings)
386
- * - The method uses base-10 parsing, so leading zeros will be dropped in numeric conversions
387
- * - reference: https://qdrant.tech/documentation/concepts/points/?q=qdrant+point+id#point-ids
388
- */
389
- private parsePointId(id: string): string | number {
390
- // Try to parse as number if it looks like one
391
- if (/^\d+$/.test(id)) {
392
- return parseInt(id, 10);
393
- }
394
- return id;
395
- }
396
- }
@@ -1,85 +0,0 @@
1
- /**
2
- * Vector store specific prompt that details supported operators and examples.
3
- * This prompt helps users construct valid filters for Qdrant Vector.
4
- */
5
- export const QDRANT_PROMPT = `When querying Qdrant, you can ONLY use the operators listed below. Any other operators will be rejected.
6
- Important: Don't explain how to construct the filter - use the specified operators and fields to search the content and return relevant results.
7
- 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.
8
-
9
- Basic Comparison Operators:
10
- - $eq: Exact match (default when using field: value)
11
- Example: { "category": "electronics" }
12
- - $ne: Not equal
13
- Example: { "category": { "$ne": "electronics" } }
14
- - $gt: Greater than
15
- Example: { "price": { "$gt": 100 } }
16
- - $gte: Greater than or equal
17
- Example: { "price": { "$gte": 100 } }
18
- - $lt: Less than
19
- Example: { "price": { "$lt": 100 } }
20
- - $lte: Less than or equal
21
- Example: { "price": { "$lte": 100 } }
22
-
23
- Array Operators:
24
- - $in: Match any value in array
25
- Example: { "category": { "$in": ["electronics", "books"] } }
26
- - $nin: Does not match any value in array
27
- Example: { "category": { "$nin": ["electronics", "books"] } }
28
-
29
- Logical Operators:
30
- - $and: Logical AND (can be implicit or explicit)
31
- Implicit Example: { "price": { "$gt": 100 }, "category": "electronics" }
32
- Explicit Example: { "$and": [{ "price": { "$gt": 100 } }, { "category": "electronics" }] }
33
- - $or: Logical OR
34
- Example: { "$or": [{ "price": { "$lt": 50 } }, { "category": "books" }] }
35
- - $not: Logical NOT
36
- Example: { "$not": { "category": "electronics" } }
37
-
38
- Element Operators:
39
- - $exists: Check if field exists
40
- Example: { "rating": { "$exists": true } }
41
- - $match: Match text using full-text search
42
- Example: { "description": { "$match": "gaming laptop" } }
43
- - $null: Check if field is null
44
- Example: { "rating": { "$null": true } }
45
-
46
- Restrictions:
47
- - Regex patterns are not supported
48
- - Only $and, $or, and $not logical operators are supported at the top level
49
- - Empty arrays in $in/$nin will return no results
50
- - Nested fields are supported using dot notation
51
- - Multiple conditions on the same field are supported with both implicit and explicit $and
52
- - At least one key-value pair is required in filter object
53
- - Empty objects and undefined values are treated as no filter
54
- - Invalid types in comparison operators will throw errors
55
- - All non-logical operators must be used within a field condition
56
- Valid: { "field": { "$gt": 100 } }
57
- Valid: { "$and": [...] }
58
- Invalid: { "$gt": 100 }
59
- - Logical operators must contain field conditions, not direct operators
60
- Valid: { "$and": [{ "field": { "$gt": 100 } }] }
61
- Invalid: { "$and": [{ "$gt": 100 }] }
62
- - Logical operators ($and, $or, $not):
63
- - Can only be used at top level or nested within other logical operators
64
- - Can not be used on a field level, or be nested inside a field
65
- - Can not be used inside an operator
66
- - Valid: { "$and": [{ "field": { "$gt": 100 } }] }
67
- - Valid: { "$or": [{ "$and": [{ "field": { "$gt": 100 } }] }] }
68
- - Invalid: { "field": { "$and": [{ "$gt": 100 }] } }
69
- - Invalid: { "field": { "$or": [{ "$gt": 100 }] } }
70
- - Invalid: { "field": { "$gt": { "$and": [{...}] } } }
71
-
72
- Example Complex Query:
73
- {
74
- "$and": [
75
- { "category": { "$in": ["electronics", "computers"] } },
76
- { "price": { "$gte": 100, "$lte": 1000 } },
77
- { "description": { "$match": "gaming laptop" } },
78
- { "rating": { "$exists": true, "$gt": 4 } },
79
- { "$or": [
80
- { "stock": { "$gt": 0 } },
81
- { "preorder": { "$null": false } }
82
- ]},
83
- { "$not": { "status": "discontinued" } }
84
- ]
85
- }`;
@@ -1,9 +0,0 @@
1
- {
2
- "extends": ["./tsconfig.json", "../../tsconfig.build.json"],
3
- "compilerOptions": {
4
- "outDir": "./dist",
5
- "rootDir": "./src"
6
- },
7
- "include": ["src/**/*"],
8
- "exclude": ["node_modules", "**/*.test.ts", "src/**/*.mock.ts"]
9
- }
package/tsconfig.json DELETED
@@ -1,5 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.node.json",
3
- "include": ["src/**/*", "tsup.config.ts"],
4
- "exclude": ["node_modules", "**/*.test.ts"]
5
- }
package/tsup.config.ts DELETED
@@ -1,17 +0,0 @@
1
- import { generateTypes } from '@internal/types-builder';
2
- import { defineConfig } from 'tsup';
3
-
4
- export default defineConfig({
5
- entry: ['src/index.ts'],
6
- format: ['esm', 'cjs'],
7
- clean: true,
8
- dts: false,
9
- splitting: true,
10
- treeshake: {
11
- preset: 'smallest',
12
- },
13
- sourcemap: true,
14
- onSuccess: async () => {
15
- await generateTypes(process.cwd());
16
- },
17
- });
package/vitest.config.ts DELETED
@@ -1,11 +0,0 @@
1
- import { defineConfig } from 'vitest/config';
2
-
3
- export default defineConfig({
4
- test: {
5
- environment: 'node',
6
- include: ['src/**/*.test.ts'],
7
- coverage: {
8
- reporter: ['text', 'json', 'html'],
9
- },
10
- },
11
- });