memento-mcp-server 1.16.3-a → 1.16.3-b

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 (68) hide show
  1. package/dist/domains/search/algorithms/vector-search-engine-migration.d.ts +13 -8
  2. package/dist/domains/search/algorithms/vector-search-engine-migration.d.ts.map +1 -1
  3. package/dist/domains/search/algorithms/vector-search-engine-migration.js +19 -41
  4. package/dist/domains/search/algorithms/vector-search-engine-migration.js.map +1 -1
  5. package/dist/domains/search/algorithms/vector-search-engine.d.ts +17 -36
  6. package/dist/domains/search/algorithms/vector-search-engine.d.ts.map +1 -1
  7. package/dist/domains/search/algorithms/vector-search-engine.js +94 -481
  8. package/dist/domains/search/algorithms/vector-search-engine.js.map +1 -1
  9. package/dist/domains/search/repositories/vector-search.repository.d.ts.map +1 -1
  10. package/dist/domains/search/repositories/vector-search.repository.js +28 -12
  11. package/dist/domains/search/repositories/vector-search.repository.js.map +1 -1
  12. package/dist/server/http-server.d.ts.map +1 -1
  13. package/dist/server/http-server.js +2 -7
  14. package/dist/server/http-server.js.map +1 -1
  15. package/dist/server/index.d.ts +3 -0
  16. package/dist/server/index.d.ts.map +1 -1
  17. package/dist/server/index.js +33 -7
  18. package/dist/server/index.js.map +1 -1
  19. package/dist/server/server-factory.d.ts +65 -0
  20. package/dist/server/server-factory.d.ts.map +1 -0
  21. package/dist/server/server-factory.js +40 -0
  22. package/dist/server/server-factory.js.map +1 -0
  23. package/dist/server/servers/sse-server.d.ts +33 -0
  24. package/dist/server/servers/sse-server.d.ts.map +1 -0
  25. package/dist/server/servers/sse-server.js +48 -0
  26. package/dist/server/servers/sse-server.js.map +1 -0
  27. package/dist/server/servers/stdio-server.d.ts +34 -0
  28. package/dist/server/servers/stdio-server.d.ts.map +1 -0
  29. package/dist/server/servers/stdio-server.js +58 -0
  30. package/dist/server/servers/stdio-server.js.map +1 -0
  31. package/dist/server/simple-mcp-server.d.ts +5 -0
  32. package/dist/server/simple-mcp-server.d.ts.map +1 -1
  33. package/dist/server/simple-mcp-server.js +17 -7
  34. package/dist/server/simple-mcp-server.js.map +1 -1
  35. package/dist/server/sse-server-impl.d.ts +22 -0
  36. package/dist/server/sse-server-impl.d.ts.map +1 -0
  37. package/dist/server/sse-server-impl.js +39 -0
  38. package/dist/server/sse-server-impl.js.map +1 -0
  39. package/dist/server/stdio-server-impl.d.ts +12 -0
  40. package/dist/server/stdio-server-impl.d.ts.map +1 -0
  41. package/dist/server/stdio-server-impl.js +19 -0
  42. package/dist/server/stdio-server-impl.js.map +1 -0
  43. package/dist/shared/types/vector-search.types.d.ts +1 -0
  44. package/dist/shared/types/vector-search.types.d.ts.map +1 -1
  45. package/package.json +1 -1
  46. package/scripts/__tests__/check-db-integrity.integration.spec.ts +163 -0
  47. package/scripts/__tests__/fix-migration.integration.spec.ts +203 -0
  48. package/scripts/__tests__/migrate-embedding-data.integration.spec.ts +219 -0
  49. package/scripts/__tests__/regenerate-embeddings.integration.spec.ts +192 -0
  50. package/scripts/backup-embeddings.js +52 -61
  51. package/scripts/check-db-integrity.js +49 -25
  52. package/scripts/check-file-sizes.ts +4 -4
  53. package/scripts/check-pii-masking.ts +0 -3
  54. package/scripts/check-sql-injection.ts +0 -12
  55. package/scripts/debug-embeddings.js +74 -93
  56. package/scripts/fix-migration.js +115 -80
  57. package/scripts/fix-vector-dimensions.js +70 -89
  58. package/scripts/migrate-embedding-data.js +111 -25
  59. package/scripts/regenerate-embeddings.js +31 -15
  60. package/scripts/run-migration.js +144 -107
  61. package/scripts/safe-migration.js +192 -142
  62. package/scripts/save-work-memory.ts +6 -7
  63. package/scripts/simple-migrate.js +66 -34
  64. package/scripts/simple-update.js +147 -109
  65. package/dist/domains/search/algorithms/vector-search-engine-refactored.d.ts +0 -56
  66. package/dist/domains/search/algorithms/vector-search-engine-refactored.d.ts.map +0 -1
  67. package/dist/domains/search/algorithms/vector-search-engine-refactored.js +0 -101
  68. package/dist/domains/search/algorithms/vector-search-engine-refactored.js.map +0 -1
