@mastra/elasticsearch 1.3.0 → 1.3.1

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