@mastra/pg 0.3.0-alpha.6 → 0.3.0-alpha.8
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 +21 -0
- package/dist/_tsup-dts-rollup.d.cts +1 -1
- package/dist/_tsup-dts-rollup.d.ts +1 -1
- package/dist/index.cjs +47 -10
- package/dist/index.js +47 -10
- package/package.json +2 -2
- package/src/vector/index.test.ts +117 -40
- package/src/vector/sql-builder.ts +49 -14
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
|
|
2
|
-
> @mastra/pg@0.3.0-alpha.
|
|
2
|
+
> @mastra/pg@0.3.0-alpha.8 build /home/runner/work/mastra/mastra/stores/pg
|
|
3
3
|
> tsup src/index.ts --format esm,cjs --experimental-dts --clean --treeshake=smallest --splitting
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
6
6
|
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
7
7
|
[34mCLI[39m tsup v8.4.0
|
|
8
8
|
[34mTSC[39m Build start
|
|
9
|
-
[32mTSC[39m ⚡️ Build success in
|
|
9
|
+
[32mTSC[39m ⚡️ Build success in 11894ms
|
|
10
10
|
[34mDTS[39m Build start
|
|
11
11
|
[34mCLI[39m Target: es2022
|
|
12
12
|
Analysis will use the bundled TypeScript version 5.8.2
|
|
13
13
|
[36mWriting package typings: /home/runner/work/mastra/mastra/stores/pg/dist/_tsup-dts-rollup.d.ts[39m
|
|
14
14
|
Analysis will use the bundled TypeScript version 5.8.2
|
|
15
15
|
[36mWriting package typings: /home/runner/work/mastra/mastra/stores/pg/dist/_tsup-dts-rollup.d.cts[39m
|
|
16
|
-
[32mDTS[39m ⚡️ Build success in
|
|
16
|
+
[32mDTS[39m ⚡️ Build success in 12889ms
|
|
17
17
|
[34mCLI[39m Cleaning output folder
|
|
18
18
|
[34mESM[39m Build start
|
|
19
19
|
[34mCJS[39m Build start
|
|
20
|
-
[
|
|
21
|
-
[
|
|
22
|
-
[
|
|
23
|
-
[
|
|
20
|
+
[32mESM[39m [1mdist/index.js [22m[32m48.21 KB[39m
|
|
21
|
+
[32mESM[39m ⚡️ Build success in 1550ms
|
|
22
|
+
[32mCJS[39m [1mdist/index.cjs [22m[32m48.63 KB[39m
|
|
23
|
+
[32mCJS[39m ⚡️ Build success in 1553ms
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# @mastra/pg
|
|
2
2
|
|
|
3
|
+
## 0.3.0-alpha.8
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- c0f22b4: [MASTRA-3130] Metadata Filter Update for PG and Libsql
|
|
8
|
+
- Updated dependencies [000a6d4]
|
|
9
|
+
- Updated dependencies [ed2f549]
|
|
10
|
+
- Updated dependencies [c0f22b4]
|
|
11
|
+
- Updated dependencies [0a033fa]
|
|
12
|
+
- Updated dependencies [9c26508]
|
|
13
|
+
- Updated dependencies [0f4eae3]
|
|
14
|
+
- Updated dependencies [16a8648]
|
|
15
|
+
- @mastra/core@0.9.0-alpha.8
|
|
16
|
+
|
|
17
|
+
## 0.3.0-alpha.7
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- Updated dependencies [71d9444]
|
|
22
|
+
- @mastra/core@0.9.0-alpha.7
|
|
23
|
+
|
|
3
24
|
## 0.3.0-alpha.6
|
|
4
25
|
|
|
5
26
|
### Patch Changes
|
|
@@ -72,7 +72,7 @@ export declare const FILTER_OPERATORS: Record<string, OperatorFn>;
|
|
|
72
72
|
declare type FilterOperator = {
|
|
73
73
|
sql: string;
|
|
74
74
|
needsValue: boolean;
|
|
75
|
-
transformValue?: (
|
|
75
|
+
transformValue?: () => any;
|
|
76
76
|
};
|
|
77
77
|
|
|
78
78
|
export declare interface FilterResult {
|
|
@@ -72,7 +72,7 @@ export declare const FILTER_OPERATORS: Record<string, OperatorFn>;
|
|
|
72
72
|
declare type FilterOperator = {
|
|
73
73
|
sql: string;
|
|
74
74
|
needsValue: boolean;
|
|
75
|
-
transformValue?: (
|
|
75
|
+
transformValue?: () => any;
|
|
76
76
|
};
|
|
77
77
|
|
|
78
78
|
export declare interface FilterResult {
|
package/dist/index.cjs
CHANGED
|
@@ -148,11 +148,29 @@ var FILTER_OPERATORS = {
|
|
|
148
148
|
$lte: createNumericOperator("<="),
|
|
149
149
|
// Array Operators
|
|
150
150
|
$in: (key, paramIndex) => ({
|
|
151
|
-
sql: `
|
|
151
|
+
sql: `(
|
|
152
|
+
CASE
|
|
153
|
+
WHEN jsonb_typeof(metadata->'${handleKey(key)}') = 'array' THEN
|
|
154
|
+
EXISTS (
|
|
155
|
+
SELECT 1 FROM jsonb_array_elements_text(metadata->'${handleKey(key)}') as elem
|
|
156
|
+
WHERE elem = ANY($${paramIndex}::text[])
|
|
157
|
+
)
|
|
158
|
+
ELSE metadata#>>'{${handleKey(key)}}' = ANY($${paramIndex}::text[])
|
|
159
|
+
END
|
|
160
|
+
)`,
|
|
152
161
|
needsValue: true
|
|
153
162
|
}),
|
|
154
163
|
$nin: (key, paramIndex) => ({
|
|
155
|
-
sql: `
|
|
164
|
+
sql: `(
|
|
165
|
+
CASE
|
|
166
|
+
WHEN jsonb_typeof(metadata->'${handleKey(key)}') = 'array' THEN
|
|
167
|
+
NOT EXISTS (
|
|
168
|
+
SELECT 1 FROM jsonb_array_elements_text(metadata->'${handleKey(key)}') as elem
|
|
169
|
+
WHERE elem = ANY($${paramIndex}::text[])
|
|
170
|
+
)
|
|
171
|
+
ELSE metadata#>>'{${handleKey(key)}}' != ALL($${paramIndex}::text[])
|
|
172
|
+
END
|
|
173
|
+
)`,
|
|
156
174
|
needsValue: true
|
|
157
175
|
}),
|
|
158
176
|
$all: (key, paramIndex) => ({
|
|
@@ -193,14 +211,33 @@ var FILTER_OPERATORS = {
|
|
|
193
211
|
sql: `metadata#>>'{${handleKey(key)}}' ~ $${paramIndex}`,
|
|
194
212
|
needsValue: true
|
|
195
213
|
}),
|
|
196
|
-
$contains: (key, paramIndex) =>
|
|
197
|
-
sql
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
214
|
+
$contains: (key, paramIndex, value) => {
|
|
215
|
+
let sql;
|
|
216
|
+
if (Array.isArray(value)) {
|
|
217
|
+
sql = `(metadata->'${handleKey(key)}') ?& $${paramIndex}`;
|
|
218
|
+
} else if (typeof value === "string") {
|
|
219
|
+
sql = `metadata->>'${handleKey(key)}' ILIKE '%' || $${paramIndex} || '%'`;
|
|
220
|
+
} else {
|
|
221
|
+
sql = `metadata->>'${handleKey(key)}' = $${paramIndex}`;
|
|
202
222
|
}
|
|
203
|
-
|
|
223
|
+
return {
|
|
224
|
+
sql,
|
|
225
|
+
needsValue: true,
|
|
226
|
+
transformValue: () => Array.isArray(value) ? value.map(String) : value
|
|
227
|
+
};
|
|
228
|
+
},
|
|
229
|
+
/**
|
|
230
|
+
* $objectContains: Postgres-only operator for true JSONB object containment.
|
|
231
|
+
* Usage: { field: { $objectContains: { ...subobject } } }
|
|
232
|
+
*/
|
|
233
|
+
// $objectContains: (key, paramIndex) => ({
|
|
234
|
+
// sql: `metadata @> $${paramIndex}::jsonb`,
|
|
235
|
+
// needsValue: true,
|
|
236
|
+
// transformValue: value => {
|
|
237
|
+
// const parts = key.split('.');
|
|
238
|
+
// return JSON.stringify(parts.reduceRight((value, key) => ({ [key]: value }), value));
|
|
239
|
+
// },
|
|
240
|
+
// }),
|
|
204
241
|
$size: (key, paramIndex) => ({
|
|
205
242
|
sql: `(
|
|
206
243
|
CASE
|
|
@@ -244,7 +281,7 @@ function buildFilterQuery(filter, minScore) {
|
|
|
244
281
|
const operatorFn = FILTER_OPERATORS[operator];
|
|
245
282
|
const operatorResult = operatorFn(key, values.length + 1, operatorValue);
|
|
246
283
|
if (operatorResult.needsValue) {
|
|
247
|
-
const transformedValue = operatorResult.transformValue ? operatorResult.transformValue(
|
|
284
|
+
const transformedValue = operatorResult.transformValue ? operatorResult.transformValue() : operatorValue;
|
|
248
285
|
if (Array.isArray(transformedValue) && operator === "$elemMatch") {
|
|
249
286
|
values.push(...transformedValue);
|
|
250
287
|
} else {
|
package/dist/index.js
CHANGED
|
@@ -140,11 +140,29 @@ var FILTER_OPERATORS = {
|
|
|
140
140
|
$lte: createNumericOperator("<="),
|
|
141
141
|
// Array Operators
|
|
142
142
|
$in: (key, paramIndex) => ({
|
|
143
|
-
sql: `
|
|
143
|
+
sql: `(
|
|
144
|
+
CASE
|
|
145
|
+
WHEN jsonb_typeof(metadata->'${handleKey(key)}') = 'array' THEN
|
|
146
|
+
EXISTS (
|
|
147
|
+
SELECT 1 FROM jsonb_array_elements_text(metadata->'${handleKey(key)}') as elem
|
|
148
|
+
WHERE elem = ANY($${paramIndex}::text[])
|
|
149
|
+
)
|
|
150
|
+
ELSE metadata#>>'{${handleKey(key)}}' = ANY($${paramIndex}::text[])
|
|
151
|
+
END
|
|
152
|
+
)`,
|
|
144
153
|
needsValue: true
|
|
145
154
|
}),
|
|
146
155
|
$nin: (key, paramIndex) => ({
|
|
147
|
-
sql: `
|
|
156
|
+
sql: `(
|
|
157
|
+
CASE
|
|
158
|
+
WHEN jsonb_typeof(metadata->'${handleKey(key)}') = 'array' THEN
|
|
159
|
+
NOT EXISTS (
|
|
160
|
+
SELECT 1 FROM jsonb_array_elements_text(metadata->'${handleKey(key)}') as elem
|
|
161
|
+
WHERE elem = ANY($${paramIndex}::text[])
|
|
162
|
+
)
|
|
163
|
+
ELSE metadata#>>'{${handleKey(key)}}' != ALL($${paramIndex}::text[])
|
|
164
|
+
END
|
|
165
|
+
)`,
|
|
148
166
|
needsValue: true
|
|
149
167
|
}),
|
|
150
168
|
$all: (key, paramIndex) => ({
|
|
@@ -185,14 +203,33 @@ var FILTER_OPERATORS = {
|
|
|
185
203
|
sql: `metadata#>>'{${handleKey(key)}}' ~ $${paramIndex}`,
|
|
186
204
|
needsValue: true
|
|
187
205
|
}),
|
|
188
|
-
$contains: (key, paramIndex) =>
|
|
189
|
-
sql
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
206
|
+
$contains: (key, paramIndex, value) => {
|
|
207
|
+
let sql;
|
|
208
|
+
if (Array.isArray(value)) {
|
|
209
|
+
sql = `(metadata->'${handleKey(key)}') ?& $${paramIndex}`;
|
|
210
|
+
} else if (typeof value === "string") {
|
|
211
|
+
sql = `metadata->>'${handleKey(key)}' ILIKE '%' || $${paramIndex} || '%'`;
|
|
212
|
+
} else {
|
|
213
|
+
sql = `metadata->>'${handleKey(key)}' = $${paramIndex}`;
|
|
194
214
|
}
|
|
195
|
-
|
|
215
|
+
return {
|
|
216
|
+
sql,
|
|
217
|
+
needsValue: true,
|
|
218
|
+
transformValue: () => Array.isArray(value) ? value.map(String) : value
|
|
219
|
+
};
|
|
220
|
+
},
|
|
221
|
+
/**
|
|
222
|
+
* $objectContains: Postgres-only operator for true JSONB object containment.
|
|
223
|
+
* Usage: { field: { $objectContains: { ...subobject } } }
|
|
224
|
+
*/
|
|
225
|
+
// $objectContains: (key, paramIndex) => ({
|
|
226
|
+
// sql: `metadata @> $${paramIndex}::jsonb`,
|
|
227
|
+
// needsValue: true,
|
|
228
|
+
// transformValue: value => {
|
|
229
|
+
// const parts = key.split('.');
|
|
230
|
+
// return JSON.stringify(parts.reduceRight((value, key) => ({ [key]: value }), value));
|
|
231
|
+
// },
|
|
232
|
+
// }),
|
|
196
233
|
$size: (key, paramIndex) => ({
|
|
197
234
|
sql: `(
|
|
198
235
|
CASE
|
|
@@ -236,7 +273,7 @@ function buildFilterQuery(filter, minScore) {
|
|
|
236
273
|
const operatorFn = FILTER_OPERATORS[operator];
|
|
237
274
|
const operatorResult = operatorFn(key, values.length + 1, operatorValue);
|
|
238
275
|
if (operatorResult.needsValue) {
|
|
239
|
-
const transformedValue = operatorResult.transformValue ? operatorResult.transformValue(
|
|
276
|
+
const transformedValue = operatorResult.transformValue ? operatorResult.transformValue() : operatorValue;
|
|
240
277
|
if (Array.isArray(transformedValue) && operator === "$elemMatch") {
|
|
241
278
|
values.push(...transformedValue);
|
|
242
279
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mastra/pg",
|
|
3
|
-
"version": "0.3.0-alpha.
|
|
3
|
+
"version": "0.3.0-alpha.8",
|
|
4
4
|
"description": "Postgres provider for Mastra - includes both vector and db storage capabilities",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"pg": "^8.13.3",
|
|
25
25
|
"pg-promise": "^11.11.0",
|
|
26
26
|
"xxhash-wasm": "^1.1.0",
|
|
27
|
-
"@mastra/core": "^0.9.0-alpha.
|
|
27
|
+
"@mastra/core": "^0.9.0-alpha.8"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@microsoft/api-extractor": "^7.52.1",
|
package/src/vector/index.test.ts
CHANGED
|
@@ -435,12 +435,7 @@ describe('PgVector', () => {
|
|
|
435
435
|
});
|
|
436
436
|
|
|
437
437
|
it('should handle filters correctly', async () => {
|
|
438
|
-
const results = await vectorDB.query({
|
|
439
|
-
indexName,
|
|
440
|
-
queryVector: [1, 0, 0],
|
|
441
|
-
topK: 10,
|
|
442
|
-
filter: { type: 'a' },
|
|
443
|
-
});
|
|
438
|
+
const results = await vectorDB.query({ indexName, queryVector: [1, 0, 0], topK: 10, filter: { type: 'a' } });
|
|
444
439
|
|
|
445
440
|
expect(results).toHaveLength(1);
|
|
446
441
|
results.forEach(result => {
|
|
@@ -606,7 +601,7 @@ describe('PgVector', () => {
|
|
|
606
601
|
|
|
607
602
|
// Array Operator Tests
|
|
608
603
|
describe('Array Operators', () => {
|
|
609
|
-
it('should filter with $in operator', async () => {
|
|
604
|
+
it('should filter with $in operator for scalar field', async () => {
|
|
610
605
|
const results = await vectorDB.query({
|
|
611
606
|
indexName,
|
|
612
607
|
queryVector: [1, 0, 0],
|
|
@@ -618,7 +613,25 @@ describe('PgVector', () => {
|
|
|
618
613
|
});
|
|
619
614
|
});
|
|
620
615
|
|
|
621
|
-
it('should filter with $
|
|
616
|
+
it('should filter with $in operator for array field', async () => {
|
|
617
|
+
// Insert a record with tags as array
|
|
618
|
+
await vectorDB.upsert({
|
|
619
|
+
indexName,
|
|
620
|
+
vectors: [[2, 0.2, 0]],
|
|
621
|
+
metadata: [{ tags: ['featured', 'sale', 'new'] }],
|
|
622
|
+
});
|
|
623
|
+
const results = await vectorDB.query({
|
|
624
|
+
indexName,
|
|
625
|
+
queryVector: [1, 0, 0],
|
|
626
|
+
filter: { tags: { $in: ['sale', 'clearance'] } },
|
|
627
|
+
});
|
|
628
|
+
expect(results.length).toBeGreaterThan(0);
|
|
629
|
+
results.forEach(result => {
|
|
630
|
+
expect(result.metadata?.tags.some((tag: string) => ['sale', 'clearance'].includes(tag))).toBe(true);
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it('should filter with $nin operator for scalar field', async () => {
|
|
622
635
|
const results = await vectorDB.query({
|
|
623
636
|
indexName,
|
|
624
637
|
queryVector: [1, 0, 0],
|
|
@@ -630,6 +643,24 @@ describe('PgVector', () => {
|
|
|
630
643
|
});
|
|
631
644
|
});
|
|
632
645
|
|
|
646
|
+
it('should filter with $nin operator for array field', async () => {
|
|
647
|
+
// Insert a record with tags as array
|
|
648
|
+
await vectorDB.upsert({
|
|
649
|
+
indexName,
|
|
650
|
+
vectors: [[2, 0.3, 0]],
|
|
651
|
+
metadata: [{ tags: ['clearance', 'used'] }],
|
|
652
|
+
});
|
|
653
|
+
const results = await vectorDB.query({
|
|
654
|
+
indexName,
|
|
655
|
+
queryVector: [1, 0, 0],
|
|
656
|
+
filter: { tags: { $nin: ['new', 'sale'] } },
|
|
657
|
+
});
|
|
658
|
+
expect(results.length).toBeGreaterThan(0);
|
|
659
|
+
results.forEach(result => {
|
|
660
|
+
expect(result.metadata?.tags.every((tag: string) => !['new', 'sale'].includes(tag))).toBe(true);
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
|
|
633
664
|
it('should handle empty arrays in in/nin operators', async () => {
|
|
634
665
|
// Should return no results for empty IN
|
|
635
666
|
const resultsIn = await vectorDB.query({
|
|
@@ -660,6 +691,52 @@ describe('PgVector', () => {
|
|
|
660
691
|
});
|
|
661
692
|
});
|
|
662
693
|
|
|
694
|
+
it('should filter with $contains operator for string substring', async () => {
|
|
695
|
+
const results = await vectorDB.query({
|
|
696
|
+
indexName,
|
|
697
|
+
queryVector: [1, 0, 0],
|
|
698
|
+
filter: { category: { $contains: 'lectro' } },
|
|
699
|
+
});
|
|
700
|
+
expect(results.length).toBeGreaterThan(0);
|
|
701
|
+
results.forEach(result => {
|
|
702
|
+
expect(result.metadata?.category).toContain('lectro');
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
it('should not match deep object containment with $contains', async () => {
|
|
707
|
+
// Insert a record with a nested object
|
|
708
|
+
await vectorDB.upsert({
|
|
709
|
+
indexName,
|
|
710
|
+
vectors: [[1, 0.1, 0]],
|
|
711
|
+
metadata: [{ details: { color: 'red', size: 'large' }, category: 'clothing' }],
|
|
712
|
+
});
|
|
713
|
+
// $contains does NOT support deep object containment in Postgres
|
|
714
|
+
const results = await vectorDB.query({
|
|
715
|
+
indexName,
|
|
716
|
+
queryVector: [1, 0.1, 0],
|
|
717
|
+
filter: { details: { $contains: { color: 'red' } } },
|
|
718
|
+
});
|
|
719
|
+
expect(results.length).toBe(0);
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
it('should fallback to direct equality for non-array, non-string', async () => {
|
|
723
|
+
// Insert a record with a numeric field
|
|
724
|
+
await vectorDB.upsert({
|
|
725
|
+
indexName,
|
|
726
|
+
vectors: [[1, 0.2, 0]],
|
|
727
|
+
metadata: [{ price: 123 }],
|
|
728
|
+
});
|
|
729
|
+
const results = await vectorDB.query({
|
|
730
|
+
indexName,
|
|
731
|
+
queryVector: [1, 0, 0],
|
|
732
|
+
filter: { price: { $contains: 123 } },
|
|
733
|
+
});
|
|
734
|
+
expect(results.length).toBeGreaterThan(0);
|
|
735
|
+
results.forEach(result => {
|
|
736
|
+
expect(result.metadata?.price).toBe(123);
|
|
737
|
+
});
|
|
738
|
+
});
|
|
739
|
+
|
|
663
740
|
it('should filter with $elemMatch operator', async () => {
|
|
664
741
|
const results = await vectorDB.query({
|
|
665
742
|
indexName,
|
|
@@ -810,29 +887,29 @@ describe('PgVector', () => {
|
|
|
810
887
|
});
|
|
811
888
|
});
|
|
812
889
|
|
|
813
|
-
it('should filter with $
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
});
|
|
890
|
+
// it('should filter with $objectContains operator for nested objects', async () => {
|
|
891
|
+
// // First insert a record with nested object
|
|
892
|
+
// await vectorDB.upsert({
|
|
893
|
+
// indexName,
|
|
894
|
+
// vectors: [[1, 0.1, 0]],
|
|
895
|
+
// metadata: [
|
|
896
|
+
// {
|
|
897
|
+
// details: { color: 'red', size: 'large' },
|
|
898
|
+
// category: 'clothing',
|
|
899
|
+
// },
|
|
900
|
+
// ],
|
|
901
|
+
// });
|
|
902
|
+
|
|
903
|
+
// const results = await vectorDB.query({
|
|
904
|
+
// indexName,
|
|
905
|
+
// queryVector: [1, 0.1, 0],
|
|
906
|
+
// filter: { details: { $objectContains: { color: 'red' } } },
|
|
907
|
+
// });
|
|
908
|
+
// expect(results.length).toBeGreaterThan(0);
|
|
909
|
+
// results.forEach(result => {
|
|
910
|
+
// expect(result.metadata?.details.color).toBe('red');
|
|
911
|
+
// });
|
|
912
|
+
// });
|
|
836
913
|
|
|
837
914
|
// String Pattern Tests
|
|
838
915
|
it('should handle exact string matches', async () => {
|
|
@@ -1252,6 +1329,7 @@ describe('PgVector', () => {
|
|
|
1252
1329
|
indexName,
|
|
1253
1330
|
queryVector: [1, 0, 0],
|
|
1254
1331
|
filter: { category: 'electronics' },
|
|
1332
|
+
includeVector: false,
|
|
1255
1333
|
minScore: 0.9,
|
|
1256
1334
|
});
|
|
1257
1335
|
expect(results.length).toBeGreaterThan(0);
|
|
@@ -1403,7 +1481,8 @@ describe('PgVector', () => {
|
|
|
1403
1481
|
indexName,
|
|
1404
1482
|
queryVector: [1, 0, 0],
|
|
1405
1483
|
filter: { category: 'electronics' },
|
|
1406
|
-
|
|
1484
|
+
includeVector: false,
|
|
1485
|
+
minScore: 0.9,
|
|
1407
1486
|
});
|
|
1408
1487
|
expect(results.length).toBeGreaterThan(0);
|
|
1409
1488
|
results.forEach(result => {
|
|
@@ -1457,7 +1536,11 @@ describe('PgVector', () => {
|
|
|
1457
1536
|
const results = await vectorDB.query({
|
|
1458
1537
|
indexName,
|
|
1459
1538
|
queryVector: [1, 0, 0],
|
|
1460
|
-
filter: {
|
|
1539
|
+
filter: {
|
|
1540
|
+
$and: [{ category: 'electronics' }],
|
|
1541
|
+
$or: [{ price: { $lt: 100 } }, { price: { $gt: 20 } }],
|
|
1542
|
+
$nor: [],
|
|
1543
|
+
},
|
|
1461
1544
|
});
|
|
1462
1545
|
expect(results.length).toBeGreaterThan(0);
|
|
1463
1546
|
results.forEach(result => {
|
|
@@ -1477,13 +1560,7 @@ describe('PgVector', () => {
|
|
|
1477
1560
|
const results = await vectorDB.query({
|
|
1478
1561
|
indexName,
|
|
1479
1562
|
queryVector: [1, 0, 0],
|
|
1480
|
-
filter: {
|
|
1481
|
-
tags: {
|
|
1482
|
-
$elemMatch: {
|
|
1483
|
-
$eq: 'value',
|
|
1484
|
-
},
|
|
1485
|
-
},
|
|
1486
|
-
},
|
|
1563
|
+
filter: { tags: { $elemMatch: { $eq: 'value' } } },
|
|
1487
1564
|
});
|
|
1488
1565
|
expect(results).toHaveLength(0); // Should return no results for non-array field
|
|
1489
1566
|
});
|
|
@@ -20,7 +20,7 @@ export type OperatorType =
|
|
|
20
20
|
type FilterOperator = {
|
|
21
21
|
sql: string;
|
|
22
22
|
needsValue: boolean;
|
|
23
|
-
transformValue?: (
|
|
23
|
+
transformValue?: () => any;
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
type OperatorFn = (key: string, paramIndex: number, value?: any) => FilterOperator;
|
|
@@ -103,11 +103,29 @@ export const FILTER_OPERATORS: Record<string, OperatorFn> = {
|
|
|
103
103
|
|
|
104
104
|
// Array Operators
|
|
105
105
|
$in: (key, paramIndex) => ({
|
|
106
|
-
sql: `
|
|
106
|
+
sql: `(
|
|
107
|
+
CASE
|
|
108
|
+
WHEN jsonb_typeof(metadata->'${handleKey(key)}') = 'array' THEN
|
|
109
|
+
EXISTS (
|
|
110
|
+
SELECT 1 FROM jsonb_array_elements_text(metadata->'${handleKey(key)}') as elem
|
|
111
|
+
WHERE elem = ANY($${paramIndex}::text[])
|
|
112
|
+
)
|
|
113
|
+
ELSE metadata#>>'{${handleKey(key)}}' = ANY($${paramIndex}::text[])
|
|
114
|
+
END
|
|
115
|
+
)`,
|
|
107
116
|
needsValue: true,
|
|
108
117
|
}),
|
|
109
118
|
$nin: (key, paramIndex) => ({
|
|
110
|
-
sql: `
|
|
119
|
+
sql: `(
|
|
120
|
+
CASE
|
|
121
|
+
WHEN jsonb_typeof(metadata->'${handleKey(key)}') = 'array' THEN
|
|
122
|
+
NOT EXISTS (
|
|
123
|
+
SELECT 1 FROM jsonb_array_elements_text(metadata->'${handleKey(key)}') as elem
|
|
124
|
+
WHERE elem = ANY($${paramIndex}::text[])
|
|
125
|
+
)
|
|
126
|
+
ELSE metadata#>>'{${handleKey(key)}}' != ALL($${paramIndex}::text[])
|
|
127
|
+
END
|
|
128
|
+
)`,
|
|
111
129
|
needsValue: true,
|
|
112
130
|
}),
|
|
113
131
|
$all: (key, paramIndex) => ({
|
|
@@ -151,14 +169,33 @@ export const FILTER_OPERATORS: Record<string, OperatorFn> = {
|
|
|
151
169
|
needsValue: true,
|
|
152
170
|
}),
|
|
153
171
|
|
|
154
|
-
$contains: (key, paramIndex) =>
|
|
155
|
-
sql
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
|
|
172
|
+
$contains: (key, paramIndex, value: any) => {
|
|
173
|
+
let sql;
|
|
174
|
+
if (Array.isArray(value)) {
|
|
175
|
+
sql = `(metadata->'${handleKey(key)}') ?& $${paramIndex}`;
|
|
176
|
+
} else if (typeof value === 'string') {
|
|
177
|
+
sql = `metadata->>'${handleKey(key)}' ILIKE '%' || $${paramIndex} || '%'`;
|
|
178
|
+
} else {
|
|
179
|
+
sql = `metadata->>'${handleKey(key)}' = $${paramIndex}`;
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
sql,
|
|
183
|
+
needsValue: true,
|
|
184
|
+
transformValue: () => (Array.isArray(value) ? value.map(String) : value),
|
|
185
|
+
};
|
|
186
|
+
},
|
|
187
|
+
/**
|
|
188
|
+
* $objectContains: Postgres-only operator for true JSONB object containment.
|
|
189
|
+
* Usage: { field: { $objectContains: { ...subobject } } }
|
|
190
|
+
*/
|
|
191
|
+
// $objectContains: (key, paramIndex) => ({
|
|
192
|
+
// sql: `metadata @> $${paramIndex}::jsonb`,
|
|
193
|
+
// needsValue: true,
|
|
194
|
+
// transformValue: value => {
|
|
195
|
+
// const parts = key.split('.');
|
|
196
|
+
// return JSON.stringify(parts.reduceRight((value, key) => ({ [key]: value }), value));
|
|
197
|
+
// },
|
|
198
|
+
// }),
|
|
162
199
|
$size: (key: string, paramIndex: number) => ({
|
|
163
200
|
sql: `(
|
|
164
201
|
CASE
|
|
@@ -220,9 +257,7 @@ export function buildFilterQuery(filter: VectorFilter, minScore: number): Filter
|
|
|
220
257
|
const operatorFn = FILTER_OPERATORS[operator as string]!;
|
|
221
258
|
const operatorResult = operatorFn(key, values.length + 1, operatorValue);
|
|
222
259
|
if (operatorResult.needsValue) {
|
|
223
|
-
const transformedValue = operatorResult.transformValue
|
|
224
|
-
? operatorResult.transformValue(operatorValue)
|
|
225
|
-
: operatorValue;
|
|
260
|
+
const transformedValue = operatorResult.transformValue ? operatorResult.transformValue() : operatorValue;
|
|
226
261
|
if (Array.isArray(transformedValue) && operator === '$elemMatch') {
|
|
227
262
|
values.push(...transformedValue);
|
|
228
263
|
} else {
|