@mastra/elasticsearch 0.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/CHANGELOG.md +11 -0
- package/README.md +126 -0
- package/dist/index.cjs +1039 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +122 -0
- package/dist/index.d.ts +122 -0
- package/dist/index.js +1037 -0
- package/dist/index.js.map +1 -0
- package/package.json +68 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1039 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var elasticsearch = require('@elastic/elasticsearch');
|
|
4
|
+
var error = require('@mastra/core/error');
|
|
5
|
+
var vector = require('@mastra/core/vector');
|
|
6
|
+
var filter = require('@mastra/core/vector/filter');
|
|
7
|
+
|
|
8
|
+
// src/vector/index.ts
|
|
9
|
+
var ElasticSearchFilterTranslator = class extends filter.BaseFilterTranslator {
|
|
10
|
+
getSupportedOperators() {
|
|
11
|
+
return {
|
|
12
|
+
...filter.BaseFilterTranslator.DEFAULT_OPERATORS,
|
|
13
|
+
logical: ["$and", "$or", "$not"],
|
|
14
|
+
array: ["$in", "$nin", "$all"],
|
|
15
|
+
regex: ["$regex"],
|
|
16
|
+
custom: []
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
translate(filter) {
|
|
20
|
+
if (this.isEmpty(filter)) return void 0;
|
|
21
|
+
this.validateFilter(filter);
|
|
22
|
+
return this.translateNode(filter);
|
|
23
|
+
}
|
|
24
|
+
translateNode(node) {
|
|
25
|
+
if (this.isPrimitive(node) || Array.isArray(node)) {
|
|
26
|
+
return node;
|
|
27
|
+
}
|
|
28
|
+
const entries = Object.entries(node);
|
|
29
|
+
const logicalOperators = [];
|
|
30
|
+
const fieldConditions = [];
|
|
31
|
+
entries.forEach(([key, value]) => {
|
|
32
|
+
if (this.isLogicalOperator(key)) {
|
|
33
|
+
logicalOperators.push([key, value]);
|
|
34
|
+
} else {
|
|
35
|
+
fieldConditions.push([key, value]);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
if (logicalOperators.length === 1 && fieldConditions.length === 0) {
|
|
39
|
+
const [operator, value] = logicalOperators[0];
|
|
40
|
+
if (!Array.isArray(value) && typeof value !== "object") {
|
|
41
|
+
throw new Error(`Invalid logical operator structure: ${operator} must have an array or object value`);
|
|
42
|
+
}
|
|
43
|
+
return this.translateLogicalOperator(operator, value);
|
|
44
|
+
}
|
|
45
|
+
const fieldConditionQueries = fieldConditions.map(([key, value]) => {
|
|
46
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
47
|
+
const hasOperators = Object.keys(value).some((k) => this.isOperator(k));
|
|
48
|
+
const nestedField = `metadata.${key}`;
|
|
49
|
+
return hasOperators ? this.translateFieldConditions(nestedField, value) : this.translateNestedObject(nestedField, value);
|
|
50
|
+
}
|
|
51
|
+
if (Array.isArray(value)) {
|
|
52
|
+
const fieldWithKeyword2 = this.addKeywordIfNeeded(`metadata.${key}`, value);
|
|
53
|
+
return { terms: { [fieldWithKeyword2]: value } };
|
|
54
|
+
}
|
|
55
|
+
const fieldWithKeyword = this.addKeywordIfNeeded(`metadata.${key}`, value);
|
|
56
|
+
return { term: { [fieldWithKeyword]: value } };
|
|
57
|
+
});
|
|
58
|
+
if (logicalOperators.length > 0) {
|
|
59
|
+
const logicalConditions = logicalOperators.map(
|
|
60
|
+
([operator, value]) => this.translateOperator(operator, value)
|
|
61
|
+
);
|
|
62
|
+
return {
|
|
63
|
+
bool: {
|
|
64
|
+
must: [...logicalConditions, ...fieldConditionQueries]
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (fieldConditionQueries.length > 1) {
|
|
69
|
+
return {
|
|
70
|
+
bool: {
|
|
71
|
+
must: fieldConditionQueries
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (fieldConditionQueries.length === 1) {
|
|
76
|
+
return fieldConditionQueries[0];
|
|
77
|
+
}
|
|
78
|
+
return { match_all: {} };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Handles translation of nested objects with dot notation fields
|
|
82
|
+
*/
|
|
83
|
+
translateNestedObject(field, value) {
|
|
84
|
+
const conditions = Object.entries(value).map(([subField, subValue]) => {
|
|
85
|
+
const fullField = `${field}.${subField}`;
|
|
86
|
+
if (this.isOperator(subField)) {
|
|
87
|
+
return this.translateOperator(subField, subValue, field);
|
|
88
|
+
}
|
|
89
|
+
if (typeof subValue === "object" && subValue !== null && !Array.isArray(subValue)) {
|
|
90
|
+
const hasOperators = Object.keys(subValue).some((k) => this.isOperator(k));
|
|
91
|
+
if (hasOperators) {
|
|
92
|
+
return this.translateFieldConditions(fullField, subValue);
|
|
93
|
+
}
|
|
94
|
+
return this.translateNestedObject(fullField, subValue);
|
|
95
|
+
}
|
|
96
|
+
const fieldWithKeyword = this.addKeywordIfNeeded(fullField, subValue);
|
|
97
|
+
return { term: { [fieldWithKeyword]: subValue } };
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
bool: {
|
|
101
|
+
must: conditions
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
translateLogicalOperator(operator, value) {
|
|
106
|
+
const conditions = Array.isArray(value) ? value.map((item) => this.translateNode(item)) : [this.translateNode(value)];
|
|
107
|
+
switch (operator) {
|
|
108
|
+
case "$and":
|
|
109
|
+
if (Array.isArray(value) && value.length === 0) {
|
|
110
|
+
return { match_all: {} };
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
bool: {
|
|
114
|
+
must: conditions
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
case "$or":
|
|
118
|
+
if (Array.isArray(value) && value.length === 0) {
|
|
119
|
+
return {
|
|
120
|
+
bool: {
|
|
121
|
+
must_not: [{ match_all: {} }]
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
bool: {
|
|
127
|
+
should: conditions,
|
|
128
|
+
minimum_should_match: 1
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
case "$not":
|
|
132
|
+
return {
|
|
133
|
+
bool: {
|
|
134
|
+
must_not: conditions
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
default:
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
translateFieldOperator(field, operator, value) {
|
|
142
|
+
if (this.isBasicOperator(operator)) {
|
|
143
|
+
const normalizedValue = this.normalizeComparisonValue(value);
|
|
144
|
+
const fieldWithKeyword2 = this.addKeywordIfNeeded(field, value);
|
|
145
|
+
switch (operator) {
|
|
146
|
+
case "$eq":
|
|
147
|
+
return { term: { [fieldWithKeyword2]: normalizedValue } };
|
|
148
|
+
case "$ne":
|
|
149
|
+
return {
|
|
150
|
+
bool: {
|
|
151
|
+
must_not: [{ term: { [fieldWithKeyword2]: normalizedValue } }]
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
default:
|
|
155
|
+
return { term: { [fieldWithKeyword2]: normalizedValue } };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (this.isNumericOperator(operator)) {
|
|
159
|
+
const normalizedValue = this.normalizeComparisonValue(value);
|
|
160
|
+
const rangeOp = operator.replace("$", "");
|
|
161
|
+
return { range: { [field]: { [rangeOp]: normalizedValue } } };
|
|
162
|
+
}
|
|
163
|
+
if (this.isArrayOperator(operator)) {
|
|
164
|
+
if (!Array.isArray(value)) {
|
|
165
|
+
throw new Error(`Invalid array operator value: ${operator} requires an array value`);
|
|
166
|
+
}
|
|
167
|
+
const normalizedValues = this.normalizeArrayValues(value);
|
|
168
|
+
const fieldWithKeyword2 = this.addKeywordIfNeeded(field, value);
|
|
169
|
+
switch (operator) {
|
|
170
|
+
case "$in":
|
|
171
|
+
return { terms: { [fieldWithKeyword2]: normalizedValues } };
|
|
172
|
+
case "$nin":
|
|
173
|
+
if (normalizedValues.length === 0) {
|
|
174
|
+
return { match_all: {} };
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
bool: {
|
|
178
|
+
must_not: [{ terms: { [fieldWithKeyword2]: normalizedValues } }]
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
case "$all":
|
|
182
|
+
if (normalizedValues.length === 0) {
|
|
183
|
+
return {
|
|
184
|
+
bool: {
|
|
185
|
+
must_not: [{ match_all: {} }]
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
bool: {
|
|
191
|
+
must: normalizedValues.map((v) => ({ term: { [fieldWithKeyword2]: v } }))
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
default:
|
|
195
|
+
return { terms: { [fieldWithKeyword2]: normalizedValues } };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (this.isElementOperator(operator)) {
|
|
199
|
+
switch (operator) {
|
|
200
|
+
case "$exists":
|
|
201
|
+
return value ? { exists: { field } } : { bool: { must_not: [{ exists: { field } }] } };
|
|
202
|
+
default:
|
|
203
|
+
return { exists: { field } };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (this.isRegexOperator(operator)) {
|
|
207
|
+
return this.translateRegexOperator(field, value);
|
|
208
|
+
}
|
|
209
|
+
const fieldWithKeyword = this.addKeywordIfNeeded(field, value);
|
|
210
|
+
return { term: { [fieldWithKeyword]: value } };
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Escapes wildcard metacharacters (* and ?) for use in wildcard queries.
|
|
214
|
+
* Existing wildcard metacharacters in the pattern are escaped before
|
|
215
|
+
* adding leading/trailing * to prevent semantic changes.
|
|
216
|
+
* First escapes backslashes to avoid ambiguous encoding sequences.
|
|
217
|
+
*/
|
|
218
|
+
escapeWildcardMetacharacters(pattern) {
|
|
219
|
+
return pattern.replace(/\\/g, "\\\\").replace(/\*/g, "\\*").replace(/\?/g, "\\?");
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Translates regex patterns to ElasticSearch query syntax
|
|
223
|
+
*/
|
|
224
|
+
translateRegexOperator(field, value) {
|
|
225
|
+
const regexValue = typeof value === "string" ? value : value.toString();
|
|
226
|
+
let processedRegex = regexValue;
|
|
227
|
+
const hasStartAnchor = regexValue.startsWith("^");
|
|
228
|
+
const hasEndAnchor = regexValue.endsWith("$");
|
|
229
|
+
if (hasStartAnchor || hasEndAnchor) {
|
|
230
|
+
if (hasStartAnchor) {
|
|
231
|
+
processedRegex = processedRegex.substring(1);
|
|
232
|
+
}
|
|
233
|
+
if (hasEndAnchor) {
|
|
234
|
+
processedRegex = processedRegex.substring(0, processedRegex.length - 1);
|
|
235
|
+
}
|
|
236
|
+
const escapedPattern = this.escapeWildcardMetacharacters(processedRegex);
|
|
237
|
+
let wildcardPattern = escapedPattern;
|
|
238
|
+
if (!hasStartAnchor) {
|
|
239
|
+
wildcardPattern = "*" + wildcardPattern;
|
|
240
|
+
}
|
|
241
|
+
if (!hasEndAnchor) {
|
|
242
|
+
wildcardPattern = wildcardPattern + "*";
|
|
243
|
+
}
|
|
244
|
+
return { wildcard: { [field]: wildcardPattern } };
|
|
245
|
+
}
|
|
246
|
+
return { regexp: { [field]: regexValue } };
|
|
247
|
+
}
|
|
248
|
+
addKeywordIfNeeded(field, value) {
|
|
249
|
+
if (typeof value === "string") {
|
|
250
|
+
return `${field}.keyword`;
|
|
251
|
+
}
|
|
252
|
+
if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
|
|
253
|
+
return `${field}.keyword`;
|
|
254
|
+
}
|
|
255
|
+
return field;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Helper method to handle special cases for the $not operator
|
|
259
|
+
*/
|
|
260
|
+
handleNotOperatorSpecialCases(value, field) {
|
|
261
|
+
if (value === null) {
|
|
262
|
+
return { exists: { field } };
|
|
263
|
+
}
|
|
264
|
+
if (typeof value === "object" && value !== null) {
|
|
265
|
+
if ("$eq" in value && value.$eq === null) {
|
|
266
|
+
return { exists: { field } };
|
|
267
|
+
}
|
|
268
|
+
if ("$ne" in value && value.$ne === null) {
|
|
269
|
+
return {
|
|
270
|
+
bool: {
|
|
271
|
+
must_not: [{ exists: { field } }]
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
translateOperator(operator, value, field) {
|
|
279
|
+
if (!this.isOperator(operator)) {
|
|
280
|
+
throw new Error(`Unsupported operator: ${operator}`);
|
|
281
|
+
}
|
|
282
|
+
if (operator === "$not" && field) {
|
|
283
|
+
const specialCaseResult = this.handleNotOperatorSpecialCases(value, field);
|
|
284
|
+
if (specialCaseResult) {
|
|
285
|
+
return specialCaseResult;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (this.isLogicalOperator(operator)) {
|
|
289
|
+
if (operator === "$not" && field && typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
290
|
+
const entries = Object.entries(value);
|
|
291
|
+
if (entries.length > 0) {
|
|
292
|
+
if (entries.every(([op]) => this.isOperator(op))) {
|
|
293
|
+
const translatedCondition = this.translateFieldConditions(field, value);
|
|
294
|
+
return {
|
|
295
|
+
bool: {
|
|
296
|
+
must_not: [translatedCondition]
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
if (entries.length === 1 && entries[0] && this.isOperator(entries[0][0])) {
|
|
301
|
+
const [nestedOp, nestedVal] = entries[0];
|
|
302
|
+
const translatedNested = this.translateFieldOperator(field, nestedOp, nestedVal);
|
|
303
|
+
return {
|
|
304
|
+
bool: {
|
|
305
|
+
must_not: [translatedNested]
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return this.translateLogicalOperator(operator, value);
|
|
312
|
+
}
|
|
313
|
+
if (field) {
|
|
314
|
+
return this.translateFieldOperator(field, operator, value);
|
|
315
|
+
}
|
|
316
|
+
return value;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Translates field conditions to ElasticSearch query syntax
|
|
320
|
+
* Handles special cases like range queries and multiple operators
|
|
321
|
+
*/
|
|
322
|
+
translateFieldConditions(field, conditions) {
|
|
323
|
+
if (this.canOptimizeToRangeQuery(conditions)) {
|
|
324
|
+
return this.createRangeQuery(field, conditions);
|
|
325
|
+
}
|
|
326
|
+
const queryConditions = [];
|
|
327
|
+
Object.entries(conditions).forEach(([operator, value]) => {
|
|
328
|
+
if (this.isOperator(operator)) {
|
|
329
|
+
queryConditions.push(this.translateOperator(operator, value, field));
|
|
330
|
+
} else {
|
|
331
|
+
const fieldWithKeyword = this.addKeywordIfNeeded(`${field}.${operator}`, value);
|
|
332
|
+
queryConditions.push({ term: { [fieldWithKeyword]: value } });
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
if (queryConditions.length === 1) {
|
|
336
|
+
return queryConditions[0];
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
bool: {
|
|
340
|
+
must: queryConditions
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Checks if conditions can be optimized to a range query
|
|
346
|
+
*/
|
|
347
|
+
canOptimizeToRangeQuery(conditions) {
|
|
348
|
+
return Object.keys(conditions).every((op) => this.isNumericOperator(op)) && Object.keys(conditions).length > 0;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Creates a range query from numeric operators
|
|
352
|
+
*/
|
|
353
|
+
createRangeQuery(field, conditions) {
|
|
354
|
+
const rangeParams = Object.fromEntries(
|
|
355
|
+
Object.entries(conditions).map(([op, val]) => [op.replace("$", ""), this.normalizeComparisonValue(val)])
|
|
356
|
+
);
|
|
357
|
+
return { range: { [field]: rangeParams } };
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
// src/vector/index.ts
|
|
362
|
+
var METRIC_MAPPING = {
|
|
363
|
+
cosine: "cosine",
|
|
364
|
+
euclidean: "l2_norm",
|
|
365
|
+
dotproduct: "dot_product"
|
|
366
|
+
};
|
|
367
|
+
var REVERSE_METRIC_MAPPING = {
|
|
368
|
+
cosine: "cosine",
|
|
369
|
+
l2_norm: "euclidean",
|
|
370
|
+
dot_product: "dotproduct"
|
|
371
|
+
};
|
|
372
|
+
var ElasticSearchVector = class extends vector.MastraVector {
|
|
373
|
+
client;
|
|
374
|
+
/**
|
|
375
|
+
* Creates a new ElasticSearchVector client.
|
|
376
|
+
*
|
|
377
|
+
* @param {string} url - The url of the ElasticSearch node.
|
|
378
|
+
*/
|
|
379
|
+
constructor({ url, id }) {
|
|
380
|
+
super({ id });
|
|
381
|
+
this.client = new elasticsearch.Client({ node: url });
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Creates a new collection with the specified configuration.
|
|
385
|
+
*
|
|
386
|
+
* @param {string} indexName - The name of the collection to create.
|
|
387
|
+
* @param {number} dimension - The dimension of the vectors to be stored in the collection.
|
|
388
|
+
* @param {'cosine' | 'euclidean' | 'dotproduct'} [metric=cosine] - The metric to use to sort vectors in the collection.
|
|
389
|
+
* @returns {Promise<void>} A promise that resolves when the collection is created.
|
|
390
|
+
*/
|
|
391
|
+
async createIndex({ indexName, dimension, metric = "cosine" }) {
|
|
392
|
+
if (!Number.isInteger(dimension) || dimension <= 0) {
|
|
393
|
+
throw new error.MastraError({
|
|
394
|
+
id: "STORAGE_ELASTICSEARCH_VECTOR_CREATE_INDEX_INVALID_ARGS",
|
|
395
|
+
domain: error.ErrorDomain.STORAGE,
|
|
396
|
+
category: error.ErrorCategory.USER,
|
|
397
|
+
text: "Dimension must be a positive integer",
|
|
398
|
+
details: { indexName, dimension }
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
try {
|
|
402
|
+
await this.client.indices.create({
|
|
403
|
+
index: indexName,
|
|
404
|
+
mappings: {
|
|
405
|
+
properties: {
|
|
406
|
+
metadata: { type: "object" },
|
|
407
|
+
id: { type: "keyword" },
|
|
408
|
+
embedding: {
|
|
409
|
+
type: "dense_vector",
|
|
410
|
+
dims: dimension,
|
|
411
|
+
index: true,
|
|
412
|
+
similarity: METRIC_MAPPING[metric]
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
} catch (error$1) {
|
|
418
|
+
const message = error$1?.message || error$1?.toString();
|
|
419
|
+
if (message && message.toLowerCase().includes("already exists")) {
|
|
420
|
+
await this.validateExistingIndex(indexName, dimension, metric);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
throw new error.MastraError(
|
|
424
|
+
{
|
|
425
|
+
id: "STORAGE_ELASTICSEARCH_VECTOR_CREATE_INDEX_FAILED",
|
|
426
|
+
domain: error.ErrorDomain.STORAGE,
|
|
427
|
+
category: error.ErrorCategory.THIRD_PARTY,
|
|
428
|
+
details: { indexName, dimension, metric }
|
|
429
|
+
},
|
|
430
|
+
error$1
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Lists all indexes.
|
|
436
|
+
*
|
|
437
|
+
* @returns {Promise<string[]>} A promise that resolves to an array of indexes.
|
|
438
|
+
*/
|
|
439
|
+
async listIndexes() {
|
|
440
|
+
try {
|
|
441
|
+
const response = await this.client.cat.indices({ format: "json" });
|
|
442
|
+
const indexes = response.map((record) => record.index).filter((index) => index !== void 0 && !index.startsWith("."));
|
|
443
|
+
return indexes;
|
|
444
|
+
} catch (error$1) {
|
|
445
|
+
throw new error.MastraError(
|
|
446
|
+
{
|
|
447
|
+
id: "STORAGE_ELASTICSEARCH_VECTOR_LIST_INDEXES_FAILED",
|
|
448
|
+
domain: error.ErrorDomain.STORAGE,
|
|
449
|
+
category: error.ErrorCategory.THIRD_PARTY
|
|
450
|
+
},
|
|
451
|
+
error$1
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Validates that an existing index matches the requested dimension and metric.
|
|
457
|
+
* Throws an error if there's a mismatch, otherwise allows idempotent creation.
|
|
458
|
+
*/
|
|
459
|
+
async validateExistingIndex(indexName, dimension, metric) {
|
|
460
|
+
let info;
|
|
461
|
+
try {
|
|
462
|
+
info = await this.describeIndex({ indexName });
|
|
463
|
+
} catch (infoError) {
|
|
464
|
+
const mastraError = new error.MastraError(
|
|
465
|
+
{
|
|
466
|
+
id: "STORAGE_ELASTICSEARCH_VECTOR_VALIDATE_INDEX_FETCH_FAILED",
|
|
467
|
+
text: `Index "${indexName}" already exists, but failed to fetch index info for dimension check.`,
|
|
468
|
+
domain: error.ErrorDomain.STORAGE,
|
|
469
|
+
category: error.ErrorCategory.SYSTEM,
|
|
470
|
+
details: { indexName }
|
|
471
|
+
},
|
|
472
|
+
infoError
|
|
473
|
+
);
|
|
474
|
+
this.logger?.trackException(mastraError);
|
|
475
|
+
this.logger?.error(mastraError.toString());
|
|
476
|
+
throw mastraError;
|
|
477
|
+
}
|
|
478
|
+
const existingDim = info?.dimension;
|
|
479
|
+
const existingMetric = info?.metric;
|
|
480
|
+
if (existingDim === dimension) {
|
|
481
|
+
this.logger?.info(
|
|
482
|
+
`Index "${indexName}" already exists with ${existingDim} dimensions and metric ${existingMetric}, skipping creation.`
|
|
483
|
+
);
|
|
484
|
+
if (existingMetric !== metric) {
|
|
485
|
+
this.logger?.warn(
|
|
486
|
+
`Attempted to create index with metric "${metric}", but index already exists with metric "${existingMetric}". To use a different metric, delete and recreate the index.`
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
} else if (info) {
|
|
490
|
+
const mastraError = new error.MastraError({
|
|
491
|
+
id: "STORAGE_ELASTICSEARCH_VECTOR_VALIDATE_INDEX_DIMENSION_MISMATCH",
|
|
492
|
+
text: `Index "${indexName}" already exists with ${existingDim} dimensions, but ${dimension} dimensions were requested`,
|
|
493
|
+
domain: error.ErrorDomain.STORAGE,
|
|
494
|
+
category: error.ErrorCategory.USER,
|
|
495
|
+
details: { indexName, existingDim, requestedDim: dimension }
|
|
496
|
+
});
|
|
497
|
+
this.logger?.trackException(mastraError);
|
|
498
|
+
this.logger?.error(mastraError.toString());
|
|
499
|
+
throw mastraError;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Retrieves statistics about a vector index.
|
|
504
|
+
*
|
|
505
|
+
* @param {string} indexName - The name of the index to describe
|
|
506
|
+
* @returns A promise that resolves to the index statistics including dimension, count and metric
|
|
507
|
+
*/
|
|
508
|
+
async describeIndex({ indexName }) {
|
|
509
|
+
const indexInfo = await this.client.indices.get({ index: indexName });
|
|
510
|
+
const mappings = indexInfo[indexName]?.mappings;
|
|
511
|
+
const embedding = mappings?.properties?.embedding;
|
|
512
|
+
const similarity = embedding.similarity;
|
|
513
|
+
const countInfo = await this.client.count({ index: indexName });
|
|
514
|
+
return {
|
|
515
|
+
dimension: Number(embedding.dims),
|
|
516
|
+
count: Number(countInfo.count),
|
|
517
|
+
metric: REVERSE_METRIC_MAPPING[similarity]
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Deletes the specified index.
|
|
522
|
+
*
|
|
523
|
+
* @param {string} indexName - The name of the index to delete.
|
|
524
|
+
* @returns {Promise<void>} A promise that resolves when the index is deleted.
|
|
525
|
+
*/
|
|
526
|
+
async deleteIndex({ indexName }) {
|
|
527
|
+
try {
|
|
528
|
+
await this.client.indices.delete({ index: indexName });
|
|
529
|
+
} catch (error$1) {
|
|
530
|
+
const isIndexNotFound = error$1?.statusCode === 404 || error$1?.body?.error?.type === "index_not_found_exception" || error$1?.meta?.statusCode === 404;
|
|
531
|
+
if (isIndexNotFound) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
const mastraError = new error.MastraError(
|
|
535
|
+
{
|
|
536
|
+
id: "STORAGE_ELASTICSEARCH_VECTOR_DELETE_INDEX_FAILED",
|
|
537
|
+
domain: error.ErrorDomain.STORAGE,
|
|
538
|
+
category: error.ErrorCategory.THIRD_PARTY,
|
|
539
|
+
details: { indexName }
|
|
540
|
+
},
|
|
541
|
+
error$1
|
|
542
|
+
);
|
|
543
|
+
this.logger?.error(mastraError.toString());
|
|
544
|
+
this.logger?.trackException(mastraError);
|
|
545
|
+
throw mastraError;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Inserts or updates vectors in the specified collection.
|
|
550
|
+
*
|
|
551
|
+
* @param {string} indexName - The name of the collection to upsert into.
|
|
552
|
+
* @param {number[][]} vectors - An array of vectors to upsert.
|
|
553
|
+
* @param {Record<string, any>[]} [metadata] - An optional array of metadata objects corresponding to each vector.
|
|
554
|
+
* @param {string[]} [ids] - An optional array of IDs corresponding to each vector. If not provided, new IDs will be generated.
|
|
555
|
+
* @returns {Promise<string[]>} A promise that resolves to an array of IDs of the upserted vectors.
|
|
556
|
+
*/
|
|
557
|
+
async upsert({ indexName, vectors, metadata = [], ids }) {
|
|
558
|
+
const vectorIds = ids || vectors.map(() => crypto.randomUUID());
|
|
559
|
+
const operations = [];
|
|
560
|
+
try {
|
|
561
|
+
const indexInfo = await this.describeIndex({ indexName });
|
|
562
|
+
this.validateVectorDimensions(vectors, indexInfo.dimension);
|
|
563
|
+
for (let i = 0; i < vectors.length; i++) {
|
|
564
|
+
const operation = {
|
|
565
|
+
index: {
|
|
566
|
+
_index: indexName,
|
|
567
|
+
_id: vectorIds[i]
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
const document = {
|
|
571
|
+
id: vectorIds[i],
|
|
572
|
+
embedding: vectors[i],
|
|
573
|
+
metadata: metadata[i] || {}
|
|
574
|
+
};
|
|
575
|
+
operations.push(operation);
|
|
576
|
+
operations.push(document);
|
|
577
|
+
}
|
|
578
|
+
if (operations.length > 0) {
|
|
579
|
+
const response = await this.client.bulk({ operations, refresh: true });
|
|
580
|
+
if (response.errors) {
|
|
581
|
+
const failedItems = [];
|
|
582
|
+
const successfulIds = [];
|
|
583
|
+
for (let i = 0; i < response.items.length; i++) {
|
|
584
|
+
const item = response.items[i];
|
|
585
|
+
if (!item) continue;
|
|
586
|
+
const operationType = Object.keys(item)[0];
|
|
587
|
+
const operationResult = item[operationType];
|
|
588
|
+
if (!operationResult) continue;
|
|
589
|
+
if (operationResult.error) {
|
|
590
|
+
const operationIndex = i * 2;
|
|
591
|
+
const operationDoc = operations[operationIndex];
|
|
592
|
+
const failedId = operationDoc?.index?._id || vectorIds[i] || `unknown-${i}`;
|
|
593
|
+
failedItems.push({
|
|
594
|
+
id: failedId,
|
|
595
|
+
status: operationResult.status || 0,
|
|
596
|
+
error: operationResult.error
|
|
597
|
+
});
|
|
598
|
+
} else if (operationResult?.status && operationResult.status < 300) {
|
|
599
|
+
const operationIndex = i * 2;
|
|
600
|
+
const operationDoc = operations[operationIndex];
|
|
601
|
+
const successId = operationDoc?.index?._id || vectorIds[i];
|
|
602
|
+
if (successId) {
|
|
603
|
+
successfulIds.push(successId);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (failedItems.length > 0) {
|
|
608
|
+
const failedItemDetails = failedItems.map((item) => `${item.id}: ${item.error?.reason || item.error?.type || JSON.stringify(item.error)}`).join("; ");
|
|
609
|
+
const mastraError = new error.MastraError(
|
|
610
|
+
{
|
|
611
|
+
id: "STORAGE_ELASTICSEARCH_VECTOR_BULK_PARTIAL_FAILURE",
|
|
612
|
+
text: `Bulk upsert partially failed: ${failedItems.length} of ${response.items.length} operations failed. Failed items: ${failedItemDetails}`,
|
|
613
|
+
domain: error.ErrorDomain.STORAGE,
|
|
614
|
+
category: error.ErrorCategory.THIRD_PARTY,
|
|
615
|
+
details: {
|
|
616
|
+
indexName,
|
|
617
|
+
totalOperations: response.items.length,
|
|
618
|
+
failedCount: failedItems.length,
|
|
619
|
+
successfulCount: successfulIds.length,
|
|
620
|
+
failedItemIds: failedItems.map((item) => item.id).join(","),
|
|
621
|
+
failedItemErrors: failedItemDetails
|
|
622
|
+
}
|
|
623
|
+
},
|
|
624
|
+
new Error(`Bulk operation had ${failedItems.length} failures`)
|
|
625
|
+
);
|
|
626
|
+
this.logger?.error(mastraError.toString());
|
|
627
|
+
this.logger?.trackException(mastraError);
|
|
628
|
+
throw mastraError;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return vectorIds;
|
|
633
|
+
} catch (error$1) {
|
|
634
|
+
throw new error.MastraError(
|
|
635
|
+
{
|
|
636
|
+
id: "STORAGE_ELASTICSEARCH_VECTOR_UPSERT_FAILED",
|
|
637
|
+
domain: error.ErrorDomain.STORAGE,
|
|
638
|
+
category: error.ErrorCategory.THIRD_PARTY,
|
|
639
|
+
details: { indexName, vectorCount: vectors?.length || 0 }
|
|
640
|
+
},
|
|
641
|
+
error$1
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Queries the specified collection using a vector and optional filter.
|
|
647
|
+
*
|
|
648
|
+
* @param {string} indexName - The name of the collection to query.
|
|
649
|
+
* @param {number[]} queryVector - The vector to query with.
|
|
650
|
+
* @param {number} [topK] - The maximum number of results to return.
|
|
651
|
+
* @param {Record<string, any>} [filter] - An optional filter to apply to the query.
|
|
652
|
+
* @param {boolean} [includeVectors=false] - Whether to include the vectors in the response.
|
|
653
|
+
* @returns {Promise<QueryResult[]>} A promise that resolves to an array of query results.
|
|
654
|
+
*/
|
|
655
|
+
async query({
|
|
656
|
+
indexName,
|
|
657
|
+
queryVector,
|
|
658
|
+
filter,
|
|
659
|
+
topK = 10,
|
|
660
|
+
includeVector = false
|
|
661
|
+
}) {
|
|
662
|
+
try {
|
|
663
|
+
const translatedFilter = this.transformFilter(filter);
|
|
664
|
+
const response = await this.client.search({
|
|
665
|
+
index: indexName,
|
|
666
|
+
knn: {
|
|
667
|
+
field: "embedding",
|
|
668
|
+
query_vector: queryVector,
|
|
669
|
+
k: topK,
|
|
670
|
+
num_candidates: topK * 2,
|
|
671
|
+
...translatedFilter ? { filter: translatedFilter } : {}
|
|
672
|
+
},
|
|
673
|
+
_source: ["id", "metadata", "embedding"]
|
|
674
|
+
});
|
|
675
|
+
const results = response.hits.hits.map((hit) => {
|
|
676
|
+
const source = hit._source || {};
|
|
677
|
+
return {
|
|
678
|
+
id: String(source.id || ""),
|
|
679
|
+
score: typeof hit._score === "number" ? hit._score : 0,
|
|
680
|
+
metadata: source.metadata || {},
|
|
681
|
+
...includeVector && { vector: source.embedding }
|
|
682
|
+
};
|
|
683
|
+
});
|
|
684
|
+
return results;
|
|
685
|
+
} catch (error$1) {
|
|
686
|
+
throw new error.MastraError(
|
|
687
|
+
{
|
|
688
|
+
id: "STORAGE_ELASTICSEARCH_VECTOR_QUERY_FAILED",
|
|
689
|
+
domain: error.ErrorDomain.STORAGE,
|
|
690
|
+
category: error.ErrorCategory.THIRD_PARTY,
|
|
691
|
+
details: { indexName, topK }
|
|
692
|
+
},
|
|
693
|
+
error$1
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Validates the dimensions of the vectors.
|
|
699
|
+
*
|
|
700
|
+
* @param {number[][]} vectors - The vectors to validate.
|
|
701
|
+
* @param {number} dimension - The dimension of the vectors.
|
|
702
|
+
* @returns {void}
|
|
703
|
+
*/
|
|
704
|
+
validateVectorDimensions(vectors, dimension) {
|
|
705
|
+
if (vectors.some((vector) => vector.length !== dimension)) {
|
|
706
|
+
throw new Error("Vector dimension does not match index dimension");
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Transforms the filter to the ElasticSearch DSL.
|
|
711
|
+
*
|
|
712
|
+
* @param {ElasticSearchVectorFilter} filter - The filter to transform.
|
|
713
|
+
* @returns {Record<string, any>} The transformed filter.
|
|
714
|
+
*/
|
|
715
|
+
transformFilter(filter) {
|
|
716
|
+
const translator = new ElasticSearchFilterTranslator();
|
|
717
|
+
return translator.translate(filter);
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Updates vectors by ID or filter with the provided vector and/or metadata.
|
|
721
|
+
* @param params - Parameters containing either id or filter for targeting vectors to update
|
|
722
|
+
* @param params.indexName - The name of the index containing the vector(s).
|
|
723
|
+
* @param params.id - The ID of a single vector to update (mutually exclusive with filter).
|
|
724
|
+
* @param params.filter - A filter to match multiple vectors to update (mutually exclusive with id).
|
|
725
|
+
* @param params.update - An object containing the vector and/or metadata to update.
|
|
726
|
+
* @returns A promise that resolves when the update is complete.
|
|
727
|
+
* @throws Will throw an error if no updates are provided or if the update operation fails.
|
|
728
|
+
*/
|
|
729
|
+
async updateVector(params) {
|
|
730
|
+
const { indexName, update } = params;
|
|
731
|
+
if ("id" in params && "filter" in params && params.id && params.filter) {
|
|
732
|
+
throw new error.MastraError({
|
|
733
|
+
id: "STORAGE_ELASTICSEARCH_VECTOR_UPDATE_INVALID_ARGS",
|
|
734
|
+
domain: error.ErrorDomain.STORAGE,
|
|
735
|
+
category: error.ErrorCategory.USER,
|
|
736
|
+
text: "id and filter are mutually exclusive",
|
|
737
|
+
details: { indexName }
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
if (!update.vector && !update.metadata) {
|
|
741
|
+
throw new error.MastraError({
|
|
742
|
+
id: "STORAGE_ELASTICSEARCH_VECTOR_UPDATE_NO_UPDATES",
|
|
743
|
+
domain: error.ErrorDomain.STORAGE,
|
|
744
|
+
category: error.ErrorCategory.USER,
|
|
745
|
+
text: "No updates provided",
|
|
746
|
+
details: { indexName }
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
if ("filter" in params && params.filter && Object.keys(params.filter).length === 0) {
|
|
750
|
+
throw new error.MastraError({
|
|
751
|
+
id: "STORAGE_ELASTICSEARCH_VECTOR_UPDATE_INVALID_ARGS",
|
|
752
|
+
domain: error.ErrorDomain.STORAGE,
|
|
753
|
+
category: error.ErrorCategory.USER,
|
|
754
|
+
text: "Cannot update with empty filter",
|
|
755
|
+
details: { indexName }
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
if ("id" in params && params.id) {
|
|
759
|
+
await this.updateVectorById(indexName, params.id, update);
|
|
760
|
+
} else if ("filter" in params && params.filter) {
|
|
761
|
+
await this.updateVectorsByFilter(indexName, params.filter, update);
|
|
762
|
+
} else {
|
|
763
|
+
throw new error.MastraError({
|
|
764
|
+
id: "STORAGE_ELASTICSEARCH_VECTOR_UPDATE_MISSING_PARAMS",
|
|
765
|
+
domain: error.ErrorDomain.STORAGE,
|
|
766
|
+
category: error.ErrorCategory.USER,
|
|
767
|
+
text: "Either id or filter must be provided",
|
|
768
|
+
details: { indexName }
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Updates a single vector by its ID.
|
|
774
|
+
*/
|
|
775
|
+
async updateVectorById(indexName, id, update) {
|
|
776
|
+
let existingDoc;
|
|
777
|
+
try {
|
|
778
|
+
const result = await this.client.get({
|
|
779
|
+
index: indexName,
|
|
780
|
+
id
|
|
781
|
+
}).catch(() => {
|
|
782
|
+
throw new Error(`Document with ID ${id} not found in index ${indexName}`);
|
|
783
|
+
});
|
|
784
|
+
if (!result || !result._source) {
|
|
785
|
+
throw new Error(`Document with ID ${id} has no source data in index ${indexName}`);
|
|
786
|
+
}
|
|
787
|
+
existingDoc = result;
|
|
788
|
+
} catch (error$1) {
|
|
789
|
+
throw new error.MastraError(
|
|
790
|
+
{
|
|
791
|
+
id: "STORAGE_ELASTICSEARCH_VECTOR_UPDATE_VECTOR_FAILED",
|
|
792
|
+
domain: error.ErrorDomain.STORAGE,
|
|
793
|
+
category: error.ErrorCategory.USER,
|
|
794
|
+
details: {
|
|
795
|
+
indexName,
|
|
796
|
+
id
|
|
797
|
+
}
|
|
798
|
+
},
|
|
799
|
+
error$1
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
const source = existingDoc._source;
|
|
803
|
+
const updatedDoc = {
|
|
804
|
+
id: source?.id || id
|
|
805
|
+
};
|
|
806
|
+
try {
|
|
807
|
+
if (update.vector) {
|
|
808
|
+
const indexInfo = await this.describeIndex({ indexName });
|
|
809
|
+
this.validateVectorDimensions([update.vector], indexInfo.dimension);
|
|
810
|
+
updatedDoc.embedding = update.vector;
|
|
811
|
+
} else if (source?.embedding) {
|
|
812
|
+
updatedDoc.embedding = source.embedding;
|
|
813
|
+
}
|
|
814
|
+
if (update.metadata) {
|
|
815
|
+
updatedDoc.metadata = update.metadata;
|
|
816
|
+
} else {
|
|
817
|
+
updatedDoc.metadata = source?.metadata || {};
|
|
818
|
+
}
|
|
819
|
+
await this.client.index({
|
|
820
|
+
index: indexName,
|
|
821
|
+
id,
|
|
822
|
+
document: updatedDoc,
|
|
823
|
+
refresh: true
|
|
824
|
+
});
|
|
825
|
+
} catch (error$1) {
|
|
826
|
+
throw new error.MastraError(
|
|
827
|
+
{
|
|
828
|
+
id: "STORAGE_ELASTICSEARCH_VECTOR_UPDATE_VECTOR_FAILED",
|
|
829
|
+
domain: error.ErrorDomain.STORAGE,
|
|
830
|
+
category: error.ErrorCategory.THIRD_PARTY,
|
|
831
|
+
details: {
|
|
832
|
+
indexName,
|
|
833
|
+
id
|
|
834
|
+
}
|
|
835
|
+
},
|
|
836
|
+
error$1
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Updates multiple vectors matching a filter.
|
|
842
|
+
*/
|
|
843
|
+
async updateVectorsByFilter(indexName, filter, update) {
|
|
844
|
+
try {
|
|
845
|
+
const translator = new ElasticSearchFilterTranslator();
|
|
846
|
+
const translatedFilter = translator.translate(filter);
|
|
847
|
+
const scriptSource = [];
|
|
848
|
+
const scriptParams = {};
|
|
849
|
+
if (update.vector) {
|
|
850
|
+
scriptSource.push("ctx._source.embedding = params.embedding");
|
|
851
|
+
scriptParams.embedding = update.vector;
|
|
852
|
+
}
|
|
853
|
+
if (update.metadata) {
|
|
854
|
+
scriptSource.push("ctx._source.metadata = params.metadata");
|
|
855
|
+
scriptParams.metadata = update.metadata;
|
|
856
|
+
}
|
|
857
|
+
await this.client.updateByQuery({
|
|
858
|
+
index: indexName,
|
|
859
|
+
query: translatedFilter || { match_all: {} },
|
|
860
|
+
script: {
|
|
861
|
+
source: scriptSource.join("; "),
|
|
862
|
+
params: scriptParams,
|
|
863
|
+
lang: "painless"
|
|
864
|
+
},
|
|
865
|
+
refresh: true
|
|
866
|
+
});
|
|
867
|
+
} catch (error$1) {
|
|
868
|
+
throw new error.MastraError(
|
|
869
|
+
{
|
|
870
|
+
id: "STORAGE_ELASTICSEARCH_VECTOR_UPDATE_BY_FILTER_FAILED",
|
|
871
|
+
domain: error.ErrorDomain.STORAGE,
|
|
872
|
+
category: error.ErrorCategory.THIRD_PARTY,
|
|
873
|
+
details: {
|
|
874
|
+
indexName,
|
|
875
|
+
filter: JSON.stringify(filter)
|
|
876
|
+
}
|
|
877
|
+
},
|
|
878
|
+
error$1
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Deletes a vector by its ID.
|
|
884
|
+
* @param indexName - The name of the index containing the vector.
|
|
885
|
+
* @param id - The ID of the vector to delete.
|
|
886
|
+
* @returns A promise that resolves when the deletion is complete.
|
|
887
|
+
* @throws Will throw an error if the deletion operation fails.
|
|
888
|
+
*/
|
|
889
|
+
async deleteVector({ indexName, id }) {
|
|
890
|
+
try {
|
|
891
|
+
await this.client.delete({
|
|
892
|
+
index: indexName,
|
|
893
|
+
id,
|
|
894
|
+
refresh: true
|
|
895
|
+
});
|
|
896
|
+
} catch (error$1) {
|
|
897
|
+
if (error$1 && typeof error$1 === "object" && "statusCode" in error$1 && error$1.statusCode === 404) {
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
throw new error.MastraError(
|
|
901
|
+
{
|
|
902
|
+
id: "STORAGE_ELASTICSEARCH_VECTOR_DELETE_VECTOR_FAILED",
|
|
903
|
+
domain: error.ErrorDomain.STORAGE,
|
|
904
|
+
category: error.ErrorCategory.THIRD_PARTY,
|
|
905
|
+
details: {
|
|
906
|
+
indexName,
|
|
907
|
+
...id && { id }
|
|
908
|
+
}
|
|
909
|
+
},
|
|
910
|
+
error$1
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
async deleteVectors({ indexName, filter, ids }) {
|
|
915
|
+
if (ids && filter) {
|
|
916
|
+
throw new error.MastraError({
|
|
917
|
+
id: "STORAGE_ELASTICSEARCH_VECTOR_DELETE_VECTORS_INVALID_ARGS",
|
|
918
|
+
domain: error.ErrorDomain.STORAGE,
|
|
919
|
+
category: error.ErrorCategory.USER,
|
|
920
|
+
text: "ids and filter are mutually exclusive",
|
|
921
|
+
details: { indexName }
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
if (!ids && !filter) {
|
|
925
|
+
throw new error.MastraError({
|
|
926
|
+
id: "STORAGE_ELASTICSEARCH_VECTOR_DELETE_VECTORS_INVALID_ARGS",
|
|
927
|
+
domain: error.ErrorDomain.STORAGE,
|
|
928
|
+
category: error.ErrorCategory.USER,
|
|
929
|
+
text: "Either filter or ids must be provided",
|
|
930
|
+
details: { indexName }
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
if (ids && ids.length === 0) {
|
|
934
|
+
throw new error.MastraError({
|
|
935
|
+
id: "STORAGE_ELASTICSEARCH_VECTOR_DELETE_VECTORS_INVALID_ARGS",
|
|
936
|
+
domain: error.ErrorDomain.STORAGE,
|
|
937
|
+
category: error.ErrorCategory.USER,
|
|
938
|
+
text: "Cannot delete with empty ids array",
|
|
939
|
+
details: { indexName }
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
if (filter && Object.keys(filter).length === 0) {
|
|
943
|
+
throw new error.MastraError({
|
|
944
|
+
id: "STORAGE_ELASTICSEARCH_VECTOR_DELETE_VECTORS_INVALID_ARGS",
|
|
945
|
+
domain: error.ErrorDomain.STORAGE,
|
|
946
|
+
category: error.ErrorCategory.USER,
|
|
947
|
+
text: "Cannot delete with empty filter",
|
|
948
|
+
details: { indexName }
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
try {
|
|
952
|
+
if (ids) {
|
|
953
|
+
const bulkBody = ids.flatMap((id) => [{ delete: { _index: indexName, _id: id } }]);
|
|
954
|
+
const response = await this.client.bulk({
|
|
955
|
+
operations: bulkBody,
|
|
956
|
+
refresh: true
|
|
957
|
+
});
|
|
958
|
+
if (response.errors) {
|
|
959
|
+
const failedItems = [];
|
|
960
|
+
const successfulIds = [];
|
|
961
|
+
for (let i = 0; i < response.items.length; i++) {
|
|
962
|
+
const item = response.items[i];
|
|
963
|
+
if (!item) continue;
|
|
964
|
+
const operationType = Object.keys(item)[0];
|
|
965
|
+
const operationResult = item[operationType];
|
|
966
|
+
if (!operationResult) continue;
|
|
967
|
+
if (operationResult.error) {
|
|
968
|
+
const operationIndex = i;
|
|
969
|
+
const operationDoc = bulkBody[operationIndex];
|
|
970
|
+
const failedId = operationDoc?.delete?._id || ids[i] || `unknown-${i}`;
|
|
971
|
+
failedItems.push({
|
|
972
|
+
id: failedId,
|
|
973
|
+
status: operationResult.status || 0,
|
|
974
|
+
error: operationResult.error
|
|
975
|
+
});
|
|
976
|
+
} else if (operationResult?.status && operationResult.status < 300) {
|
|
977
|
+
const operationIndex = i;
|
|
978
|
+
const operationDoc = bulkBody[operationIndex];
|
|
979
|
+
const successId = operationDoc?.delete?._id || ids[i];
|
|
980
|
+
if (successId) {
|
|
981
|
+
successfulIds.push(successId);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
if (failedItems.length > 0) {
|
|
986
|
+
const failedItemDetails = failedItems.map((item) => `${item.id}: ${item.error?.reason || item.error?.type || JSON.stringify(item.error)}`).join("; ");
|
|
987
|
+
const mastraError = new error.MastraError(
|
|
988
|
+
{
|
|
989
|
+
id: "STORAGE_ELASTICSEARCH_VECTOR_BULK_DELETE_PARTIAL_FAILURE",
|
|
990
|
+
text: `Bulk delete partially failed: ${failedItems.length} of ${response.items.length} operations failed. Failed items: ${failedItemDetails}`,
|
|
991
|
+
domain: error.ErrorDomain.STORAGE,
|
|
992
|
+
category: error.ErrorCategory.THIRD_PARTY,
|
|
993
|
+
details: {
|
|
994
|
+
indexName,
|
|
995
|
+
totalOperations: response.items.length,
|
|
996
|
+
failedCount: failedItems.length,
|
|
997
|
+
successfulCount: successfulIds.length,
|
|
998
|
+
failedItemIds: failedItems.map((item) => item.id).join(","),
|
|
999
|
+
failedItemErrors: failedItemDetails
|
|
1000
|
+
}
|
|
1001
|
+
},
|
|
1002
|
+
new Error(`Bulk delete operation had ${failedItems.length} failures`)
|
|
1003
|
+
);
|
|
1004
|
+
this.logger?.error(mastraError.toString());
|
|
1005
|
+
this.logger?.trackException(mastraError);
|
|
1006
|
+
throw mastraError;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
} else if (filter) {
|
|
1010
|
+
const translator = new ElasticSearchFilterTranslator();
|
|
1011
|
+
const translatedFilter = translator.translate(filter);
|
|
1012
|
+
await this.client.deleteByQuery({
|
|
1013
|
+
index: indexName,
|
|
1014
|
+
query: translatedFilter || { match_all: {} },
|
|
1015
|
+
refresh: true
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
} catch (error$1) {
|
|
1019
|
+
if (error$1 instanceof error.MastraError) throw error$1;
|
|
1020
|
+
throw new error.MastraError(
|
|
1021
|
+
{
|
|
1022
|
+
id: "STORAGE_ELASTICSEARCH_VECTOR_DELETE_VECTORS_FAILED",
|
|
1023
|
+
domain: error.ErrorDomain.STORAGE,
|
|
1024
|
+
category: error.ErrorCategory.THIRD_PARTY,
|
|
1025
|
+
details: {
|
|
1026
|
+
indexName,
|
|
1027
|
+
...filter && { filter: JSON.stringify(filter) },
|
|
1028
|
+
...ids && { idsCount: ids.length }
|
|
1029
|
+
}
|
|
1030
|
+
},
|
|
1031
|
+
error$1
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
exports.ElasticSearchVector = ElasticSearchVector;
|
|
1038
|
+
//# sourceMappingURL=index.cjs.map
|
|
1039
|
+
//# sourceMappingURL=index.cjs.map
|