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)
4.7 KiB
4.7 KiB
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) 合併三組結果:
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 '%槍%'只找到中文文字包含「槍」的 chunksILIKE '%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() 函數
需要修改的區塊
- 移除 RRF 常數(
rrf_k = 60.0) - Semantic 結果:保留 Qdrant 回傳的
score(已在h.score as f64取得) - Keyword 結果:固定設為
0.5_f64(忽略原本combined_score) - Identity 結果:固定設為
0.85_f64(忽略原本硬編碼的0.85但保留值) - 排序邏輯:改為
max(semantic, keyword, identity)降冪 - 輸出 similarity:改為回傳最終分數,而非
rrf_score
注意事項
- Qdrant 回傳的
score是f32,需 cast 為f64 keyword_results的combined_score實際上是1.0(search_bm25固定值),不應使用- 修改後需
cargo build --release再重啟 server
驗證測試
手動測試
# 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 配合。