# 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 配合。