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.
- package/dist/domains/search/algorithms/vector-search-engine-migration.d.ts +13 -8
- package/dist/domains/search/algorithms/vector-search-engine-migration.d.ts.map +1 -1
- package/dist/domains/search/algorithms/vector-search-engine-migration.js +19 -41
- package/dist/domains/search/algorithms/vector-search-engine-migration.js.map +1 -1
- package/dist/domains/search/algorithms/vector-search-engine.d.ts +17 -36
- package/dist/domains/search/algorithms/vector-search-engine.d.ts.map +1 -1
- package/dist/domains/search/algorithms/vector-search-engine.js +94 -481
- package/dist/domains/search/algorithms/vector-search-engine.js.map +1 -1
- package/dist/domains/search/repositories/vector-search.repository.d.ts.map +1 -1
- package/dist/domains/search/repositories/vector-search.repository.js +28 -12
- package/dist/domains/search/repositories/vector-search.repository.js.map +1 -1
- package/dist/server/http-server.d.ts.map +1 -1
- package/dist/server/http-server.js +2 -7
- package/dist/server/http-server.js.map +1 -1
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +33 -7
- package/dist/server/index.js.map +1 -1
- package/dist/server/server-factory.d.ts +65 -0
- package/dist/server/server-factory.d.ts.map +1 -0
- package/dist/server/server-factory.js +40 -0
- package/dist/server/server-factory.js.map +1 -0
- package/dist/server/servers/sse-server.d.ts +33 -0
- package/dist/server/servers/sse-server.d.ts.map +1 -0
- package/dist/server/servers/sse-server.js +48 -0
- package/dist/server/servers/sse-server.js.map +1 -0
- package/dist/server/servers/stdio-server.d.ts +34 -0
- package/dist/server/servers/stdio-server.d.ts.map +1 -0
- package/dist/server/servers/stdio-server.js +58 -0
- package/dist/server/servers/stdio-server.js.map +1 -0
- package/dist/server/simple-mcp-server.d.ts +5 -0
- package/dist/server/simple-mcp-server.d.ts.map +1 -1
- package/dist/server/simple-mcp-server.js +17 -7
- package/dist/server/simple-mcp-server.js.map +1 -1
- package/dist/server/sse-server-impl.d.ts +22 -0
- package/dist/server/sse-server-impl.d.ts.map +1 -0
- package/dist/server/sse-server-impl.js +39 -0
- package/dist/server/sse-server-impl.js.map +1 -0
- package/dist/server/stdio-server-impl.d.ts +12 -0
- package/dist/server/stdio-server-impl.d.ts.map +1 -0
- package/dist/server/stdio-server-impl.js +19 -0
- package/dist/server/stdio-server-impl.js.map +1 -0
- package/dist/shared/types/vector-search.types.d.ts +1 -0
- package/dist/shared/types/vector-search.types.d.ts.map +1 -1
- package/package.json +1 -1
- package/scripts/__tests__/check-db-integrity.integration.spec.ts +163 -0
- package/scripts/__tests__/fix-migration.integration.spec.ts +203 -0
- package/scripts/__tests__/migrate-embedding-data.integration.spec.ts +219 -0
- package/scripts/__tests__/regenerate-embeddings.integration.spec.ts +192 -0
- package/scripts/backup-embeddings.js +52 -61
- package/scripts/check-db-integrity.js +49 -25
- package/scripts/check-file-sizes.ts +4 -4
- package/scripts/check-pii-masking.ts +0 -3
- package/scripts/check-sql-injection.ts +0 -12
- package/scripts/debug-embeddings.js +74 -93
- package/scripts/fix-migration.js +115 -80
- package/scripts/fix-vector-dimensions.js +70 -89
- package/scripts/migrate-embedding-data.js +111 -25
- package/scripts/regenerate-embeddings.js +31 -15
- package/scripts/run-migration.js +144 -107
- package/scripts/safe-migration.js +192 -142
- package/scripts/save-work-memory.ts +6 -7
- package/scripts/simple-migrate.js +66 -34
- package/scripts/simple-update.js +147 -109
- package/dist/domains/search/algorithms/vector-search-engine-refactored.d.ts +0 -56
- package/dist/domains/search/algorithms/vector-search-engine-refactored.d.ts.map +0 -1
- package/dist/domains/search/algorithms/vector-search-engine-refactored.js +0 -101
- 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 {
|
|
8
|
-
import {
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
68
|
+
if (!this.container.isConnected()) {
|
|
428
69
|
return {
|
|
429
70
|
available: false,
|
|
430
71
|
tableExists: false,
|
|
431
72
|
recordCount: 0,
|
|
432
|
-
dimensions:
|
|
73
|
+
dimensions: 512, // TF-IDF 기본 차원
|
|
433
74
|
vecExtensionLoaded: false
|
|
434
75
|
};
|
|
435
76
|
}
|
|
436
77
|
try {
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
recordCount,
|
|
457
|
-
dimensions: this.getExpectedDimensions('tfidf'),
|
|
458
|
-
vecExtensionLoaded: this.vecExtensionLoaded
|
|
82
|
+
...status,
|
|
83
|
+
dimensions: 512
|
|
459
84
|
};
|
|
460
85
|
}
|
|
461
|
-
catch
|
|
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:
|
|
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.
|
|
479
|
-
console.warn('⚠️ VEC를 사용할 수 없습니다.');
|
|
100
|
+
if (!this.container.isConnected()) {
|
|
480
101
|
return false;
|
|
481
102
|
}
|
|
482
103
|
try {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
console.log('✅ 벡터 인덱스 재구성 완료 (sqlite-vec는 자동 인덱스 관리)');
|
|
486
|
-
return true;
|
|
104
|
+
const facade = this.container.getFacade();
|
|
105
|
+
return await facade.rebuildIndex();
|
|
487
106
|
}
|
|
488
|
-
catch
|
|
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.
|
|
500
|
-
return {
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
|
|
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
|
-
|
|
544
|
-
return
|
|
158
|
+
getVectorTableName(provider) {
|
|
159
|
+
return getValidatedVectorTableName(provider);
|
|
545
160
|
}
|
|
546
161
|
/**
|
|
547
|
-
*
|
|
162
|
+
* 벡터 검색 기능이 사용 가능한지 확인하여 호출자가 적절한 처리를 할 수 있도록 합니다.
|
|
548
163
|
*/
|
|
549
|
-
|
|
550
|
-
|
|
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
|
|
558
|
-
return
|
|
169
|
+
const facade = this.container.getFacade();
|
|
170
|
+
return facade.isAvailable();
|
|
559
171
|
}
|
|
560
|
-
catch
|
|
561
|
-
|
|
562
|
-
console.warn('⚠️ 태그 JSON 파싱 실패, 빈 배열로 대체합니다.', maskedError.message);
|
|
563
|
-
return [];
|
|
172
|
+
catch {
|
|
173
|
+
return false;
|
|
564
174
|
}
|
|
565
175
|
}
|
|
566
|
-
|
|
567
|
-
|
|
176
|
+
/**
|
|
177
|
+
* 데이터베이스 연결 상태를 확인하여 검색 실행 전 안전성을 보장합니다.
|
|
178
|
+
*/
|
|
179
|
+
isConnected() {
|
|
180
|
+
return this.container.isConnected();
|
|
568
181
|
}
|
|
569
182
|
}
|
|
570
183
|
// 전역에서 단일 인스턴스를 공유하여 메모리 사용을 최적화하고 일관된 상태를 유지합니다.
|