sonamu 0.7.10 → 0.7.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/dist/api/config.d.ts +10 -3
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +2 -1
- package/dist/api/sonamu.d.ts +4 -0
- package/dist/api/sonamu.d.ts.map +1 -1
- package/dist/api/sonamu.js +36 -2
- package/dist/bin/cli.js +121 -117
- package/dist/database/base-model.d.ts +10 -50
- package/dist/database/base-model.d.ts.map +1 -1
- package/dist/database/base-model.js +19 -84
- package/dist/database/base-model.types.d.ts +4 -4
- package/dist/database/base-model.types.d.ts.map +1 -1
- package/dist/database/base-model.types.js +1 -1
- package/dist/database/db.d.ts +1 -0
- package/dist/database/db.d.ts.map +1 -1
- package/dist/database/db.js +24 -13
- package/dist/database/puri-subset.test-d.js +1 -1
- package/dist/database/puri-subset.types.d.ts +1 -0
- package/dist/database/puri-subset.types.d.ts.map +1 -1
- package/dist/database/puri-subset.types.js +2 -2
- package/dist/database/puri.d.ts +96 -1
- package/dist/database/puri.d.ts.map +1 -1
- package/dist/database/puri.js +214 -2
- package/dist/database/puri.types.d.ts +60 -5
- package/dist/database/puri.types.d.ts.map +1 -1
- package/dist/database/puri.types.js +2 -3
- package/dist/database/puri.types.test-d.js +1 -1
- package/dist/database/upsert-builder.d.ts +3 -1
- package/dist/database/upsert-builder.d.ts.map +1 -1
- package/dist/database/upsert-builder.js +19 -4
- package/dist/entity/entity-manager.d.ts +5 -4
- package/dist/entity/entity-manager.d.ts.map +1 -1
- package/dist/entity/entity-manager.js +8 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/migration/code-generation.d.ts.map +1 -1
- package/dist/migration/code-generation.js +33 -2
- package/dist/migration/postgresql-schema-reader.d.ts.map +1 -1
- package/dist/migration/postgresql-schema-reader.js +53 -22
- package/dist/naite/messaging-types.d.ts.map +1 -1
- package/dist/naite/messaging-types.js +1 -1
- package/dist/naite/naite-reporter.d.ts +7 -4
- package/dist/naite/naite-reporter.d.ts.map +1 -1
- package/dist/naite/naite-reporter.js +45 -21
- package/dist/naite/naite.js +2 -2
- package/dist/stream/sse.d.ts +2 -6
- package/dist/stream/sse.d.ts.map +1 -1
- package/dist/stream/sse.js +9 -3
- package/dist/syncer/api-parser.js +5 -1
- package/dist/syncer/file-patterns.d.ts +1 -1
- package/dist/syncer/file-patterns.d.ts.map +1 -1
- package/dist/syncer/file-patterns.js +6 -5
- package/dist/syncer/module-loader.d.ts +5 -0
- package/dist/syncer/module-loader.d.ts.map +1 -1
- package/dist/syncer/module-loader.js +17 -1
- package/dist/syncer/syncer.d.ts +3 -0
- package/dist/syncer/syncer.d.ts.map +1 -1
- package/dist/syncer/syncer.js +12 -2
- package/dist/tasks/decorator.d.ts +26 -0
- package/dist/tasks/decorator.d.ts.map +1 -0
- package/dist/tasks/decorator.js +28 -0
- package/dist/tasks/step-wrapper.d.ts +18 -0
- package/dist/tasks/step-wrapper.d.ts.map +1 -0
- package/dist/tasks/step-wrapper.js +38 -0
- package/dist/tasks/workflow-manager.d.ts +40 -0
- package/dist/tasks/workflow-manager.d.ts.map +1 -0
- package/dist/tasks/workflow-manager.js +193 -0
- package/dist/template/implementations/generated.template.d.ts.map +1 -1
- package/dist/template/implementations/generated.template.js +7 -3
- package/dist/template/implementations/model.template.js +2 -2
- package/dist/template/zod-converter.d.ts.map +1 -1
- package/dist/template/zod-converter.js +4 -2
- package/dist/types/types.d.ts +28 -11
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/types.js +18 -2
- package/dist/utils/console-util.js +2 -2
- package/dist/utils/formatter.d.ts.map +1 -1
- package/dist/utils/formatter.js +10 -2
- package/dist/utils/model.d.ts +9 -2
- package/dist/utils/model.d.ts.map +1 -1
- package/dist/utils/model.js +16 -1
- package/dist/utils/type-utils.d.ts.map +1 -1
- package/dist/utils/type-utils.js +3 -1
- package/dist/vector/embedding.d.ts +2 -5
- package/dist/vector/embedding.d.ts.map +1 -1
- package/dist/vector/embedding.js +3 -7
- package/dist/vector/types.d.ts.map +1 -1
- package/dist/vector/types.js +1 -1
- package/package.json +4 -2
- package/src/api/config.ts +15 -8
- package/src/api/sonamu.ts +43 -2
- package/src/bin/cli.ts +58 -54
- package/src/database/base-model.ts +21 -128
- package/src/database/base-model.types.ts +3 -4
- package/src/database/db.ts +28 -18
- package/src/database/puri-subset.test-d.ts +1 -0
- package/src/database/puri-subset.types.ts +2 -0
- package/src/database/puri.ts +292 -1
- package/src/database/puri.types.test-d.ts +1 -1
- package/src/database/puri.types.ts +81 -7
- package/src/database/upsert-builder.ts +27 -9
- package/src/entity/entity-manager.ts +9 -0
- package/src/index.ts +1 -1
- package/src/migration/code-generation.ts +40 -1
- package/src/migration/postgresql-schema-reader.ts +53 -22
- package/src/naite/messaging-types.ts +43 -44
- package/src/naite/naite-reporter.ts +51 -20
- package/src/naite/naite.ts +1 -1
- package/src/shared/app.shared.ts.txt +13 -0
- package/src/shared/web.shared.ts.txt +13 -0
- package/src/stream/sse.ts +15 -3
- package/src/syncer/api-parser.ts +4 -0
- package/src/syncer/file-patterns.ts +11 -9
- package/src/syncer/module-loader.ts +35 -0
- package/src/syncer/syncer.ts +14 -0
- package/src/tasks/decorator.ts +71 -0
- package/src/tasks/step-wrapper.ts +84 -0
- package/src/tasks/workflow-manager.ts +330 -0
- package/src/template/implementations/generated.template.ts +19 -6
- package/src/template/implementations/model.template.ts +1 -1
- package/src/template/zod-converter.ts +3 -0
- package/src/types/types.ts +23 -4
- package/src/utils/console-util.ts +1 -1
- package/src/utils/formatter.ts +8 -1
- package/src/utils/model.ts +26 -2
- package/src/utils/type-utils.ts +2 -0
- package/src/vector/embedding.ts +2 -8
- package/src/vector/types.ts +1 -2
- package/dist/vector/vector-search.d.ts +0 -47
- package/dist/vector/vector-search.d.ts.map +0 -1
- package/dist/vector/vector-search.js +0 -176
- package/src/vector/vector-search.ts +0 -261
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Enhancer, SubsetQuery 교집합 등 Model 계층에서 필요한 타입 정의.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import type { ListResult } from "..";
|
|
10
11
|
import type { DatabaseSchemaExtend } from "../types/types";
|
|
11
12
|
import type { Puri } from "./puri";
|
|
12
13
|
import type { PuriSubsetFn } from "./puri-subset.types";
|
|
@@ -151,7 +152,5 @@ export type ExecuteSubsetQueryParams<
|
|
|
151
152
|
export type ExecuteSubsetQueryResult<
|
|
152
153
|
TSubsetMapping extends Record<string, any>,
|
|
153
154
|
T extends string,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
total: number;
|
|
157
|
-
};
|
|
155
|
+
LP extends { queryMode?: "list" | "count" | "both" },
|
|
156
|
+
> = ListResult<LP, TSubsetMapping[T]>;
|
package/src/database/db.ts
CHANGED
|
@@ -56,30 +56,40 @@ export class DBClass {
|
|
|
56
56
|
const instanceName = which === "w" ? "wdb" : "rdb";
|
|
57
57
|
|
|
58
58
|
if (!this[instanceName]) {
|
|
59
|
-
|
|
60
|
-
switch (process.env.NODE_ENV ?? "development") {
|
|
61
|
-
case "development":
|
|
62
|
-
case "staging":
|
|
63
|
-
config =
|
|
64
|
-
which === "w"
|
|
65
|
-
? dbConfig.development_master
|
|
66
|
-
: (dbConfig.development_slave ?? dbConfig.development_master);
|
|
67
|
-
break;
|
|
68
|
-
case "production":
|
|
69
|
-
config =
|
|
70
|
-
which === "w"
|
|
71
|
-
? dbConfig.production_master
|
|
72
|
-
: (dbConfig.production_slave ?? dbConfig.production_master);
|
|
73
|
-
break;
|
|
74
|
-
default:
|
|
75
|
-
throw new Error(`현재 ENV ${process.env.NODE_ENV}에는 설정 가능한 DB설정이 없습니다.`);
|
|
76
|
-
}
|
|
59
|
+
const config = this.getDBConfig(which);
|
|
77
60
|
this[instanceName] = knex(config);
|
|
78
61
|
}
|
|
79
62
|
|
|
80
63
|
return this[instanceName];
|
|
81
64
|
}
|
|
82
65
|
|
|
66
|
+
getDBConfig(which: DBPreset): Knex.Config {
|
|
67
|
+
const dbConfig = Sonamu.dbConfig;
|
|
68
|
+
if (process.env.NODE_ENV === "test") {
|
|
69
|
+
return {
|
|
70
|
+
...dbConfig.test,
|
|
71
|
+
// 단일 풀
|
|
72
|
+
pool: {
|
|
73
|
+
min: 1,
|
|
74
|
+
max: 1,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
switch (process.env.NODE_ENV ?? "development") {
|
|
79
|
+
case "development":
|
|
80
|
+
case "staging":
|
|
81
|
+
return which === "w"
|
|
82
|
+
? dbConfig.development_master
|
|
83
|
+
: (dbConfig.development_slave ?? dbConfig.development_master);
|
|
84
|
+
case "production":
|
|
85
|
+
return which === "w"
|
|
86
|
+
? dbConfig.production_master
|
|
87
|
+
: (dbConfig.production_slave ?? dbConfig.production_master);
|
|
88
|
+
default:
|
|
89
|
+
throw new Error(`현재 ENV ${process.env.NODE_ENV}에는 설정 가능한 DB설정이 없습니다.`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
83
93
|
async destroy(): Promise<void> {
|
|
84
94
|
if (this.wdb !== undefined) {
|
|
85
95
|
await this.wdb.destroy();
|
|
@@ -4,6 +4,7 @@ import type { Puri } from "./puri";
|
|
|
4
4
|
import type { Hydrate, InferAllSubsets, LoadersResult } from "./puri-subset.types";
|
|
5
5
|
import type { PuriWrapper } from "./puri-wrapper";
|
|
6
6
|
|
|
7
|
+
/** biome-ignore lint/suspicious/noExplicitAny: Puri Subset 타입 시스템에서 any를 허용함 */
|
|
7
8
|
type MockPuri<T> = Puri<DatabaseSchemaExtend, any, T>;
|
|
8
9
|
|
|
9
10
|
describe("Hydrate", () => {
|
package/src/database/puri.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import assert from "assert";
|
|
5
5
|
import chalk from "chalk";
|
|
6
|
+
import inflection from "inflection";
|
|
6
7
|
import type { Knex } from "knex";
|
|
7
8
|
import { Naite } from "../naite/naite";
|
|
8
9
|
import type {
|
|
@@ -24,6 +25,11 @@ import type {
|
|
|
24
25
|
SelectObject,
|
|
25
26
|
SingleTableValue,
|
|
26
27
|
SqlExpression,
|
|
28
|
+
TsHighlightOptions,
|
|
29
|
+
TsQueryConfig,
|
|
30
|
+
TsQueryOptions,
|
|
31
|
+
TsRankOptions,
|
|
32
|
+
VectorColumns,
|
|
27
33
|
WhereCondition,
|
|
28
34
|
WhereOperator,
|
|
29
35
|
} from "./puri.types";
|
|
@@ -124,6 +130,9 @@ export class Puri<TSchema, TTables extends Record<string, any>, TResult> {
|
|
|
124
130
|
static rawString(sql: string): SqlExpression<"string"> {
|
|
125
131
|
return { _type: "sql_expression", _return: "string", _sql: sql };
|
|
126
132
|
}
|
|
133
|
+
static rawStringArray(sql: string): SqlExpression<"string[]"> {
|
|
134
|
+
return { _type: "sql_expression", _return: "string[]", _sql: sql };
|
|
135
|
+
}
|
|
127
136
|
static rawNumber(sql: string): SqlExpression<"number"> {
|
|
128
137
|
return { _type: "sql_expression", _return: "number", _sql: sql };
|
|
129
138
|
}
|
|
@@ -134,6 +143,109 @@ export class Puri<TSchema, TTables extends Record<string, any>, TResult> {
|
|
|
134
143
|
return { _type: "sql_expression", _return: "date", _sql: sql };
|
|
135
144
|
}
|
|
136
145
|
|
|
146
|
+
/**
|
|
147
|
+
* FTS 검색어 하이라이팅
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* .select({
|
|
151
|
+
* title: Puri.highlight("posts.title", search),
|
|
152
|
+
* content: Puri.highlight("posts.content", search, {
|
|
153
|
+
* startSel: "<mark>",
|
|
154
|
+
* stopSel: "</mark>",
|
|
155
|
+
* maxFragments: 3,
|
|
156
|
+
* }),
|
|
157
|
+
* })
|
|
158
|
+
*/
|
|
159
|
+
static tsHighlight(
|
|
160
|
+
column: string,
|
|
161
|
+
query: string,
|
|
162
|
+
_options?: TsHighlightOptions,
|
|
163
|
+
): SqlExpression<"string"> {
|
|
164
|
+
const { parser = "websearch_to_tsquery", config = "simple", ...options } = _options ?? {};
|
|
165
|
+
|
|
166
|
+
const hlOptionParts = Object.entries(options).map(([key, value]) => {
|
|
167
|
+
return `${inflection.camelize(key)}=${value}`;
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const hlOptions = hlOptionParts.length > 0 ? `, '${hlOptionParts.join(", ")}'` : "";
|
|
171
|
+
|
|
172
|
+
// TODO: rawBinding 메서드 만들어서 XSS 방지
|
|
173
|
+
return Puri.rawString(
|
|
174
|
+
`ts_headline('${config}', ${column}, ${parser}('${config}', '${query}')${hlOptions})`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ts_rank
|
|
179
|
+
static tsRank(column: string, query: string, options?: TsRankOptions): SqlExpression<"number"> {
|
|
180
|
+
return Puri._tsRank("ts_rank", column, query, options);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ts_rank_cd
|
|
184
|
+
static tsRankCd(column: string, query: string, options?: TsRankOptions): SqlExpression<"number"> {
|
|
185
|
+
return Puri._tsRank("ts_rank_cd", column, query, options);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
static _tsRank(
|
|
189
|
+
type: "ts_rank" | "ts_rank_cd",
|
|
190
|
+
column: string,
|
|
191
|
+
query: string,
|
|
192
|
+
options?: TsRankOptions,
|
|
193
|
+
): SqlExpression<"number"> {
|
|
194
|
+
const {
|
|
195
|
+
parser = "websearch_to_tsquery",
|
|
196
|
+
config = "simple",
|
|
197
|
+
normalization,
|
|
198
|
+
weights,
|
|
199
|
+
} = options ?? {};
|
|
200
|
+
|
|
201
|
+
const weightClause = weights ? `ARRAY[${weights.join(", ")}], ` : "";
|
|
202
|
+
const normalizationClause = normalization ? `, ${normalization}` : "";
|
|
203
|
+
|
|
204
|
+
return Puri.rawNumber(
|
|
205
|
+
`${type}(${weightClause}${column}, ${parser}('${config}', '${query}')${normalizationClause})`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* PGroonga FullText 인덱스 검색 점수
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* .select({
|
|
214
|
+
* score: Puri.score(),
|
|
215
|
+
* })
|
|
216
|
+
*/
|
|
217
|
+
static score(): SqlExpression<"number"> {
|
|
218
|
+
return Puri.rawNumber("pgroonga_score(tableoid, ctid)");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* PGroonga FullText 인덱스 검색 하이라이팅
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* .select({
|
|
226
|
+
* title: Puri.highlight("posts.title", search),
|
|
227
|
+
* })
|
|
228
|
+
*/
|
|
229
|
+
static highlight(column: string, query: string | string[]): SqlExpression<"string">;
|
|
230
|
+
static highlight(columns: string[], query: string | string[]): SqlExpression<"string[]">;
|
|
231
|
+
|
|
232
|
+
static highlight(
|
|
233
|
+
columnOrColumns: string | string[],
|
|
234
|
+
query: string | string[],
|
|
235
|
+
): SqlExpression<"string"> | SqlExpression<"string[]"> {
|
|
236
|
+
const queryClause = `ARRAY[${(Array.isArray(query) ? query : [query]).map((q) => `'${q}'`).join(",")}]`;
|
|
237
|
+
|
|
238
|
+
// 단일 컬럼인 경우
|
|
239
|
+
if (typeof columnOrColumns === "string") {
|
|
240
|
+
return Puri.rawString(`pgroonga_highlight_html(${columnOrColumns}, ${queryClause})`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 컬럼 배열인 경우
|
|
244
|
+
return Puri.rawStringArray(
|
|
245
|
+
`pgroonga_highlight_html(ARRAY[${columnOrColumns.join(",")}], ${queryClause})`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
137
249
|
// SELECT (overwrite)
|
|
138
250
|
select<TSelect extends SelectObject<TTables>>(
|
|
139
251
|
selectObj: TSelect,
|
|
@@ -146,7 +258,7 @@ export class Puri<TSchema, TTables extends Record<string, any>, TResult> {
|
|
|
146
258
|
for (const [alias, columnOrFunction] of Object.entries(flatSelect)) {
|
|
147
259
|
if (typeof columnOrFunction === "object" && columnOrFunction._type === "sql_expression") {
|
|
148
260
|
// SQL 함수인 경우
|
|
149
|
-
selectClauses.push(this.knex.raw(`${columnOrFunction._sql} as ${alias}`));
|
|
261
|
+
selectClauses.push(this.knex.raw(`${columnOrFunction._sql} as "${alias}"`));
|
|
150
262
|
} else {
|
|
151
263
|
// 일반 컬럼인 경우
|
|
152
264
|
const columnPath = columnOrFunction as string;
|
|
@@ -472,6 +584,64 @@ export class Puri<TSchema, TTables extends Record<string, any>, TResult> {
|
|
|
472
584
|
return this;
|
|
473
585
|
}
|
|
474
586
|
|
|
587
|
+
/**
|
|
588
|
+
* PGroonga FullText 인덱스 검색
|
|
589
|
+
* - 사용할 PGroonga 인덱스와 동일한 컬럼 구성으로 검색해야 인덱스가 사용됩니다.
|
|
590
|
+
*
|
|
591
|
+
* 단일 컬럼 검색:
|
|
592
|
+
* ```sql
|
|
593
|
+
* WHERE name &@~ 'search'
|
|
594
|
+
* ```
|
|
595
|
+
*
|
|
596
|
+
* 복합 컬럼 검색:
|
|
597
|
+
* ```sql
|
|
598
|
+
* WHERE ARRAY[name::text, description::text] &@~ 'search'
|
|
599
|
+
* ```
|
|
600
|
+
*/
|
|
601
|
+
whereSearch<TColumn extends AvailableColumns<TTables>>(
|
|
602
|
+
column: TColumn | TColumn[],
|
|
603
|
+
value: string,
|
|
604
|
+
options?: {
|
|
605
|
+
weights?: number[]; // 정수 배열
|
|
606
|
+
},
|
|
607
|
+
): this {
|
|
608
|
+
const { weights } = options ?? {};
|
|
609
|
+
const columnExpr = Array.isArray(column)
|
|
610
|
+
? `ARRAY[${column.map((c) => `${c}::text`).join(",")}]`
|
|
611
|
+
: column;
|
|
612
|
+
const pgroongaCondition = `pgroonga_condition(?${weights?.length ? `, weights => ARRAY[${weights.join(",")}]` : ""})`;
|
|
613
|
+
|
|
614
|
+
this.knexQuery.whereRaw(`${columnExpr} &@~ ${pgroongaCondition}`, [value]);
|
|
615
|
+
|
|
616
|
+
return this;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// WHERE FULLTEXT
|
|
620
|
+
whereTsSearch<TColumn extends AvailableColumns<TTables> | SqlExpression<"string">>(
|
|
621
|
+
column: TColumn,
|
|
622
|
+
value: string,
|
|
623
|
+
options?: TsQueryOptions | TsQueryConfig,
|
|
624
|
+
): this {
|
|
625
|
+
const opts =
|
|
626
|
+
typeof options === "string" ? ({ config: options } as TsQueryOptions) : (options ?? {});
|
|
627
|
+
|
|
628
|
+
const parser = opts.parser ?? "websearch_to_tsquery";
|
|
629
|
+
const config = opts.config ?? "simple";
|
|
630
|
+
const columnExpr =
|
|
631
|
+
typeof column === "object" && column._type === "sql_expression"
|
|
632
|
+
? column._sql
|
|
633
|
+
: String(column);
|
|
634
|
+
|
|
635
|
+
this.knexQuery.whereRaw(`${columnExpr} @@ ${parser}(?, ?)`, [config, value]);
|
|
636
|
+
return this;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// WHERE RAW
|
|
640
|
+
whereRaw(sql: string, bindings?: readonly unknown[]): this {
|
|
641
|
+
this.knexQuery.whereRaw(sql, bindings);
|
|
642
|
+
return this;
|
|
643
|
+
}
|
|
644
|
+
|
|
475
645
|
// WHERE 괄호 그룹핑
|
|
476
646
|
whereGroup(callback: (g: WhereGroup<TTables>) => void): this {
|
|
477
647
|
this.knexQuery.where((builder) => {
|
|
@@ -498,6 +668,127 @@ export class Puri<TSchema, TTables extends Record<string, any>, TResult> {
|
|
|
498
668
|
return this;
|
|
499
669
|
}
|
|
500
670
|
|
|
671
|
+
/**
|
|
672
|
+
* 벡터 유사도 검색 설정
|
|
673
|
+
*
|
|
674
|
+
* - SELECT에 similarity 컬럼 추가
|
|
675
|
+
* - WHERE col IS NOT NULL 추가
|
|
676
|
+
* - threshold가 있으면 WHERE 조건 추가
|
|
677
|
+
* - 기존 ORDER BY를 clear하고 원시 연산자로 정렬 (HNSW 인덱스 최적화)
|
|
678
|
+
*
|
|
679
|
+
* @param column 벡터 컬럼 경로
|
|
680
|
+
* @param embedding 쿼리 임베딩 벡터
|
|
681
|
+
* @param options method, threshold, as 등 옵션
|
|
682
|
+
*
|
|
683
|
+
* @example
|
|
684
|
+
* ```typescript
|
|
685
|
+
* // cosine similarity (기본값)
|
|
686
|
+
* qb.vectorSimilarity("columnName", queryVector, {
|
|
687
|
+
* method: "cosine",
|
|
688
|
+
* threshold: 0.5
|
|
689
|
+
* });
|
|
690
|
+
*
|
|
691
|
+
* // L2 distance
|
|
692
|
+
* qb.vectorSimilarity("columnName", queryVector, {
|
|
693
|
+
* method: "l2",
|
|
694
|
+
* threshold: 1.5 // 거리가 1.5 이하인 결과만
|
|
695
|
+
* });
|
|
696
|
+
*
|
|
697
|
+
* // Inner product
|
|
698
|
+
* qb.vectorSimilarity("columnName", queryVector, {
|
|
699
|
+
* method: "inner_product",
|
|
700
|
+
* threshold: 0.7
|
|
701
|
+
* });
|
|
702
|
+
* ```
|
|
703
|
+
*/
|
|
704
|
+
vectorSimilarity(
|
|
705
|
+
column: VectorColumns<TTables>,
|
|
706
|
+
embedding: number[],
|
|
707
|
+
options: {
|
|
708
|
+
method?: "cosine" | "l2" | "inner_product";
|
|
709
|
+
threshold?: number;
|
|
710
|
+
} = {},
|
|
711
|
+
): Puri<TSchema, TTables, TResult & { similarity: number }> {
|
|
712
|
+
const { method = "cosine", threshold } = options;
|
|
713
|
+
if (
|
|
714
|
+
!Array.isArray(embedding) ||
|
|
715
|
+
embedding.length === 0 ||
|
|
716
|
+
embedding.some((v) => !Number.isFinite(v))
|
|
717
|
+
) {
|
|
718
|
+
throw new Error("Invalid embedding vector: expected a non-empty array of finite numbers");
|
|
719
|
+
}
|
|
720
|
+
const vectorLiteral = JSON.stringify(embedding.map((v) => Number(v)));
|
|
721
|
+
|
|
722
|
+
// method별 연산자 및 similarity 계산식
|
|
723
|
+
// - cosine: <=> (cosine distance, 0~2), similarity = 1 - distance
|
|
724
|
+
// - l2: <-> (euclidean distance), similarity = distance (낮을수록 유사)
|
|
725
|
+
// - inner_product: <#> (negative inner product), similarity = -distance (높을수록 유사)
|
|
726
|
+
const operatorMap = {
|
|
727
|
+
cosine: "<=>",
|
|
728
|
+
l2: "<->",
|
|
729
|
+
inner_product: "<#>",
|
|
730
|
+
} as const;
|
|
731
|
+
const operator = operatorMap[method];
|
|
732
|
+
|
|
733
|
+
// SELECT에 similarity 추가
|
|
734
|
+
if (method === "cosine") {
|
|
735
|
+
// cosine: similarity = 1 - cosine_distance (0~1, 높을수록 유사)
|
|
736
|
+
this.knexQuery.select(
|
|
737
|
+
this.knex.raw(`1 - (?? <=> ?::vector) as similarity`, [column, vectorLiteral]),
|
|
738
|
+
);
|
|
739
|
+
} else if (method === "l2") {
|
|
740
|
+
// l2: distance 그대로 반환 (낮을수록 유사)
|
|
741
|
+
this.knexQuery.select(
|
|
742
|
+
this.knex.raw(`?? <-> ?::vector as similarity`, [column, vectorLiteral]),
|
|
743
|
+
);
|
|
744
|
+
} else {
|
|
745
|
+
// inner_product: pgvector는 음수 반환하므로 부호 반전 (높을수록 유사)
|
|
746
|
+
this.knexQuery.select(
|
|
747
|
+
this.knex.raw(`-(?? <#> ?::vector) as similarity`, [column, vectorLiteral]),
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// WHERE col IS NOT NULL
|
|
752
|
+
this.knexQuery.whereNotNull(column);
|
|
753
|
+
|
|
754
|
+
// threshold가 있으면 WHERE 추가
|
|
755
|
+
if (typeof threshold === "number") {
|
|
756
|
+
if (!Number.isFinite(threshold)) {
|
|
757
|
+
throw new Error(`Invalid vectorSimilarity threshold: ${threshold}`);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (method === "cosine") {
|
|
761
|
+
// similarity >= threshold <=> cosine_distance <= (1 - threshold)
|
|
762
|
+
this.knexQuery.whereRaw(`?? ${operator} ?::vector <= ?`, [
|
|
763
|
+
column,
|
|
764
|
+
vectorLiteral,
|
|
765
|
+
1 - threshold,
|
|
766
|
+
]);
|
|
767
|
+
} else if (method === "l2") {
|
|
768
|
+
// distance <= threshold (거리가 threshold 이하)
|
|
769
|
+
this.knexQuery.whereRaw(`?? ${operator} ?::vector <= ?`, [
|
|
770
|
+
column,
|
|
771
|
+
vectorLiteral,
|
|
772
|
+
threshold,
|
|
773
|
+
]);
|
|
774
|
+
} else {
|
|
775
|
+
// inner_product: -distance >= threshold <=> distance <= -threshold
|
|
776
|
+
this.knexQuery.whereRaw(`?? ${operator} ?::vector <= ?`, [
|
|
777
|
+
column,
|
|
778
|
+
vectorLiteral,
|
|
779
|
+
-threshold,
|
|
780
|
+
]);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// 기존 ORDER BY clear 후 원시 연산자로 정렬 (HNSW 인덱스 최적화)
|
|
785
|
+
// 모든 method에서 ASC: 거리/음수값이 작을수록 유사
|
|
786
|
+
this.knexQuery.clear("order");
|
|
787
|
+
this.knexQuery.orderByRaw(`?? ${operator} ?::vector`, [column, vectorLiteral]);
|
|
788
|
+
|
|
789
|
+
return this as any;
|
|
790
|
+
}
|
|
791
|
+
|
|
501
792
|
// 기본 쿼리 메서드들
|
|
502
793
|
limit(count: number): this {
|
|
503
794
|
if (count < 0) {
|
|
@@ -108,7 +108,7 @@ describe("ExtractColumnType", () => {
|
|
|
108
108
|
type Tables = {
|
|
109
109
|
users: MockSchema["users"];
|
|
110
110
|
department: MockSchema["departments"] & LeftJoinedMarker; // nullable FK
|
|
111
|
-
company: MockSchema["companies"]
|
|
111
|
+
company: MockSchema["companies"]; // non-null FK → 마커 없음
|
|
112
112
|
};
|
|
113
113
|
type Result = ExtractColumnType<Tables, "company.id">;
|
|
114
114
|
|
|
@@ -13,13 +13,31 @@ type VirtualKey = "__virtual__";
|
|
|
13
13
|
type LeftJoinedKey = "__leftJoined__";
|
|
14
14
|
type HasDefault = "__hasDefault__";
|
|
15
15
|
type GeneratedKey = "__generated__";
|
|
16
|
+
type VectorKey = "__vector__";
|
|
16
17
|
|
|
17
|
-
type InternalTypeKeys =
|
|
18
|
+
type InternalTypeKeys =
|
|
19
|
+
| FulltextKey
|
|
20
|
+
| VirtualKey
|
|
21
|
+
| LeftJoinedKey
|
|
22
|
+
| HasDefault
|
|
23
|
+
| GeneratedKey
|
|
24
|
+
| VectorKey;
|
|
18
25
|
|
|
19
26
|
// ============================================
|
|
20
27
|
// 타입 유틸리티
|
|
21
28
|
// ============================================
|
|
22
29
|
|
|
30
|
+
// __vector__ 메타데이터에서 벡터 컬럼 추출
|
|
31
|
+
type VectorColumnKeys<T> = T extends { [K in VectorKey]: readonly (infer V)[] }
|
|
32
|
+
? V & string
|
|
33
|
+
: never;
|
|
34
|
+
|
|
35
|
+
export type VectorColumns<TTables extends Record<string, any>> =
|
|
36
|
+
| {
|
|
37
|
+
[TAlias in keyof TTables]: `${TAlias & string}.${VectorColumnKeys<TTables[TAlias]>}`;
|
|
38
|
+
}[keyof TTables]
|
|
39
|
+
| (IsSingleKey<TTables> extends true ? VectorColumnKeys<TTables[keyof TTables]> : never);
|
|
40
|
+
|
|
23
41
|
// 테이블명 타입
|
|
24
42
|
export type TableName<TSchema> = keyof TSchema & string;
|
|
25
43
|
|
|
@@ -78,7 +96,7 @@ export type ResultAvailableColumns<TTables extends Record<string, any>, TResult
|
|
|
78
96
|
// Select 값 타입 확장 (단일 컬럼 또는 SQL 표현식)
|
|
79
97
|
export type SelectValue<TTables extends Record<string, any>> =
|
|
80
98
|
| AvailableColumns<TTables>
|
|
81
|
-
| SqlExpression<"string" | "number" | "boolean" | "date">;
|
|
99
|
+
| SqlExpression<"string" | "number" | "boolean" | "date" | "string[]">;
|
|
82
100
|
|
|
83
101
|
// 중첩 Select 객체 타입 (재귀적)
|
|
84
102
|
// 예: { parent: { id: "parent.id", name: "parent.name" } }
|
|
@@ -143,8 +161,8 @@ type JoinPath<Prefix extends string, Key extends string> = Prefix extends ""
|
|
|
143
161
|
// Schema를 읽어서 FK의 nullability에 따라 join된 객체의 타입을 추론해주는 기능이 있습니다.
|
|
144
162
|
// 이게 무슨 소리냐? FK가 nullable인데 leftJoin되었다면, 해당 객체는 nullable 해야 함을 타입 추론으로 반영해준다는 것입니다.
|
|
145
163
|
// 반면 FK가 non-nullable이거나 그냥 join으로 이어졌다면 해당 객체는 non-nullable할 겁니다.
|
|
146
|
-
// 물론 객체 내부의 nullability는 또 별개로 추론됩니다.
|
|
147
|
-
//
|
|
164
|
+
// 물론 객체 내부의 nullability는 또 별개로 추론됩니다.
|
|
165
|
+
//
|
|
148
166
|
// 아래에도 ParseSelectObjectWithPath를 비롯해 ExtractColumnType, ExtractColumnTypeRaw 등의 타입이 있습니다.
|
|
149
167
|
// 이들의 역할은 다음과 같습니다:
|
|
150
168
|
// - Parse*: 객체 레벨에서 중첩 구조를 순회하며 객체에 | null을 붙일지 결정합니다.
|
|
@@ -178,7 +196,9 @@ type ParseSelectObjectWithPath<
|
|
|
178
196
|
? boolean
|
|
179
197
|
: R extends "date"
|
|
180
198
|
? Date
|
|
181
|
-
:
|
|
199
|
+
: R extends "string[]"
|
|
200
|
+
? string[]
|
|
201
|
+
: never
|
|
182
202
|
: IsNestedObject<TSelect[K]> extends true
|
|
183
203
|
? TSelect[K] extends NestedSelectObject<TTables>
|
|
184
204
|
? IsNullableJoinedTable<TTables, JoinPath<Prefix, K & string>> extends true // 주어진 테이블이 FK nullable에 leftJoin되었는지 여부에 따라 select 결과 객체의 타입이 달라집니다.
|
|
@@ -205,7 +225,9 @@ type ParseSelectObjectInner<
|
|
|
205
225
|
? boolean
|
|
206
226
|
: R extends "date"
|
|
207
227
|
? Date
|
|
208
|
-
:
|
|
228
|
+
: R extends "string[]"
|
|
229
|
+
? string[]
|
|
230
|
+
: never
|
|
209
231
|
: IsNestedObject<TSelect[K]> extends true
|
|
210
232
|
? TSelect[K] extends NestedSelectObject<TTables>
|
|
211
233
|
? IsNullableJoinedTable<TTables, JoinPath<Prefix, K & string>> extends true
|
|
@@ -274,7 +296,7 @@ export type ComparisonOperator = "=" | ">" | ">=" | "<" | "<=" | "<>" | "!=";
|
|
|
274
296
|
export type WhereOperator = ComparisonOperator | "like" | "not like";
|
|
275
297
|
|
|
276
298
|
// SQL Expression 타입 정의
|
|
277
|
-
export type SqlExpression<T extends "string" | "number" | "boolean" | "date"> = {
|
|
299
|
+
export type SqlExpression<T extends "string" | "number" | "boolean" | "date" | "string[]"> = {
|
|
278
300
|
_type: "sql_expression"; // 또는 "computed_value"
|
|
279
301
|
_return: T;
|
|
280
302
|
_sql: string;
|
|
@@ -368,3 +390,55 @@ export type SelectAllResult<TTables extends Record<string, any>> = UnionToInters
|
|
|
368
390
|
: never;
|
|
369
391
|
}[keyof TTables]
|
|
370
392
|
>;
|
|
393
|
+
|
|
394
|
+
// FTS 타입
|
|
395
|
+
type TsQueryParser = "to_tsquery" | "plainto_tsquery" | "phraseto_tsquery" | "websearch_to_tsquery";
|
|
396
|
+
export type TsQueryConfig = "simple" | "english";
|
|
397
|
+
export type TsQueryOptions = {
|
|
398
|
+
parser?: TsQueryParser;
|
|
399
|
+
config?: TsQueryConfig;
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
export type TsHighlightOptions = {
|
|
403
|
+
/** 쿼리 변환 함수 (기본값: "websearch_to_tsquery") */
|
|
404
|
+
parser?: TsQueryParser;
|
|
405
|
+
/** 텍스트 검색 설정 (기본값: "simple") */
|
|
406
|
+
config?: TsQueryConfig;
|
|
407
|
+
/** 최대 단어 수 (기본값: 35) */
|
|
408
|
+
maxWords?: number;
|
|
409
|
+
/** 최소 단어 수 (기본값: 15) */
|
|
410
|
+
minWords?: number;
|
|
411
|
+
/** 헤드라인 시작/끝에서 제거할 짧은 단어 길이 (기본값: 3) */
|
|
412
|
+
shortWord?: number;
|
|
413
|
+
/** true면 전체 문서를 헤드라인으로 사용 (기본값: false) */
|
|
414
|
+
highlightAll?: boolean;
|
|
415
|
+
/** 표시할 최대 텍스트 조각 수 (기본값: 0, 조각 미사용) */
|
|
416
|
+
maxFragments?: number;
|
|
417
|
+
/** 쿼리 단어 시작 구분자 (기본값: "<b>") */
|
|
418
|
+
startSel?: string;
|
|
419
|
+
/** 쿼리 단어 끝 구분자 (기본값: "</b>") */
|
|
420
|
+
stopSel?: string;
|
|
421
|
+
/** 조각 구분자 (기본값: " ... ") */
|
|
422
|
+
fragmentDelimiter?: string;
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
export type TsRankOptions = {
|
|
426
|
+
parser?: TsQueryParser;
|
|
427
|
+
config?: TsQueryConfig;
|
|
428
|
+
/** 가중치 배열 [D, C, B, A] (기본값: [0.1, 0.2, 0.4, 1.0]) */
|
|
429
|
+
weights?: [number, number, number, number];
|
|
430
|
+
/**
|
|
431
|
+
* 정규화 옵션
|
|
432
|
+
* 0: 문서 길이 무시 (기본값)
|
|
433
|
+
* 1: 1 + log(문서 길이)로 나눔
|
|
434
|
+
* 2: 문서 길이로 나눔
|
|
435
|
+
* 4: 평균 조화 거리로 나눔 (ts_rank_cd만)
|
|
436
|
+
* 8: 고유 단어 수로 나눔
|
|
437
|
+
* 16: 1 + log(고유 단어 수)로 나눔
|
|
438
|
+
* 32: rank/(rank+1) -> 0~1 사이의 값으로 스케일링
|
|
439
|
+
*
|
|
440
|
+
* 비트마스크를 사용하여 옵션을 조합할 수 있음
|
|
441
|
+
* 예: 8 | 32 -> 고유 단어 수로 나누고 0~1 스케일링
|
|
442
|
+
*/
|
|
443
|
+
normalization?: number;
|
|
444
|
+
};
|
|
@@ -6,7 +6,7 @@ import { Naite } from "../naite/naite";
|
|
|
6
6
|
import type { DatabaseForeignKeys, DatabaseSchemaExtend, EntityIndex } from "../types/types";
|
|
7
7
|
import { assertDefined, chunk, nonNullable } from "../utils/utils";
|
|
8
8
|
import { batchUpdate, type RowWithId } from "./_batch_update";
|
|
9
|
-
import type { ForeignKeyColumns, TableName } from "./puri.types";
|
|
9
|
+
import type { ColumnKeys, ForeignKeyColumns, TableName } from "./puri.types";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* FK 타입 추론을 위해 DatabaseForeignKeys export
|
|
@@ -14,6 +14,9 @@ import type { ForeignKeyColumns, TableName } from "./puri.types";
|
|
|
14
14
|
*/
|
|
15
15
|
export type { DatabaseForeignKeys };
|
|
16
16
|
|
|
17
|
+
type InheritableColumns<TTable extends TableName<DatabaseSchemaExtend>> =
|
|
18
|
+
TTable extends keyof DatabaseSchemaExtend ? ColumnKeys<DatabaseSchemaExtend[TTable]> : never;
|
|
19
|
+
|
|
17
20
|
// 테이블 데이터 타입
|
|
18
21
|
type TableData = {
|
|
19
22
|
references: Set<string>;
|
|
@@ -33,6 +36,7 @@ export type UBRef = {
|
|
|
33
36
|
export type UpsertOptions<TTable extends TableName<DatabaseSchemaExtend>> = {
|
|
34
37
|
chunkSize?: number;
|
|
35
38
|
cleanOrphans?: ForeignKeyColumns<TTable> | ForeignKeyColumns<TTable>[];
|
|
39
|
+
inherit?: InheritableColumns<TTable>[];
|
|
36
40
|
};
|
|
37
41
|
|
|
38
42
|
// insertOnly 옵션
|
|
@@ -289,16 +293,30 @@ export class UpsertBuilder {
|
|
|
289
293
|
resultRows = await wdb.insert(dataForDb).into(tableName).returning(selectFields);
|
|
290
294
|
} else {
|
|
291
295
|
// UPSERT 모드 - onConflict 사용 (unique index 없으면 PK fallback)
|
|
292
|
-
const conflictColumns =
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
)
|
|
296
|
+
const conflictColumns = table.uniqueIndexes[0]?.columns.map((c) => c.name) ?? ["id"];
|
|
297
|
+
|
|
298
|
+
const allColumns = Object.keys(dataForDb[0]);
|
|
299
|
+
let updateColumns = allColumns.filter((c) => !conflictColumns.includes(c));
|
|
300
|
+
|
|
301
|
+
// inherit 옵션 처리 - inherit 컬럼은 update 대상에서 제외
|
|
302
|
+
if (options?.inherit?.length) {
|
|
303
|
+
const inheritColumns = options.inherit as string[];
|
|
304
|
+
|
|
305
|
+
const excludedFromUpdate = updateColumns.filter((c) => inheritColumns.includes(c));
|
|
306
|
+
updateColumns = updateColumns.filter((c) => !inheritColumns.includes(c));
|
|
307
|
+
|
|
308
|
+
// 실제로 제외된 컬럼 로깅
|
|
309
|
+
if (excludedFromUpdate.length) {
|
|
310
|
+
Naite.t("puri:ub-inherit", {
|
|
311
|
+
tableName,
|
|
312
|
+
inheritColumns,
|
|
313
|
+
excludedFromUpdate,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
299
317
|
|
|
300
318
|
// updateColumns가 비어있어도 merge()를 사용하여 모든 행이 RETURNING되도록 보장
|
|
301
|
-
const mergeColumns = updateColumns.length
|
|
319
|
+
const mergeColumns = updateColumns.length ? updateColumns : conflictColumns;
|
|
302
320
|
|
|
303
321
|
resultRows = await wdb
|
|
304
322
|
.insert(dataForDb)
|
|
@@ -85,6 +85,15 @@ class EntityManagerClass {
|
|
|
85
85
|
return entity;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
getByTable(table: string): Entity {
|
|
89
|
+
const entity = Array.from(this.entities.values()).find((entity) => entity.table === table);
|
|
90
|
+
if (entity === undefined) {
|
|
91
|
+
throw new Error(`존재하지 않는 Entity 요청 ${table}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return entity;
|
|
95
|
+
}
|
|
96
|
+
|
|
88
97
|
exists(entityId: string): boolean {
|
|
89
98
|
const entity = this.entities.get(entityId);
|
|
90
99
|
return entity !== undefined;
|
package/src/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ export * from "./migration/types";
|
|
|
22
22
|
export * from "./naite/naite";
|
|
23
23
|
export * from "./naite/naite-reporter";
|
|
24
24
|
export * from "./stream/sse";
|
|
25
|
+
export * from "./tasks/decorator";
|
|
25
26
|
export * from "./template/template";
|
|
26
27
|
export * from "./template/template-manager";
|
|
27
28
|
export * from "./testing/fixture-manager";
|
|
@@ -35,7 +36,6 @@ export * from "./vector/chunking";
|
|
|
35
36
|
export * from "./vector/config";
|
|
36
37
|
export * from "./vector/embedding";
|
|
37
38
|
export * from "./vector/types";
|
|
38
|
-
export * from "./vector/vector-search";
|
|
39
39
|
|
|
40
40
|
// export * from "./api/code-converters";
|
|
41
41
|
// export * from "./syncer/syncer";
|