memento-mcp-server 1.12.0-a1 → 1.12.0-a2
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/algorithms/hybrid-search-engine.d.ts +98 -4
- package/dist/algorithms/hybrid-search-engine.d.ts.map +1 -1
- package/dist/algorithms/hybrid-search-engine.js +417 -54
- package/dist/algorithms/hybrid-search-engine.js.map +1 -1
- package/dist/tools/base-tool.d.ts +7 -2
- package/dist/tools/base-tool.d.ts.map +1 -1
- package/dist/tools/base-tool.js +11 -0
- package/dist/tools/base-tool.js.map +1 -1
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/migrate-embeddings-tool.d.ts +21 -0
- package/dist/tools/migrate-embeddings-tool.d.ts.map +1 -0
- package/dist/tools/migrate-embeddings-tool.js +272 -0
- package/dist/tools/migrate-embeddings-tool.js.map +1 -0
- package/dist/tools/recall-tool.d.ts.map +1 -1
- package/dist/tools/recall-tool.js +25 -4
- package/dist/tools/recall-tool.js.map +1 -1
- package/dist/types/index.d.ts +9 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +3 -1
- package/scripts/save-work-memory.ts +177 -0
|
@@ -11,6 +11,28 @@ import { SearchRanking } from './search-ranking.js';
|
|
|
11
11
|
import { mementoConfig } from '../config/index.js';
|
|
12
12
|
import { RelationGraph } from '../services/relation-graph.js';
|
|
13
13
|
import { getRankingWeights } from '../config/ranking-weights-loader.js';
|
|
14
|
+
// 검색 관련 상수
|
|
15
|
+
/**
|
|
16
|
+
* 개별 provider 검색 타임아웃 (밀리초)
|
|
17
|
+
* 각 provider별 검색 작업이 이 시간 내에 완료되지 않으면 타임아웃 처리
|
|
18
|
+
*/
|
|
19
|
+
const PROVIDER_SEARCH_TIMEOUT_MS = 2000;
|
|
20
|
+
/**
|
|
21
|
+
* 전체 검색 프로세스 타임아웃 (밀리초)
|
|
22
|
+
* 모든 provider 검색이 이 시간 내에 완료되지 않으면 현재까지 완료된 결과만 반환
|
|
23
|
+
* 개별 provider 타임아웃보다 충분히 길어야 함 (여러 provider가 병렬로 실행되므로)
|
|
24
|
+
*/
|
|
25
|
+
const OVERALL_SEARCH_TIMEOUT_MS = 5000;
|
|
26
|
+
/**
|
|
27
|
+
* 벡터 검색 결과 limit 배수
|
|
28
|
+
* 중복 제거 전에 더 많은 결과를 가져와서 최종 결과의 품질을 보장
|
|
29
|
+
*/
|
|
30
|
+
const VECTOR_SEARCH_LIMIT_MULTIPLIER = 2;
|
|
31
|
+
/**
|
|
32
|
+
* 벡터 검색 similarity threshold
|
|
33
|
+
* 이 값보다 낮은 similarity를 가진 결과는 제외
|
|
34
|
+
*/
|
|
35
|
+
const VECTOR_SEARCH_THRESHOLD = 0.5;
|
|
14
36
|
// 에러 타입 정의
|
|
15
37
|
export var SearchErrorType;
|
|
16
38
|
(function (SearchErrorType) {
|
|
@@ -349,30 +371,45 @@ export class HybridSearchEngine {
|
|
|
349
371
|
return await this.executeFallbackSearch(db, query, searchId, vectorSearchStart);
|
|
350
372
|
}
|
|
351
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* 벡터 검색 실행 (다중 provider 지원)
|
|
376
|
+
*
|
|
377
|
+
* @param db - 데이터베이스 연결
|
|
378
|
+
* @param query - 하이브리드 검색 쿼리
|
|
379
|
+
* @param searchId - 검색 ID (로깅용)
|
|
380
|
+
* @param startTime - 시작 시간 (성능 측정용)
|
|
381
|
+
* @returns 벡터 검색 결과 배열
|
|
382
|
+
*/
|
|
352
383
|
async executeVecSearch(db, query, searchId, startTime) {
|
|
353
384
|
try {
|
|
354
|
-
// 저장된 임베딩의
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
385
|
+
// 저장된 임베딩의 모든 provider 감지
|
|
386
|
+
const detectedProviders = await this.detectAllStoredEmbeddingProviders(db);
|
|
387
|
+
// provider 필터링
|
|
388
|
+
const providersToSearch = this.filterProvidersToSearch(detectedProviders, query.provider_filter, searchId);
|
|
389
|
+
if (providersToSearch.length === 0) {
|
|
390
|
+
return [];
|
|
391
|
+
}
|
|
392
|
+
// 다중 provider 병렬 검색 실행
|
|
393
|
+
const searchOptions = {
|
|
394
|
+
limit: (query.limit || 10) * VECTOR_SEARCH_LIMIT_MULTIPLIER,
|
|
395
|
+
threshold: VECTOR_SEARCH_THRESHOLD,
|
|
360
396
|
types: query.filters?.type,
|
|
361
397
|
includeContent: true
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
score: result.similarity,
|
|
371
|
-
similarity: result.similarity
|
|
372
|
-
}));
|
|
398
|
+
};
|
|
399
|
+
// 각 provider별 검색 작업 생성
|
|
400
|
+
const searchPromises = providersToSearch.map(provider => this.createProviderSearchTask(provider, query.query, searchOptions, searchId));
|
|
401
|
+
// 모든 provider 검색 실행 및 결과 수집
|
|
402
|
+
const { allResults, providerStats, overallTimeoutOccurred } = await this.executeProviderSearchesWithTimeout(searchPromises, providersToSearch, searchId);
|
|
403
|
+
// 결과 정규화 및 중복 제거
|
|
404
|
+
const vectorResults = this.normalizeAndDeduplicateResults(allResults);
|
|
405
|
+
const totalTime = Number(process.hrtime.bigint() - startTime) / 1_000_000;
|
|
373
406
|
this.logger.logSearchStep(searchId, 'VEC 벡터 검색 완료', {
|
|
374
407
|
resultCount: vectorResults.length,
|
|
375
|
-
totalVectorTime: `${
|
|
408
|
+
totalVectorTime: `${totalTime.toFixed(2)}ms`,
|
|
409
|
+
providerStats,
|
|
410
|
+
searchedProviders: providersToSearch.length,
|
|
411
|
+
successfulProviders: providerStats.filter(s => s.success).length,
|
|
412
|
+
overallTimeoutOccurred
|
|
376
413
|
});
|
|
377
414
|
return vectorResults;
|
|
378
415
|
}
|
|
@@ -383,6 +420,299 @@ export class HybridSearchEngine {
|
|
|
383
420
|
return await this.executeFallbackSearch(db, query, searchId, startTime);
|
|
384
421
|
}
|
|
385
422
|
}
|
|
423
|
+
/**
|
|
424
|
+
* Provider 필터링
|
|
425
|
+
*
|
|
426
|
+
* @param detectedProviders - 감지된 모든 provider 목록
|
|
427
|
+
* @param providerFilter - 필터링할 provider 목록 (선택적, 빈 배열이면 undefined로 처리되어 모든 provider 검색)
|
|
428
|
+
* @param searchId - 검색 ID (로깅용)
|
|
429
|
+
* @returns 필터링된 provider 목록
|
|
430
|
+
*/
|
|
431
|
+
filterProvidersToSearch(detectedProviders, providerFilter, searchId) {
|
|
432
|
+
let providersToSearch = detectedProviders.map(p => p.provider);
|
|
433
|
+
// providerFilter가 있고 비어있지 않은 경우에만 필터링
|
|
434
|
+
// 빈 배열은 undefined로 처리되어 모든 provider를 검색함
|
|
435
|
+
if (providerFilter && providerFilter.length > 0) {
|
|
436
|
+
providersToSearch = providersToSearch.filter(p => providerFilter.includes(p));
|
|
437
|
+
}
|
|
438
|
+
// 검색할 provider가 없으면 로깅
|
|
439
|
+
if (providersToSearch.length === 0) {
|
|
440
|
+
this.logger.logSearchStep(searchId, 'VEC 벡터 검색 - 검색할 provider 없음', {
|
|
441
|
+
detectedProviders: detectedProviders.map(p => p.provider),
|
|
442
|
+
providerFilter: providerFilter || []
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
return providersToSearch;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* 단일 provider 검색 작업 생성 (타임아웃 포함)
|
|
449
|
+
*
|
|
450
|
+
* @param provider - 검색할 provider
|
|
451
|
+
* @param query - 검색 쿼리 문자열
|
|
452
|
+
* @param searchOptions - 검색 옵션
|
|
453
|
+
* @param searchId - 검색 ID (로깅용)
|
|
454
|
+
* @returns 검색 결과 Promise (타임아웃 포함)
|
|
455
|
+
*/
|
|
456
|
+
createProviderSearchTask(provider, query, searchOptions, searchId) {
|
|
457
|
+
const providerStartTime = process.hrtime.bigint();
|
|
458
|
+
// 타임아웃 Promise 생성
|
|
459
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
460
|
+
setTimeout(() => {
|
|
461
|
+
const providerTime = Number(process.hrtime.bigint() - providerStartTime) / 1_000_000;
|
|
462
|
+
resolve({
|
|
463
|
+
provider,
|
|
464
|
+
results: [],
|
|
465
|
+
success: false,
|
|
466
|
+
timeMs: providerTime,
|
|
467
|
+
error: `Provider 검색 타임아웃 (${PROVIDER_SEARCH_TIMEOUT_MS}ms 초과)`
|
|
468
|
+
});
|
|
469
|
+
}, PROVIDER_SEARCH_TIMEOUT_MS);
|
|
470
|
+
});
|
|
471
|
+
// 실제 검색 작업 Promise
|
|
472
|
+
const searchTask = (async () => {
|
|
473
|
+
try {
|
|
474
|
+
// 각 provider에 맞는 쿼리 임베딩 생성
|
|
475
|
+
const queryVector = await this.generateQueryVector(query, searchId, provider);
|
|
476
|
+
// 벡터 검색 실행
|
|
477
|
+
const vecResults = await this.vectorSearchEngine.search(queryVector, searchOptions, provider);
|
|
478
|
+
const providerTime = Number(process.hrtime.bigint() - providerStartTime) / 1_000_000;
|
|
479
|
+
return {
|
|
480
|
+
provider,
|
|
481
|
+
results: vecResults.map(result => ({
|
|
482
|
+
id: result.memory_id,
|
|
483
|
+
content: result.content,
|
|
484
|
+
type: result.type,
|
|
485
|
+
importance: result.importance,
|
|
486
|
+
created_at: result.created_at,
|
|
487
|
+
pinned: false,
|
|
488
|
+
score: result.similarity,
|
|
489
|
+
similarity: result.similarity,
|
|
490
|
+
provider // provider 정보 추가
|
|
491
|
+
})),
|
|
492
|
+
success: true,
|
|
493
|
+
timeMs: providerTime,
|
|
494
|
+
error: null
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
const providerTime = Number(process.hrtime.bigint() - providerStartTime) / 1_000_000;
|
|
499
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
500
|
+
this.logger.logSearchStep(searchId, `VEC 벡터 검색 실패 - ${provider}`, {
|
|
501
|
+
provider,
|
|
502
|
+
error: errorMessage,
|
|
503
|
+
timeMs: providerTime
|
|
504
|
+
});
|
|
505
|
+
return {
|
|
506
|
+
provider,
|
|
507
|
+
results: [],
|
|
508
|
+
success: false,
|
|
509
|
+
timeMs: providerTime,
|
|
510
|
+
error: errorMessage
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
})();
|
|
514
|
+
// Promise.race로 타임아웃과 검색 작업 중 먼저 완료되는 것 반환
|
|
515
|
+
return Promise.race([searchTask, timeoutPromise]);
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* 모든 provider 검색 실행 및 결과 수집 (전체 타임아웃 포함)
|
|
519
|
+
*
|
|
520
|
+
* @param searchPromises - 각 provider별 검색 Promise 배열
|
|
521
|
+
* @param providersToSearch - 검색할 provider 목록
|
|
522
|
+
* @param searchId - 검색 ID (로깅용)
|
|
523
|
+
* @returns 검색 결과 및 통계
|
|
524
|
+
*/
|
|
525
|
+
async executeProviderSearchesWithTimeout(searchPromises, providersToSearch, searchId) {
|
|
526
|
+
// 전체 검색 프로세스의 maximum timeout 설정
|
|
527
|
+
// 모든 provider가 타임아웃되어도 응답을 보장하기 위함
|
|
528
|
+
// 개별 provider 타임아웃보다 충분히 길어야 여러 provider가 병렬로 실행될 수 있음
|
|
529
|
+
let overallTimeoutOccurred = false;
|
|
530
|
+
let overallTimeoutHandle = null;
|
|
531
|
+
const overallTimeoutPromise = new Promise((resolve) => {
|
|
532
|
+
overallTimeoutHandle = setTimeout(() => {
|
|
533
|
+
overallTimeoutOccurred = true;
|
|
534
|
+
this.logger.logSearchStep(searchId, 'VEC 벡터 검색 전체 타임아웃', {
|
|
535
|
+
timeoutMs: OVERALL_SEARCH_TIMEOUT_MS,
|
|
536
|
+
message: '전체 검색 프로세스 타임아웃 발생 - 현재까지 완료된 결과만 반환'
|
|
537
|
+
});
|
|
538
|
+
resolve();
|
|
539
|
+
}, OVERALL_SEARCH_TIMEOUT_MS);
|
|
540
|
+
});
|
|
541
|
+
// 타임아웃 타이머 정리 함수
|
|
542
|
+
const cleanupTimeout = () => {
|
|
543
|
+
if (overallTimeoutHandle !== null) {
|
|
544
|
+
clearTimeout(overallTimeoutHandle);
|
|
545
|
+
overallTimeoutHandle = null;
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
try {
|
|
549
|
+
// Promise.allSettled()를 사용하여 모든 provider 검색 실행 (일부 실패해도 계속 진행)
|
|
550
|
+
// 전체 타임아웃과 병렬 검색 중 먼저 완료되는 것 사용
|
|
551
|
+
const searchResults = await Promise.race([
|
|
552
|
+
Promise.allSettled(searchPromises).then(results => {
|
|
553
|
+
cleanupTimeout();
|
|
554
|
+
return results;
|
|
555
|
+
}),
|
|
556
|
+
overallTimeoutPromise.then(() => {
|
|
557
|
+
// 타임아웃 발생 시 현재까지 완료된 Promise만 수집
|
|
558
|
+
// Promise.allSettled는 이미 실행 중이므로 결과를 기다림
|
|
559
|
+
return Promise.allSettled(searchPromises);
|
|
560
|
+
})
|
|
561
|
+
]);
|
|
562
|
+
// 성공한 검색 결과 수집
|
|
563
|
+
const allResults = [];
|
|
564
|
+
const providerStats = [];
|
|
565
|
+
searchResults.forEach((result, index) => {
|
|
566
|
+
if (result.status === 'fulfilled') {
|
|
567
|
+
const providerResult = result.value;
|
|
568
|
+
providerStats.push({
|
|
569
|
+
provider: providerResult.provider,
|
|
570
|
+
resultCount: providerResult.results.length,
|
|
571
|
+
success: providerResult.success,
|
|
572
|
+
timeMs: providerResult.timeMs,
|
|
573
|
+
error: providerResult.error || undefined
|
|
574
|
+
});
|
|
575
|
+
// 타임아웃 또는 실패 시 상세 로깅
|
|
576
|
+
if (!providerResult.success) {
|
|
577
|
+
const isTimeout = providerResult.error?.includes('타임아웃');
|
|
578
|
+
this.logger.logSearchStep(searchId, `VEC 벡터 검색 실패 - ${providerResult.provider}`, {
|
|
579
|
+
provider: providerResult.provider,
|
|
580
|
+
error: providerResult.error,
|
|
581
|
+
timeMs: providerResult.timeMs,
|
|
582
|
+
isTimeout,
|
|
583
|
+
resultCount: providerResult.results.length
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
if (providerResult.success) {
|
|
587
|
+
allResults.push(...providerResult.results);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
// Promise 자체가 실패한 경우 (매우 드묾)
|
|
592
|
+
const provider = providersToSearch[index];
|
|
593
|
+
if (!provider) {
|
|
594
|
+
// provider가 없는 경우 (매우 드묾, 인덱스 불일치) - 스킵
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
598
|
+
providerStats.push({
|
|
599
|
+
provider,
|
|
600
|
+
resultCount: 0,
|
|
601
|
+
success: false,
|
|
602
|
+
timeMs: 0,
|
|
603
|
+
error: errorMessage
|
|
604
|
+
});
|
|
605
|
+
this.logger.logSearchStep(searchId, `VEC 벡터 검색 Promise 실패 - ${provider}`, {
|
|
606
|
+
provider,
|
|
607
|
+
error: errorMessage,
|
|
608
|
+
isTimeout: false
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
return { allResults, providerStats, overallTimeoutOccurred };
|
|
613
|
+
}
|
|
614
|
+
finally {
|
|
615
|
+
// 예외 발생 시에도 타임아웃 타이머 정리 보장
|
|
616
|
+
cleanupTimeout();
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* 결과 정규화 및 중복 제거
|
|
621
|
+
* Provider별로 Min-Max 정규화를 수행하고, 중복된 memory_id는 최고 점수만 유지
|
|
622
|
+
*
|
|
623
|
+
* @param allResults - 모든 provider의 검색 결과
|
|
624
|
+
* @returns 정규화 및 중복 제거된 결과 배열
|
|
625
|
+
*/
|
|
626
|
+
normalizeAndDeduplicateResults(allResults) {
|
|
627
|
+
const resultsByProvider = this.groupResultsByProvider(allResults);
|
|
628
|
+
const normalizedResults = this.normalizeResultsByProvider(resultsByProvider);
|
|
629
|
+
const deduplicatedResults = this.deduplicateResults(normalizedResults);
|
|
630
|
+
return this.rankResults(deduplicatedResults);
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Provider별로 결과 그룹화
|
|
634
|
+
*/
|
|
635
|
+
groupResultsByProvider(allResults) {
|
|
636
|
+
const resultsByProvider = new Map();
|
|
637
|
+
allResults.forEach(result => {
|
|
638
|
+
const provider = result.provider;
|
|
639
|
+
if (!resultsByProvider.has(provider)) {
|
|
640
|
+
resultsByProvider.set(provider, []);
|
|
641
|
+
}
|
|
642
|
+
const providerResults = resultsByProvider.get(provider);
|
|
643
|
+
if (providerResults) {
|
|
644
|
+
providerResults.push(result);
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
return resultsByProvider;
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Provider별로 Min-Max 정규화 수행
|
|
651
|
+
*/
|
|
652
|
+
normalizeResultsByProvider(resultsByProvider) {
|
|
653
|
+
const normalizedResults = [];
|
|
654
|
+
resultsByProvider.forEach((results) => {
|
|
655
|
+
if (results.length === 0) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
const scores = results.map(r => r.similarity);
|
|
659
|
+
const minScore = Math.min(...scores);
|
|
660
|
+
const maxScore = Math.max(...scores);
|
|
661
|
+
// 모든 점수가 동일한 경우 원본 점수 유지, 그 외에는 Min-Max 정규화
|
|
662
|
+
if (maxScore === minScore) {
|
|
663
|
+
results.forEach(result => {
|
|
664
|
+
normalizedResults.push({
|
|
665
|
+
...result,
|
|
666
|
+
normalizedScore: result.similarity
|
|
667
|
+
});
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
results.forEach(result => {
|
|
672
|
+
const normalizedScore = (result.similarity - minScore) / (maxScore - minScore);
|
|
673
|
+
normalizedResults.push({
|
|
674
|
+
...result,
|
|
675
|
+
normalizedScore
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
return normalizedResults;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* 중복 제거 (memory_id 기준, 정규화된 점수 중 최고 점수만 유지)
|
|
684
|
+
*/
|
|
685
|
+
deduplicateResults(normalizedResults) {
|
|
686
|
+
const resultMap = new Map();
|
|
687
|
+
normalizedResults.forEach(result => {
|
|
688
|
+
const existing = resultMap.get(result.id);
|
|
689
|
+
if (!existing || result.normalizedScore > existing.normalizedScore) {
|
|
690
|
+
resultMap.set(result.id, result);
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
return Array.from(resultMap.values());
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* 정규화된 점수로 재랭킹
|
|
697
|
+
*/
|
|
698
|
+
rankResults(deduplicatedResults) {
|
|
699
|
+
return deduplicatedResults
|
|
700
|
+
.map(({ provider, normalizedScore, ...result }) => ({
|
|
701
|
+
...result,
|
|
702
|
+
similarity: normalizedScore
|
|
703
|
+
}))
|
|
704
|
+
.sort((a, b) => b.similarity - a.similarity);
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Fallback 벡터 검색 실행
|
|
708
|
+
* 벡터 인덱스가 사용 불가능한 경우 임베딩 서비스를 직접 사용하여 검색
|
|
709
|
+
*
|
|
710
|
+
* @param db - 데이터베이스 연결
|
|
711
|
+
* @param query - 하이브리드 검색 쿼리
|
|
712
|
+
* @param searchId - 검색 ID (로깅용)
|
|
713
|
+
* @param startTime - 시작 시간 (성능 측정용, 현재는 사용하지 않음)
|
|
714
|
+
* @returns 벡터 검색 결과 배열
|
|
715
|
+
*/
|
|
386
716
|
async executeFallbackSearch(db, query, searchId, startTime) {
|
|
387
717
|
if (!this.embeddingService.isAvailable()) {
|
|
388
718
|
this.logger.logSearchStep(searchId, '임베딩 서비스 사용 불가', {});
|
|
@@ -391,8 +721,8 @@ export class HybridSearchEngine {
|
|
|
391
721
|
const fallbackStart = process.hrtime.bigint();
|
|
392
722
|
const vectorResults = await this.embeddingService.searchBySimilarity(db, query.query, {
|
|
393
723
|
type: query.filters?.type,
|
|
394
|
-
limit: (query.limit || 10) *
|
|
395
|
-
threshold:
|
|
724
|
+
limit: (query.limit || 10) * VECTOR_SEARCH_LIMIT_MULTIPLIER,
|
|
725
|
+
threshold: VECTOR_SEARCH_THRESHOLD,
|
|
396
726
|
});
|
|
397
727
|
const fallbackTime = Number(process.hrtime.bigint() - fallbackStart) / 1_000_000;
|
|
398
728
|
this.logger.logSearchStep(searchId, 'Fallback 벡터 검색 완료', {
|
|
@@ -402,66 +732,99 @@ export class HybridSearchEngine {
|
|
|
402
732
|
return vectorResults;
|
|
403
733
|
}
|
|
404
734
|
/**
|
|
405
|
-
*
|
|
406
|
-
*
|
|
735
|
+
* Provider 목록 캐시 (메모리 캐시)
|
|
736
|
+
* Provider 목록은 자주 변경되지 않으므로 캐싱하여 성능 개선
|
|
737
|
+
*/
|
|
738
|
+
providerCache = null;
|
|
739
|
+
/**
|
|
740
|
+
* Provider 캐시 TTL (밀리초)
|
|
741
|
+
* 5분간 캐시 유지
|
|
407
742
|
*/
|
|
408
|
-
|
|
743
|
+
static PROVIDER_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
744
|
+
/**
|
|
745
|
+
* 저장된 임베딩의 모든 provider 감지
|
|
746
|
+
* 모든 provider 목록을 반환 (count 내림차순 정렬)
|
|
747
|
+
* 캐싱을 사용하여 성능 개선
|
|
748
|
+
*/
|
|
749
|
+
async detectAllStoredEmbeddingProviders(db) {
|
|
750
|
+
// 캐시 확인
|
|
751
|
+
const now = Date.now();
|
|
752
|
+
if (this.providerCache && (now - this.providerCache.timestamp) < HybridSearchEngine.PROVIDER_CACHE_TTL_MS) {
|
|
753
|
+
return this.providerCache.stats;
|
|
754
|
+
}
|
|
409
755
|
try {
|
|
410
|
-
const
|
|
756
|
+
const providerStatsList = db.prepare(`
|
|
411
757
|
SELECT
|
|
412
|
-
embedding_provider as provider,
|
|
758
|
+
LOWER(embedding_provider) as provider,
|
|
413
759
|
COUNT(*) as count,
|
|
414
760
|
AVG(dimensions) as avg_dimensions
|
|
415
761
|
FROM memory_embedding
|
|
416
762
|
WHERE embedding_provider IS NOT NULL
|
|
417
763
|
AND embedding_provider != ''
|
|
418
764
|
AND dimensions IS NOT NULL
|
|
419
|
-
GROUP BY embedding_provider
|
|
765
|
+
GROUP BY LOWER(embedding_provider)
|
|
420
766
|
ORDER BY count DESC
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
767
|
+
`).all();
|
|
768
|
+
if (providerStatsList && providerStatsList.length > 0) {
|
|
769
|
+
const normalizedStats = providerStatsList
|
|
770
|
+
.filter(stat => {
|
|
771
|
+
// 유효한 EmbeddingProvider인지 확인
|
|
772
|
+
const validProviders = ['tfidf', 'lightweight', 'minilm', 'openai', 'gemini'];
|
|
773
|
+
return validProviders.includes(stat.provider);
|
|
774
|
+
})
|
|
775
|
+
.map(stat => ({
|
|
776
|
+
provider: stat.provider,
|
|
777
|
+
count: stat.count,
|
|
778
|
+
avg_dimensions: Math.round(stat.avg_dimensions || 0)
|
|
779
|
+
}));
|
|
780
|
+
// 캐시 업데이트
|
|
781
|
+
this.providerCache = {
|
|
782
|
+
stats: normalizedStats,
|
|
783
|
+
timestamp: now
|
|
784
|
+
};
|
|
425
785
|
this.logger.logSearchStep('', '저장된 임베딩 provider 감지', {
|
|
426
|
-
provider,
|
|
427
|
-
|
|
428
|
-
dimensions: Math.round(providerStats.avg_dimensions || 0)
|
|
786
|
+
providers: normalizedStats.map(s => s.provider),
|
|
787
|
+
total_providers: normalizedStats.length
|
|
429
788
|
});
|
|
430
|
-
return
|
|
789
|
+
return normalizedStats;
|
|
431
790
|
}
|
|
432
791
|
}
|
|
433
792
|
catch (error) {
|
|
434
793
|
console.warn('⚠️ 저장된 임베딩 provider 감지 실패:', error);
|
|
435
794
|
}
|
|
436
|
-
// 기본값:
|
|
437
|
-
|
|
795
|
+
// 기본값: 빈 배열 반환 (provider가 없는 경우)
|
|
796
|
+
const emptyStats = [];
|
|
797
|
+
this.providerCache = {
|
|
798
|
+
stats: emptyStats,
|
|
799
|
+
timestamp: now
|
|
800
|
+
};
|
|
801
|
+
return emptyStats;
|
|
438
802
|
}
|
|
803
|
+
/**
|
|
804
|
+
* 쿼리 임베딩 벡터 생성
|
|
805
|
+
* preferredProvider가 지정된 경우 해당 provider로만 임베딩 생성
|
|
806
|
+
* 각 provider는 서로 다른 차원의 임베딩을 사용할 수 있으므로 fallback을 사용하지 않음
|
|
807
|
+
*
|
|
808
|
+
* @param query - 검색 쿼리 문자열
|
|
809
|
+
* @param searchId - 검색 ID (로깅용)
|
|
810
|
+
* @param preferredProvider - 선호하는 임베딩 provider (필수, 각 provider별로 다른 차원의 임베딩 필요)
|
|
811
|
+
* @returns 임베딩 벡터 배열
|
|
812
|
+
* @throws SearchError - 임베딩 생성 실패 시
|
|
813
|
+
*/
|
|
439
814
|
async generateQueryVector(query, searchId, preferredProvider) {
|
|
440
815
|
try {
|
|
441
816
|
const embeddingStart = process.hrtime.bigint();
|
|
442
|
-
//
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
try {
|
|
446
|
-
embeddingResult = await this.queryEmbeddingService.generateEmbedding(query, preferredProvider);
|
|
447
|
-
}
|
|
448
|
-
catch (error) {
|
|
449
|
-
// preferred provider 실패 시 fallback
|
|
450
|
-
console.warn(`⚠️ Preferred provider '${preferredProvider}' 실패, fallback 시도:`, error);
|
|
451
|
-
embeddingResult = await this.queryEmbeddingService.generateEmbedding(query);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
else {
|
|
455
|
-
embeddingResult = await this.queryEmbeddingService.generateEmbedding(query);
|
|
456
|
-
}
|
|
817
|
+
// 각 provider는 서로 다른 차원의 임베딩을 사용할 수 있으므로
|
|
818
|
+
// preferredProvider로만 임베딩 생성 (fallback 사용 안 함)
|
|
819
|
+
const embeddingResult = await this.queryEmbeddingService.generateEmbedding(query, preferredProvider);
|
|
457
820
|
if (!embeddingResult) {
|
|
458
|
-
throw new SearchError(SearchErrorType.EMBEDDING_GENERATION_FAILED, '임베딩 생성에 실패했습니다', undefined, { query, searchId });
|
|
821
|
+
throw new SearchError(SearchErrorType.EMBEDDING_GENERATION_FAILED, '임베딩 생성에 실패했습니다', undefined, { query, searchId, preferredProvider });
|
|
459
822
|
}
|
|
460
823
|
const embeddingTime = Number(process.hrtime.bigint() - embeddingStart) / 1_000_000;
|
|
461
824
|
this.logger.logSearchStep(searchId, '임베딩 생성 완료', {
|
|
462
825
|
embeddingTime: `${embeddingTime.toFixed(2)}ms`,
|
|
463
826
|
vectorLength: embeddingResult.embedding.length,
|
|
464
|
-
provider: embeddingResult.provider ||
|
|
827
|
+
provider: embeddingResult.provider || preferredProvider
|
|
465
828
|
});
|
|
466
829
|
return embeddingResult.embedding;
|
|
467
830
|
}
|
|
@@ -469,7 +832,7 @@ export class HybridSearchEngine {
|
|
|
469
832
|
if (error instanceof SearchError) {
|
|
470
833
|
throw error;
|
|
471
834
|
}
|
|
472
|
-
throw new SearchError(SearchErrorType.EMBEDDING_GENERATION_FAILED,
|
|
835
|
+
throw new SearchError(SearchErrorType.EMBEDDING_GENERATION_FAILED, `임베딩 생성 중 오류가 발생했습니다 (provider: ${preferredProvider})`, error instanceof Error ? error : new Error(String(error)), { query, searchId, preferredProvider });
|
|
473
836
|
}
|
|
474
837
|
}
|
|
475
838
|
async combineAndSortResults(textResults, vectorResults, weights, limit, db, includeRelations = false) {
|