@mastra/qdrant 0.1.0-alpha.36 → 0.1.0-alpha.38
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 +14 -0
- package/dist/_tsup-dts-rollup.d.ts +58 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +334 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @mastra/qdrant
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.38
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [4534e77]
|
|
8
|
+
- @mastra/core@0.2.0-alpha.103
|
|
9
|
+
|
|
10
|
+
## 0.1.0-alpha.37
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Updated dependencies [a9345f9]
|
|
15
|
+
- @mastra/core@0.2.0-alpha.102
|
|
16
|
+
|
|
3
17
|
## 0.1.0-alpha.36
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { BaseFilterTranslator } from '@mastra/core/filter';
|
|
2
|
+
import { Filter } from '@mastra/core/filter';
|
|
3
|
+
import { IndexStats } from '@mastra/core/vector';
|
|
4
|
+
import { LogicalOperator } from '@mastra/core/filter';
|
|
5
|
+
import { MastraVector } from '@mastra/core/vector';
|
|
6
|
+
import { OperatorSupport } from '@mastra/core/filter';
|
|
7
|
+
import { QueryResult } from '@mastra/core/vector';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Translates MongoDB-style filters to Qdrant compatible filters.
|
|
11
|
+
*
|
|
12
|
+
* Key transformations:
|
|
13
|
+
* - $and -> must
|
|
14
|
+
* - $or -> should
|
|
15
|
+
* - $not -> must_not
|
|
16
|
+
* - { field: { $op: value } } -> { key: field, match/range: { value/gt/lt: value } }
|
|
17
|
+
*
|
|
18
|
+
* Custom operators (Qdrant-specific):
|
|
19
|
+
* - $count -> values_count (array length/value count)
|
|
20
|
+
* - $geo -> geo filters (box, radius, polygon)
|
|
21
|
+
* - $hasId -> has_id filter
|
|
22
|
+
* - $nested -> nested object filters
|
|
23
|
+
* - $hasVector -> vector existence check
|
|
24
|
+
* - $datetime -> RFC 3339 datetime range
|
|
25
|
+
* - $null -> is_null check
|
|
26
|
+
* - $empty -> is_empty check
|
|
27
|
+
*/
|
|
28
|
+
export declare class QdrantFilterTranslator extends BaseFilterTranslator {
|
|
29
|
+
protected isLogicalOperator(key: string): key is LogicalOperator;
|
|
30
|
+
protected getSupportedOperators(): OperatorSupport;
|
|
31
|
+
translate(filter?: Filter): Filter | undefined;
|
|
32
|
+
private createCondition;
|
|
33
|
+
private translateNode;
|
|
34
|
+
private buildFinalConditions;
|
|
35
|
+
private handleLogicalOperators;
|
|
36
|
+
private handleFieldConditions;
|
|
37
|
+
private translateCustomOperator;
|
|
38
|
+
private getQdrantLogicalOp;
|
|
39
|
+
private translateOperatorValue;
|
|
40
|
+
private translateGeoFilter;
|
|
41
|
+
private normalizeDatetimeRange;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
declare class QdrantVector extends MastraVector {
|
|
45
|
+
private client;
|
|
46
|
+
constructor(url: string, apiKey?: string, https?: boolean);
|
|
47
|
+
upsert(indexName: string, vectors: number[][], metadata?: Record<string, any>[], ids?: string[]): Promise<string[]>;
|
|
48
|
+
createIndex(indexName: string, dimension: number, metric?: 'cosine' | 'euclidean' | 'dotproduct'): Promise<void>;
|
|
49
|
+
transformFilter(filter?: Filter): Filter | undefined;
|
|
50
|
+
query(indexName: string, queryVector: number[], topK?: number, filter?: Filter, includeVector?: boolean): Promise<QueryResult[]>;
|
|
51
|
+
listIndexes(): Promise<string[]>;
|
|
52
|
+
describeIndex(indexName: string): Promise<IndexStats>;
|
|
53
|
+
deleteIndex(indexName: string): Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
export { QdrantVector }
|
|
56
|
+
export { QdrantVector as QdrantVector_alias_1 }
|
|
57
|
+
|
|
58
|
+
export { }
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { QdrantVector } from './_tsup-dts-rollup.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { BaseFilterTranslator } from '@mastra/core/filter';
|
|
2
|
+
import { MastraVector } from '@mastra/core/vector';
|
|
3
|
+
import { QdrantClient } from '@qdrant/js-client-rest';
|
|
4
|
+
|
|
5
|
+
// src/vector/index.ts
|
|
6
|
+
var QdrantFilterTranslator = class extends BaseFilterTranslator {
|
|
7
|
+
isLogicalOperator(key) {
|
|
8
|
+
return super.isLogicalOperator(key) || key === "$hasId" || key === "$hasVector";
|
|
9
|
+
}
|
|
10
|
+
getSupportedOperators() {
|
|
11
|
+
return {
|
|
12
|
+
...BaseFilterTranslator.DEFAULT_OPERATORS,
|
|
13
|
+
logical: ["$and", "$or", "$not"],
|
|
14
|
+
array: ["$in", "$nin"],
|
|
15
|
+
regex: ["$regex"],
|
|
16
|
+
custom: ["$count", "$geo", "$nested", "$datetime", "$null", "$empty", "$hasId", "$hasVector"]
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
translate(filter) {
|
|
20
|
+
if (this.isEmpty(filter)) return filter;
|
|
21
|
+
this.validateFilter(filter);
|
|
22
|
+
return this.translateNode(filter);
|
|
23
|
+
}
|
|
24
|
+
createCondition(type, value, fieldKey) {
|
|
25
|
+
const condition = { [type]: value };
|
|
26
|
+
return fieldKey ? { key: fieldKey, ...condition } : condition;
|
|
27
|
+
}
|
|
28
|
+
translateNode(node, isNested = false, fieldKey) {
|
|
29
|
+
if (!this.isEmpty(node) && typeof node === "object" && "must" in node) {
|
|
30
|
+
return node;
|
|
31
|
+
}
|
|
32
|
+
if (this.isPrimitive(node)) {
|
|
33
|
+
if (node === null) {
|
|
34
|
+
return { is_null: { key: fieldKey } };
|
|
35
|
+
}
|
|
36
|
+
return this.createCondition("match", { value: this.normalizeComparisonValue(node) }, fieldKey);
|
|
37
|
+
}
|
|
38
|
+
if (this.isRegex(node)) {
|
|
39
|
+
throw new Error("Direct regex pattern format is not supported in Qdrant");
|
|
40
|
+
}
|
|
41
|
+
if (Array.isArray(node)) {
|
|
42
|
+
return node.length === 0 ? { is_empty: { key: fieldKey } } : this.createCondition("match", { any: this.normalizeArrayValues(node) }, fieldKey);
|
|
43
|
+
}
|
|
44
|
+
const entries = Object.entries(node);
|
|
45
|
+
const logicalResult = this.handleLogicalOperators(entries, isNested);
|
|
46
|
+
if (logicalResult) {
|
|
47
|
+
return logicalResult;
|
|
48
|
+
}
|
|
49
|
+
const { conditions, range, matchCondition } = this.handleFieldConditions(entries, fieldKey);
|
|
50
|
+
if (Object.keys(range).length > 0) {
|
|
51
|
+
conditions.push({ key: fieldKey, range });
|
|
52
|
+
}
|
|
53
|
+
if (matchCondition) {
|
|
54
|
+
conditions.push({ key: fieldKey, match: matchCondition });
|
|
55
|
+
}
|
|
56
|
+
return this.buildFinalConditions(conditions, isNested);
|
|
57
|
+
}
|
|
58
|
+
buildFinalConditions(conditions, isNested) {
|
|
59
|
+
if (conditions.length === 0) {
|
|
60
|
+
return {};
|
|
61
|
+
} else if (conditions.length === 1 && isNested) {
|
|
62
|
+
return conditions[0];
|
|
63
|
+
} else {
|
|
64
|
+
return { must: conditions };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
handleLogicalOperators(entries, isNested) {
|
|
68
|
+
const firstKey = entries[0]?.[0];
|
|
69
|
+
if (firstKey && this.isLogicalOperator(firstKey) && !this.isCustomOperator(firstKey)) {
|
|
70
|
+
const [key, value] = entries[0];
|
|
71
|
+
const qdrantOp = this.getQdrantLogicalOp(key);
|
|
72
|
+
return {
|
|
73
|
+
[qdrantOp]: Array.isArray(value) ? value.map((v) => this.translateNode(v, true)) : [this.translateNode(value, true)]
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (entries.length > 1 && !isNested && entries.every(([key]) => !this.isOperator(key) && !this.isCustomOperator(key))) {
|
|
77
|
+
return {
|
|
78
|
+
must: entries.map(([key, value]) => this.translateNode(value, true, key))
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
handleFieldConditions(entries, fieldKey) {
|
|
84
|
+
const conditions = [];
|
|
85
|
+
let range = {};
|
|
86
|
+
let matchCondition = null;
|
|
87
|
+
for (const [key, value] of entries) {
|
|
88
|
+
if (this.isCustomOperator(key)) {
|
|
89
|
+
const customOp = this.translateCustomOperator(key, value, fieldKey);
|
|
90
|
+
conditions.push(customOp);
|
|
91
|
+
} else if (this.isOperator(key)) {
|
|
92
|
+
const opResult = this.translateOperatorValue(key, value);
|
|
93
|
+
if (opResult.range) {
|
|
94
|
+
Object.assign(range, opResult.range);
|
|
95
|
+
} else {
|
|
96
|
+
matchCondition = opResult;
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
const nestedKey = fieldKey ? `${fieldKey}.${key}` : key;
|
|
100
|
+
const nestedCondition = this.translateNode(value, true, nestedKey);
|
|
101
|
+
if (nestedCondition.must) {
|
|
102
|
+
conditions.push(...nestedCondition.must);
|
|
103
|
+
} else if (!this.isEmpty(nestedCondition)) {
|
|
104
|
+
conditions.push(nestedCondition);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { conditions, range, matchCondition };
|
|
109
|
+
}
|
|
110
|
+
translateCustomOperator(op, value, fieldKey) {
|
|
111
|
+
switch (op) {
|
|
112
|
+
case "$count":
|
|
113
|
+
const countConditions = Object.entries(value).reduce(
|
|
114
|
+
(acc, [k, v]) => ({
|
|
115
|
+
...acc,
|
|
116
|
+
[k.replace("$", "")]: v
|
|
117
|
+
}),
|
|
118
|
+
{}
|
|
119
|
+
);
|
|
120
|
+
return { key: fieldKey, values_count: countConditions };
|
|
121
|
+
case "$geo":
|
|
122
|
+
const geoOp = this.translateGeoFilter(value.type, value);
|
|
123
|
+
return { key: fieldKey, ...geoOp };
|
|
124
|
+
case "$hasId":
|
|
125
|
+
return { has_id: Array.isArray(value) ? value : [value] };
|
|
126
|
+
case "$nested":
|
|
127
|
+
return {
|
|
128
|
+
nested: {
|
|
129
|
+
key: fieldKey,
|
|
130
|
+
filter: this.translateNode(value)
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
case "$hasVector":
|
|
134
|
+
return { has_vector: value };
|
|
135
|
+
case "$datetime":
|
|
136
|
+
return {
|
|
137
|
+
key: fieldKey,
|
|
138
|
+
range: this.normalizeDatetimeRange(value.range)
|
|
139
|
+
};
|
|
140
|
+
case "$null":
|
|
141
|
+
return { is_null: { key: fieldKey } };
|
|
142
|
+
case "$empty":
|
|
143
|
+
return { is_empty: { key: fieldKey } };
|
|
144
|
+
default:
|
|
145
|
+
throw new Error(`Unsupported custom operator: ${op}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
getQdrantLogicalOp(op) {
|
|
149
|
+
switch (op) {
|
|
150
|
+
case "$and":
|
|
151
|
+
return "must";
|
|
152
|
+
case "$or":
|
|
153
|
+
return "should";
|
|
154
|
+
case "$not":
|
|
155
|
+
return "must_not";
|
|
156
|
+
default:
|
|
157
|
+
throw new Error(`Unsupported logical operator: ${op}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
translateOperatorValue(operator, value) {
|
|
161
|
+
const normalizedValue = this.normalizeComparisonValue(value);
|
|
162
|
+
switch (operator) {
|
|
163
|
+
case "$eq":
|
|
164
|
+
return { value: normalizedValue };
|
|
165
|
+
case "$ne":
|
|
166
|
+
return { except: [normalizedValue] };
|
|
167
|
+
case "$gt":
|
|
168
|
+
return { range: { gt: normalizedValue } };
|
|
169
|
+
case "$gte":
|
|
170
|
+
return { range: { gte: normalizedValue } };
|
|
171
|
+
case "$lt":
|
|
172
|
+
return { range: { lt: normalizedValue } };
|
|
173
|
+
case "$lte":
|
|
174
|
+
return { range: { lte: normalizedValue } };
|
|
175
|
+
case "$in":
|
|
176
|
+
return { any: this.normalizeArrayValues(value) };
|
|
177
|
+
case "$nin":
|
|
178
|
+
return { except: this.normalizeArrayValues(value) };
|
|
179
|
+
case "$regex":
|
|
180
|
+
return { text: value };
|
|
181
|
+
case "exists":
|
|
182
|
+
return value ? {
|
|
183
|
+
must_not: [{ is_null: { key: value } }, { is_empty: { key: value } }]
|
|
184
|
+
} : {
|
|
185
|
+
is_empty: { key: value }
|
|
186
|
+
};
|
|
187
|
+
default:
|
|
188
|
+
throw new Error(`Unsupported operator: ${operator}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
translateGeoFilter(type, value) {
|
|
192
|
+
switch (type) {
|
|
193
|
+
case "box":
|
|
194
|
+
return {
|
|
195
|
+
geo_bounding_box: {
|
|
196
|
+
top_left: value.top_left,
|
|
197
|
+
bottom_right: value.bottom_right
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
case "radius":
|
|
201
|
+
return {
|
|
202
|
+
geo_radius: {
|
|
203
|
+
center: value.center,
|
|
204
|
+
radius: value.radius
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
case "polygon":
|
|
208
|
+
return {
|
|
209
|
+
geo_polygon: {
|
|
210
|
+
exterior: value.exterior,
|
|
211
|
+
interiors: value.interiors
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
default:
|
|
215
|
+
throw new Error(`Unsupported geo filter type: ${type}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
normalizeDatetimeRange(value) {
|
|
219
|
+
const range = {};
|
|
220
|
+
for (const [op, val] of Object.entries(value)) {
|
|
221
|
+
if (val instanceof Date) {
|
|
222
|
+
range[op] = val.toISOString();
|
|
223
|
+
} else if (typeof val === "string") {
|
|
224
|
+
range[op] = val;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return range;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// src/vector/index.ts
|
|
232
|
+
var BATCH_SIZE = 256;
|
|
233
|
+
var DISTANCE_MAPPING = {
|
|
234
|
+
cosine: "Cosine",
|
|
235
|
+
euclidean: "Euclid",
|
|
236
|
+
dotproduct: "Dot"
|
|
237
|
+
};
|
|
238
|
+
var QdrantVector = class extends MastraVector {
|
|
239
|
+
client;
|
|
240
|
+
constructor(url, apiKey, https) {
|
|
241
|
+
super();
|
|
242
|
+
const baseClient = new QdrantClient({
|
|
243
|
+
url,
|
|
244
|
+
apiKey,
|
|
245
|
+
https
|
|
246
|
+
});
|
|
247
|
+
const telemetry = this.__getTelemetry();
|
|
248
|
+
this.client = telemetry?.traceClass(baseClient, {
|
|
249
|
+
spanNamePrefix: "qdrant-vector",
|
|
250
|
+
attributes: {
|
|
251
|
+
"vector.type": "qdrant"
|
|
252
|
+
}
|
|
253
|
+
}) ?? baseClient;
|
|
254
|
+
}
|
|
255
|
+
async upsert(indexName, vectors, metadata, ids) {
|
|
256
|
+
const pointIds = ids || vectors.map(() => crypto.randomUUID());
|
|
257
|
+
const records = vectors.map((vector, i) => ({
|
|
258
|
+
id: pointIds[i],
|
|
259
|
+
vector,
|
|
260
|
+
payload: metadata?.[i] || {}
|
|
261
|
+
}));
|
|
262
|
+
for (let i = 0; i < records.length; i += BATCH_SIZE) {
|
|
263
|
+
const batch = records.slice(i, i + BATCH_SIZE);
|
|
264
|
+
await this.client.upsert(indexName, {
|
|
265
|
+
// @ts-expect-error
|
|
266
|
+
points: batch,
|
|
267
|
+
wait: true
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return pointIds;
|
|
271
|
+
}
|
|
272
|
+
async createIndex(indexName, dimension, metric = "cosine") {
|
|
273
|
+
if (!Number.isInteger(dimension) || dimension <= 0) {
|
|
274
|
+
throw new Error("Dimension must be a positive integer");
|
|
275
|
+
}
|
|
276
|
+
await this.client.createCollection(indexName, {
|
|
277
|
+
vectors: {
|
|
278
|
+
// @ts-expect-error
|
|
279
|
+
size: dimension,
|
|
280
|
+
// @ts-expect-error
|
|
281
|
+
distance: DISTANCE_MAPPING[metric]
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
transformFilter(filter) {
|
|
286
|
+
const translator = new QdrantFilterTranslator();
|
|
287
|
+
return translator.translate(filter);
|
|
288
|
+
}
|
|
289
|
+
async query(indexName, queryVector, topK = 10, filter, includeVector = false) {
|
|
290
|
+
const translatedFilter = this.transformFilter(filter);
|
|
291
|
+
const results = (await this.client.query(indexName, {
|
|
292
|
+
query: queryVector,
|
|
293
|
+
limit: topK,
|
|
294
|
+
filter: translatedFilter,
|
|
295
|
+
with_payload: true,
|
|
296
|
+
with_vector: includeVector
|
|
297
|
+
})).points;
|
|
298
|
+
return results.map((match) => {
|
|
299
|
+
let vector = [];
|
|
300
|
+
if (includeVector) {
|
|
301
|
+
if (Array.isArray(match.vector)) {
|
|
302
|
+
vector = match.vector;
|
|
303
|
+
} else if (typeof match.vector === "object" && match.vector !== null) {
|
|
304
|
+
vector = Object.values(match.vector).filter((v) => typeof v === "number");
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
id: match.id,
|
|
309
|
+
score: match.score || 0,
|
|
310
|
+
metadata: match.payload,
|
|
311
|
+
...includeVector && { vector }
|
|
312
|
+
};
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
async listIndexes() {
|
|
316
|
+
const response = await this.client.getCollections();
|
|
317
|
+
return response.collections.map((collection) => collection.name) || [];
|
|
318
|
+
}
|
|
319
|
+
async describeIndex(indexName) {
|
|
320
|
+
const { config, points_count } = await this.client.getCollection(indexName);
|
|
321
|
+
const distance = config.params.vectors?.distance;
|
|
322
|
+
return {
|
|
323
|
+
dimension: config.params.vectors?.size,
|
|
324
|
+
count: points_count || 0,
|
|
325
|
+
// @ts-expect-error
|
|
326
|
+
metric: Object.keys(DISTANCE_MAPPING).find((key) => DISTANCE_MAPPING[key] === distance)
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
async deleteIndex(indexName) {
|
|
330
|
+
await this.client.deleteCollection(indexName);
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
export { QdrantVector };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mastra/qdrant",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.38",
|
|
4
4
|
"description": "Qdrant vector store provider for Mastra",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@qdrant/js-client-rest": "^1.12.0",
|
|
19
|
-
"@mastra/core": "^0.2.0-alpha.
|
|
19
|
+
"@mastra/core": "^0.2.0-alpha.103"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@microsoft/api-extractor": "^7.49.2",
|