@kernl-sdk/pg 0.1.10 → 0.1.12
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 +4 -5
- package/.turbo/turbo-check-types.log +36 -0
- package/CHANGELOG.md +41 -0
- package/README.md +124 -0
- package/dist/__tests__/integration.test.js +81 -1
- package/dist/__tests__/memory-integration.test.d.ts +2 -0
- package/dist/__tests__/memory-integration.test.d.ts.map +1 -0
- package/dist/__tests__/memory-integration.test.js +287 -0
- package/dist/__tests__/memory.test.d.ts +2 -0
- package/dist/__tests__/memory.test.d.ts.map +1 -0
- package/dist/__tests__/memory.test.js +357 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/memory/sql.d.ts +30 -0
- package/dist/memory/sql.d.ts.map +1 -0
- package/dist/memory/sql.js +100 -0
- package/dist/memory/store.d.ts +41 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +114 -0
- package/dist/migrations.d.ts +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +9 -3
- package/dist/pgvector/__tests__/handle.test.d.ts +2 -0
- package/dist/pgvector/__tests__/handle.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/handle.test.js +277 -0
- package/dist/pgvector/__tests__/hit.test.d.ts +2 -0
- package/dist/pgvector/__tests__/hit.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/hit.test.js +134 -0
- package/dist/pgvector/__tests__/integration/document.integration.test.d.ts +7 -0
- package/dist/pgvector/__tests__/integration/document.integration.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/integration/document.integration.test.js +587 -0
- package/dist/pgvector/__tests__/integration/edge.integration.test.d.ts +8 -0
- package/dist/pgvector/__tests__/integration/edge.integration.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/integration/edge.integration.test.js +663 -0
- package/dist/pgvector/__tests__/integration/filters.integration.test.d.ts +8 -0
- package/dist/pgvector/__tests__/integration/filters.integration.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/integration/filters.integration.test.js +609 -0
- package/dist/pgvector/__tests__/integration/lifecycle.integration.test.d.ts +8 -0
- package/dist/pgvector/__tests__/integration/lifecycle.integration.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/integration/lifecycle.integration.test.js +449 -0
- package/dist/pgvector/__tests__/integration/query.integration.test.d.ts +8 -0
- package/dist/pgvector/__tests__/integration/query.integration.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/integration/query.integration.test.js +544 -0
- package/dist/pgvector/__tests__/search.test.d.ts +2 -0
- package/dist/pgvector/__tests__/search.test.d.ts.map +1 -0
- package/dist/pgvector/__tests__/search.test.js +279 -0
- package/dist/pgvector/handle.d.ts +60 -0
- package/dist/pgvector/handle.d.ts.map +1 -0
- package/dist/pgvector/handle.js +213 -0
- package/dist/pgvector/hit.d.ts +10 -0
- package/dist/pgvector/hit.d.ts.map +1 -0
- package/dist/pgvector/hit.js +44 -0
- package/dist/pgvector/index.d.ts +7 -0
- package/dist/pgvector/index.d.ts.map +1 -0
- package/dist/pgvector/index.js +5 -0
- package/dist/pgvector/search.d.ts +60 -0
- package/dist/pgvector/search.d.ts.map +1 -0
- package/dist/pgvector/search.js +227 -0
- package/dist/pgvector/sql/__tests__/limit.test.d.ts +2 -0
- package/dist/pgvector/sql/__tests__/limit.test.d.ts.map +1 -0
- package/dist/pgvector/sql/__tests__/limit.test.js +161 -0
- package/dist/pgvector/sql/__tests__/order.test.d.ts +2 -0
- package/dist/pgvector/sql/__tests__/order.test.d.ts.map +1 -0
- package/dist/pgvector/sql/__tests__/order.test.js +218 -0
- package/dist/pgvector/sql/__tests__/query.test.d.ts +2 -0
- package/dist/pgvector/sql/__tests__/query.test.d.ts.map +1 -0
- package/dist/pgvector/sql/__tests__/query.test.js +392 -0
- package/dist/pgvector/sql/__tests__/select.test.d.ts +2 -0
- package/dist/pgvector/sql/__tests__/select.test.d.ts.map +1 -0
- package/dist/pgvector/sql/__tests__/select.test.js +293 -0
- package/dist/pgvector/sql/__tests__/where.test.d.ts +2 -0
- package/dist/pgvector/sql/__tests__/where.test.d.ts.map +1 -0
- package/dist/pgvector/sql/__tests__/where.test.js +488 -0
- package/dist/pgvector/sql/index.d.ts +7 -0
- package/dist/pgvector/sql/index.d.ts.map +1 -0
- package/dist/pgvector/sql/index.js +6 -0
- package/dist/pgvector/sql/limit.d.ts +8 -0
- package/dist/pgvector/sql/limit.d.ts.map +1 -0
- package/dist/pgvector/sql/limit.js +20 -0
- package/dist/pgvector/sql/order.d.ts +9 -0
- package/dist/pgvector/sql/order.d.ts.map +1 -0
- package/dist/pgvector/sql/order.js +47 -0
- package/dist/pgvector/sql/query.d.ts +46 -0
- package/dist/pgvector/sql/query.d.ts.map +1 -0
- package/dist/pgvector/sql/query.js +54 -0
- package/dist/pgvector/sql/schema.d.ts +16 -0
- package/dist/pgvector/sql/schema.d.ts.map +1 -0
- package/dist/pgvector/sql/schema.js +47 -0
- package/dist/pgvector/sql/select.d.ts +11 -0
- package/dist/pgvector/sql/select.d.ts.map +1 -0
- package/dist/pgvector/sql/select.js +87 -0
- package/dist/pgvector/sql/where.d.ts +8 -0
- package/dist/pgvector/sql/where.d.ts.map +1 -0
- package/dist/pgvector/sql/where.js +137 -0
- package/dist/pgvector/types.d.ts +20 -0
- package/dist/pgvector/types.d.ts.map +1 -0
- package/dist/pgvector/types.js +1 -0
- package/dist/pgvector/utils.d.ts +18 -0
- package/dist/pgvector/utils.d.ts.map +1 -0
- package/dist/pgvector/utils.js +22 -0
- package/dist/postgres.d.ts +19 -26
- package/dist/postgres.d.ts.map +1 -1
- package/dist/postgres.js +15 -27
- package/dist/storage.d.ts +62 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +55 -10
- package/dist/thread/sql.d.ts +38 -0
- package/dist/thread/sql.d.ts.map +1 -0
- package/dist/thread/sql.js +112 -0
- package/dist/thread/store.d.ts +7 -3
- package/dist/thread/store.d.ts.map +1 -1
- package/dist/thread/store.js +46 -105
- package/package.json +8 -5
- package/src/__tests__/integration.test.ts +114 -15
- package/src/__tests__/memory-integration.test.ts +355 -0
- package/src/__tests__/memory.test.ts +428 -0
- package/src/index.ts +19 -3
- package/src/memory/sql.ts +141 -0
- package/src/memory/store.ts +166 -0
- package/src/migrations.ts +13 -3
- package/src/pgvector/README.md +50 -0
- package/src/pgvector/__tests__/handle.test.ts +335 -0
- package/src/pgvector/__tests__/hit.test.ts +165 -0
- package/src/pgvector/__tests__/integration/document.integration.test.ts +717 -0
- package/src/pgvector/__tests__/integration/edge.integration.test.ts +835 -0
- package/src/pgvector/__tests__/integration/filters.integration.test.ts +721 -0
- package/src/pgvector/__tests__/integration/lifecycle.integration.test.ts +570 -0
- package/src/pgvector/__tests__/integration/query.integration.test.ts +667 -0
- package/src/pgvector/__tests__/search.test.ts +366 -0
- package/src/pgvector/handle.ts +285 -0
- package/src/pgvector/hit.ts +56 -0
- package/src/pgvector/index.ts +7 -0
- package/src/pgvector/search.ts +330 -0
- package/src/pgvector/sql/__tests__/limit.test.ts +180 -0
- package/src/pgvector/sql/__tests__/order.test.ts +248 -0
- package/src/pgvector/sql/__tests__/query.test.ts +548 -0
- package/src/pgvector/sql/__tests__/select.test.ts +367 -0
- package/src/pgvector/sql/__tests__/where.test.ts +554 -0
- package/src/pgvector/sql/index.ts +14 -0
- package/src/pgvector/sql/limit.ts +29 -0
- package/src/pgvector/sql/order.ts +55 -0
- package/src/pgvector/sql/query.ts +112 -0
- package/src/pgvector/sql/schema.ts +61 -0
- package/src/pgvector/sql/select.ts +100 -0
- package/src/pgvector/sql/where.ts +152 -0
- package/src/pgvector/types.ts +21 -0
- package/src/pgvector/utils.ts +24 -0
- package/src/postgres.ts +31 -33
- package/src/storage.ts +102 -11
- package/src/thread/sql.ts +159 -0
- package/src/thread/store.ts +58 -127
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Filter,
|
|
3
|
+
OrderBy,
|
|
4
|
+
SearchQuery,
|
|
5
|
+
RankingSignal,
|
|
6
|
+
} from "@kernl-sdk/retrieval";
|
|
7
|
+
|
|
8
|
+
import type { PGIndexConfig } from "../types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Convert QueryOptions to SQL codec inputs.
|
|
12
|
+
*
|
|
13
|
+
* pgvector constraints:
|
|
14
|
+
* - Only single vector signal supported (no multi-signal fusion)
|
|
15
|
+
* - No hybrid (vector + text) in same signal
|
|
16
|
+
* - Filter-only and orderBy-only queries are allowed
|
|
17
|
+
*/
|
|
18
|
+
export function sqlize(query: SearchQuery, config: SqlizeConfig): SqlizedQuery {
|
|
19
|
+
const signals = query.query ?? query.max ?? [];
|
|
20
|
+
|
|
21
|
+
// Validate pgvector constraints
|
|
22
|
+
if (signals.length > 1) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
"pgvector does not support multi-signal fusion. " +
|
|
25
|
+
"Use a single vector signal, or run multiple queries and fuse client-side.",
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (signals.length === 1) {
|
|
30
|
+
const { weight, ...fields } = signals[0];
|
|
31
|
+
let vectorCount = 0;
|
|
32
|
+
let textCount = 0;
|
|
33
|
+
|
|
34
|
+
for (const value of Object.values(fields)) {
|
|
35
|
+
if (value === undefined) continue;
|
|
36
|
+
if (Array.isArray(value)) vectorCount++;
|
|
37
|
+
else if (typeof value === "string") textCount++;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (vectorCount > 1) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
"pgvector does not support multi-vector fusion. " +
|
|
43
|
+
"Use a single vector signal, or run multiple queries and fuse client-side.",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (vectorCount > 0 && textCount > 0) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
"pgvector does not support hybrid (vector + text) fusion. " +
|
|
50
|
+
"Use a single vector signal, or run multiple queries and fuse client-side.",
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
select: {
|
|
57
|
+
pkey: config.pkey,
|
|
58
|
+
signals,
|
|
59
|
+
binding: config.binding,
|
|
60
|
+
include: query.include,
|
|
61
|
+
},
|
|
62
|
+
where: { filter: query.filter },
|
|
63
|
+
order: {
|
|
64
|
+
signals,
|
|
65
|
+
orderBy: query.orderBy,
|
|
66
|
+
binding: config.binding,
|
|
67
|
+
schema: config.schema,
|
|
68
|
+
table: config.table,
|
|
69
|
+
},
|
|
70
|
+
limit: { topK: query.topK ?? 10, offset: query.offset ?? 0 },
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface SqlizedQuery {
|
|
75
|
+
select: SelectInput;
|
|
76
|
+
where: Omit<WhereInput, "startIdx">;
|
|
77
|
+
order: OrderInput;
|
|
78
|
+
limit: Omit<LimitInput, "startIdx">;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface SqlizeConfig {
|
|
82
|
+
pkey: string;
|
|
83
|
+
schema: string;
|
|
84
|
+
table: string;
|
|
85
|
+
binding?: PGIndexConfig;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface SelectInput {
|
|
89
|
+
pkey: string;
|
|
90
|
+
signals: RankingSignal[];
|
|
91
|
+
binding?: PGIndexConfig;
|
|
92
|
+
include?: string[] | boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface WhereInput {
|
|
96
|
+
filter?: Filter;
|
|
97
|
+
startIdx: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface OrderInput {
|
|
101
|
+
signals: RankingSignal[];
|
|
102
|
+
orderBy?: OrderBy;
|
|
103
|
+
binding?: PGIndexConfig;
|
|
104
|
+
schema?: string;
|
|
105
|
+
table?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface LimitInput {
|
|
109
|
+
topK: number;
|
|
110
|
+
offset: number;
|
|
111
|
+
startIdx: number;
|
|
112
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema conversion codecs for pgvector.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Codec } from "@kernl-sdk/shared/lib";
|
|
6
|
+
import type {
|
|
7
|
+
FieldSchema,
|
|
8
|
+
VectorFieldSchema,
|
|
9
|
+
ScalarFieldSchema,
|
|
10
|
+
} from "@kernl-sdk/retrieval";
|
|
11
|
+
|
|
12
|
+
type ScalarType = ScalarFieldSchema["type"];
|
|
13
|
+
type Similarity = VectorFieldSchema["similarity"];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Mapping from kernl scalar types to Postgres column types.
|
|
17
|
+
*/
|
|
18
|
+
const SCALAR_TO_PG: Record<string, string> = {
|
|
19
|
+
string: "TEXT",
|
|
20
|
+
int: "INTEGER",
|
|
21
|
+
bigint: "BIGINT",
|
|
22
|
+
float: "DOUBLE PRECISION",
|
|
23
|
+
boolean: "BOOLEAN",
|
|
24
|
+
date: "TIMESTAMPTZ",
|
|
25
|
+
object: "JSONB",
|
|
26
|
+
geopoint: "POINT",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Codec for converting FieldSchema to Postgres column type.
|
|
31
|
+
*/
|
|
32
|
+
export const FIELD_TYPE: Codec<FieldSchema, string> = {
|
|
33
|
+
encode: (schema) => {
|
|
34
|
+
if (schema.type === "vector") {
|
|
35
|
+
return `vector(${schema.dimensions})`;
|
|
36
|
+
}
|
|
37
|
+
return SCALAR_TO_PG[schema.type] ?? "TEXT";
|
|
38
|
+
},
|
|
39
|
+
decode: () => {
|
|
40
|
+
throw new Error("FIELD_TYPE.decode not implemented");
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Mapping from similarity metric to pgvector operator class.
|
|
46
|
+
*/
|
|
47
|
+
const SIMILARITY_TO_OPCLASS: Record<string, string> = {
|
|
48
|
+
cosine: "vector_cosine_ops",
|
|
49
|
+
euclidean: "vector_l2_ops",
|
|
50
|
+
dot_product: "vector_ip_ops",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Codec for converting similarity metric to pgvector HNSW operator class.
|
|
55
|
+
*/
|
|
56
|
+
export const SIMILARITY: Codec<Similarity | undefined, string> = {
|
|
57
|
+
encode: (similarity) => SIMILARITY_TO_OPCLASS[similarity ?? "cosine"],
|
|
58
|
+
decode: () => {
|
|
59
|
+
throw new Error("SIMILARITY.decode not implemented");
|
|
60
|
+
},
|
|
61
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Codec } from "@kernl-sdk/shared/lib";
|
|
2
|
+
import type { SelectInput } from "./query";
|
|
3
|
+
|
|
4
|
+
export interface SQLClause {
|
|
5
|
+
sql: string;
|
|
6
|
+
params: unknown[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* pgvector distance operators by similarity metric.
|
|
11
|
+
*/
|
|
12
|
+
const DISTANCE_OPS = {
|
|
13
|
+
cosine: "<=>",
|
|
14
|
+
euclidean: "<->",
|
|
15
|
+
dot_product: "<#>",
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Codec for building SELECT clause with score expression.
|
|
20
|
+
*/
|
|
21
|
+
export const SQL_SELECT: Codec<SelectInput, SQLClause> = {
|
|
22
|
+
encode({ pkey, signals, binding, include }) {
|
|
23
|
+
const parts: string[] = [`"${pkey}" as id`];
|
|
24
|
+
const params: unknown[] = [];
|
|
25
|
+
|
|
26
|
+
// find vector signal for scoring
|
|
27
|
+
const vsig = signals.find((s) => {
|
|
28
|
+
for (const [key, val] of Object.entries(s)) {
|
|
29
|
+
if (key !== "weight" && Array.isArray(val)) return true;
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (vsig) {
|
|
35
|
+
// Extract vector field and value
|
|
36
|
+
let vecField: string | null = null;
|
|
37
|
+
let vecValue: number[] | null = null;
|
|
38
|
+
|
|
39
|
+
for (const [key, val] of Object.entries(vsig)) {
|
|
40
|
+
if (key !== "weight" && Array.isArray(val)) {
|
|
41
|
+
vecField = key;
|
|
42
|
+
vecValue = val as number[];
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (vecField && vecValue) {
|
|
48
|
+
const col = binding?.fields[vecField]?.column ?? vecField;
|
|
49
|
+
const similarity = binding?.fields[vecField]?.similarity ?? "cosine";
|
|
50
|
+
const op = DISTANCE_OPS[similarity];
|
|
51
|
+
|
|
52
|
+
params.push(JSON.stringify(vecValue));
|
|
53
|
+
|
|
54
|
+
// score expression varies by metric
|
|
55
|
+
switch (similarity) {
|
|
56
|
+
case "cosine":
|
|
57
|
+
parts.push(`1 - ("${col}" ${op} $1::vector) as score`);
|
|
58
|
+
break;
|
|
59
|
+
case "euclidean":
|
|
60
|
+
parts.push(`1 / (1 + ("${col}" ${op} $1::vector)) as score`);
|
|
61
|
+
break;
|
|
62
|
+
case "dot_product":
|
|
63
|
+
parts.push(`-("${col}" ${op} $1::vector) as score`);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
parts.push("1 as score");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Select columns based on include
|
|
72
|
+
// Note: We always select a calculated "score", so skip any document field named "score"
|
|
73
|
+
// to avoid ambiguity and ensure hit.score is always the similarity score.
|
|
74
|
+
if (binding && include !== false) {
|
|
75
|
+
const fields = Object.entries(binding.fields);
|
|
76
|
+
if (Array.isArray(include)) {
|
|
77
|
+
// Select only specified fields
|
|
78
|
+
for (const fieldName of include) {
|
|
79
|
+
if (fieldName === "score") continue; // Skip to avoid overriding calculated score
|
|
80
|
+
const field = binding.fields[fieldName];
|
|
81
|
+
if (field) {
|
|
82
|
+
parts.push(`"${field.column}"`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
// include: true or undefined → select all (except score)
|
|
87
|
+
for (const [fieldName, field] of fields) {
|
|
88
|
+
if (fieldName === "score") continue; // Skip to avoid overriding calculated score
|
|
89
|
+
parts.push(`"${field.column}"`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { sql: parts.join(", "), params };
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
decode() {
|
|
98
|
+
throw new Error("SQL_SELECT.decode not implemented");
|
|
99
|
+
},
|
|
100
|
+
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { Codec } from "@kernl-sdk/shared/lib";
|
|
2
|
+
import type { Filter, FieldOps } from "@kernl-sdk/retrieval";
|
|
3
|
+
import type { WhereInput } from "./query";
|
|
4
|
+
import type { SQLClause } from "./select";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Codec for building WHERE clause from MongoDB-style filter.
|
|
8
|
+
*/
|
|
9
|
+
export const SQL_WHERE: Codec<WhereInput, SQLClause> = {
|
|
10
|
+
encode({ filter, startIdx }) {
|
|
11
|
+
if (!filter) {
|
|
12
|
+
return { sql: "", params: [] };
|
|
13
|
+
}
|
|
14
|
+
return encodeFilter(filter, startIdx);
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
decode() {
|
|
18
|
+
throw new Error("SQL_WHERE.decode not implemented");
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Encode a filter to SQL (recursive).
|
|
24
|
+
*/
|
|
25
|
+
function encodeFilter(filter: Filter, startIdx: number): SQLClause {
|
|
26
|
+
const conditions: string[] = [];
|
|
27
|
+
const params: unknown[] = [];
|
|
28
|
+
let idx = startIdx;
|
|
29
|
+
|
|
30
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
31
|
+
if (value === undefined) continue;
|
|
32
|
+
|
|
33
|
+
// $and
|
|
34
|
+
if (key === "$and" && Array.isArray(value)) {
|
|
35
|
+
const parts: string[] = [];
|
|
36
|
+
for (const sub of value as Filter[]) {
|
|
37
|
+
const clause = encodeFilter(sub, idx);
|
|
38
|
+
if (clause.sql) {
|
|
39
|
+
parts.push(`(${clause.sql})`);
|
|
40
|
+
params.push(...clause.params);
|
|
41
|
+
idx += clause.params.length;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (parts.length > 0) {
|
|
45
|
+
conditions.push(`(${parts.join(" AND ")})`);
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// $or
|
|
51
|
+
if (key === "$or" && Array.isArray(value)) {
|
|
52
|
+
const parts: string[] = [];
|
|
53
|
+
for (const sub of value as Filter[]) {
|
|
54
|
+
const clause = encodeFilter(sub, idx);
|
|
55
|
+
if (clause.sql) {
|
|
56
|
+
parts.push(`(${clause.sql})`);
|
|
57
|
+
params.push(...clause.params);
|
|
58
|
+
idx += clause.params.length;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (parts.length > 0) {
|
|
62
|
+
conditions.push(`(${parts.join(" OR ")})`);
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// $not
|
|
68
|
+
if (key === "$not" && typeof value === "object" && !Array.isArray(value)) {
|
|
69
|
+
const clause = encodeFilter(value as Filter, idx);
|
|
70
|
+
if (clause.sql) {
|
|
71
|
+
conditions.push(`NOT (${clause.sql})`);
|
|
72
|
+
params.push(...clause.params);
|
|
73
|
+
idx += clause.params.length;
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// field operators
|
|
79
|
+
if (isFieldOps(value)) {
|
|
80
|
+
const ops = value;
|
|
81
|
+
|
|
82
|
+
if (ops.$eq !== undefined) {
|
|
83
|
+
conditions.push(`"${key}" = $${idx++}`);
|
|
84
|
+
params.push(ops.$eq);
|
|
85
|
+
}
|
|
86
|
+
if (ops.$neq !== undefined) {
|
|
87
|
+
conditions.push(`"${key}" != $${idx++}`);
|
|
88
|
+
params.push(ops.$neq);
|
|
89
|
+
}
|
|
90
|
+
if (ops.$gt !== undefined) {
|
|
91
|
+
conditions.push(`"${key}" > $${idx++}`);
|
|
92
|
+
params.push(ops.$gt);
|
|
93
|
+
}
|
|
94
|
+
if (ops.$gte !== undefined) {
|
|
95
|
+
conditions.push(`"${key}" >= $${idx++}`);
|
|
96
|
+
params.push(ops.$gte);
|
|
97
|
+
}
|
|
98
|
+
if (ops.$lt !== undefined) {
|
|
99
|
+
conditions.push(`"${key}" < $${idx++}`);
|
|
100
|
+
params.push(ops.$lt);
|
|
101
|
+
}
|
|
102
|
+
if (ops.$lte !== undefined) {
|
|
103
|
+
conditions.push(`"${key}" <= $${idx++}`);
|
|
104
|
+
params.push(ops.$lte);
|
|
105
|
+
}
|
|
106
|
+
if (ops.$in !== undefined) {
|
|
107
|
+
conditions.push(`"${key}" = ANY($${idx++})`);
|
|
108
|
+
params.push(ops.$in);
|
|
109
|
+
}
|
|
110
|
+
if (ops.$nin !== undefined) {
|
|
111
|
+
conditions.push(`"${key}" != ALL($${idx++})`);
|
|
112
|
+
params.push(ops.$nin);
|
|
113
|
+
}
|
|
114
|
+
if (ops.$contains !== undefined) {
|
|
115
|
+
conditions.push(`"${key}" ILIKE $${idx++}`);
|
|
116
|
+
params.push(`%${ops.$contains}%`);
|
|
117
|
+
}
|
|
118
|
+
if (ops.$startsWith !== undefined) {
|
|
119
|
+
conditions.push(`"${key}" ILIKE $${idx++}`);
|
|
120
|
+
params.push(`${ops.$startsWith}%`);
|
|
121
|
+
}
|
|
122
|
+
if (ops.$endsWith !== undefined) {
|
|
123
|
+
conditions.push(`"${key}" ILIKE $${idx++}`);
|
|
124
|
+
params.push(`%${ops.$endsWith}`);
|
|
125
|
+
}
|
|
126
|
+
if (ops.$exists !== undefined) {
|
|
127
|
+
conditions.push(
|
|
128
|
+
ops.$exists ? `"${key}" IS NOT NULL` : `"${key}" IS NULL`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Equality shorthand: { status: "active" }
|
|
135
|
+
if (value === null) {
|
|
136
|
+
conditions.push(`"${key}" IS NULL`);
|
|
137
|
+
} else {
|
|
138
|
+
conditions.push(`"${key}" = $${idx++}`);
|
|
139
|
+
params.push(value);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { sql: conditions.join(" AND "), params };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Check if value is an operator object (has $-prefixed keys).
|
|
148
|
+
*/
|
|
149
|
+
function isFieldOps(value: unknown): value is FieldOps {
|
|
150
|
+
if (typeof value !== "object" || value === null) return false;
|
|
151
|
+
return Object.keys(value).some((k) => k.startsWith("$"));
|
|
152
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FieldSchema } from "@kernl-sdk/retrieval";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mapping from a logical field name to a Postgres column.
|
|
5
|
+
*/
|
|
6
|
+
export interface PGFieldBinding {
|
|
7
|
+
column: string;
|
|
8
|
+
type: FieldSchema["type"];
|
|
9
|
+
dimensions?: number;
|
|
10
|
+
similarity?: "cosine" | "euclidean" | "dot_product";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Configuration for binding a Postgres table as a search index.
|
|
15
|
+
*/
|
|
16
|
+
export interface PGIndexConfig {
|
|
17
|
+
schema: string;
|
|
18
|
+
table: string;
|
|
19
|
+
pkey: string;
|
|
20
|
+
fields: Record<string, PGFieldBinding>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pgvector utility functions.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parse index id into schema and table.
|
|
7
|
+
*
|
|
8
|
+
* - "docs" → { schema: "public", table: "docs" }
|
|
9
|
+
* - "analytics.events" → { schema: "analytics", table: "events" }
|
|
10
|
+
*/
|
|
11
|
+
export function parseIndexId(id: string): { schema: string; table: string } {
|
|
12
|
+
const parts = id.split(".");
|
|
13
|
+
if (parts.length === 2) {
|
|
14
|
+
return { schema: parts[0], table: parts[1] };
|
|
15
|
+
}
|
|
16
|
+
return { schema: "public", table: id };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if a value is a numeric array (vector).
|
|
21
|
+
*/
|
|
22
|
+
export function isVector(val: unknown): val is number[] {
|
|
23
|
+
return Array.isArray(val) && val.length > 0 && typeof val[0] === "number";
|
|
24
|
+
}
|
package/src/postgres.ts
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
import { Pool } from "pg";
|
|
2
2
|
import type { KernlStorage } from "kernl";
|
|
3
3
|
|
|
4
|
-
import { PGStorage } from "./storage";
|
|
4
|
+
import { PGStorage, type PGVectorConfig } from "./storage";
|
|
5
|
+
import { PGSearchIndex } from "./pgvector/search";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
* PostgreSQL
|
|
8
|
+
* Create a PostgreSQL storage adapter for Kernl.
|
|
8
9
|
*/
|
|
9
|
-
export
|
|
10
|
+
export function postgres(config: PostgresConfig): KernlStorage {
|
|
11
|
+
const p = pool(config);
|
|
12
|
+
return new PGStorage({ pool: p, vector: config.vector });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a pgvector-backed search index.
|
|
17
|
+
*/
|
|
18
|
+
export function pgvector(config: ConnectionConfig) {
|
|
19
|
+
const p = pool(config);
|
|
20
|
+
return new PGSearchIndex({ pool: p });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Connection options for PostgreSQL.
|
|
25
|
+
*/
|
|
26
|
+
export type ConnectionConfig =
|
|
10
27
|
| { pool: Pool }
|
|
11
28
|
| { connstr: string }
|
|
12
29
|
| {
|
|
@@ -18,39 +35,22 @@ export type PostgresConfig =
|
|
|
18
35
|
};
|
|
19
36
|
|
|
20
37
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* @param config - Connection configuration (pool, connection string, or credentials)
|
|
24
|
-
* @returns KernlStorage instance backed by PostgreSQL
|
|
25
|
-
*
|
|
26
|
-
* @example
|
|
27
|
-
* ```ts
|
|
28
|
-
* // with connection string
|
|
29
|
-
* const storage = postgres({ connstr: "postgresql://localhost/mydb" });
|
|
30
|
-
*
|
|
31
|
-
* // with connection options
|
|
32
|
-
* const storage = postgres({
|
|
33
|
-
* host: "localhost",
|
|
34
|
-
* port: 5432,
|
|
35
|
-
* database: "mydb",
|
|
36
|
-
* user: "user",
|
|
37
|
-
* password: "password"
|
|
38
|
-
* });
|
|
39
|
-
*
|
|
40
|
-
* // existing pool
|
|
41
|
-
* const pool = new Pool({ ... });
|
|
42
|
-
* const storage = postgres({ pool });
|
|
43
|
-
* ```
|
|
38
|
+
* PostgreSQL storage configuration.
|
|
44
39
|
*/
|
|
45
|
-
export
|
|
46
|
-
|
|
40
|
+
export type PostgresConfig = ConnectionConfig & {
|
|
41
|
+
/**
|
|
42
|
+
* Enable pgvector support for semantic search.
|
|
43
|
+
*/
|
|
44
|
+
vector?: boolean | PGVectorConfig;
|
|
45
|
+
};
|
|
47
46
|
|
|
47
|
+
function pool(config: ConnectionConfig): Pool {
|
|
48
48
|
if ("pool" in config) {
|
|
49
|
-
|
|
49
|
+
return config.pool;
|
|
50
50
|
} else if ("connstr" in config) {
|
|
51
|
-
|
|
51
|
+
return new Pool({ connectionString: config.connstr });
|
|
52
52
|
} else {
|
|
53
|
-
|
|
53
|
+
return new Pool({
|
|
54
54
|
host: config.host,
|
|
55
55
|
port: config.port,
|
|
56
56
|
database: config.database,
|
|
@@ -58,6 +58,4 @@ export function postgres(config: PostgresConfig): KernlStorage {
|
|
|
58
58
|
password: config.password,
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
|
-
|
|
62
|
-
return new PGStorage({ pool });
|
|
63
61
|
}
|