@mastra/pg 0.3.4 → 0.3.5-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +15 -0
- package/dist/_tsup-dts-rollup.d.cts +19 -31
- package/dist/_tsup-dts-rollup.d.ts +19 -31
- package/dist/index.cjs +151 -90
- package/dist/index.js +151 -90
- package/docker-compose.perf.yaml +9 -9
- package/package.json +2 -2
- package/src/storage/index.ts +12 -6
- package/src/vector/index.test.ts +53 -51
- package/src/vector/index.ts +47 -21
- package/src/vector/sql-builder.ts +110 -77
- package/src/vector/vector.performance.test.ts +2 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseFieldKey } from '@mastra/core/utils';
|
|
1
2
|
import type {
|
|
2
3
|
BasicOperator,
|
|
3
4
|
NumericOperator,
|
|
@@ -8,14 +9,15 @@ import type {
|
|
|
8
9
|
VectorFilter,
|
|
9
10
|
} from '@mastra/core/vector/filter';
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
type OperatorType =
|
|
12
13
|
| BasicOperator
|
|
13
14
|
| NumericOperator
|
|
14
15
|
| ArrayOperator
|
|
15
16
|
| ElementOperator
|
|
16
17
|
| LogicalOperator
|
|
17
18
|
| '$contains'
|
|
18
|
-
| Exclude<RegexOperator, '$options'
|
|
19
|
+
| Exclude<RegexOperator, '$options'>
|
|
20
|
+
| '$size';
|
|
19
21
|
|
|
20
22
|
type FilterOperator = {
|
|
21
23
|
sql: string;
|
|
@@ -25,22 +27,27 @@ type FilterOperator = {
|
|
|
25
27
|
|
|
26
28
|
type OperatorFn = (key: string, paramIndex: number, value?: any) => FilterOperator;
|
|
27
29
|
|
|
28
|
-
// Helper functions to create operators
|
|
29
30
|
const createBasicOperator = (symbol: string) => {
|
|
30
|
-
return (key: string, paramIndex: number) =>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
return (key: string, paramIndex: number) => {
|
|
32
|
+
const jsonPathKey = parseJsonPathKey(key);
|
|
33
|
+
return {
|
|
34
|
+
sql: `CASE
|
|
35
|
+
WHEN $${paramIndex}::text IS NULL THEN metadata#>>'{${jsonPathKey}}' IS ${symbol === '=' ? '' : 'NOT'} NULL
|
|
36
|
+
ELSE metadata#>>'{${jsonPathKey}}' ${symbol} $${paramIndex}::text
|
|
37
|
+
END`,
|
|
38
|
+
needsValue: true,
|
|
39
|
+
};
|
|
40
|
+
};
|
|
37
41
|
};
|
|
38
42
|
|
|
39
43
|
const createNumericOperator = (symbol: string) => {
|
|
40
|
-
return (key: string, paramIndex: number) =>
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
return (key: string, paramIndex: number) => {
|
|
45
|
+
const jsonPathKey = parseJsonPathKey(key);
|
|
46
|
+
return {
|
|
47
|
+
sql: `(metadata#>>'{${jsonPathKey}}')::numeric ${symbol} $${paramIndex}`,
|
|
48
|
+
needsValue: true,
|
|
49
|
+
};
|
|
50
|
+
};
|
|
44
51
|
};
|
|
45
52
|
|
|
46
53
|
function buildElemMatchConditions(value: any, paramIndex: number): { sql: string; values: any[] } {
|
|
@@ -73,7 +80,7 @@ function buildElemMatchConditions(value: any, paramIndex: number): { sql: string
|
|
|
73
80
|
paramValue = val;
|
|
74
81
|
}
|
|
75
82
|
|
|
76
|
-
const operatorFn = FILTER_OPERATORS[paramOperator as
|
|
83
|
+
const operatorFn = FILTER_OPERATORS[paramOperator as OperatorType];
|
|
77
84
|
if (!operatorFn) {
|
|
78
85
|
throw new Error(`Invalid operator: ${paramOperator}`);
|
|
79
86
|
}
|
|
@@ -93,7 +100,7 @@ function buildElemMatchConditions(value: any, paramIndex: number): { sql: string
|
|
|
93
100
|
}
|
|
94
101
|
|
|
95
102
|
// Define all filter operators
|
|
96
|
-
|
|
103
|
+
const FILTER_OPERATORS: Record<OperatorType, OperatorFn> = {
|
|
97
104
|
$eq: createBasicOperator('='),
|
|
98
105
|
$ne: createBasicOperator('!='),
|
|
99
106
|
$gt: createNumericOperator('>'),
|
|
@@ -102,46 +109,56 @@ export const FILTER_OPERATORS: Record<string, OperatorFn> = {
|
|
|
102
109
|
$lte: createNumericOperator('<='),
|
|
103
110
|
|
|
104
111
|
// Array Operators
|
|
105
|
-
$in: (key, paramIndex) =>
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
112
|
+
$in: (key, paramIndex) => {
|
|
113
|
+
const jsonPathKey = parseJsonPathKey(key);
|
|
114
|
+
return {
|
|
115
|
+
sql: `(
|
|
116
|
+
CASE
|
|
117
|
+
WHEN jsonb_typeof(metadata->'${jsonPathKey}') = 'array' THEN
|
|
118
|
+
EXISTS (
|
|
119
|
+
SELECT 1 FROM jsonb_array_elements_text(metadata->'${jsonPathKey}') as elem
|
|
120
|
+
WHERE elem = ANY($${paramIndex}::text[])
|
|
121
|
+
)
|
|
122
|
+
ELSE metadata#>>'{${jsonPathKey}}' = ANY($${paramIndex}::text[])
|
|
123
|
+
END
|
|
124
|
+
)`,
|
|
125
|
+
needsValue: true,
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
$nin: (key, paramIndex) => {
|
|
129
|
+
const jsonPathKey = parseJsonPathKey(key);
|
|
130
|
+
return {
|
|
131
|
+
sql: `(
|
|
132
|
+
CASE
|
|
133
|
+
WHEN jsonb_typeof(metadata->'${jsonPathKey}') = 'array' THEN
|
|
134
|
+
NOT EXISTS (
|
|
135
|
+
SELECT 1 FROM jsonb_array_elements_text(metadata->'${jsonPathKey}') as elem
|
|
136
|
+
WHERE elem = ANY($${paramIndex}::text[])
|
|
137
|
+
)
|
|
138
|
+
ELSE metadata#>>'{${jsonPathKey}}' != ALL($${paramIndex}::text[])
|
|
139
|
+
END
|
|
140
|
+
)`,
|
|
141
|
+
needsValue: true,
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
$all: (key, paramIndex) => {
|
|
145
|
+
const jsonPathKey = parseJsonPathKey(key);
|
|
146
|
+
return {
|
|
147
|
+
sql: `CASE WHEN array_length($${paramIndex}::text[], 1) IS NULL THEN false
|
|
148
|
+
ELSE (metadata#>'{${jsonPathKey}}')::jsonb ?& $${paramIndex}::text[] END`,
|
|
149
|
+
needsValue: true,
|
|
150
|
+
};
|
|
151
|
+
},
|
|
136
152
|
$elemMatch: (key: string, paramIndex: number, value: any): FilterOperator => {
|
|
137
153
|
const { sql, values } = buildElemMatchConditions(value, paramIndex);
|
|
154
|
+
const jsonPathKey = parseJsonPathKey(key);
|
|
138
155
|
return {
|
|
139
156
|
sql: `(
|
|
140
157
|
CASE
|
|
141
|
-
WHEN jsonb_typeof(metadata->'${
|
|
158
|
+
WHEN jsonb_typeof(metadata->'${jsonPathKey}') = 'array' THEN
|
|
142
159
|
EXISTS (
|
|
143
160
|
SELECT 1
|
|
144
|
-
FROM jsonb_array_elements(metadata->'${
|
|
161
|
+
FROM jsonb_array_elements(metadata->'${jsonPathKey}') as elem
|
|
145
162
|
WHERE ${sql}
|
|
146
163
|
)
|
|
147
164
|
ELSE FALSE
|
|
@@ -152,10 +169,13 @@ export const FILTER_OPERATORS: Record<string, OperatorFn> = {
|
|
|
152
169
|
};
|
|
153
170
|
},
|
|
154
171
|
// Element Operators
|
|
155
|
-
$exists: key =>
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
172
|
+
$exists: key => {
|
|
173
|
+
const jsonPathKey = parseJsonPathKey(key);
|
|
174
|
+
return {
|
|
175
|
+
sql: `metadata ? '${jsonPathKey}'`,
|
|
176
|
+
needsValue: false,
|
|
177
|
+
};
|
|
178
|
+
},
|
|
159
179
|
|
|
160
180
|
// Logical Operators
|
|
161
181
|
$and: key => ({ sql: `(${key})`, needsValue: false }),
|
|
@@ -164,24 +184,29 @@ export const FILTER_OPERATORS: Record<string, OperatorFn> = {
|
|
|
164
184
|
$nor: key => ({ sql: `NOT (${key})`, needsValue: false }),
|
|
165
185
|
|
|
166
186
|
// Regex Operators
|
|
167
|
-
$regex: (key, paramIndex) =>
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
187
|
+
$regex: (key, paramIndex) => {
|
|
188
|
+
const jsonPathKey = parseJsonPathKey(key);
|
|
189
|
+
return {
|
|
190
|
+
sql: `metadata#>>'{${jsonPathKey}}' ~ $${paramIndex}`,
|
|
191
|
+
needsValue: true,
|
|
192
|
+
};
|
|
193
|
+
},
|
|
171
194
|
|
|
172
195
|
$contains: (key, paramIndex, value: any) => {
|
|
196
|
+
const jsonPathKey = parseJsonPathKey(key);
|
|
173
197
|
let sql;
|
|
174
198
|
if (Array.isArray(value)) {
|
|
175
|
-
sql = `(metadata->'${
|
|
199
|
+
sql = `(metadata->'${jsonPathKey}') ?& $${paramIndex}`;
|
|
176
200
|
} else if (typeof value === 'string') {
|
|
177
|
-
sql = `metadata->>'${
|
|
201
|
+
sql = `metadata->>'${jsonPathKey}' ILIKE '%' || $${paramIndex} || '%' ESCAPE '\\'`;
|
|
178
202
|
} else {
|
|
179
|
-
sql = `metadata->>'${
|
|
203
|
+
sql = `metadata->>'${jsonPathKey}' = $${paramIndex}`;
|
|
180
204
|
}
|
|
181
205
|
return {
|
|
182
206
|
sql,
|
|
183
207
|
needsValue: true,
|
|
184
|
-
transformValue: () =>
|
|
208
|
+
transformValue: () =>
|
|
209
|
+
Array.isArray(value) ? value.map(String) : typeof value === 'string' ? escapeLikePattern(value) : value,
|
|
185
210
|
};
|
|
186
211
|
},
|
|
187
212
|
/**
|
|
@@ -196,29 +221,37 @@ export const FILTER_OPERATORS: Record<string, OperatorFn> = {
|
|
|
196
221
|
// return JSON.stringify(parts.reduceRight((value, key) => ({ [key]: value }), value));
|
|
197
222
|
// },
|
|
198
223
|
// }),
|
|
199
|
-
$size: (key: string, paramIndex: number) =>
|
|
200
|
-
|
|
224
|
+
$size: (key: string, paramIndex: number) => {
|
|
225
|
+
const jsonPathKey = parseJsonPathKey(key);
|
|
226
|
+
return {
|
|
227
|
+
sql: `(
|
|
201
228
|
CASE
|
|
202
|
-
WHEN jsonb_typeof(metadata#>'{${
|
|
203
|
-
jsonb_array_length(metadata#>'{${
|
|
229
|
+
WHEN jsonb_typeof(metadata#>'{${jsonPathKey}}') = 'array' THEN
|
|
230
|
+
jsonb_array_length(metadata#>'{${jsonPathKey}}') = $${paramIndex}
|
|
204
231
|
ELSE FALSE
|
|
205
232
|
END
|
|
206
233
|
)`,
|
|
207
|
-
|
|
208
|
-
|
|
234
|
+
needsValue: true,
|
|
235
|
+
};
|
|
236
|
+
},
|
|
209
237
|
};
|
|
210
238
|
|
|
211
|
-
|
|
239
|
+
interface FilterResult {
|
|
212
240
|
sql: string;
|
|
213
241
|
values: any[];
|
|
214
242
|
}
|
|
215
243
|
|
|
216
|
-
|
|
217
|
-
|
|
244
|
+
const parseJsonPathKey = (key: string) => {
|
|
245
|
+
const parsedKey = key !== '' ? parseFieldKey(key) : '';
|
|
246
|
+
return parsedKey.replace(/\./g, ',');
|
|
218
247
|
};
|
|
219
248
|
|
|
220
|
-
|
|
221
|
-
|
|
249
|
+
function escapeLikePattern(str: string): string {
|
|
250
|
+
return str.replace(/([%_\\])/g, '\\$1');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function buildFilterQuery(filter: VectorFilter, minScore: number, topK: number): FilterResult {
|
|
254
|
+
const values = [minScore, topK];
|
|
222
255
|
|
|
223
256
|
function buildCondition(key: string, value: any, parentPath: string): string {
|
|
224
257
|
// Handle logical operators ($and/$or)
|
|
@@ -229,7 +262,7 @@ export function buildFilterQuery(filter: VectorFilter, minScore: number): Filter
|
|
|
229
262
|
// If condition is not a FilterCondition object, assume it's an equality check
|
|
230
263
|
if (!value || typeof value !== 'object') {
|
|
231
264
|
values.push(value);
|
|
232
|
-
return `metadata#>>'{${
|
|
265
|
+
return `metadata#>>'{${parseJsonPathKey(key)}}' = $${values.length}`;
|
|
233
266
|
}
|
|
234
267
|
|
|
235
268
|
// Handle operator conditions
|
|
@@ -240,11 +273,11 @@ export function buildFilterQuery(filter: VectorFilter, minScore: number): Filter
|
|
|
240
273
|
const entries = Object.entries(operatorValue as Record<string, unknown>);
|
|
241
274
|
const conditions = entries
|
|
242
275
|
.map(([nestedOp, nestedValue]) => {
|
|
243
|
-
if (!FILTER_OPERATORS[nestedOp as
|
|
276
|
+
if (!FILTER_OPERATORS[nestedOp as OperatorType]) {
|
|
244
277
|
throw new Error(`Invalid operator in $not condition: ${nestedOp}`);
|
|
245
278
|
}
|
|
246
|
-
const operatorFn = FILTER_OPERATORS[nestedOp]!;
|
|
247
|
-
const operatorResult = operatorFn(key, values.length + 1);
|
|
279
|
+
const operatorFn = FILTER_OPERATORS[nestedOp as OperatorType]!;
|
|
280
|
+
const operatorResult = operatorFn(key, values.length + 1, nestedValue);
|
|
248
281
|
if (operatorResult.needsValue) {
|
|
249
282
|
values.push(nestedValue as number);
|
|
250
283
|
}
|
|
@@ -254,7 +287,7 @@ export function buildFilterQuery(filter: VectorFilter, minScore: number): Filter
|
|
|
254
287
|
|
|
255
288
|
return `NOT (${conditions})`;
|
|
256
289
|
}
|
|
257
|
-
const operatorFn = FILTER_OPERATORS[operator as
|
|
290
|
+
const operatorFn = FILTER_OPERATORS[operator as OperatorType]!;
|
|
258
291
|
const operatorResult = operatorFn(key, values.length + 1, operatorValue);
|
|
259
292
|
if (operatorResult.needsValue) {
|
|
260
293
|
const transformedValue = operatorResult.transformValue ? operatorResult.transformValue() : operatorValue;
|
|
@@ -116,11 +116,11 @@ describe('PostgreSQL Index Performance', () => {
|
|
|
116
116
|
vectorDB = new PGPerformanceVector(connectionString);
|
|
117
117
|
});
|
|
118
118
|
beforeEach(async () => {
|
|
119
|
-
await vectorDB.deleteIndex(testIndexName);
|
|
119
|
+
await vectorDB.deleteIndex({ indexName: testIndexName });
|
|
120
120
|
});
|
|
121
121
|
|
|
122
122
|
afterEach(async () => {
|
|
123
|
-
await vectorDB.deleteIndex(testIndexName);
|
|
123
|
+
await vectorDB.deleteIndex({ indexName: testIndexName });
|
|
124
124
|
});
|
|
125
125
|
|
|
126
126
|
afterAll(async () => {
|