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.
@@ -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
- // 저장된 임베딩의 provider와 차원을 확인하여 동일한 provider 쿼리 임베딩 생성
355
- const detectedProvider = await this.detectStoredEmbeddingProvider(db);
356
- const queryVector = await this.generateQueryVector(query.query, searchId, detectedProvider);
357
- const vecResults = await this.vectorSearchEngine.search(queryVector, {
358
- limit: (query.limit || 10) * 2,
359
- threshold: 0.5,
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
- }, detectedProvider);
363
- const vectorResults = vecResults.map(result => ({
364
- id: result.memory_id,
365
- content: result.content,
366
- type: result.type,
367
- importance: result.importance,
368
- created_at: result.created_at,
369
- pinned: false,
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: `${(Number(process.hrtime.bigint() - startTime) / 1_000_000).toFixed(2)}ms`
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) * 2,
395
- threshold: 0.5,
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
- * 저장된 임베딩의 provider 감지
406
- * 가장 많이 사용된 provider를 반환 (기본값: 'minilm')
735
+ * Provider 목록 캐시 (메모리 캐시)
736
+ * Provider 목록은 자주 변경되지 않으므로 캐싱하여 성능 개선
737
+ */
738
+ providerCache = null;
739
+ /**
740
+ * Provider 캐시 TTL (밀리초)
741
+ * 5분간 캐시 유지
407
742
  */
408
- async detectStoredEmbeddingProvider(db) {
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 providerStats = db.prepare(`
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
- LIMIT 1
422
- `).get();
423
- if (providerStats && providerStats.provider) {
424
- const provider = providerStats.provider.toLowerCase();
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
- count: providerStats.count,
428
- dimensions: Math.round(providerStats.avg_dimensions || 0)
786
+ providers: normalizedStats.map(s => s.provider),
787
+ total_providers: normalizedStats.length
429
788
  });
430
- return provider;
789
+ return normalizedStats;
431
790
  }
432
791
  }
433
792
  catch (error) {
434
793
  console.warn('⚠️ 저장된 임베딩 provider 감지 실패:', error);
435
794
  }
436
- // 기본값: minilm (가장 많이 사용되는 provider)
437
- return 'minilm';
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
- // preferredProvider가 있으면 해당 provider로 임베딩 생성 시도
443
- let embeddingResult;
444
- if (preferredProvider) {
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 || 'unknown'
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, '임베딩 생성 중 오류가 발생했습니다', error instanceof Error ? error : new Error(String(error)), { query, searchId });
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) {