0a9588abb7
- Add pagination support to findAll (page, limit query params) - Add findByTemplateId method to service - Add GET /by-template/:templateId endpoint to controller - Service already includes CRUD for QuestionBank and QuestionBankItem
250 lines
7.6 KiB
Markdown
250 lines
7.6 KiB
Markdown
# 相似度スコアが 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 メソッド:**
|
|
|
|
```typescript
|
|
// 問題: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 - 表示ロジック:**
|
|
|
|
```typescript
|
|
// 問題:スコアが 0〜1 の間であることを前提に 100 倍している
|
|
{(source.score * 100).toFixed(1)}% // 1.05 * 100 = 105%
|
|
```
|
|
|
|
## ✅ 解決策
|
|
|
|
### 1. ElasticsearchService にスコアの正規化を追加
|
|
|
|
**新規メソッド `normalizeScore` の追加:**
|
|
|
|
```typescript
|
|
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 メソッド:**
|
|
|
|
```typescript
|
|
const results = response.hits.hits.map((hit: any) => ({
|
|
id: hit._id,
|
|
score: this.normalizeScore(hit._score), // ✅ 正規化を適用
|
|
// ...
|
|
}));
|
|
```
|
|
|
|
**searchFullText メソッド:**
|
|
|
|
```typescript
|
|
const results = response.hits.hits.map((hit: any) => ({
|
|
id: hit._id,
|
|
score: this.normalizeScore(hit._score), // ✅ 正規化を適用
|
|
// ...
|
|
}));
|
|
```
|
|
|
|
**hybridSearch メソッド:**
|
|
|
|
```typescript
|
|
// 結合された全スコアを取得して最大・最小を確認
|
|
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 の間に収まることを保証したため、フロントエンドの修正は不要です:
|
|
|
|
```typescript
|
|
{(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. **テストドキュメントのアップロード**
|
|
|
|
```bash
|
|
# 異なる内容を含むテストドキュメントを作成
|
|
echo "人工知能 機械学習 深層学習" > test1.txt
|
|
echo "Python JavaScript TypeScript" > test2.txt
|
|
```
|
|
|
|
2. **検索クエリの実行**
|
|
- クエリ:「人工知能」
|
|
- 期待値:関連ドキュメントが表示され、スコアが 50〜100% の間であること。
|
|
|
|
3. **スコア範囲の検証**
|
|
|
|
```typescript
|
|
// ブラウザのコンソールでチェック
|
|
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`)を使用していた場合、調整が必要になる可能性があります:
|
|
|
|
```typescript
|
|
// 旧設定(生のスコアベース)
|
|
similarityThreshold: 0.7
|
|
|
|
// 新設定(正規化スコアベース)
|
|
similarityThreshold: 0.6 // 以前の 0.7 に相当する目安
|
|
```
|
|
|
|
### 3. パフォーマンスへの影響
|
|
|
|
- 正規化の計算は非常に軽量です (O(1))。
|
|
- 検索パフォーマンスへの影響はありません。
|
|
|
|
## 📚 参考文献
|
|
|
|
- [Elasticsearch Similarity Scoring](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-script-score-query.html)
|
|
- [Vector Search Cosine Similarity](https://www.elastic.co/guide/en/elasticsearch/reference/current/dense-vector.html)
|
|
- [Min-Max Normalization](https://en.wikipedia.org/wiki/Feature_scaling#Rescaling_(min-max_normalization))
|