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/utils/formatter.ts
CHANGED
|
@@ -105,5 +105,12 @@ export function formatCode(code: string, parser: "typescript" | "json", filePath
|
|
|
105
105
|
}
|
|
106
106
|
Naite.t("formatCode:linted", linted);
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
// 포맷팅 한 번 더 (import 구문에 type 키워드 추가되는 경우 maxWidth 초과로 인한 에러 발생)
|
|
109
|
+
const formattedAgain = biome.formatContent(projectKey, linted.content, { filePath });
|
|
110
|
+
if (formattedAgain.diagnostics.filter((d) => d.severity === "error").length > 0) {
|
|
111
|
+
console.error(formattedAgain.diagnostics);
|
|
112
|
+
throw new Error("Biome format error");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return formattedAgain.content;
|
|
109
116
|
}
|
package/src/utils/model.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import type { SonamuQueryMode } from "..";
|
|
2
2
|
|
|
3
|
+
// semanticQuery가 있으면 similarity를 추가하는 조건부 타입
|
|
4
|
+
type WithSimilarity<LP, T> = LP extends { semanticQuery: Record<string, unknown> }
|
|
5
|
+
? T & { similarity: number }
|
|
6
|
+
: T;
|
|
7
|
+
|
|
3
8
|
export type ListResult<
|
|
4
9
|
LP extends { queryMode?: SonamuQueryMode },
|
|
5
10
|
T,
|
|
6
11
|
> = LP["queryMode"] extends "list"
|
|
7
|
-
? { rows: T[] }
|
|
12
|
+
? { rows: WithSimilarity<LP, T>[] }
|
|
8
13
|
: LP["queryMode"] extends "count"
|
|
9
14
|
? { total: number }
|
|
10
|
-
: { rows: T[]; total: number };
|
|
15
|
+
: { rows: WithSimilarity<LP, T>[]; total: number };
|
|
11
16
|
|
|
12
17
|
export type ArrayOr<T> = T | T[];
|
|
13
18
|
|
|
@@ -34,4 +39,23 @@ export interface BaseListParams {
|
|
|
34
39
|
page?: number;
|
|
35
40
|
keyword?: string;
|
|
36
41
|
queryMode?: "list" | "count" | "both";
|
|
42
|
+
semanticQuery?: Record<string, unknown>;
|
|
37
43
|
}
|
|
44
|
+
|
|
45
|
+
// const a: ListResult<{ queryMode: "list"; semanticQuery: {} }, { id: number; name: string }> = {
|
|
46
|
+
// rows: [{ id: 1, name: "test", similarity: 0.5 }],
|
|
47
|
+
// };
|
|
48
|
+
// a.rows[0].similarity;
|
|
49
|
+
|
|
50
|
+
// // const b: ListResult<{ queryMode: "count" }, { id: number; name: string }> = {
|
|
51
|
+
// // total: 1,
|
|
52
|
+
// // };
|
|
53
|
+
|
|
54
|
+
// const c: ListResult<
|
|
55
|
+
// { queryMode: "both"; semanticQuery: { embedding: number[] } },
|
|
56
|
+
// { id: number; name: string }
|
|
57
|
+
// > = {
|
|
58
|
+
// rows: [{ id: 1, name: "test", similarity: 0.5 }],
|
|
59
|
+
// total: 1,
|
|
60
|
+
// };
|
|
61
|
+
// c.rows[0].similarity;
|
package/src/utils/type-utils.ts
CHANGED
|
@@ -31,6 +31,7 @@ export function withProp<T extends object, P extends string, V>(
|
|
|
31
31
|
if (keys.length === 0) throw new Error("Path cannot be empty");
|
|
32
32
|
const result = structuredClone(obj);
|
|
33
33
|
|
|
34
|
+
// biome-ignore lint/suspicious/noExplicitAny: 범용 배열 요소 타입
|
|
34
35
|
const setDeep = (current: any, keys: string[], value: V): void => {
|
|
35
36
|
if (keys.length === 0) return;
|
|
36
37
|
const [key, ...rest] = keys;
|
|
@@ -48,6 +49,7 @@ export function withProp<T extends object, P extends string, V>(
|
|
|
48
49
|
current[key] = {};
|
|
49
50
|
}
|
|
50
51
|
if (Array.isArray(current[key])) {
|
|
52
|
+
// biome-ignore lint/suspicious/noExplicitAny: 범용 배열 요소 타입
|
|
51
53
|
current[key].forEach((item: any) => {
|
|
52
54
|
setDeep(item, rest, value);
|
|
53
55
|
});
|
package/src/vector/embedding.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { OpenAIProvider } from "@ai-sdk/openai";
|
|
2
2
|
import { type EmbeddingModel, embedMany } from "ai";
|
|
3
|
-
import { VoyageAIClient } from "voyageai";
|
|
3
|
+
import type { VoyageAIClient } from "voyageai";
|
|
4
4
|
import { Sonamu } from "../api/sonamu";
|
|
5
5
|
import { DEFAULT_VECTOR_CONFIG } from "./config";
|
|
6
6
|
import type {
|
|
@@ -15,7 +15,7 @@ import type {
|
|
|
15
15
|
* 임베딩 클라이언트
|
|
16
16
|
* Voyage AI와 OpenAI 임베딩을 SDK 방식으로 통합 지원
|
|
17
17
|
*/
|
|
18
|
-
export class
|
|
18
|
+
export class EmbeddingClass {
|
|
19
19
|
private config: VectorConfig;
|
|
20
20
|
|
|
21
21
|
constructor(config: Partial<VectorConfig> = {}) {
|
|
@@ -31,7 +31,8 @@ export class Embedding {
|
|
|
31
31
|
/**
|
|
32
32
|
* Voyage AI 클라이언트 초기화
|
|
33
33
|
*/
|
|
34
|
-
private getVoyageClient(): VoyageAIClient {
|
|
34
|
+
private async getVoyageClient(): Promise<VoyageAIClient> {
|
|
35
|
+
const { VoyageAIClient } = await import("voyageai");
|
|
35
36
|
const apiKey = Sonamu.secrets?.voyage_api_key ?? process.env.VOYAGE_API_KEY;
|
|
36
37
|
if (!apiKey) {
|
|
37
38
|
throw new Error("VOYAGE_API_KEY가 설정되지 않았습니다. 환경변수를 확인하세요.");
|
|
@@ -42,7 +43,8 @@ export class Embedding {
|
|
|
42
43
|
/**
|
|
43
44
|
* OpenAI provider 생성
|
|
44
45
|
*/
|
|
45
|
-
private getOpenAIProvider(): OpenAIProvider {
|
|
46
|
+
private async getOpenAIProvider(): Promise<OpenAIProvider> {
|
|
47
|
+
const { createOpenAI } = await import("@ai-sdk/openai");
|
|
46
48
|
const apiKey = Sonamu.secrets?.openai_api_key ?? process.env.OPENAI_API_KEY;
|
|
47
49
|
if (!apiKey) {
|
|
48
50
|
throw new Error("OPENAI_API_KEY가 설정되지 않았습니다. 환경변수를 확인하세요.");
|
|
@@ -107,7 +109,7 @@ export class Embedding {
|
|
|
107
109
|
texts: string[],
|
|
108
110
|
inputType: VectorInputType,
|
|
109
111
|
): Promise<EmbeddingResult[]> {
|
|
110
|
-
const client = this.getVoyageClient();
|
|
112
|
+
const client = await this.getVoyageClient();
|
|
111
113
|
const voyageConfig = this.config.voyage;
|
|
112
114
|
|
|
113
115
|
const response = await client.embed({
|
|
@@ -130,7 +132,7 @@ export class Embedding {
|
|
|
130
132
|
* OpenAI 임베딩
|
|
131
133
|
*/
|
|
132
134
|
private async embedOpenAI(texts: string[]): Promise<EmbeddingResult[]> {
|
|
133
|
-
const openai = this.getOpenAIProvider();
|
|
135
|
+
const openai = await this.getOpenAIProvider();
|
|
134
136
|
const openaiConfig = this.config.openai;
|
|
135
137
|
const model = openai.embeddingModel(openaiConfig.model);
|
|
136
138
|
|
|
@@ -146,13 +148,6 @@ export class Embedding {
|
|
|
146
148
|
}));
|
|
147
149
|
}
|
|
148
150
|
|
|
149
|
-
/**
|
|
150
|
-
* 벡터를 PostgreSQL vector 타입 문자열로 변환
|
|
151
|
-
*/
|
|
152
|
-
static toVectorString(embedding: number[]): string {
|
|
153
|
-
return `[${embedding.join(",")}]`;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
151
|
/**
|
|
157
152
|
* 임베딩 provider의 차원 수 반환
|
|
158
153
|
*/
|
|
@@ -160,3 +155,4 @@ export class Embedding {
|
|
|
160
155
|
return provider === "voyage" ? this.config.voyage.dimensions : this.config.openai.dimensions;
|
|
161
156
|
}
|
|
162
157
|
}
|
|
158
|
+
export const Embedding = new EmbeddingClass();
|
package/src/vector/types.ts
CHANGED
|
@@ -31,8 +31,7 @@ export interface VectorSearchResult<T = Record<string, unknown>> {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/** 하이브리드 검색 결과 (Vector + FTS) */
|
|
34
|
-
export interface HybridSearchResult<T = Record<string, unknown>>
|
|
35
|
-
extends VectorSearchResult<T> {
|
|
34
|
+
export interface HybridSearchResult<T = Record<string, unknown>> extends VectorSearchResult<T> {
|
|
36
35
|
vectorScore?: number;
|
|
37
36
|
ftsScore?: number;
|
|
38
37
|
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import type { Knex } from "knex";
|
|
2
|
-
import { Embedding } from "./embedding";
|
|
3
|
-
import type { EmbeddingItem, EmbeddingProvider, HybridSearchOptions, HybridSearchResult, ProgressCallback, VectorConfig, VectorSearchOptions, VectorSearchResult } from "./types";
|
|
4
|
-
/**
|
|
5
|
-
* 벡터 검색
|
|
6
|
-
* pgvector를 활용한 벡터 검색 및 하이브리드 검색 지원
|
|
7
|
-
*/
|
|
8
|
-
export declare class VectorSearch<T = Record<string, unknown>> {
|
|
9
|
-
private db;
|
|
10
|
-
private config;
|
|
11
|
-
private embedding;
|
|
12
|
-
private tableName;
|
|
13
|
-
constructor(db: Knex, tableName: string, config?: Partial<VectorConfig>);
|
|
14
|
-
/**
|
|
15
|
-
* 단일 항목에 임베딩 저장
|
|
16
|
-
*/
|
|
17
|
-
saveEmbedding(id: number, text: string, provider: EmbeddingProvider, embeddingColumn?: string): Promise<void>;
|
|
18
|
-
/**
|
|
19
|
-
* 여러 항목에 임베딩 일괄 저장
|
|
20
|
-
*/
|
|
21
|
-
saveEmbeddingsBatch(items: EmbeddingItem[], provider: EmbeddingProvider, embeddingColumn?: string, onProgress?: ProgressCallback): Promise<void>;
|
|
22
|
-
/**
|
|
23
|
-
* 벡터 검색 (코사인 유사도)
|
|
24
|
-
*/
|
|
25
|
-
search(query: string, provider: EmbeddingProvider, options?: VectorSearchOptions): Promise<VectorSearchResult<T>[]>;
|
|
26
|
-
/**
|
|
27
|
-
* 하이브리드 검색 (Vector + FTS)
|
|
28
|
-
*/
|
|
29
|
-
hybridSearch(query: string, provider: EmbeddingProvider, options?: HybridSearchOptions): Promise<HybridSearchResult<T>[]>;
|
|
30
|
-
/**
|
|
31
|
-
* 임베딩 현황 조회
|
|
32
|
-
*/
|
|
33
|
-
getEmbeddingStatus(embeddingColumn?: string): Promise<{
|
|
34
|
-
total: number;
|
|
35
|
-
withEmbedding: number;
|
|
36
|
-
withoutEmbedding: number;
|
|
37
|
-
}>;
|
|
38
|
-
/**
|
|
39
|
-
* 임베딩이 없는 항목 ID 조회
|
|
40
|
-
*/
|
|
41
|
-
getItemsWithoutEmbedding(embeddingColumn?: string, limit?: number): Promise<number[]>;
|
|
42
|
-
/**
|
|
43
|
-
* Embedding 인스턴스 반환 (고급 사용)
|
|
44
|
-
*/
|
|
45
|
-
getEmbedding(): Embedding;
|
|
46
|
-
}
|
|
47
|
-
//# sourceMappingURL=vector-search.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"vector-search.d.ts","sourceRoot":"","sources":["../../src/vector/vector-search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAGjC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,KAAK,EACV,aAAa,EACb,iBAAiB,EACjB,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,EAChB,YAAY,EACZ,mBAAmB,EACnB,kBAAkB,EACnB,MAAM,SAAS,CAAC;AAEjB;;;GAGG;AACH,qBAAa,YAAY,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACnD,OAAO,CAAC,EAAE,CAAO;IACjB,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,SAAS,CAAS;gBAEd,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,GAAE,OAAO,CAAC,YAAY,CAAM;IAa3E;;OAEG;IACG,aAAa,CACjB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,iBAAiB,EAC3B,eAAe,GAAE,MAA4B,GAC5C,OAAO,CAAC,IAAI,CAAC;IAUhB;;OAEG;IACG,mBAAmB,CACvB,KAAK,EAAE,aAAa,EAAE,EACtB,QAAQ,EAAE,iBAAiB,EAC3B,eAAe,GAAE,MAA4B,EAC7C,UAAU,CAAC,EAAE,gBAAgB,GAC5B,OAAO,CAAC,IAAI,CAAC;IAehB;;OAEG;IACG,MAAM,CACV,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,iBAAiB,EAC3B,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC,EAAE,CAAC;IAyCnC;;OAEG;IACG,YAAY,CAChB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,iBAAiB,EAC3B,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC,EAAE,CAAC;IAiFnC;;OAEG;IACG,kBAAkB,CAAC,eAAe,GAAE,MAA4B,GAAG,OAAO,CAAC;QAC/E,KAAK,EAAE,MAAM,CAAC;QACd,aAAa,EAAE,MAAM,CAAC;QACtB,gBAAgB,EAAE,MAAM,CAAC;KAC1B,CAAC;IAgBF;;OAEG;IACG,wBAAwB,CAC5B,eAAe,GAAE,MAA4B,EAC7C,KAAK,GAAE,MAAY,GAClB,OAAO,CAAC,MAAM,EAAE,CAAC;IAUpB;;OAEG;IACH,YAAY,IAAI,SAAS;CAG1B"}
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import pgvector from "pgvector/knex";
|
|
2
|
-
import { DEFAULT_VECTOR_CONFIG } from "./config.js";
|
|
3
|
-
import { Embedding } from "./embedding.js";
|
|
4
|
-
/**
|
|
5
|
-
* 벡터 검색
|
|
6
|
-
* pgvector를 활용한 벡터 검색 및 하이브리드 검색 지원
|
|
7
|
-
*/ export class VectorSearch {
|
|
8
|
-
db;
|
|
9
|
-
config;
|
|
10
|
-
embedding;
|
|
11
|
-
tableName;
|
|
12
|
-
constructor(db, tableName, config = {}){
|
|
13
|
-
this.db = db;
|
|
14
|
-
this.tableName = tableName;
|
|
15
|
-
this.config = {
|
|
16
|
-
voyage: {
|
|
17
|
-
...DEFAULT_VECTOR_CONFIG.voyage,
|
|
18
|
-
...config.voyage
|
|
19
|
-
},
|
|
20
|
-
openai: {
|
|
21
|
-
...DEFAULT_VECTOR_CONFIG.openai,
|
|
22
|
-
...config.openai
|
|
23
|
-
},
|
|
24
|
-
chunking: {
|
|
25
|
-
...DEFAULT_VECTOR_CONFIG.chunking,
|
|
26
|
-
...config.chunking
|
|
27
|
-
},
|
|
28
|
-
search: {
|
|
29
|
-
...DEFAULT_VECTOR_CONFIG.search,
|
|
30
|
-
...config.search
|
|
31
|
-
},
|
|
32
|
-
pgvector: {
|
|
33
|
-
...DEFAULT_VECTOR_CONFIG.pgvector,
|
|
34
|
-
...config.pgvector
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
this.embedding = new Embedding(config);
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* 단일 항목에 임베딩 저장
|
|
41
|
-
*/ async saveEmbedding(id, text, provider, embeddingColumn = "content_embedding") {
|
|
42
|
-
const { embedding } = await this.embedding.embedOne(text, provider, "document");
|
|
43
|
-
await this.db(this.tableName).where("id", id).update({
|
|
44
|
-
[embeddingColumn]: pgvector.toSql(embedding)
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* 여러 항목에 임베딩 일괄 저장
|
|
49
|
-
*/ async saveEmbeddingsBatch(items, provider, embeddingColumn = "content_embedding", onProgress) {
|
|
50
|
-
const texts = items.map((item)=>item.text);
|
|
51
|
-
const embeddings = await this.embedding.embed(texts, provider, "document", onProgress);
|
|
52
|
-
await this.db.transaction(async (trx)=>{
|
|
53
|
-
for(let i = 0; i < items.length; i++){
|
|
54
|
-
await trx(this.tableName).where("id", items[i].id).update({
|
|
55
|
-
[embeddingColumn]: pgvector.toSql(embeddings[i].embedding)
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* 벡터 검색 (코사인 유사도)
|
|
62
|
-
*/ async search(query, provider, options = {}) {
|
|
63
|
-
const { embeddingColumn = "content_embedding", limit = this.config.search.defaultLimit, threshold = this.config.search.similarityThreshold, where } = options;
|
|
64
|
-
// 쿼리 임베딩 (input_type: 'query' 중요!)
|
|
65
|
-
const { embedding } = await this.embedding.embedOne(query, provider, "query");
|
|
66
|
-
// pgvector 세션 설정
|
|
67
|
-
if (this.config.pgvector.iterativeScan) {
|
|
68
|
-
await this.db.raw("SET hnsw.iterative_scan = relaxed_order");
|
|
69
|
-
}
|
|
70
|
-
await this.db.raw(`SET hnsw.ef_search = ${this.config.pgvector.efSearch}`);
|
|
71
|
-
// 코사인 유사도 = 1 - 코사인 거리
|
|
72
|
-
const vectorStr = pgvector.toSql(embedding);
|
|
73
|
-
let queryBuilder = this.db(this.tableName).select("*").select(this.db.raw(`1 - (${embeddingColumn} <=> ?::vector) AS similarity`, [
|
|
74
|
-
vectorStr
|
|
75
|
-
])).whereNotNull(embeddingColumn).orderByRaw(`${embeddingColumn} <=> ?::vector`, [
|
|
76
|
-
vectorStr
|
|
77
|
-
]).limit(limit);
|
|
78
|
-
if (where) {
|
|
79
|
-
queryBuilder = queryBuilder.whereRaw(where);
|
|
80
|
-
}
|
|
81
|
-
const rows = await queryBuilder;
|
|
82
|
-
return rows.filter((row)=>row.similarity >= threshold).map((row)=>({
|
|
83
|
-
id: row.id,
|
|
84
|
-
similarity: parseFloat(String(row.similarity)),
|
|
85
|
-
data: row
|
|
86
|
-
}));
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* 하이브리드 검색 (Vector + FTS)
|
|
90
|
-
*/ async hybridSearch(query, provider, options = {}) {
|
|
91
|
-
const { embeddingColumn = "content_embedding", ftsColumn = "content_tsv", limit = this.config.search.defaultLimit, vectorWeight = this.config.search.vectorWeight, ftsWeight = this.config.search.ftsWeight } = options;
|
|
92
|
-
const { embedding } = await this.embedding.embedOne(query, provider, "query");
|
|
93
|
-
const vectorStr = pgvector.toSql(embedding);
|
|
94
|
-
// pgvector 세션 설정
|
|
95
|
-
if (this.config.pgvector.iterativeScan) {
|
|
96
|
-
await this.db.raw("SET hnsw.iterative_scan = relaxed_order");
|
|
97
|
-
}
|
|
98
|
-
await this.db.raw(`SET hnsw.ef_search = ${this.config.pgvector.efSearch}`);
|
|
99
|
-
const sql = `
|
|
100
|
-
WITH vector_search AS (
|
|
101
|
-
SELECT
|
|
102
|
-
id,
|
|
103
|
-
ROW_NUMBER() OVER (ORDER BY ${embeddingColumn} <=> ?::vector) AS rank
|
|
104
|
-
FROM ${this.tableName}
|
|
105
|
-
WHERE ${embeddingColumn} IS NOT NULL
|
|
106
|
-
ORDER BY ${embeddingColumn} <=> ?::vector
|
|
107
|
-
LIMIT 50
|
|
108
|
-
),
|
|
109
|
-
fts_search AS (
|
|
110
|
-
SELECT
|
|
111
|
-
id,
|
|
112
|
-
ROW_NUMBER() OVER (ORDER BY ts_rank(${ftsColumn}, query) DESC) AS rank
|
|
113
|
-
FROM ${this.tableName}, plainto_tsquery('simple', ?) query
|
|
114
|
-
WHERE ${ftsColumn} @@ query
|
|
115
|
-
LIMIT 50
|
|
116
|
-
),
|
|
117
|
-
combined AS (
|
|
118
|
-
SELECT
|
|
119
|
-
COALESCE(v.id, f.id) AS id,
|
|
120
|
-
COALESCE(1.0 / (60 + v.rank), 0) AS vector_score,
|
|
121
|
-
COALESCE(1.0 / (60 + f.rank), 0) AS fts_score
|
|
122
|
-
FROM vector_search v
|
|
123
|
-
FULL OUTER JOIN fts_search f ON v.id = f.id
|
|
124
|
-
)
|
|
125
|
-
SELECT
|
|
126
|
-
t.*,
|
|
127
|
-
c.vector_score,
|
|
128
|
-
c.fts_score,
|
|
129
|
-
(c.vector_score * ? + c.fts_score * ?) AS similarity
|
|
130
|
-
FROM combined c
|
|
131
|
-
JOIN ${this.tableName} t ON c.id = t.id
|
|
132
|
-
ORDER BY similarity DESC
|
|
133
|
-
LIMIT ?
|
|
134
|
-
`;
|
|
135
|
-
const { rows } = await this.db.raw(sql, [
|
|
136
|
-
vectorStr,
|
|
137
|
-
vectorStr,
|
|
138
|
-
query,
|
|
139
|
-
vectorWeight,
|
|
140
|
-
ftsWeight,
|
|
141
|
-
limit
|
|
142
|
-
]);
|
|
143
|
-
return rows.map((row)=>({
|
|
144
|
-
id: row.id,
|
|
145
|
-
similarity: parseFloat(String(row.similarity)),
|
|
146
|
-
vectorScore: parseFloat(String(row.vector_score)),
|
|
147
|
-
ftsScore: parseFloat(String(row.fts_score)),
|
|
148
|
-
data: row
|
|
149
|
-
}));
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* 임베딩 현황 조회
|
|
153
|
-
*/ async getEmbeddingStatus(embeddingColumn = "content_embedding") {
|
|
154
|
-
const result = await this.db(this.tableName).count("* as total").count(`${embeddingColumn} as with_embedding`).first();
|
|
155
|
-
const total = parseInt(String(result?.total ?? 0), 10);
|
|
156
|
-
const withEmbedding = parseInt(String(result?.with_embedding ?? 0), 10);
|
|
157
|
-
return {
|
|
158
|
-
total,
|
|
159
|
-
withEmbedding,
|
|
160
|
-
withoutEmbedding: total - withEmbedding
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* 임베딩이 없는 항목 ID 조회
|
|
165
|
-
*/ async getItemsWithoutEmbedding(embeddingColumn = "content_embedding", limit = 100) {
|
|
166
|
-
const rows = await this.db(this.tableName).select("id").whereNull(embeddingColumn).orderBy("id").limit(limit);
|
|
167
|
-
return rows.map((row)=>row.id);
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* Embedding 인스턴스 반환 (고급 사용)
|
|
171
|
-
*/ getEmbedding() {
|
|
172
|
-
return this.embedding;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy92ZWN0b3IvdmVjdG9yLXNlYXJjaC50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgdHlwZSB7IEtuZXggfSBmcm9tIFwia25leFwiO1xuaW1wb3J0IHBndmVjdG9yIGZyb20gXCJwZ3ZlY3Rvci9rbmV4XCI7XG5pbXBvcnQgeyBERUZBVUxUX1ZFQ1RPUl9DT05GSUcgfSBmcm9tIFwiLi9jb25maWdcIjtcbmltcG9ydCB7IEVtYmVkZGluZyB9IGZyb20gXCIuL2VtYmVkZGluZ1wiO1xuaW1wb3J0IHR5cGUge1xuICBFbWJlZGRpbmdJdGVtLFxuICBFbWJlZGRpbmdQcm92aWRlcixcbiAgSHlicmlkU2VhcmNoT3B0aW9ucyxcbiAgSHlicmlkU2VhcmNoUmVzdWx0LFxuICBQcm9ncmVzc0NhbGxiYWNrLFxuICBWZWN0b3JDb25maWcsXG4gIFZlY3RvclNlYXJjaE9wdGlvbnMsXG4gIFZlY3RvclNlYXJjaFJlc3VsdCxcbn0gZnJvbSBcIi4vdHlwZXNcIjtcblxuLyoqXG4gKiDrsqHthLAg6rKA7IOJXG4gKiBwZ3ZlY3RvcuulvCDtmZzsmqntlZwg67Kh7YSwIOqygOyDiSDrsI8g7ZWY7J2067iM66as65OcIOqygOyDiSDsp4Dsm5BcbiAqL1xuZXhwb3J0IGNsYXNzIFZlY3RvclNlYXJjaDxUID0gUmVjb3JkPHN0cmluZywgdW5rbm93bj4+IHtcbiAgcHJpdmF0ZSBkYjogS25leDtcbiAgcHJpdmF0ZSBjb25maWc6IFZlY3RvckNvbmZpZztcbiAgcHJpdmF0ZSBlbWJlZGRpbmc6IEVtYmVkZGluZztcbiAgcHJpdmF0ZSB0YWJsZU5hbWU6IHN0cmluZztcblxuICBjb25zdHJ1Y3RvcihkYjogS25leCwgdGFibGVOYW1lOiBzdHJpbmcsIGNvbmZpZzogUGFydGlhbDxWZWN0b3JDb25maWc+ID0ge30pIHtcbiAgICB0aGlzLmRiID0gZGI7XG4gICAgdGhpcy50YWJsZU5hbWUgPSB0YWJsZU5hbWU7XG4gICAgdGhpcy5jb25maWcgPSB7XG4gICAgICB2b3lhZ2U6IHsgLi4uREVGQVVMVF9WRUNUT1JfQ09ORklHLnZveWFnZSwgLi4uY29uZmlnLnZveWFnZSB9LFxuICAgICAgb3BlbmFpOiB7IC4uLkRFRkFVTFRfVkVDVE9SX0NPTkZJRy5vcGVuYWksIC4uLmNvbmZpZy5vcGVuYWkgfSxcbiAgICAgIGNodW5raW5nOiB7IC4uLkRFRkFVTFRfVkVDVE9SX0NPTkZJRy5jaHVua2luZywgLi4uY29uZmlnLmNodW5raW5nIH0sXG4gICAgICBzZWFyY2g6IHsgLi4uREVGQVVMVF9WRUNUT1JfQ09ORklHLnNlYXJjaCwgLi4uY29uZmlnLnNlYXJjaCB9LFxuICAgICAgcGd2ZWN0b3I6IHsgLi4uREVGQVVMVF9WRUNUT1JfQ09ORklHLnBndmVjdG9yLCAuLi5jb25maWcucGd2ZWN0b3IgfSxcbiAgICB9O1xuICAgIHRoaXMuZW1iZWRkaW5nID0gbmV3IEVtYmVkZGluZyhjb25maWcpO1xuICB9XG5cbiAgLyoqXG4gICAqIOuLqOydvCDtla3rqqnsl5Ag7J6E67Kg65SpIOyggOyepVxuICAgKi9cbiAgYXN5bmMgc2F2ZUVtYmVkZGluZyhcbiAgICBpZDogbnVtYmVyLFxuICAgIHRleHQ6IHN0cmluZyxcbiAgICBwcm92aWRlcjogRW1iZWRkaW5nUHJvdmlkZXIsXG4gICAgZW1iZWRkaW5nQ29sdW1uOiBzdHJpbmcgPSBcImNvbnRlbnRfZW1iZWRkaW5nXCIsXG4gICk6IFByb21pc2U8dm9pZD4ge1xuICAgIGNvbnN0IHsgZW1iZWRkaW5nIH0gPSBhd2FpdCB0aGlzLmVtYmVkZGluZy5lbWJlZE9uZSh0ZXh0LCBwcm92aWRlciwgXCJkb2N1bWVudFwiKTtcblxuICAgIGF3YWl0IHRoaXMuZGIodGhpcy50YWJsZU5hbWUpXG4gICAgICAud2hlcmUoXCJpZFwiLCBpZClcbiAgICAgIC51cGRhdGUoe1xuICAgICAgICBbZW1iZWRkaW5nQ29sdW1uXTogcGd2ZWN0b3IudG9TcWwoZW1iZWRkaW5nKSxcbiAgICAgIH0pO1xuICB9XG5cbiAgLyoqXG4gICAqIOyXrOufrCDtla3rqqnsl5Ag7J6E67Kg65SpIOydvOq0hCDsoIDsnqVcbiAgICovXG4gIGFzeW5jIHNhdmVFbWJlZGRpbmdzQmF0Y2goXG4gICAgaXRlbXM6IEVtYmVkZGluZ0l0ZW1bXSxcbiAgICBwcm92aWRlcjogRW1iZWRkaW5nUHJvdmlkZXIsXG4gICAgZW1iZWRkaW5nQ29sdW1uOiBzdHJpbmcgPSBcImNvbnRlbnRfZW1iZWRkaW5nXCIsXG4gICAgb25Qcm9ncmVzcz86IFByb2dyZXNzQ2FsbGJhY2ssXG4gICk6IFByb21pc2U8dm9pZD4ge1xuICAgIGNvbnN0IHRleHRzID0gaXRlbXMubWFwKChpdGVtKSA9PiBpdGVtLnRleHQpO1xuICAgIGNvbnN0IGVtYmVkZGluZ3MgPSBhd2FpdCB0aGlzLmVtYmVkZGluZy5lbWJlZCh0ZXh0cywgcHJvdmlkZXIsIFwiZG9jdW1lbnRcIiwgb25Qcm9ncmVzcyk7XG5cbiAgICBhd2FpdCB0aGlzLmRiLnRyYW5zYWN0aW9uKGFzeW5jICh0cngpID0+IHtcbiAgICAgIGZvciAobGV0IGkgPSAwOyBpIDwgaXRlbXMubGVuZ3RoOyBpKyspIHtcbiAgICAgICAgYXdhaXQgdHJ4KHRoaXMudGFibGVOYW1lKVxuICAgICAgICAgIC53aGVyZShcImlkXCIsIGl0ZW1zW2ldLmlkKVxuICAgICAgICAgIC51cGRhdGUoe1xuICAgICAgICAgICAgW2VtYmVkZGluZ0NvbHVtbl06IHBndmVjdG9yLnRvU3FsKGVtYmVkZGluZ3NbaV0uZW1iZWRkaW5nKSxcbiAgICAgICAgICB9KTtcbiAgICAgIH1cbiAgICB9KTtcbiAgfVxuXG4gIC8qKlxuICAgKiDrsqHthLAg6rKA7IOJICjsvZTsgqzsnbgg7Jyg7IKs64+EKVxuICAgKi9cbiAgYXN5bmMgc2VhcmNoKFxuICAgIHF1ZXJ5OiBzdHJpbmcsXG4gICAgcHJvdmlkZXI6IEVtYmVkZGluZ1Byb3ZpZGVyLFxuICAgIG9wdGlvbnM6IFZlY3RvclNlYXJjaE9wdGlvbnMgPSB7fSxcbiAgKTogUHJvbWlzZTxWZWN0b3JTZWFyY2hSZXN1bHQ8VD5bXT4ge1xuICAgIGNvbnN0IHtcbiAgICAgIGVtYmVkZGluZ0NvbHVtbiA9IFwiY29udGVudF9lbWJlZGRpbmdcIixcbiAgICAgIGxpbWl0ID0gdGhpcy5jb25maWcuc2VhcmNoLmRlZmF1bHRMaW1pdCxcbiAgICAgIHRocmVzaG9sZCA9IHRoaXMuY29uZmlnLnNlYXJjaC5zaW1pbGFyaXR5VGhyZXNob2xkLFxuICAgICAgd2hlcmUsXG4gICAgfSA9IG9wdGlvbnM7XG5cbiAgICAvLyDsv7zrpqwg7J6E67Kg65SpIChpbnB1dF90eXBlOiAncXVlcnknIOykkeyalCEpXG4gICAgY29uc3QgeyBlbWJlZGRpbmcgfSA9IGF3YWl0IHRoaXMuZW1iZWRkaW5nLmVtYmVkT25lKHF1ZXJ5LCBwcm92aWRlciwgXCJxdWVyeVwiKTtcblxuICAgIC8vIHBndmVjdG9yIOyEuOyFmCDshKTsoJVcbiAgICBpZiAodGhpcy5jb25maWcucGd2ZWN0b3IuaXRlcmF0aXZlU2Nhbikge1xuICAgICAgYXdhaXQgdGhpcy5kYi5yYXcoXCJTRVQgaG5zdy5pdGVyYXRpdmVfc2NhbiA9IHJlbGF4ZWRfb3JkZXJcIik7XG4gICAgfVxuICAgIGF3YWl0IHRoaXMuZGIucmF3KGBTRVQgaG5zdy5lZl9zZWFyY2ggPSAke3RoaXMuY29uZmlnLnBndmVjdG9yLmVmU2VhcmNofWApO1xuXG4gICAgLy8g7L2U7IKs7J24IOycoOyCrOuPhCA9IDEgLSDsvZTsgqzsnbgg6rGw66asXG4gICAgY29uc3QgdmVjdG9yU3RyID0gcGd2ZWN0b3IudG9TcWwoZW1iZWRkaW5nKTtcbiAgICBsZXQgcXVlcnlCdWlsZGVyID0gdGhpcy5kYih0aGlzLnRhYmxlTmFtZSlcbiAgICAgIC5zZWxlY3QoXCIqXCIpXG4gICAgICAuc2VsZWN0KHRoaXMuZGIucmF3KGAxIC0gKCR7ZW1iZWRkaW5nQ29sdW1ufSA8PT4gPzo6dmVjdG9yKSBBUyBzaW1pbGFyaXR5YCwgW3ZlY3RvclN0cl0pKVxuICAgICAgLndoZXJlTm90TnVsbChlbWJlZGRpbmdDb2x1bW4pXG4gICAgICAub3JkZXJCeVJhdyhgJHtlbWJlZGRpbmdDb2x1bW59IDw9PiA/Ojp2ZWN0b3JgLCBbdmVjdG9yU3RyXSlcbiAgICAgIC5saW1pdChsaW1pdCk7XG5cbiAgICBpZiAod2hlcmUpIHtcbiAgICAgIHF1ZXJ5QnVpbGRlciA9IHF1ZXJ5QnVpbGRlci53aGVyZVJhdyh3aGVyZSk7XG4gICAgfVxuXG4gICAgY29uc3Qgcm93cyA9IGF3YWl0IHF1ZXJ5QnVpbGRlcjtcblxuICAgIHJldHVybiByb3dzXG4gICAgICAuZmlsdGVyKChyb3c6IHsgc2ltaWxhcml0eTogbnVtYmVyIH0pID0+IHJvdy5zaW1pbGFyaXR5ID49IHRocmVzaG9sZClcbiAgICAgIC5tYXAoKHJvdzogVCAmIHsgc2ltaWxhcml0eTogbnVtYmVyIH0pID0+ICh7XG4gICAgICAgIGlkOiAocm93IGFzIHVua25vd24gYXMgeyBpZDogbnVtYmVyIH0pLmlkLFxuICAgICAgICBzaW1pbGFyaXR5OiBwYXJzZUZsb2F0KFN0cmluZyhyb3cuc2ltaWxhcml0eSkpLFxuICAgICAgICBkYXRhOiByb3cgYXMgVCxcbiAgICAgIH0pKTtcbiAgfVxuXG4gIC8qKlxuICAgKiDtlZjsnbTruIzrpqzrk5wg6rKA7IOJIChWZWN0b3IgKyBGVFMpXG4gICAqL1xuICBhc3luYyBoeWJyaWRTZWFyY2goXG4gICAgcXVlcnk6IHN0cmluZyxcbiAgICBwcm92aWRlcjogRW1iZWRkaW5nUHJvdmlkZXIsXG4gICAgb3B0aW9uczogSHlicmlkU2VhcmNoT3B0aW9ucyA9IHt9LFxuICApOiBQcm9taXNlPEh5YnJpZFNlYXJjaFJlc3VsdDxUPltdPiB7XG4gICAgY29uc3Qge1xuICAgICAgZW1iZWRkaW5nQ29sdW1uID0gXCJjb250ZW50X2VtYmVkZGluZ1wiLFxuICAgICAgZnRzQ29sdW1uID0gXCJjb250ZW50X3RzdlwiLFxuICAgICAgbGltaXQgPSB0aGlzLmNvbmZpZy5zZWFyY2guZGVmYXVsdExpbWl0LFxuICAgICAgdmVjdG9yV2VpZ2h0ID0gdGhpcy5jb25maWcuc2VhcmNoLnZlY3RvcldlaWdodCxcbiAgICAgIGZ0c1dlaWdodCA9IHRoaXMuY29uZmlnLnNlYXJjaC5mdHNXZWlnaHQsXG4gICAgfSA9IG9wdGlvbnM7XG5cbiAgICBjb25zdCB7IGVtYmVkZGluZyB9ID0gYXdhaXQgdGhpcy5lbWJlZGRpbmcuZW1iZWRPbmUocXVlcnksIHByb3ZpZGVyLCBcInF1ZXJ5XCIpO1xuICAgIGNvbnN0IHZlY3RvclN0ciA9IHBndmVjdG9yLnRvU3FsKGVtYmVkZGluZyk7XG5cbiAgICAvLyBwZ3ZlY3RvciDshLjshZgg7ISk7KCVXG4gICAgaWYgKHRoaXMuY29uZmlnLnBndmVjdG9yLml0ZXJhdGl2ZVNjYW4pIHtcbiAgICAgIGF3YWl0IHRoaXMuZGIucmF3KFwiU0VUIGhuc3cuaXRlcmF0aXZlX3NjYW4gPSByZWxheGVkX29yZGVyXCIpO1xuICAgIH1cbiAgICBhd2FpdCB0aGlzLmRiLnJhdyhgU0VUIGhuc3cuZWZfc2VhcmNoID0gJHt0aGlzLmNvbmZpZy5wZ3ZlY3Rvci5lZlNlYXJjaH1gKTtcblxuICAgIGNvbnN0IHNxbCA9IGBcbiAgICAgIFdJVEggdmVjdG9yX3NlYXJjaCBBUyAoXG4gICAgICAgIFNFTEVDVFxuICAgICAgICAgIGlkLFxuICAgICAgICAgIFJPV19OVU1CRVIoKSBPVkVSIChPUkRFUiBCWSAke2VtYmVkZGluZ0NvbHVtbn0gPD0+ID86OnZlY3RvcikgQVMgcmFua1xuICAgICAgICBGUk9NICR7dGhpcy50YWJsZU5hbWV9XG4gICAgICAgIFdIRVJFICR7ZW1iZWRkaW5nQ29sdW1ufSBJUyBOT1QgTlVMTFxuICAgICAgICBPUkRFUiBCWSAke2VtYmVkZGluZ0NvbHVtbn0gPD0+ID86OnZlY3RvclxuICAgICAgICBMSU1JVCA1MFxuICAgICAgKSxcbiAgICAgIGZ0c19zZWFyY2ggQVMgKFxuICAgICAgICBTRUxFQ1RcbiAgICAgICAgICBpZCxcbiAgICAgICAgICBST1dfTlVNQkVSKCkgT1ZFUiAoT1JERVIgQlkgdHNfcmFuaygke2Z0c0NvbHVtbn0sIHF1ZXJ5KSBERVNDKSBBUyByYW5rXG4gICAgICAgIEZST00gJHt0aGlzLnRhYmxlTmFtZX0sIHBsYWludG9fdHNxdWVyeSgnc2ltcGxlJywgPykgcXVlcnlcbiAgICAgICAgV0hFUkUgJHtmdHNDb2x1bW59IEBAIHF1ZXJ5XG4gICAgICAgIExJTUlUIDUwXG4gICAgICApLFxuICAgICAgY29tYmluZWQgQVMgKFxuICAgICAgICBTRUxFQ1RcbiAgICAgICAgICBDT0FMRVNDRSh2LmlkLCBmLmlkKSBBUyBpZCxcbiAgICAgICAgICBDT0FMRVNDRSgxLjAgLyAoNjAgKyB2LnJhbmspLCAwKSBBUyB2ZWN0b3Jfc2NvcmUsXG4gICAgICAgICAgQ09BTEVTQ0UoMS4wIC8gKDYwICsgZi5yYW5rKSwgMCkgQVMgZnRzX3Njb3JlXG4gICAgICAgIEZST00gdmVjdG9yX3NlYXJjaCB2XG4gICAgICAgIEZVTEwgT1VURVIgSk9JTiBmdHNfc2VhcmNoIGYgT04gdi5pZCA9IGYuaWRcbiAgICAgIClcbiAgICAgIFNFTEVDVFxuICAgICAgICB0LiosXG4gICAgICAgIGMudmVjdG9yX3Njb3JlLFxuICAgICAgICBjLmZ0c19zY29yZSxcbiAgICAgICAgKGMudmVjdG9yX3Njb3JlICogPyArIGMuZnRzX3Njb3JlICogPykgQVMgc2ltaWxhcml0eVxuICAgICAgRlJPTSBjb21iaW5lZCBjXG4gICAgICBKT0lOICR7dGhpcy50YWJsZU5hbWV9IHQgT04gYy5pZCA9IHQuaWRcbiAgICAgIE9SREVSIEJZIHNpbWlsYXJpdHkgREVTQ1xuICAgICAgTElNSVQgP1xuICAgIGA7XG5cbiAgICBjb25zdCB7IHJvd3MgfSA9IGF3YWl0IHRoaXMuZGIucmF3KHNxbCwgW1xuICAgICAgdmVjdG9yU3RyLFxuICAgICAgdmVjdG9yU3RyLFxuICAgICAgcXVlcnksXG4gICAgICB2ZWN0b3JXZWlnaHQsXG4gICAgICBmdHNXZWlnaHQsXG4gICAgICBsaW1pdCxcbiAgICBdKTtcblxuICAgIHJldHVybiByb3dzLm1hcChcbiAgICAgIChcbiAgICAgICAgcm93OiBUICYge1xuICAgICAgICAgIHNpbWlsYXJpdHk6IG51bWJlcjtcbiAgICAgICAgICB2ZWN0b3Jfc2NvcmU6IG51bWJlcjtcbiAgICAgICAgICBmdHNfc2NvcmU6IG51bWJlcjtcbiAgICAgICAgfSxcbiAgICAgICkgPT4gKHtcbiAgICAgICAgaWQ6IChyb3cgYXMgdW5rbm93biBhcyB7IGlkOiBudW1iZXIgfSkuaWQsXG4gICAgICAgIHNpbWlsYXJpdHk6IHBhcnNlRmxvYXQoU3RyaW5nKHJvdy5zaW1pbGFyaXR5KSksXG4gICAgICAgIHZlY3RvclNjb3JlOiBwYXJzZUZsb2F0KFN0cmluZyhyb3cudmVjdG9yX3Njb3JlKSksXG4gICAgICAgIGZ0c1Njb3JlOiBwYXJzZUZsb2F0KFN0cmluZyhyb3cuZnRzX3Njb3JlKSksXG4gICAgICAgIGRhdGE6IHJvdyBhcyBULFxuICAgICAgfSksXG4gICAgKTtcbiAgfVxuXG4gIC8qKlxuICAgKiDsnoTrsqDrlKkg7ZiE7ZmpIOyhsO2ajFxuICAgKi9cbiAgYXN5bmMgZ2V0RW1iZWRkaW5nU3RhdHVzKGVtYmVkZGluZ0NvbHVtbjogc3RyaW5nID0gXCJjb250ZW50X2VtYmVkZGluZ1wiKTogUHJvbWlzZTx7XG4gICAgdG90YWw6IG51bWJlcjtcbiAgICB3aXRoRW1iZWRkaW5nOiBudW1iZXI7XG4gICAgd2l0aG91dEVtYmVkZGluZzogbnVtYmVyO1xuICB9PiB7XG4gICAgY29uc3QgcmVzdWx0ID0gYXdhaXQgdGhpcy5kYih0aGlzLnRhYmxlTmFtZSlcbiAgICAgIC5jb3VudChcIiogYXMgdG90YWxcIilcbiAgICAgIC5jb3VudChgJHtlbWJlZGRpbmdDb2x1bW59IGFzIHdpdGhfZW1iZWRkaW5nYClcbiAgICAgIC5maXJzdCgpO1xuXG4gICAgY29uc3QgdG90YWwgPSBwYXJzZUludChTdHJpbmcocmVzdWx0Py50b3RhbCA/PyAwKSwgMTApO1xuICAgIGNvbnN0IHdpdGhFbWJlZGRpbmcgPSBwYXJzZUludChTdHJpbmcocmVzdWx0Py53aXRoX2VtYmVkZGluZyA/PyAwKSwgMTApO1xuXG4gICAgcmV0dXJuIHtcbiAgICAgIHRvdGFsLFxuICAgICAgd2l0aEVtYmVkZGluZyxcbiAgICAgIHdpdGhvdXRFbWJlZGRpbmc6IHRvdGFsIC0gd2l0aEVtYmVkZGluZyxcbiAgICB9O1xuICB9XG5cbiAgLyoqXG4gICAqIOyehOuyoOuUqeydtCDsl4bripQg7ZWt66qpIElEIOyhsO2ajFxuICAgKi9cbiAgYXN5bmMgZ2V0SXRlbXNXaXRob3V0RW1iZWRkaW5nKFxuICAgIGVtYmVkZGluZ0NvbHVtbjogc3RyaW5nID0gXCJjb250ZW50X2VtYmVkZGluZ1wiLFxuICAgIGxpbWl0OiBudW1iZXIgPSAxMDAsXG4gICk6IFByb21pc2U8bnVtYmVyW10+IHtcbiAgICBjb25zdCByb3dzID0gYXdhaXQgdGhpcy5kYih0aGlzLnRhYmxlTmFtZSlcbiAgICAgIC5zZWxlY3QoXCJpZFwiKVxuICAgICAgLndoZXJlTnVsbChlbWJlZGRpbmdDb2x1bW4pXG4gICAgICAub3JkZXJCeShcImlkXCIpXG4gICAgICAubGltaXQobGltaXQpO1xuXG4gICAgcmV0dXJuIHJvd3MubWFwKChyb3c6IHsgaWQ6IG51bWJlciB9KSA9PiByb3cuaWQpO1xuICB9XG5cbiAgLyoqXG4gICAqIEVtYmVkZGluZyDsnbjsiqTthLTsiqQg67CY7ZmYICjqs6DquIkg7IKs7JqpKVxuICAgKi9cbiAgZ2V0RW1iZWRkaW5nKCk6IEVtYmVkZGluZyB7XG4gICAgcmV0dXJuIHRoaXMuZW1iZWRkaW5nO1xuICB9XG59XG4iXSwibmFtZXMiOlsicGd2ZWN0b3IiLCJERUZBVUxUX1ZFQ1RPUl9DT05GSUciLCJFbWJlZGRpbmciLCJWZWN0b3JTZWFyY2giLCJkYiIsImNvbmZpZyIsImVtYmVkZGluZyIsInRhYmxlTmFtZSIsInZveWFnZSIsIm9wZW5haSIsImNodW5raW5nIiwic2VhcmNoIiwic2F2ZUVtYmVkZGluZyIsImlkIiwidGV4dCIsInByb3ZpZGVyIiwiZW1iZWRkaW5nQ29sdW1uIiwiZW1iZWRPbmUiLCJ3aGVyZSIsInVwZGF0ZSIsInRvU3FsIiwic2F2ZUVtYmVkZGluZ3NCYXRjaCIsIml0ZW1zIiwib25Qcm9ncmVzcyIsInRleHRzIiwibWFwIiwiaXRlbSIsImVtYmVkZGluZ3MiLCJlbWJlZCIsInRyYW5zYWN0aW9uIiwidHJ4IiwiaSIsImxlbmd0aCIsInF1ZXJ5Iiwib3B0aW9ucyIsImxpbWl0IiwiZGVmYXVsdExpbWl0IiwidGhyZXNob2xkIiwic2ltaWxhcml0eVRocmVzaG9sZCIsIml0ZXJhdGl2ZVNjYW4iLCJyYXciLCJlZlNlYXJjaCIsInZlY3RvclN0ciIsInF1ZXJ5QnVpbGRlciIsInNlbGVjdCIsIndoZXJlTm90TnVsbCIsIm9yZGVyQnlSYXciLCJ3aGVyZVJhdyIsInJvd3MiLCJmaWx0ZXIiLCJyb3ciLCJzaW1pbGFyaXR5IiwicGFyc2VGbG9hdCIsIlN0cmluZyIsImRhdGEiLCJoeWJyaWRTZWFyY2giLCJmdHNDb2x1bW4iLCJ2ZWN0b3JXZWlnaHQiLCJmdHNXZWlnaHQiLCJzcWwiLCJ2ZWN0b3JTY29yZSIsInZlY3Rvcl9zY29yZSIsImZ0c1Njb3JlIiwiZnRzX3Njb3JlIiwiZ2V0RW1iZWRkaW5nU3RhdHVzIiwicmVzdWx0IiwiY291bnQiLCJmaXJzdCIsInRvdGFsIiwicGFyc2VJbnQiLCJ3aXRoRW1iZWRkaW5nIiwid2l0aF9lbWJlZGRpbmciLCJ3aXRob3V0RW1iZWRkaW5nIiwiZ2V0SXRlbXNXaXRob3V0RW1iZWRkaW5nIiwid2hlcmVOdWxsIiwib3JkZXJCeSIsImdldEVtYmVkZGluZyJdLCJtYXBwaW5ncyI6IkFBQ0EsT0FBT0EsY0FBYyxnQkFBZ0I7QUFDckMsU0FBU0MscUJBQXFCLFFBQVEsY0FBVztBQUNqRCxTQUFTQyxTQUFTLFFBQVEsaUJBQWM7QUFZeEM7OztDQUdDLEdBQ0QsT0FBTyxNQUFNQztJQUNIQyxHQUFTO0lBQ1RDLE9BQXFCO0lBQ3JCQyxVQUFxQjtJQUNyQkMsVUFBa0I7SUFFMUIsWUFBWUgsRUFBUSxFQUFFRyxTQUFpQixFQUFFRixT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
|