feat: score-based search, LLM re-ranking endpoint, video title search, pipeline module
Core search changes: - Replace RRF with score-based merge (max of semantic/keyword/identity) - Add video title ILIKE search for brand/name queries (score 0.9) - Add /api/v1/search/llm-smart endpoint with Gemma 4 re-ranking - Fix LLM JSON parsing (markdown fences, empty responses) Infrastructure: - Rebuild Qdrant collection (clear 347K contaminated points) - Add dotenv loading to main.rs for config parity - Implement store_pre_chunk in postgres_db.rs Pipeline module (WordPress): - store-asrx, rule1, vectorize, phase1, complete endpoints - CLI commands for pipeline operations Docs: - SEARCH_SCORE_IMPROVEMENT.md (score-based merge proposal)
This commit is contained in:
134
docs/SEARCH_SCORE_IMPROVEMENT.md
Normal file
134
docs/SEARCH_SCORE_IMPROVEMENT.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# Search Scoring Improvement: Score-based Merge for search/smart
|
||||
|
||||
## 發現者
|
||||
WordPress 前端專案(search-chat 頁面)
|
||||
|
||||
## 問題描述
|
||||
|
||||
### 症狀
|
||||
跨語言搜尋結果不一致:
|
||||
- 搜尋「槍」(中文)→ 回傳無關結果(如「讓T-shirt」、「靠直的後製神器」)
|
||||
- 搜尋 `gun`(英文)→ 回傳 "So where's your gun?"、"He has a gun"
|
||||
- 兩者應該找到相同語意主題的結果(武器相關片段),但實際回傳完全不同的集合
|
||||
|
||||
### 影響範圍
|
||||
`GET/POST /api/v1/search/smart` endpoint
|
||||
|
||||
## 根因分析
|
||||
|
||||
### 1. Qdrant 語意搜尋本身是正確的
|
||||
|
||||
直接查詢 Qdrant 驗證:
|
||||
|
||||
```
|
||||
cos(search_query: 槍, search_document: "So where's your gun?") = 0.6905
|
||||
cos(search_query: 槍, search_document: "這是一把槍") = 0.8256
|
||||
cos(search_query: gun, search_document: "So where's your gun?") = 0.7435
|
||||
```
|
||||
|
||||
**embedding model (EmbeddingGemma-300m) 的 cross-lingual 對齊正常。**
|
||||
|
||||
### 2. 問題在 RRF 合併邏輯
|
||||
|
||||
`search/smart` 用 **RRF (Reciprocal Rank Fusion)** 合併三組結果:
|
||||
|
||||
```rust
|
||||
let rrf_k = 60.0;
|
||||
// RRF 貢獻 = 1 / (60 + rank + 1)
|
||||
// Semantic rank 0: 貢獻 1/61 = 0.016
|
||||
// Keyword rank 0: 貢獻 1/61 = 0.016
|
||||
```
|
||||
|
||||
RRF 的權重只看**排名位置**,不看**實際相似度分數**。
|
||||
- cosine similarity = 0.69 的語意結果 → RRF 貢獻 0.016
|
||||
- ILIKE 隨便撈到的 keyword 匹配 → RRF 貢獻也是 0.016
|
||||
- 兩者在排序中權重完全相等
|
||||
|
||||
### 3. Keyword (ILIKE) 對跨語言有害
|
||||
|
||||
- `ILIKE '%槍%'` 只找到中文文字包含「槍」的 chunks
|
||||
- `ILIKE '%gun%'` 只找到英文文字包含 "gun" 的 chunks
|
||||
- 這兩組結果在語意上完全不同,卻透過 RRF 被提升到與語意結果同權重
|
||||
- 導致「槍」和 `gun` 的結果各自被自己的 ILIKE 匹配汙染
|
||||
|
||||
## 建議方案
|
||||
|
||||
### 核心原則
|
||||
向量高信心度時應該優先。
|
||||
|
||||
### 合併方式
|
||||
|
||||
將 RRF 改為 score-based merge,各來源分數定義:
|
||||
|
||||
| 來源 | 分數 | 說明 |
|
||||
|---|---|---|
|
||||
| **Semantic (Qdrant)** | `cosine_similarity` (0~1) | 原始 Qdrant 分數,不加權 |
|
||||
| **Identity** | 固定 `0.85` | 人名精準匹配,維持高度信心 |
|
||||
| **Keyword (ILIKE)** | 固定 `0.5` | 降權至低分,只作為語意找不到時的補底 |
|
||||
|
||||
最終分數 = `max(semantic, keyword, identity)`
|
||||
依最終分數降冪排序。
|
||||
|
||||
### 預期效果
|
||||
|
||||
| 情況 | 排序行為 |
|
||||
|---|---|
|
||||
| cosine > 0.5 的語意結果 | 排在 keyword 前面 ✅ |
|
||||
| cosine 在 0.3~0.5 | 與 keyword 穿插(都不太確定,合理) |
|
||||
| cosine < 0.3 | keyword 補底(語意沒找到,靠文字比對) |
|
||||
| 跨語言查詢(槍 vs gun) | 各自的高分 cross-lingual 結果優先呈現 ✅ |
|
||||
|
||||
### 不建議的方案
|
||||
|
||||
- **不要用 weight-based average**(如 `0.7*semantic + 0.3*keyword`):兩種模型的 score scale 不同,加權無法通用
|
||||
- **不要保留 RRF 只調 k 值**:k 值調再高也無法區分品質,只能稀釋影響
|
||||
|
||||
## 修改範圍
|
||||
|
||||
### 檔案
|
||||
`src/api/search.rs` 中的 `smart_search()` 函數
|
||||
|
||||
### 需要修改的區塊
|
||||
|
||||
1. **移除 RRF 常數**(`rrf_k = 60.0`)
|
||||
2. **Semantic 結果**:保留 Qdrant 回傳的 `score`(已在 `h.score as f64` 取得)
|
||||
3. **Keyword 結果**:固定設為 `0.5_f64`(忽略原本 `combined_score`)
|
||||
4. **Identity 結果**:固定設為 `0.85_f64`(忽略原本硬編碼的 `0.85` 但保留值)
|
||||
5. **排序邏輯**:改為 `max(semantic, keyword, identity)` 降冪
|
||||
6. **輸出 similarity**:改為回傳最終分數,而非 `rrf_score`
|
||||
|
||||
### 注意事項
|
||||
|
||||
- Qdrant 回傳的 `score` 是 `f32`,需 cast 為 `f64`
|
||||
- `keyword_results` 的 `combined_score` 實際上是 `1.0`(`search_bm25` 固定值),不應使用
|
||||
- 修改後需 **`cargo build --release`** 再重啟 server
|
||||
|
||||
## 驗證測試
|
||||
|
||||
### 手動測試
|
||||
|
||||
```bash
|
||||
# 1. 槍 vs gun 應該回傳相似主題
|
||||
curl -X POST 'http://localhost:3002/api/v1/search/smart' \
|
||||
-H 'X-API-Key: {KEY}' -H 'Content-Type: application/json' \
|
||||
-d '{"query":"槍","limit":10}'
|
||||
|
||||
curl -X POST 'http://localhost:3002/api/v1/search/smart' \
|
||||
-H 'X-API-Key: {KEY}' -H 'Content-Type: application/json' \
|
||||
-d '{"query":"gun","limit":10}'
|
||||
|
||||
# 2. 確認 similarity 值為實際 cosine (e.g. 0.6~0.9) 而非 RRF 值 (~0.016)
|
||||
```
|
||||
|
||||
### 預期結果
|
||||
|
||||
| Query | Top 結果應包含 |
|
||||
|---|---|
|
||||
| `槍` | gun 相關片段、「這是一把槍」、武器相關語意匹配 |
|
||||
| `gun` | 與 `槍` 主題一致(都是武器) |
|
||||
| `車` / `car` | 行車相關片段,非姓名含「車」的人物 |
|
||||
| `So where's your gun?` | 自身為 top-1(self-match cosine ≈ 1.0) |
|
||||
|
||||
## 附錄:前端處理
|
||||
|
||||
WordPress 側 (`snippet #37`) 已配合修正:`mode=semantic` 不再疊加 `search/universal`(ILIKE)結果,僅回傳 `search/smart` 的輸出。這部分無需 backend 配合。
|
||||
Reference in New Issue
Block a user