Files
momentry_core/docs/SEARCH_SCORE_IMPROVEMENT.md
Accusys 834b0d4865 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)
2026-06-04 07:40:41 +08:00

4.7 KiB
Raw Blame History

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/smartRRF (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 '%槍%' 只找到中文文字包含「槍」的 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 回傳的 scoref32,需 cast 為 f64
  • keyword_resultscombined_score 實際上是 1.0search_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-1self-match cosine ≈ 1.0

附錄:前端處理

WordPress 側 (snippet #37) 已配合修正:mode=semantic 不再疊加 search/universalILIKE結果僅回傳 search/smart 的輸出。這部分無需 backend 配合。