SIMILARITY_SCORE_BUGFIX.md 7.6 KB

相似度スコアが 100% を超えるバグの修正

🐛 問題の記述

ユーザーがチャットインターフェースにて、引用ソースの適合度スコアが 100% を超えている現象を確認しました。これは数学的に不可能です(相似度スコアは 0〜100% の間であるべきです)。

発生していた現象:

引用元表示:適合度 123.5%
          適合度 165.2%
          適合度 201.8%

🔍 根本原因の分析

問題の発生源

Elasticsearch が返す生のスコア(_score)は、特に以下の場合に 1.0 を超えることがあります:

  1. ベクトル検索 (Vector Search):コサイン類似度を使用しますが、戻り値が 1.0 を超える場合があります。
  2. 全文検索 (Full-text Search):TF-IDF スコアが非常に大きくなる場合があります。
  3. ハイブリッド検索 (Hybrid Search):ウェイトを組み合わせた後の合計が 1.0 を超える場合があります。

データフローの分析

Elasticsearch がスコアを返却 (_score = 1.5)
    ↓
elasticsearch.service.ts: searchSimilar() / searchFullText()
    ↓
chat.service.ts: hybridSearch()
    ↓
ChatService: result.score を返却
    ↓
フロントエンド ChatInterface.tsx: (source.score * 100).toFixed(1)%
    ↓
表示:150% ❌

問題のあったコード

elasticsearch.service.ts - hybridSearch メソッド:

// 問題:Elasticsearch の _score をそのまま使用しており、1.0 を超える可能性がある
vectorResults.forEach((result) => {
  combinedResults.set(result.id, {
    ...result,
    vectorScore: result.score,  // 例えば 1.5 になる可能性がある
    textScore: 0,
    combinedScore: result.score * vectorWeight,  // 1.5 * 0.7 = 1.05
  });
});

ChatInterface.tsx - 表示ロジック:

// 問題:スコアが 0〜1 の間であることを前提に 100 倍している
{(source.score * 100).toFixed(1)}%  // 1.05 * 100 = 105%

✅ 解決策

1. ElasticsearchService にスコアの正規化を追加

新規メソッド normalizeScore の追加:

private normalizeScore(rawScore: number): number {
  if (!rawScore || rawScore <= 0) return 0.5;

  // 広範囲のスコアを処理するため、対数正規化を使用
  const logScore = Math.log10(rawScore + 1);

  // 0.5〜1.0 の範囲にマッピング
  const normalized = 0.5 + (logScore * 0.25);

  // 最終的に 0.5〜1.0 の間に制限
  return Math.max(0.5, Math.min(1.0, normalized));
}

なぜ対数正規化を使用するのか?

  • Elasticsearch のスコア範囲:1〜100 以上
  • log10(1) = 0 → 0.5
  • log10(10) = 1 → 0.75
  • log10(100) = 2 → 1.0
  • 結果が常に 0.5〜1.0 の間に収まるようになります。

2. すべての検索メソッドで正規化を適用

searchSimilar メソッド:

const results = response.hits.hits.map((hit: any) => ({
  id: hit._id,
  score: this.normalizeScore(hit._score),  // ✅ 正規化を適用
  // ...
}));

searchFullText メソッド:

const results = response.hits.hits.map((hit: any) => ({
  id: hit._id,
  score: this.normalizeScore(hit._score),  // ✅ 正規化を適用
  // ...
}));

hybridSearch メソッド:

// 結合された全スコアを取得して最大・最小を確認
const allScores = Array.from(combinedResults.values()).map(r => r.combinedScore);
const maxScore = Math.max(...allScores, 1);
const minScore = Math.min(...allScores);

// 総合スコアでソートして上位 topK を取得し、0〜1 の範囲に正規化
return Array.from(combinedResults.values())
  .sort((a, b) => b.combinedScore - a.combinedScore)
  .slice(0, topK)
  .map((result) => {
    // Min-Max 正規化
    let normalizedScore = (result.combinedScore - minScore) / (maxScore - minScore);

    // 0.5〜1.0 の範囲にマッピング
    normalizedScore = 0.5 + (normalizedScore * 0.5);

    // 0.5〜1.0 の間に制限
    normalizedScore = Math.max(0.5, Math.min(1.0, normalizedScore));

    return {
      ...result,
      score: normalizedScore,
    };
  });

3. フロントエンドの表示ロジック(変更なし)

バックエンド側でスコアが 0〜1 の間に収まることを保証したため、フロントエンドの修正は不要です:

{(source.score * 100).toFixed(1)}%  // 常に 50.0% 〜 100.0% が表示される

📊 修正後の効果

修正前

元のスコア 表示結果 問題点
1.5 150% ❌ 100% を超える
2.0 200% ❌ 100% を超える
0.8 80% ✅ 正常

修正後

元のスコア 正規化後 表示結果 ステータス
1.5 0.875 87.5%
2.0 0.938 93.8%
0.8 0.750 75.0%

🧪 テスト・検証

テスト手順

  1. テストドキュメントのアップロード

    # 異なる内容を含むテストドキュメントを作成
    echo "人工知能 機械学習 深層学習" > test1.txt
    echo "Python JavaScript TypeScript" > test2.txt
    
  2. 検索クエリの実行

    • クエリ:「人工知能」
    • 期待値:関連ドキュメントが表示され、スコアが 50〜100% の間であること。
  3. スコア範囲の検証

    // ブラウザのコンソールでチェック
    console.log('すべてのスコアが 50〜100 の間であるべきです');
    sources.forEach(s => {
     if (s.score * 100 > 100) console.error('スコアが 100% を超えています:', s);
    });
    

期待される結果

  • ✅ すべての相似度スコアが 50.0% 〜 100.0% の間にある。
  • ✅ 関連性の高いドキュメントは 100% に近い値を示す。
  • ✅ 関連性の低いドキュメントは 50% に近い値を示す。
  • ✅ 100% を超えるスコアは表示されない。

📝 修正ファイル

バックエンド

  • server/src/elasticsearch/elasticsearch.service.ts
    • プライベートメソッド normalizeScore() を追加
    • searchSimilar() にて正規化を適用
    • searchFullText() にて正規化を適用
    • hybridSearch() にて正規化を適用

フロントエンド

  • 修正なし(バックエンドでスコア範囲を保証)

⚠️ 注意事項

1. スコアの意味の変化

修正後、スコアは Elasticsearch の生の相似度を直接示すのではなく、以下の目安となります:

  • 50-60%:低い関連性
  • 60-75%:中程度の関連性
  • 75-90%:高い関連性
  • 90-100%:非常に高い関連性

2. しきい値の調整

以前に相似度フィルタリング(例:similarityThreshold: 0.7)を使用していた場合、調整が必要になる可能性があります:

// 旧設定(生のスコアベース)
similarityThreshold: 0.7

// 新設定(正規化スコアベース)
similarityThreshold: 0.6  // 以前の 0.7 に相当する目安

3. パフォーマンスへの影響

  • 正規化の計算は非常に軽量です (O(1))。
  • 検索パフォーマンスへの影響はありません。

📚 参考文献