sonamu 0.7.11 → 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.
Files changed (118) hide show
  1. package/dist/api/config.d.ts +10 -3
  2. package/dist/api/config.d.ts.map +1 -1
  3. package/dist/api/config.js +2 -1
  4. package/dist/api/sonamu.d.ts +4 -0
  5. package/dist/api/sonamu.d.ts.map +1 -1
  6. package/dist/api/sonamu.js +36 -2
  7. package/dist/bin/cli.js +121 -117
  8. package/dist/database/base-model.d.ts +10 -50
  9. package/dist/database/base-model.d.ts.map +1 -1
  10. package/dist/database/base-model.js +19 -84
  11. package/dist/database/base-model.types.d.ts +4 -4
  12. package/dist/database/base-model.types.d.ts.map +1 -1
  13. package/dist/database/base-model.types.js +1 -1
  14. package/dist/database/db.d.ts +1 -0
  15. package/dist/database/db.d.ts.map +1 -1
  16. package/dist/database/db.js +24 -13
  17. package/dist/database/puri-subset.test-d.js +1 -1
  18. package/dist/database/puri-subset.types.d.ts +1 -0
  19. package/dist/database/puri-subset.types.d.ts.map +1 -1
  20. package/dist/database/puri-subset.types.js +2 -2
  21. package/dist/database/puri.d.ts +82 -3
  22. package/dist/database/puri.d.ts.map +1 -1
  23. package/dist/database/puri.js +180 -14
  24. package/dist/database/puri.types.d.ts +33 -6
  25. package/dist/database/puri.types.d.ts.map +1 -1
  26. package/dist/database/puri.types.js +1 -1
  27. package/dist/database/puri.types.test-d.js +1 -1
  28. package/dist/entity/entity-manager.d.ts +5 -4
  29. package/dist/entity/entity-manager.d.ts.map +1 -1
  30. package/dist/entity/entity-manager.js +8 -1
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +3 -3
  34. package/dist/migration/code-generation.d.ts.map +1 -1
  35. package/dist/migration/code-generation.js +33 -2
  36. package/dist/migration/postgresql-schema-reader.d.ts.map +1 -1
  37. package/dist/migration/postgresql-schema-reader.js +53 -22
  38. package/dist/naite/messaging-types.d.ts.map +1 -1
  39. package/dist/naite/messaging-types.js +1 -1
  40. package/dist/naite/naite.js +2 -2
  41. package/dist/stream/sse.d.ts +2 -6
  42. package/dist/stream/sse.d.ts.map +1 -1
  43. package/dist/stream/sse.js +9 -3
  44. package/dist/syncer/api-parser.js +5 -1
  45. package/dist/syncer/file-patterns.d.ts +1 -1
  46. package/dist/syncer/file-patterns.d.ts.map +1 -1
  47. package/dist/syncer/file-patterns.js +6 -5
  48. package/dist/syncer/module-loader.d.ts +5 -0
  49. package/dist/syncer/module-loader.d.ts.map +1 -1
  50. package/dist/syncer/module-loader.js +17 -1
  51. package/dist/syncer/syncer.d.ts +3 -0
  52. package/dist/syncer/syncer.d.ts.map +1 -1
  53. package/dist/syncer/syncer.js +12 -2
  54. package/dist/tasks/decorator.d.ts +26 -0
  55. package/dist/tasks/decorator.d.ts.map +1 -0
  56. package/dist/tasks/decorator.js +28 -0
  57. package/dist/tasks/step-wrapper.d.ts +18 -0
  58. package/dist/tasks/step-wrapper.d.ts.map +1 -0
  59. package/dist/tasks/step-wrapper.js +38 -0
  60. package/dist/tasks/workflow-manager.d.ts +40 -0
  61. package/dist/tasks/workflow-manager.d.ts.map +1 -0
  62. package/dist/tasks/workflow-manager.js +193 -0
  63. package/dist/template/implementations/generated.template.d.ts.map +1 -1
  64. package/dist/template/implementations/generated.template.js +7 -3
  65. package/dist/types/types.d.ts +27 -11
  66. package/dist/types/types.d.ts.map +1 -1
  67. package/dist/types/types.js +15 -2
  68. package/dist/utils/formatter.d.ts.map +1 -1
  69. package/dist/utils/formatter.js +10 -2
  70. package/dist/utils/model.d.ts +9 -2
  71. package/dist/utils/model.d.ts.map +1 -1
  72. package/dist/utils/model.js +16 -1
  73. package/dist/utils/type-utils.d.ts.map +1 -1
  74. package/dist/utils/type-utils.js +3 -1
  75. package/dist/vector/embedding.d.ts +2 -5
  76. package/dist/vector/embedding.d.ts.map +1 -1
  77. package/dist/vector/embedding.js +3 -7
  78. package/dist/vector/types.d.ts.map +1 -1
  79. package/dist/vector/types.js +1 -1
  80. package/package.json +5 -3
  81. package/src/api/config.ts +15 -8
  82. package/src/api/sonamu.ts +43 -2
  83. package/src/bin/cli.ts +58 -54
  84. package/src/database/base-model.ts +21 -128
  85. package/src/database/base-model.types.ts +3 -4
  86. package/src/database/db.ts +28 -18
  87. package/src/database/puri-subset.test-d.ts +1 -0
  88. package/src/database/puri-subset.types.ts +2 -0
  89. package/src/database/puri.ts +238 -27
  90. package/src/database/puri.types.test-d.ts +1 -1
  91. package/src/database/puri.types.ts +49 -6
  92. package/src/entity/entity-manager.ts +9 -0
  93. package/src/index.ts +1 -1
  94. package/src/migration/code-generation.ts +40 -1
  95. package/src/migration/postgresql-schema-reader.ts +53 -22
  96. package/src/naite/messaging-types.ts +43 -44
  97. package/src/naite/naite.ts +1 -1
  98. package/src/shared/app.shared.ts.txt +13 -0
  99. package/src/shared/web.shared.ts.txt +13 -0
  100. package/src/stream/sse.ts +15 -3
  101. package/src/syncer/api-parser.ts +4 -0
  102. package/src/syncer/file-patterns.ts +11 -9
  103. package/src/syncer/module-loader.ts +35 -0
  104. package/src/syncer/syncer.ts +14 -0
  105. package/src/tasks/decorator.ts +71 -0
  106. package/src/tasks/step-wrapper.ts +84 -0
  107. package/src/tasks/workflow-manager.ts +330 -0
  108. package/src/template/implementations/generated.template.ts +19 -6
  109. package/src/types/types.ts +20 -4
  110. package/src/utils/formatter.ts +8 -1
  111. package/src/utils/model.ts +26 -2
  112. package/src/utils/type-utils.ts +2 -0
  113. package/src/vector/embedding.ts +2 -8
  114. package/src/vector/types.ts +1 -2
  115. package/dist/vector/vector-search.d.ts +0 -47
  116. package/dist/vector/vector-search.d.ts.map +0 -1
  117. package/dist/vector/vector-search.js +0 -176
  118. package/src/vector/vector-search.ts +0 -261
