ユーザーがチャットインターフェースにて、引用ソースの適合度スコアが 100% を超えている現象を確認しました。これは数学的に不可能です(相似度スコアは 0〜100% の間であるべきです)。
発生していた現象:
引用元表示:適合度 123.5%
適合度 165.2%
適合度 201.8%
Elasticsearch が返す生のスコア(_score)は、特に以下の場合に 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%
新規メソッド 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));
}
なぜ対数正規化を使用するのか?
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,
};
});
バックエンド側でスコアが 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% | ✅ |
テストドキュメントのアップロード
# 異なる内容を含むテストドキュメントを作成
echo "人工知能 機械学習 深層学習" > test1.txt
echo "Python JavaScript TypeScript" > test2.txt
検索クエリの実行
スコア範囲の検証
// ブラウザのコンソールでチェック
console.log('すべてのスコアが 50〜100 の間であるべきです');
sources.forEach(s => {
if (s.score * 100 > 100) console.error('スコアが 100% を超えています:', s);
});
server/src/elasticsearch/elasticsearch.service.ts
normalizeScore() を追加searchSimilar() にて正規化を適用searchFullText() にて正規化を適用hybridSearch() にて正規化を適用修正後、スコアは Elasticsearch の生の相似度を直接示すのではなく、以下の目安となります:
以前に相似度フィルタリング(例:similarityThreshold: 0.7)を使用していた場合、調整が必要になる可能性があります:
// 旧設定(生のスコアベース)
similarityThreshold: 0.7
// 新設定(正規化スコアベース)
similarityThreshold: 0.6 // 以前の 0.7 に相当する目安