sonamu 0.7.11 → 0.7.13
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 -6
- 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 +49 -5
- package/dist/bin/cli.js +118 -170
- 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 +82 -3
- package/dist/database/puri.d.ts.map +1 -1
- package/dist/database/puri.js +180 -14
- package/dist/database/puri.types.d.ts +33 -6
- package/dist/database/puri.types.d.ts.map +1 -1
- package/dist/database/puri.types.js +1 -1
- package/dist/database/puri.types.test-d.js +1 -1
- 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.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.d.ts.map +1 -1
- package/dist/syncer/api-parser.js +7 -2
- 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 +5 -1
- package/dist/syncer/syncer.d.ts.map +1 -1
- package/dist/syncer/syncer.js +28 -19
- 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/types/types.d.ts +26 -10
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/types.js +15 -2
- package/dist/ui/ai-api.d.ts +1 -0
- package/dist/ui/ai-api.d.ts.map +1 -0
- package/dist/ui/ai-api.js +50 -0
- package/dist/ui/ai-client.d.ts +1 -0
- package/dist/ui/ai-client.d.ts.map +1 -0
- package/dist/ui/ai-client.js +438 -0
- package/dist/ui/api.d.ts +3 -0
- package/dist/ui/api.d.ts.map +1 -0
- package/dist/ui/api.js +680 -0
- package/dist/ui-web/assets/brand-icons-Cu_C0hZ4.svg +1008 -0
- package/dist/ui-web/assets/brand-icons-F3SPCeH1.woff +0 -0
- package/dist/ui-web/assets/brand-icons-XL9sxUpA.woff2 +0 -0
- package/dist/ui-web/assets/brand-icons-sqJ2Pg7a.eot +0 -0
- package/dist/ui-web/assets/brand-icons-ubhWoxly.ttf +0 -0
- package/dist/ui-web/assets/flags-DOLqOU7Y.png +0 -0
- package/dist/ui-web/assets/icons-BOCtAERH.woff +0 -0
- package/dist/ui-web/assets/icons-CHzK1VD9.eot +0 -0
- package/dist/ui-web/assets/icons-D29ZQHHw.ttf +0 -0
- package/dist/ui-web/assets/icons-Du6TOHnR.woff2 +0 -0
- package/dist/ui-web/assets/icons-RwhydX30.svg +1518 -0
- package/dist/ui-web/assets/index-CpaB9P6g.css +1 -0
- package/dist/ui-web/assets/index-J9MCfjCd.js +95 -0
- package/dist/ui-web/assets/outline-icons-BfdLr8tr.svg +366 -0
- package/dist/ui-web/assets/outline-icons-DD8jm0uy.ttf +0 -0
- package/dist/ui-web/assets/outline-icons-DInHoiqI.woff2 +0 -0
- package/dist/ui-web/assets/outline-icons-LX8adJ4n.eot +0 -0
- package/dist/ui-web/assets/outline-icons-aQ88nltS.woff +0 -0
- package/dist/ui-web/assets/provider-utils_false-BKJD46kk.js +1 -0
- package/dist/ui-web/assets/provider-utils_false-Bu5lmX18.js +1 -0
- package/dist/ui-web/index.html +13 -0
- package/dist/ui-web/vite.svg +1 -0
- 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 +9 -13
- package/dist/vector/types.d.ts.map +1 -1
- package/dist/vector/types.js +1 -1
- package/package.json +9 -5
- package/src/api/config.ts +15 -11
- package/src/api/sonamu.ts +60 -6
- package/src/bin/cli.ts +57 -119
- 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 +238 -27
- package/src/database/puri.types.test-d.ts +1 -1
- package/src/database/puri.types.ts +49 -6
- 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.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 +6 -1
- package/src/syncer/file-patterns.ts +11 -9
- package/src/syncer/module-loader.ts +35 -0
- package/src/syncer/syncer.ts +34 -21
- 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/types/types.ts +20 -4
- package/src/ui/ai-api.ts +60 -0
- package/src/ui/ai-client.ts +499 -0
- package/src/ui/api.ts +786 -0
- 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 +10 -14
- 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
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 {
|
|
@@ -12,7 +13,6 @@ import type {
|
|
|
12
13
|
Expand,
|
|
13
14
|
ExtractColumnType,
|
|
14
15
|
FulltextColumns,
|
|
15
|
-
HighlightOptions,
|
|
16
16
|
InsertData,
|
|
17
17
|
InsertResult,
|
|
18
18
|
LeftJoinedMarker,
|
|
@@ -25,8 +25,11 @@ import type {
|
|
|
25
25
|
SelectObject,
|
|
26
26
|
SingleTableValue,
|
|
27
27
|
SqlExpression,
|
|
28
|
+
TsHighlightOptions,
|
|
28
29
|
TsQueryConfig,
|
|
29
30
|
TsQueryOptions,
|
|
31
|
+
TsRankOptions,
|
|
32
|
+
VectorColumns,
|
|
30
33
|
WhereCondition,
|
|
31
34
|
WhereOperator,
|
|
32
35
|
} from "./puri.types";
|
|
@@ -127,6 +130,9 @@ export class Puri<TSchema, TTables extends Record<string, any>, TResult> {
|
|
|
127
130
|
static rawString(sql: string): SqlExpression<"string"> {
|
|
128
131
|
return { _type: "sql_expression", _return: "string", _sql: sql };
|
|
129
132
|
}
|
|
133
|
+
static rawStringArray(sql: string): SqlExpression<"string[]"> {
|
|
134
|
+
return { _type: "sql_expression", _return: "string[]", _sql: sql };
|
|
135
|
+
}
|
|
130
136
|
static rawNumber(sql: string): SqlExpression<"number"> {
|
|
131
137
|
return { _type: "sql_expression", _return: "number", _sql: sql };
|
|
132
138
|
}
|
|
@@ -150,41 +156,93 @@ export class Puri<TSchema, TTables extends Record<string, any>, TResult> {
|
|
|
150
156
|
* }),
|
|
151
157
|
* })
|
|
152
158
|
*/
|
|
153
|
-
static
|
|
159
|
+
static tsHighlight(
|
|
154
160
|
column: string,
|
|
155
161
|
query: string,
|
|
156
|
-
|
|
162
|
+
_options?: TsHighlightOptions,
|
|
157
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"> {
|
|
158
194
|
const {
|
|
159
195
|
parser = "websearch_to_tsquery",
|
|
160
196
|
config = "simple",
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
shortWord,
|
|
164
|
-
highlightAll,
|
|
165
|
-
maxFragments,
|
|
166
|
-
startSel,
|
|
167
|
-
stopSel,
|
|
168
|
-
fragmentDelimiter,
|
|
197
|
+
normalization,
|
|
198
|
+
weights,
|
|
169
199
|
} = options ?? {};
|
|
170
200
|
|
|
171
|
-
const
|
|
201
|
+
const weightClause = weights ? `ARRAY[${weights.join(", ")}], ` : "";
|
|
202
|
+
const normalizationClause = normalization ? `, ${normalization}` : "";
|
|
172
203
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (maxFragments !== undefined) hlOptionParts.push(`MaxFragments=${maxFragments}`);
|
|
178
|
-
if (startSel !== undefined) hlOptionParts.push(`StartSel="${startSel}"`);
|
|
179
|
-
if (stopSel !== undefined) hlOptionParts.push(`StopSel="${stopSel}"`);
|
|
180
|
-
if (fragmentDelimiter !== undefined)
|
|
181
|
-
hlOptionParts.push(`FragmentDelimiter="${fragmentDelimiter}"`);
|
|
204
|
+
return Puri.rawNumber(
|
|
205
|
+
`${type}(${weightClause}${column}, ${parser}('${config}', '${query}')${normalizationClause})`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
182
208
|
|
|
183
|
-
|
|
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
|
+
}
|
|
184
220
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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})`,
|
|
188
246
|
);
|
|
189
247
|
}
|
|
190
248
|
|
|
@@ -200,7 +258,7 @@ export class Puri<TSchema, TTables extends Record<string, any>, TResult> {
|
|
|
200
258
|
for (const [alias, columnOrFunction] of Object.entries(flatSelect)) {
|
|
201
259
|
if (typeof columnOrFunction === "object" && columnOrFunction._type === "sql_expression") {
|
|
202
260
|
// SQL 함수인 경우
|
|
203
|
-
selectClauses.push(this.knex.raw(`${columnOrFunction._sql} as ${alias}`));
|
|
261
|
+
selectClauses.push(this.knex.raw(`${columnOrFunction._sql} as "${alias}"`));
|
|
204
262
|
} else {
|
|
205
263
|
// 일반 컬럼인 경우
|
|
206
264
|
const columnPath = columnOrFunction as string;
|
|
@@ -526,8 +584,40 @@ export class Puri<TSchema, TTables extends Record<string, any>, TResult> {
|
|
|
526
584
|
return this;
|
|
527
585
|
}
|
|
528
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
|
+
|
|
529
619
|
// WHERE FULLTEXT
|
|
530
|
-
|
|
620
|
+
whereTsSearch<TColumn extends AvailableColumns<TTables> | SqlExpression<"string">>(
|
|
531
621
|
column: TColumn,
|
|
532
622
|
value: string,
|
|
533
623
|
options?: TsQueryOptions | TsQueryConfig,
|
|
@@ -578,6 +668,127 @@ export class Puri<TSchema, TTables extends Record<string, any>, TResult> {
|
|
|
578
668
|
return this;
|
|
579
669
|
}
|
|
580
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
|
+
|
|
581
792
|
// 기본 쿼리 메서드들
|
|
582
793
|
limit(count: number): this {
|
|
583
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" } }
|
|
@@ -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;
|
|
@@ -377,7 +399,7 @@ export type TsQueryOptions = {
|
|
|
377
399
|
config?: TsQueryConfig;
|
|
378
400
|
};
|
|
379
401
|
|
|
380
|
-
export type
|
|
402
|
+
export type TsHighlightOptions = {
|
|
381
403
|
/** 쿼리 변환 함수 (기본값: "websearch_to_tsquery") */
|
|
382
404
|
parser?: TsQueryParser;
|
|
383
405
|
/** 텍스트 검색 설정 (기본값: "simple") */
|
|
@@ -399,3 +421,24 @@ export type HighlightOptions = {
|
|
|
399
421
|
/** 조각 구분자 (기본값: " ... ") */
|
|
400
422
|
fragmentDelimiter?: string;
|
|
401
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
|
+
};
|
|
@@ -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";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import equal from "fast-deep-equal";
|
|
2
2
|
import { alphabetical, diff } from "radashi";
|
|
3
|
-
import { Naite } from "..";
|
|
3
|
+
import { EntityManager, Naite } from "..";
|
|
4
4
|
import type {
|
|
5
|
+
EntityProp,
|
|
5
6
|
GenMigrationCode,
|
|
6
7
|
MigrationColumn,
|
|
7
8
|
MigrationForeign,
|
|
@@ -216,6 +217,10 @@ function genIndexDefinition(index: MigrationIndex, table: string): string {
|
|
|
216
217
|
return genVectorIndexDefinition(index, table);
|
|
217
218
|
}
|
|
218
219
|
|
|
220
|
+
if (index.using === "pgroonga") {
|
|
221
|
+
return genPgroongaIndexDefinition(index, table);
|
|
222
|
+
}
|
|
223
|
+
|
|
219
224
|
const methodMap = {
|
|
220
225
|
index: "INDEX",
|
|
221
226
|
unique: "UNIQUE INDEX",
|
|
@@ -245,6 +250,40 @@ function genIndexDefinition(index: MigrationIndex, table: string): string {
|
|
|
245
250
|
);`;
|
|
246
251
|
}
|
|
247
252
|
|
|
253
|
+
function genPgroongaIndexDefinition(index: MigrationIndex, table: string) {
|
|
254
|
+
const entity = EntityManager.getByTable(table);
|
|
255
|
+
|
|
256
|
+
// 복합 인덱스인 경우 ARRAY 사용
|
|
257
|
+
const columnClause = (() => {
|
|
258
|
+
if (index.columns.length === 1) {
|
|
259
|
+
const column = entity.propsDict[index.columns[0].name];
|
|
260
|
+
const option = getPgroongaColumnOption(column);
|
|
261
|
+
return `${index.columns[0].name}${option ? ` ${option}` : ""}`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return `(ARRAY[${index.columns.map((col) => `${col.name}::text`).join(",")}])`;
|
|
265
|
+
})();
|
|
266
|
+
|
|
267
|
+
return `await knex.raw(
|
|
268
|
+
\`CREATE INDEX ${index.name} ON ${table} USING pgroonga (${columnClause}) WITH (tokenizer='TokenMecab');\`
|
|
269
|
+
)`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* PGroonga 컬럼 옵션 추출
|
|
274
|
+
*
|
|
275
|
+
* FullText 오퍼레이터를 지원하는 경우 우선 설정, 나머지는 디폴트 이용
|
|
276
|
+
* @link https://pgroonga.github.io/reference
|
|
277
|
+
*/
|
|
278
|
+
function getPgroongaColumnOption(column: EntityProp) {
|
|
279
|
+
if (column.type === "string" && column.length !== undefined) {
|
|
280
|
+
return "pgroonga_varchar_full_text_search_ops_v2";
|
|
281
|
+
} else if (column.type === "json") {
|
|
282
|
+
return "pgroonga_jsonb_full_text_search_ops_v2";
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
248
287
|
/**
|
|
249
288
|
* @description
|
|
250
289
|
* - HNSW (Hierarchical Navigable Small World): 느린 빌드, 빠른 검색 속도, 높은 메모리 및 정확도
|
|
@@ -148,7 +148,7 @@ class PostgreSQLSchemaReaderClass {
|
|
|
148
148
|
sortOrder: idx.sort_order,
|
|
149
149
|
})),
|
|
150
150
|
nullsNotDistinct: firstIndex.nulls_not_distinct,
|
|
151
|
-
using: firstIndex.index_type as "btree" | "hash" | "gin" | "gist" | undefined,
|
|
151
|
+
using: firstIndex.index_type as "btree" | "hash" | "gin" | "gist" | "pgroonga" | undefined,
|
|
152
152
|
};
|
|
153
153
|
});
|
|
154
154
|
|
|
@@ -219,34 +219,65 @@ class PostgreSQLSchemaReaderClass {
|
|
|
219
219
|
throw new Error(`Table not found: ${tableName}`);
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
// Indexes 조회
|
|
222
|
+
// Indexes 조회 (PGroonga 표현식 인덱스 포함)
|
|
223
223
|
const indexesQuery = `
|
|
224
224
|
SELECT
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
225
|
+
i.relname AS index_name,
|
|
226
|
+
CASE
|
|
227
|
+
WHEN am.amname = 'pgroonga' AND u.attnum = 0 THEN
|
|
228
|
+
regexp_replace(
|
|
229
|
+
regexp_replace(
|
|
230
|
+
TRIM(pgroonga_col.column_expr),
|
|
231
|
+
'::text',
|
|
232
|
+
'',
|
|
233
|
+
'g'
|
|
234
|
+
),
|
|
235
|
+
'[()]',
|
|
236
|
+
'',
|
|
237
|
+
'g'
|
|
238
|
+
)
|
|
239
|
+
ELSE a.attname
|
|
240
|
+
END AS column_name,
|
|
241
|
+
ix.indisunique AS is_unique,
|
|
242
|
+
ix.indisprimary AS is_primary,
|
|
243
|
+
am.amname AS index_type,
|
|
244
|
+
COALESCE((u.opt & 2) = 2, FALSE) AS nulls_first,
|
|
245
|
+
CASE
|
|
246
|
+
WHEN (u.opt & 1) = 1 THEN 'DESC'
|
|
247
|
+
ELSE 'ASC'
|
|
248
|
+
END AS sort_order,
|
|
249
|
+
ix.indnullsnotdistinct AS nulls_not_distinct
|
|
238
250
|
FROM pg_class t
|
|
239
251
|
JOIN pg_index ix ON t.oid = ix.indrelid
|
|
240
252
|
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
241
253
|
JOIN pg_am am ON i.relam = am.oid
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
254
|
+
JOIN LATERAL unnest(ix.indkey, ix.indoption)
|
|
255
|
+
WITH ORDINALITY AS u(attnum, opt, ord) ON true
|
|
256
|
+
LEFT JOIN pg_attribute a ON a.attrelid = t.oid
|
|
257
|
+
AND a.attnum = u.attnum
|
|
258
|
+
AND u.attnum > 0
|
|
259
|
+
LEFT JOIN LATERAL (
|
|
260
|
+
SELECT
|
|
261
|
+
unnest(
|
|
262
|
+
CASE
|
|
263
|
+
WHEN pg_get_expr(ix.indexprs, ix.indrelid) ~ '^ARRAY\\[' THEN
|
|
264
|
+
string_to_array(
|
|
265
|
+
regexp_replace(
|
|
266
|
+
pg_get_expr(ix.indexprs, ix.indrelid),
|
|
267
|
+
'^ARRAY\\[(.*)\\]$',
|
|
268
|
+
'\\1'
|
|
269
|
+
),
|
|
270
|
+
', '
|
|
271
|
+
)
|
|
272
|
+
ELSE
|
|
273
|
+
ARRAY[pg_get_expr(ix.indexprs, ix.indrelid)]
|
|
274
|
+
END
|
|
275
|
+
) as column_expr
|
|
276
|
+
) pgroonga_col ON am.amname = 'pgroonga' AND u.attnum = 0
|
|
246
277
|
WHERE t.relname = ?
|
|
247
|
-
|
|
248
|
-
ORDER BY i.relname,
|
|
249
|
-
|
|
278
|
+
AND (u.attnum > 0 OR (am.amname = 'pgroonga' AND u.attnum = 0))
|
|
279
|
+
ORDER BY i.relname, u.ord;
|
|
280
|
+
`;
|
|
250
281
|
const indexes = (await compareDB.raw(indexesQuery, [tableName])).rows;
|
|
251
282
|
|
|
252
283
|
// Foreign Keys 조회
|