@@ -15,7 +15,7 @@ import type {
15
15
  * 임베딩 클라이언트
16
16
  * Voyage AI와 OpenAI 임베딩을 SDK 방식으로 통합 지원
17
17
  */
18
- export class Embedding {
18
+ export class EmbeddingClass {
19
19
  private config: VectorConfig;
20
20
 
21
21
  constructor(config: Partial<VectorConfig> = {}) {
@@ -146,13 +146,6 @@ export class Embedding {
146
146
  }));
147
147
  }
148
148
 
149
- /**
150
- * 벡터를 PostgreSQL vector 타입 문자열로 변환
151
- */
152
- static toVectorString(embedding: number[]): string {
153
- return `[${embedding.join(",")}]`;
154
- }
155
-
156
149
  /**
157
150
  * 임베딩 provider의 차원 수 반환
158
151
  */
@@ -160,3 +153,4 @@ export class Embedding {
160
153
  return provider === "voyage" ? this.config.voyage.dimensions : this.config.openai.dimensions;
161
154
  }
162
155
  }
156
+ export const Embedding = new EmbeddingClass();
@@ -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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
@@ -1,261 +0,0 @@
1
- import type { Knex } from "knex";
2
- import pgvector from "pgvector/knex";
3
- import { DEFAULT_VECTOR_CONFIG } from "./config";
4
- import { Embedding } from "./embedding";
5
- import type {
6
- EmbeddingItem,
7
- EmbeddingProvider,
8
- HybridSearchOptions,
9
- HybridSearchResult,
10
- ProgressCallback,
11
- VectorConfig,
12
- VectorSearchOptions,
13
- VectorSearchResult,
14
- } from "./types";
15
-
16
- /**
17
- * 벡터 검색
18
- * pgvector를 활용한 벡터 검색 및 하이브리드 검색 지원
19
- */
20
- export class VectorSearch<T = Record<string, unknown>> {
21
- private db: Knex;
22
- private config: VectorConfig;
23
- private embedding: Embedding;
24
- private tableName: string;
25
-
26
- constructor(db: Knex, tableName: string, config: Partial<VectorConfig> = {}) {
27
- this.db = db;
28
- this.tableName = tableName;
29
- this.config = {
30
- voyage: { ...DEFAULT_VECTOR_CONFIG.voyage, ...config.voyage },
31
- openai: { ...DEFAULT_VECTOR_CONFIG.openai, ...config.openai },
32
- chunking: { ...DEFAULT_VECTOR_CONFIG.chunking, ...config.chunking },
33
- search: { ...DEFAULT_VECTOR_CONFIG.search, ...config.search },
34
- pgvector: { ...DEFAULT_VECTOR_CONFIG.pgvector, ...config.pgvector },
35
- };
36
- this.embedding = new Embedding(config);
37
- }
38
-
39
- /**
40
- * 단일 항목에 임베딩 저장
41
- */
42
- async saveEmbedding(
43
- id: number,
44
- text: string,
45
- provider: EmbeddingProvider,
46
- embeddingColumn: string = "content_embedding",
47
- ): Promise<void> {
48
- const { embedding } = await this.embedding.embedOne(text, provider, "document");
49
-
50
- await this.db(this.tableName)
51
- .where("id", id)
52
- .update({
53
- [embeddingColumn]: pgvector.toSql(embedding),
54
- });
55
- }
56
-
57
- /**
58
- * 여러 항목에 임베딩 일괄 저장
59
- */
60
- async saveEmbeddingsBatch(
61
- items: EmbeddingItem[],
62
- provider: EmbeddingProvider,
63
- embeddingColumn: string = "content_embedding",
64
- onProgress?: ProgressCallback,
65
- ): Promise<void> {
66
- const texts = items.map((item) => item.text);
67
- const embeddings = await this.embedding.embed(texts, provider, "document", onProgress);
68
-
69
- await this.db.transaction(async (trx) => {
70
- for (let i = 0; i < items.length; i++) {
71
- await trx(this.tableName)
72
- .where("id", items[i].id)
73
- .update({
74
- [embeddingColumn]: pgvector.toSql(embeddings[i].embedding),
75
- });
76
- }
77
- });
78
- }
79
-
80
- /**
81
- * 벡터 검색 (코사인 유사도)
82
- */
83
- async search(
84
- query: string,
85
- provider: EmbeddingProvider,
86
- options: VectorSearchOptions = {},
87
- ): Promise<VectorSearchResult<T>[]> {
88
- const {
89
- embeddingColumn = "content_embedding",
90
- limit = this.config.search.defaultLimit,
91
- threshold = this.config.search.similarityThreshold,
92
- where,
93
- } = options;
94
-
95
- // 쿼리 임베딩 (input_type: 'query' 중요!)
96
- const { embedding } = await this.embedding.embedOne(query, provider, "query");
97
-
98
- // pgvector 세션 설정
99
- if (this.config.pgvector.iterativeScan) {
100
- await this.db.raw("SET hnsw.iterative_scan = relaxed_order");
101
- }
102
- await this.db.raw(`SET hnsw.ef_search = ${this.config.pgvector.efSearch}`);
103
-
104
- // 코사인 유사도 = 1 - 코사인 거리
105
- const vectorStr = pgvector.toSql(embedding);
106
- let queryBuilder = this.db(this.tableName)
107
- .select("*")
108
- .select(this.db.raw(`1 - (${embeddingColumn} <=> ?::vector) AS similarity`, [vectorStr]))
109
- .whereNotNull(embeddingColumn)
110
- .orderByRaw(`${embeddingColumn} <=> ?::vector`, [vectorStr])
111
- .limit(limit);
112
-
113
- if (where) {
114
- queryBuilder = queryBuilder.whereRaw(where);
115
- }
116
-
117
- const rows = await queryBuilder;
118
-
119
- return rows
120
- .filter((row: { similarity: number }) => row.similarity >= threshold)
121
- .map((row: T & { similarity: number }) => ({
122
- id: (row as unknown as { id: number }).id,
123
- similarity: parseFloat(String(row.similarity)),
124
- data: row as T,
125
- }));
126
- }
127
-
128
- /**
129
- * 하이브리드 검색 (Vector + FTS)
130
- */
131
- async hybridSearch(
132
- query: string,
133
- provider: EmbeddingProvider,
134
- options: HybridSearchOptions = {},
135
- ): Promise<HybridSearchResult<T>[]> {
136
- const {
137
- embeddingColumn = "content_embedding",
138
- ftsColumn = "content_tsv",
139
- limit = this.config.search.defaultLimit,
140
- vectorWeight = this.config.search.vectorWeight,
141
- ftsWeight = this.config.search.ftsWeight,
142
- } = options;
143
-
144
- const { embedding } = await this.embedding.embedOne(query, provider, "query");
145
- const vectorStr = pgvector.toSql(embedding);
146
-
147
- // pgvector 세션 설정
148
- if (this.config.pgvector.iterativeScan) {
149
- await this.db.raw("SET hnsw.iterative_scan = relaxed_order");
150
- }
151
- await this.db.raw(`SET hnsw.ef_search = ${this.config.pgvector.efSearch}`);
152
-
153
- const sql = `
154
- WITH vector_search AS (
155
- SELECT
156
- id,
157
- ROW_NUMBER() OVER (ORDER BY ${embeddingColumn} <=> ?::vector) AS rank
158
- FROM ${this.tableName}
159
- WHERE ${embeddingColumn} IS NOT NULL
160
- ORDER BY ${embeddingColumn} <=> ?::vector
161
- LIMIT 50
162
- ),
163
- fts_search AS (
164
- SELECT
165
- id,
166
- ROW_NUMBER() OVER (ORDER BY ts_rank(${ftsColumn}, query) DESC) AS rank
167
- FROM ${this.tableName}, plainto_tsquery('simple', ?) query
168
- WHERE ${ftsColumn} @@ query
169
- LIMIT 50
170
- ),
171
- combined AS (
172
- SELECT
173
- COALESCE(v.id, f.id) AS id,
174
- COALESCE(1.0 / (60 + v.rank), 0) AS vector_score,
175
- COALESCE(1.0 / (60 + f.rank), 0) AS fts_score
176
- FROM vector_search v
177
- FULL OUTER JOIN fts_search f ON v.id = f.id
178
- )
179
- SELECT
180
- t.*,
181
- c.vector_score,
182
- c.fts_score,
183
- (c.vector_score * ? + c.fts_score * ?) AS similarity
184
- FROM combined c
185
- JOIN ${this.tableName} t ON c.id = t.id
186
- ORDER BY similarity DESC
187
- LIMIT ?
188
- `;
189
-
190
- const { rows } = await this.db.raw(sql, [
191
- vectorStr,
192
- vectorStr,
193
- query,
194
- vectorWeight,
195
- ftsWeight,
196
- limit,
197
- ]);
198
-
199
- return rows.map(
200
- (
201
- row: T & {
202
- similarity: number;
203
- vector_score: number;
204
- fts_score: number;
205
- },
206
- ) => ({
207
- id: (row as unknown as { id: number }).id,
208
- similarity: parseFloat(String(row.similarity)),
209
- vectorScore: parseFloat(String(row.vector_score)),
210
- ftsScore: parseFloat(String(row.fts_score)),
211
- data: row as T,
212
- }),
213
- );
214
- }
215
-
216
- /**
217
- * 임베딩 현황 조회
218
- */
219
- async getEmbeddingStatus(embeddingColumn: string = "content_embedding"): Promise<{
220
- total: number;
221
- withEmbedding: number;
222
- withoutEmbedding: number;
223
- }> {
224
- const result = await this.db(this.tableName)
225
- .count("* as total")
226
- .count(`${embeddingColumn} as with_embedding`)
227
- .first();
228
-
229
- const total = parseInt(String(result?.total ?? 0), 10);
230
- const withEmbedding = parseInt(String(result?.with_embedding ?? 0), 10);
231
-
232
- return {
233
- total,
234
- withEmbedding,
235
- withoutEmbedding: total - withEmbedding,
236
- };
237
- }
238
-
239
- /**
240
- * 임베딩이 없는 항목 ID 조회
241
- */
242
- async getItemsWithoutEmbedding(
243
- embeddingColumn: string = "content_embedding",
244
- limit: number = 100,
245
- ): Promise<number[]> {
246
- const rows = await this.db(this.tableName)
247
- .select("id")
248
- .whereNull(embeddingColumn)
249
- .orderBy("id")
250
- .limit(limit);
251
-
252
- return rows.map((row: { id: number }) => row.id);
253
- }
254
-
255
- /**
256
- * Embedding 인스턴스 반환 (고급 사용)
257
- */
258
- getEmbedding(): Embedding {
259
- return this.embedding;
260
- }
261
- }