@@ -2,569 +2,182 @@
2
2
  * 의미적 유사성을 기반으로 한 고성능 벡터 검색을 제공합니다.
3
3
  * sqlite-vec를 사용하여 대용량 벡터 데이터에서도 빠른 유사도 검색을 수행합니다.
4
4
  * Memento MCP Server의 핵심 벡터 검색 컴포넌트로서 의미 기반 검색 기능을 제공합니다.
5
+ *
6
+ * 리팩토링: VectorSearchContainer와 Facade 패턴을 사용하여 개선된 구조를 활용합니다.
7
+ * 기존 인터페이스는 유지하여 하위 호환성을 보장합니다.
5
8
  */
6
9
  import Database from 'better-sqlite3';
7
- import { VECTOR_SEARCH_CONFIG } from '../../../shared/config/vector-search.config.js';
8
- import { validateTableName, getVectorTableName as getValidatedVectorTableName } from '../../../shared/utils/sql-security-validator.js';
9
- import { PIIMasker } from '../../../shared/utils/pii-masker.js';
10
+ import { VectorSearchContainer } from '../services/vector-search/vector-search-container.js';
11
+ import { getVectorTableName as getValidatedVectorTableName } from '../../../shared/utils/sql-security-validator.js';
12
+ /**
13
+ * 벡터 검색 엔진
14
+ * 리팩토링된 구조를 사용하여 개선된 유지보수성과 테스트 가능성을 제공합니다.
15
+ */
10
16
  export class VectorSearchEngine {
11
- db = null;
12
- isVecAvailable = false;
13
- vecExtensionLoaded = false;
14
- defaultDimensions = 384;
15
- providerDimensions = {
16
- tfidf: 512, // LightweightEmbeddingService는 512차원을 생성
17
- minilm: 384,
18
- openai: 1536,
19
- gemini: 768
20
- };
21
- defaultThreshold = 0.7;
22
- defaultLimit = 10;
17
+ container;
23
18
  constructor() {
24
- // VEC 사용 가능 여부는 데이터베이스 연결 시 확인하여 런타임에 동적으로 판단합니다.
25
- }
26
- /**
27
- * 각 임베딩 provider별로 다른 벡터 테이블을 사용하여 차원 불일치를 방지합니다.
28
- * provider에 따라 적절한 테이블명을 반환하여 정확한 검색을 보장합니다.
29
- * SQL Injection 방지를 위해 화이트리스트 기반 검증을 수행합니다.
30
- */
31
- getVectorTableName(provider) {
32
- return getValidatedVectorTableName(provider);
19
+ this.container = VectorSearchContainer.getInstance();
33
20
  }
34
21
  /**
35
22
  * 데이터베이스 연결을 설정하고 벡터 검색 기능의 사용 가능 여부를 확인합니다.
36
- * provider별 차원 정보를 갱신하여 정확한 검색을 보장합니다.
37
23
  */
38
24
  initialize(db) {
39
- this.db = db;
40
- this.checkVecAvailability();
41
- this.refreshProviderDimensions();
42
- }
43
- /**
44
- * 벡터 검색 기능이 실제로 사용 가능한지 확인하여 안전한 검색을 보장합니다.
45
- * sqlite-vec 확장 로드 여부와 테이블 존재를 모두 확인하여 런타임 오류를 방지합니다.
46
- */
47
- checkVecAvailability() {
48
- if (!this.db) {
49
- this.isVecAvailable = false;
50
- this.vecExtensionLoaded = false;
51
- return;
52
- }
53
- try {
54
- // 벡터 검색을 수행할 수 있는 테이블이 존재하는지 확인하여 기능 사용 가능 여부를 판단합니다.
55
- const tableStatement = this.db.prepare(`
56
- SELECT name FROM sqlite_master
57
- WHERE type='table' AND name IN (
58
- 'memory_item_vec_tfidf',
59
- 'memory_item_vec_minilm',
60
- 'memory_item_vec_openai',
61
- 'memory_item_vec_gemini'
62
- )
63
- `);
64
- const tableRows = typeof tableStatement.all === 'function'
65
- ? tableStatement.all()
66
- : [];
67
- const tableCheck = Array.isArray(tableRows)
68
- ? tableRows
69
- : [];
70
- if (tableCheck.length === 0) {
71
- console.log('⚠️ VEC 테이블이 없습니다. 벡터 검색이 비활성화됩니다.');
72
- this.isVecAvailable = false;
73
- this.vecExtensionLoaded = false;
74
- return;
75
- }
76
- // VEC 함수가 실제로 동작하는지 테스트하여 런타임 오류를 사전에 방지합니다.
77
- try {
78
- const testTableEntry = tableCheck.find((table) => typeof table === 'object' && table !== null && typeof table.name === 'string');
79
- const testTable = testTableEntry?.name ?? 'memory_item_vec_tfidf';
80
- const testStatement = this.db.prepare(`
81
- SELECT distance FROM ${testTable}
82
- WHERE embedding MATCH ?
83
- LIMIT 0
84
- `);
85
- if (typeof testStatement.get !== 'function') {
86
- console.warn('⚠️ VEC 테스트 쿼리를 실행할 수 없습니다: get() 메서드가 없습니다.');
87
- this.vecExtensionLoaded = false;
88
- this.isVecAvailable = false;
89
- return;
90
- }
91
- testStatement.get(JSON.stringify(new Array(this.defaultDimensions).fill(0)));
92
- this.vecExtensionLoaded = true;
93
- this.isVecAvailable = true;
94
- console.log('✅ VEC (Vector Search) 사용 가능');
95
- }
96
- catch (vecError) {
97
- const maskedVecError = vecError instanceof Error ? PIIMasker.maskError(vecError) : { message: String(vecError), name: 'Error' };
98
- console.warn('⚠️ VEC 함수를 사용할 수 없습니다:', maskedVecError.message);
99
- this.vecExtensionLoaded = false;
100
- this.isVecAvailable = false;
101
- }
25
+ if (db) {
26
+ this.container.setDatabase(db);
102
27
  }
103
- catch (error) {
104
- const maskedError = error instanceof Error ? PIIMasker.maskError(error) : { message: String(error), name: 'Error' };
105
- console.error('❌ VEC 가용성 확인 실패:', maskedError.message);
106
- this.isVecAvailable = false;
107
- this.vecExtensionLoaded = false;
108
- }
109
- }
110
- /**
111
- * 데이터베이스에 저장된 실제 임베딩 차원을 조회하여 정확한 벡터 검색을 보장합니다.
112
- * provider별로 다른 차원을 사용할 수 있으므로 메타데이터에서 차원 정보를 갱신합니다.
113
- */
114
- refreshProviderDimensions() {
115
- if (!this.db) {
116
- return;
117
- }
118
- try {
119
- const dimensionStatement = this.db.prepare(`
120
- SELECT embedding_provider as provider, MAX(dimensions) as dimensions
121
- FROM memory_embedding
122
- WHERE embedding_provider IS NOT NULL
123
- AND embedding_provider != ''
124
- AND dimensions IS NOT NULL
125
- GROUP BY embedding_provider
126
- `);
127
- const dimensionRows = typeof dimensionStatement.all === 'function'
128
- ? dimensionStatement.all()
129
- : [];
130
- const rows = Array.isArray(dimensionRows)
131
- ? dimensionRows
132
- : [];
133
- for (const row of rows) {
134
- const provider = (row.provider || '').toLowerCase();
135
- const dimensions = row.dimensions ?? 0;
136
- if (provider && dimensions > 0) {
137
- this.providerDimensions[provider] = dimensions;
138
- }
139
- }
140
- }
141
- catch (error) {
142
- const maskedError = error instanceof Error ? PIIMasker.maskError(error) : { message: String(error), name: 'Error' };
143
- console.warn('⚠️ 임베딩 차원 정보를 불러오지 못했습니다:', maskedError.message);
144
- }
145
- }
146
- /**
147
- * 특정 provider의 실제 저장된 임베딩 차원을 조회하여 벡터 검색 시 차원 불일치를 방지합니다.
148
- */
149
- async getActualStoredDimensions(provider) {
150
- if (!this.db) {
151
- return null;
152
- }
153
- try {
154
- const result = this.db.prepare(`
155
- SELECT dimensions
156
- FROM memory_embedding
157
- WHERE embedding_provider = ?
158
- AND dimensions IS NOT NULL
159
- LIMIT 1
160
- `).get(provider);
161
- return result?.dimensions ?? null;
162
- }
163
- catch (error) {
164
- const maskedError = error instanceof Error ? PIIMasker.maskError(error) : { message: String(error), name: 'Error' };
165
- console.warn(`⚠️ 저장된 임베딩 차원 조회 실패 (${provider}):`, maskedError.message);
166
- return null;
28
+ else {
29
+ // null인 경우 명시적으로 연결 해제
30
+ this.container.reset();
167
31
  }
168
32
  }
169
33
  /**
170
34
  * 쿼리 벡터와 유사한 메모리를 검색하여 의미적으로 관련된 결과를 제공합니다.
171
- * sqlite-vec의 고성능 벡터 검색 기능을 활용하여 빠르고 정확한 검색을 수행합니다.
172
35
  */
173
36
  async search(queryVector, options = {}, provider = 'tfidf') {
174
- if (!this.db) {
175
- console.warn('⚠️ 데이터베이스 연결이 없어 벡터 검색을 진행할 수 없습니다.');
176
- return [];
177
- }
178
- if (!this.isVecAvailable) {
179
- console.warn('⚠️ VEC를 사용할 수 없습니다. 빈 결과를 반환합니다.');
180
- return [];
181
- }
182
- const { limit = this.defaultLimit, threshold = this.defaultThreshold, types, includeContent = true, includeMetadata = false } = options;
183
- const normalizedProvider = provider.toLowerCase();
184
- const expectedDimensions = this.getExpectedDimensions(normalizedProvider);
185
- const typeFilters = Array.isArray(types) ? types.filter(Boolean) : [];
186
- const typeClause = typeFilters.length > 0
187
- ? `AND mi.type IN (${typeFilters.map(() => '?').join(',')})`
188
- : '';
189
- // 쿼리 벡터의 차원이 저장된 임베딩과 일치하는지 검증하여 검색 오류를 방지합니다.
190
- let adjustedQueryVector = queryVector;
191
- if (queryVector.length !== expectedDimensions) {
192
- // 데이터베이스에 저장된 실제 임베딩 차원을 확인하여 차원 불일치를 처리합니다.
193
- const actualDimensions = await this.getActualStoredDimensions(normalizedProvider);
194
- if (actualDimensions && queryVector.length === actualDimensions) {
195
- // 쿼리 벡터가 저장된 임베딩의 실제 차원과 일치하면 사용하여 정확한 검색을 수행합니다.
196
- console.log(`ℹ️ 벡터 차원 조정: 제공자 ${normalizedProvider}, 예상 ${expectedDimensions}, 실제 저장된 차원 ${actualDimensions}, 쿼리 ${queryVector.length}`);
197
- // 실제 차원이 일치하므로 검증을 통과하여 정상적인 검색을 진행합니다.
198
- }
199
- else if (actualDimensions && queryVector.length !== actualDimensions) {
200
- // 차원 불일치로 인한 검색 오류를 방지하기 위해 빈 결과를 반환합니다.
201
- console.error(`❌ 벡터 차원 불일치: 제공자 ${normalizedProvider}, 예상 ${expectedDimensions}, 저장된 차원 ${actualDimensions}, 쿼리 ${queryVector.length}`);
202
- console.error(`💡 해결 방법: 저장된 임베딩과 동일한 provider로 쿼리 임베딩을 생성해야 합니다.`);
203
- return [];
204
- }
205
- else {
206
- // 차원 정보를 확인할 수 없는 경우 안전하게 빈 결과를 반환하여 오류를 방지합니다.
207
- console.warn(`⚠️ 벡터 차원 불일치: 제공자 ${normalizedProvider}, 예상 ${expectedDimensions}, 실제 ${queryVector.length}`);
208
- console.warn(`⚠️ 저장된 임베딩 정보를 확인할 수 없어 차원 불일치를 처리할 수 없습니다. 빈 결과를 반환합니다.`);
209
- return [];
210
- }
211
- }
212
- try {
213
- // 제공자별 테이블명 결정
214
- const tableName = this.getVectorTableName(normalizedProvider);
215
- // 타입 필터링으로 인해 결과가 줄어들 수 있으므로 충분한 후보를 확보합니다.
216
- const prefetchLimit = typeFilters.length > 0 ? limit * 5 : limit;
217
- // VEC 검색 쿼리를 구성하여 벡터 유사도 검색을 수행합니다.
218
- // JOIN 전에 서브쿼리로 벡터 검색을 먼저 수행하여 성능을 최적화하고 LIMIT을 적용합니다.
219
- const vecQuery = `
220
- SELECT
221
- me.memory_id as memory_id,
222
- t.distance as similarity,
223
- mi.content,
224
- mi.type,
225
- mi.importance,
226
- mi.created_at,
227
- mi.last_accessed,
228
- mi.pinned,
229
- mi.tags
230
- FROM (
231
- SELECT rowid, distance
232
- FROM ${tableName}
233
- WHERE embedding MATCH ?
234
- ORDER BY distance ASC
235
- LIMIT ?
236
- ) t
237
- JOIN memory_embedding me ON t.rowid = me.id
238
- JOIN memory_item mi ON mi.id = me.memory_id
239
- WHERE 1=1
240
- ${typeClause}
241
- ORDER BY t.distance ASC
242
- LIMIT ?
243
- `;
244
- const params = [
245
- JSON.stringify(adjustedQueryVector),
246
- prefetchLimit,
247
- ...typeFilters,
248
- limit
249
- ];
250
- const queryStatement = this.db.prepare(vecQuery);
251
- if (typeof queryStatement.all !== 'function') {
252
- console.warn('⚠️ 벡터 검색 쿼리를 실행할 수 없습니다: all() 메서드가 없습니다.');
253
- return [];
254
- }
255
- const rawResults = queryStatement.all(...params);
256
- const results = Array.isArray(rawResults) ? rawResults : [];
257
- // distance를 similarity로 변환하여 0-1 범위로 정규화하고 직관적인 점수 체계를 제공합니다.
258
- const normalizedResults = results
259
- .map(result => ({
260
- ...result,
261
- similarity: Math.max(0, 1 - result.similarity), // distance를 similarity로 변환
262
- tags: this.safeParseTags(result.tags)
263
- }));
264
- // 검색 품질을 모니터링하고 디버깅을 위해 임계값 적용 전 상위 결과를 로깅합니다.
265
- console.log('🔍 [Debug] Top 5 results before threshold filtering:');
266
- normalizedResults.slice(0, 5).forEach(r => {
267
- console.log(` - Memory ID: ${r.memory_id}, Similarity: ${r.similarity.toFixed(4)}`);
268
- });
269
- const finalResults = normalizedResults
270
- .filter(result => result.similarity >= threshold)
271
- .map(result => ({
272
- memory_id: result.memory_id,
273
- similarity: result.similarity,
274
- content: includeContent ? result.content : '',
275
- type: result.type,
276
- importance: result.importance,
277
- created_at: result.created_at,
278
- last_accessed: includeMetadata ? result.last_accessed : undefined,
279
- pinned: includeMetadata ? Boolean(result.pinned) : false,
280
- tags: includeMetadata ? result.tags : undefined
281
- }));
282
- return finalResults;
283
- }
284
- catch (error) {
285
- const maskedError = error instanceof Error ? PIIMasker.maskError(error) : { message: String(error), name: 'Error' };
286
- console.error('❌ 벡터 검색 실패:', maskedError.message);
37
+ if (!this.container.isConnected()) {
287
38
  return [];
288
39
  }
40
+ const query = {
41
+ queryVector,
42
+ options,
43
+ provider
44
+ };
45
+ const facade = this.container.getFacade();
46
+ return await facade.search(query);
289
47
  }
290
48
  /**
291
49
  * 벡터 검색과 메타데이터 검색을 결합하여 검색 정확도와 포괄성을 동시에 확보합니다.
292
- * SQLite 호환성을 위해 LEFT JOIN을 사용하여 안정적인 검색을 보장합니다.
293
50
  */
294
51
  async hybridSearch(queryVector, textQuery, options = {}, provider = 'tfidf') {
295
- if (!this.db) {
296
- console.warn('⚠️ 데이터베이스 연결이 없어 하이브리드 검색을 진행할 수 없습니다.');
297
- return [];
298
- }
299
- if (!this.isVecAvailable) {
300
- console.warn('⚠️ VEC를 사용할 수 없습니다. 빈 결과를 반환합니다.');
301
- return [];
302
- }
303
- const { limit = this.defaultLimit, threshold = this.defaultThreshold, types, includeContent = true, includeMetadata = true } = options;
304
- const normalizedProvider = provider.toLowerCase();
305
- const expectedDimensions = this.getExpectedDimensions(normalizedProvider);
306
- const typeFilters = Array.isArray(types) ? types.filter(Boolean) : [];
307
- const typeClause = typeFilters.length > 0
308
- ? `AND mi.type IN (${typeFilters.map(() => '?').join(',')})`
309
- : '';
310
- // 쿼리 벡터의 차원이 저장된 임베딩과 일치하는지 검증하여 검색 오류를 방지합니다.
311
- if (queryVector.length !== expectedDimensions) {
312
- console.error(`❌ 벡터 차원 불일치: 제공자 ${normalizedProvider}, 예상 ${expectedDimensions}, 실제 ${queryVector.length}`);
313
- return [];
314
- }
315
- try {
316
- // 제공자별 테이블명 결정
317
- const tableName = this.getVectorTableName(normalizedProvider);
318
- // 벡터 검색과 텍스트 검색을 결합하여 검색 정확도와 포괄성을 동시에 확보합니다.
319
- const hybridQuery = `
320
- WITH vector_search AS (
321
- SELECT
322
- me.memory_id as memory_id,
323
- vec.distance as vector_distance,
324
- mi.content,
325
- mi.type,
326
- mi.importance,
327
- mi.created_at,
328
- mi.last_accessed,
329
- mi.pinned,
330
- mi.tags
331
- FROM ${tableName} vec
332
- JOIN memory_embedding me ON vec.rowid = me.id
333
- JOIN memory_item mi ON mi.id = me.memory_id
334
- WHERE vec.embedding MATCH ?
335
- ${typeClause}
336
- ),
337
- text_search AS (
338
- SELECT
339
- mi.id as memory_id,
340
- mi.content,
341
- mi.type,
342
- mi.importance,
343
- mi.created_at,
344
- mi.last_accessed,
345
- mi.pinned,
346
- mi.tags,
347
- fts.rank as text_rank
348
- FROM memory_item_fts fts
349
- JOIN memory_item mi ON fts.rowid = mi.rowid
350
- WHERE memory_item_fts MATCH ?
351
- ${typeClause}
352
- )
353
- SELECT
354
- COALESCE(vs.memory_id, ts.memory_id) as memory_id,
355
- COALESCE(1 - vs.vector_distance, 0) as vector_similarity,
356
- COALESCE(ts.text_rank, 0) as text_similarity,
357
- COALESCE(vs.content, ts.content) as content,
358
- COALESCE(vs.type, ts.type) as type,
359
- COALESCE(vs.importance, ts.importance) as importance,
360
- COALESCE(vs.created_at, ts.created_at) as created_at,
361
- COALESCE(vs.last_accessed, ts.last_accessed) as last_accessed,
362
- COALESCE(vs.pinned, ts.pinned) as pinned,
363
- COALESCE(vs.tags, ts.tags) as tags
364
- FROM vector_search vs
365
- LEFT JOIN text_search ts ON vs.memory_id = ts.memory_id
366
- WHERE vs.memory_id IS NOT NULL
367
- UNION
368
- SELECT
369
- ts.memory_id,
370
- 0 as vector_similarity,
371
- ts.text_rank as text_similarity,
372
- ts.content,
373
- ts.type,
374
- ts.importance,
375
- ts.created_at,
376
- ts.last_accessed,
377
- ts.pinned,
378
- ts.tags
379
- FROM text_search ts
380
- LEFT JOIN vector_search vs ON ts.memory_id = vs.memory_id
381
- WHERE vs.memory_id IS NULL
382
- ORDER BY (vector_similarity * 0.6 + text_similarity * 0.4) DESC
383
- LIMIT ?
384
- `;
385
- const params = [
386
- JSON.stringify(queryVector),
387
- ...typeFilters,
388
- textQuery,
389
- ...typeFilters,
390
- limit
391
- ];
392
- const hybridStatement = this.db.prepare(hybridQuery);
393
- if (typeof hybridStatement.all !== 'function') {
394
- console.warn('⚠️ 하이브리드 검색 쿼리를 실행할 수 없습니다: all() 메서드가 없습니다.');
395
- return [];
396
- }
397
- const hybridResults = hybridStatement.all(...params);
398
- const results = Array.isArray(hybridResults) ? hybridResults : [];
399
- // 벡터 유사도와 텍스트 유사도를 가중 평균하여 종합적인 유사도 점수를 계산합니다.
400
- const normalizedResults = results
401
- .map(result => ({
402
- memory_id: result.memory_id,
403
- similarity: result.vector_similarity * 0.6 + result.text_similarity * 0.4,
404
- content: includeContent ? result.content : '',
405
- type: result.type,
406
- importance: result.importance,
407
- created_at: result.created_at,
408
- last_accessed: includeMetadata ? result.last_accessed : undefined,
409
- pinned: includeMetadata ? Boolean(result.pinned) : false,
410
- tags: includeMetadata ? this.safeParseTags(result.tags) : undefined
411
- }))
412
- .filter(result => result.similarity >= threshold);
413
- console.log(`🔍 하이브리드 검색 완료: ${normalizedResults.length}개 결과`);
414
- return normalizedResults;
415
- }
416
- catch (error) {
417
- const maskedError = error instanceof Error ? PIIMasker.maskError(error) : { message: String(error), name: 'Error' };
418
- console.error('❌ 하이브리드 검색 실패:', maskedError.message);
52
+ if (!this.container.isConnected()) {
419
53
  return [];
420
54
  }
55
+ const query = {
56
+ queryVector,
57
+ textQuery,
58
+ options,
59
+ provider
60
+ };
61
+ const facade = this.container.getFacade();
62
+ return await facade.hybridSearch(query);
421
63
  }
422
64
  /**
423
65
  * 벡터 검색 기능의 현재 상태를 확인하여 사용 가능 여부와 인덱스 정보를 제공합니다.
424
- * 시스템 모니터링과 디버깅을 위해 인덱스 상태를 조회합니다.
425
66
  */
426
67
  getIndexStatus() {
427
- if (!this.db) {
68
+ if (!this.container.isConnected()) {
428
69
  return {
429
70
  available: false,
430
71
  tableExists: false,
431
72
  recordCount: 0,
432
- dimensions: this.getExpectedDimensions('tfidf'), // TF-IDF 기본 차원 사용
73
+ dimensions: 512, // TF-IDF 기본 차원
433
74
  vecExtensionLoaded: false
434
75
  };
435
76
  }
436
77
  try {
437
- const tableExists = this.isVecAvailable;
438
- let recordCount = 0;
439
- if (tableExists) {
440
- // 모든 provider별 테이블의 레코드 수를 합산하여 전체 벡터 데이터 규모를 파악합니다.
441
- const providers = ['tfidf', 'minilm', 'openai', 'gemini'];
442
- for (const provider of providers) {
443
- const tableName = this.getVectorTableName(provider);
444
- try {
445
- const result = this.db.prepare(`SELECT COUNT(*) as count FROM ${tableName}`).get();
446
- recordCount += result.count;
447
- }
448
- catch (error) {
449
- // 테이블이 존재하지 않는 경우 무시하여 일부 provider가 없어도 안정적으로 동작합니다.
450
- }
451
- }
452
- }
78
+ const facade = this.container.getFacade();
79
+ const status = facade.getIndexStatus();
80
+ // dimensions를 512로 설정 (기존 동작 유지)
453
81
  return {
454
- available: this.isVecAvailable,
455
- tableExists,
456
- recordCount,
457
- dimensions: this.getExpectedDimensions('tfidf'),
458
- vecExtensionLoaded: this.vecExtensionLoaded
82
+ ...status,
83
+ dimensions: 512
459
84
  };
460
85
  }
461
- catch (error) {
462
- const maskedError = error instanceof Error ? PIIMasker.maskError(error) : { message: String(error), name: 'Error' };
463
- console.error('❌ 인덱스 상태 확인 실패:', maskedError.message);
86
+ catch {
464
87
  return {
465
88
  available: false,
466
89
  tableExists: false,
467
90
  recordCount: 0,
468
- dimensions: this.getExpectedDimensions('tfidf'),
91
+ dimensions: 512, // TF-IDF 기본 차원
469
92
  vecExtensionLoaded: false
470
93
  };
471
94
  }
472
95
  }
473
96
  /**
474
97
  * 벡터 인덱스를 재구성하여 검색 성능을 최적화합니다.
475
- * sqlite-vec는 자동으로 인덱스를 관리하므로 수동 재구성이 필요 없는 경우를 처리합니다.
476
98
  */
477
99
  async rebuildIndex() {
478
- if (!this.db || !this.isVecAvailable) {
479
- console.warn('⚠️ VEC를 사용할 수 없습니다.');
100
+ if (!this.container.isConnected()) {
480
101
  return false;
481
102
  }
482
103
  try {
483
- console.log('🔄 벡터 인덱스 재구성 시작...');
484
- // sqlite-vec는 자동으로 인덱스를 관리하므로 수동 재구성이 필요 없지만 호환성을 위해 메서드를 제공합니다.
485
- console.log('✅ 벡터 인덱스 재구성 완료 (sqlite-vec는 자동 인덱스 관리)');
486
- return true;
104
+ const facade = this.container.getFacade();
105
+ return await facade.rebuildIndex();
487
106
  }
488
- catch (error) {
489
- const maskedError = error instanceof Error ? PIIMasker.maskError(error) : { message: String(error), name: 'Error' };
490
- console.error('❌ 벡터 인덱스 재구성 실패:', maskedError.message);
107
+ catch {
491
108
  return false;
492
109
  }
493
110
  }
494
111
  /**
495
112
  * 벡터 검색의 성능을 측정하여 최적화 지점을 파악합니다.
496
- * 반복 실행을 통해 평균, 최소, 최대 응답 시간과 성공률을 계산합니다.
497
113
  */
498
114
  async performanceTest(queryVector, iterations = 10) {
499
- if (!this.db || !this.isVecAvailable) {
500
- return { averageTime: 0, minTime: 0, maxTime: 0, results: 0, successRate: 0 };
115
+ if (!this.container.isConnected()) {
116
+ return {
117
+ averageTime: 0,
118
+ minTime: 0,
119
+ maxTime: 0,
120
+ results: 0,
121
+ successRate: 0
122
+ };
501
123
  }
502
- const times = [];
503
- let resultCount = 0;
504
- let successCount = 0;
505
- for (let i = 0; i < iterations; i++) {
506
- try {
507
- const startTime = Date.now();
508
- const results = await this.search(queryVector, { limit: 10 });
509
- const endTime = Date.now();
510
- times.push(endTime - startTime);
511
- if (i === 0)
512
- resultCount = results.length;
513
- successCount++;
514
- }
515
- catch (error) {
516
- const maskedError = error instanceof Error ? PIIMasker.maskError(error) : { message: String(error), name: 'Error' };
517
- console.warn(`⚠️ 성능 테스트 ${i + 1}회차 실패:`, maskedError.message);
518
- times.push(0);
519
- }
124
+ try {
125
+ const facade = this.container.getFacade();
126
+ return await facade.runPerformanceTest(queryVector, iterations);
127
+ }
128
+ catch {
129
+ return {
130
+ averageTime: 0,
131
+ minTime: 0,
132
+ maxTime: 0,
133
+ results: 0,
134
+ successRate: 0
135
+ };
520
136
  }
521
- const averageTime = times.reduce((a, b) => a + b, 0) / times.length;
522
- const minTime = Math.min(...times.filter(t => t > 0));
523
- const maxTime = Math.max(...times);
524
- const successRate = successCount / iterations;
525
- console.log(`🔍 벡터 검색 성능 테스트: 평균 ${averageTime.toFixed(2)}ms (${iterations}회, 성공률: ${(successRate * 100).toFixed(1)}%)`);
526
- return {
527
- averageTime,
528
- minTime: minTime || 0,
529
- maxTime,
530
- results: resultCount,
531
- successRate
532
- };
533
137
  }
534
138
  /**
535
139
  * 특정 provider의 벡터 차원을 조회하여 벡터 검색 시 차원 정보를 제공합니다.
536
140
  */
537
141
  getDimensions(provider = 'tfidf') {
538
- return this.getExpectedDimensions(provider.toLowerCase());
142
+ // 기존 동작 유지: provider별 차원 반환
143
+ const providerDimensions = {
144
+ tfidf: 512,
145
+ minilm: 384,
146
+ openai: 1536,
147
+ gemini: 768
148
+ };
149
+ return providerDimensions[provider.toLowerCase()] ?? 384;
539
150
  }
540
151
  /**
541
- * 벡터 검색 기능이 사용 가능한지 확인하여 호출자가 적절한 처리를 할 수 있도록 합니다.
152
+ * 임베딩 provider별로 다른 벡터 테이블을 사용하여 차원 불일치를 방지합니다.
153
+ * provider에 따라 적절한 테이블명을 반환하여 정확한 검색을 보장합니다.
154
+ * SQL Injection 방지를 위해 화이트리스트 기반 검증을 수행합니다.
155
+ *
156
+ * @internal 테스트를 위해 public으로 유지
542
157
  */
543
- isAvailable() {
544
- return this.isVecAvailable;
158
+ getVectorTableName(provider) {
159
+ return getValidatedVectorTableName(provider);
545
160
  }
546
161
  /**
547
- * 데이터베이스 연결 상태를 확인하여 검색 실행 안전성을 보장합니다.
162
+ * 벡터 검색 기능이 사용 가능한지 확인하여 호출자가 적절한 처리를 수 있도록 합니다.
548
163
  */
549
- isConnected() {
550
- return this.db !== null;
551
- }
552
- safeParseTags(raw) {
553
- if (!raw) {
554
- return [];
164
+ isAvailable() {
165
+ if (!this.container.isConnected()) {
166
+ return false;
555
167
  }
556
168
  try {
557
- const parsed = JSON.parse(raw);
558
- return Array.isArray(parsed) ? parsed : [];
169
+ const facade = this.container.getFacade();
170
+ return facade.isAvailable();
559
171
  }
560
- catch (error) {
561
- const maskedError = error instanceof Error ? PIIMasker.maskError(error) : { message: String(error), name: 'Error' };
562
- console.warn('⚠️ 태그 JSON 파싱 실패, 빈 배열로 대체합니다.', maskedError.message);
563
- return [];
172
+ catch {
173
+ return false;
564
174
  }
565
175
  }
566
- getExpectedDimensions(provider) {
567
- return this.providerDimensions[provider] ?? this.defaultDimensions;
176
+ /**
177
+ * 데이터베이스 연결 상태를 확인하여 검색 실행 전 안전성을 보장합니다.
178
+ */
179
+ isConnected() {
180
+ return this.container.isConnected();
568
181
  }
569
182
  }
570
183
  // 전역에서 단일 인스턴스를 공유하여 메모리 사용을 최적화하고 일관된 상태를 유지합니다.