繁體中文全文搜尋引擎實戰筆記(三):進階優化與系統評估

閱讀偏好
繁體中文全文搜尋引擎實戰筆記(三):進階優化與系統評估

繁體中文全文搜尋引擎實戰筆記(三):進階優化與系統評估

系列最後一篇。前兩篇記錄了選型決策和基礎建置,這篇進入進階優化 —— Entity-Aware Scoring 如何消除「主角不在」的假陽性、Multi-scale RRF 如何用段落搜尋補回遺漏文章、七輪測試的完整品質演進,以及一段誠實的投入產出檢討:哪些做對了、哪些做過頭了、哪些至今無法量化。


Entity-Aware Scoring:解決「主角不在」

繁體中文搜尋預處理工具-trad-zh-search-開源

假如有人想試試我的方案,我已經將它打包可以單獨搭配的套件,有興趣的人可以試試:

參考文章: 繁體中文搜尋預處理工具-trad-zh-search-開源

GitHub: notoriouslab/trad-zh-search

問題

搜尋「某人在某組織的事工」時,Meilisearch 的 ranking score 不知道誰是主角——只要文章提到該組織(遍地都是),分數就可能很高,即使文章根本沒提到那個人。

這不是 Meilisearch 的 bug,而是所有基於 TF-IDF / BM25 的搜尋引擎的通病:它們不理解查詢中的實體角色。

理論基礎

參考 DREQ(Edinburgh, ECIR 2024)和 REGENT(Missouri, 2025)兩篇 Entity-Centric Re-ranking 論文。核心觀點:不是所有實體對文件相關性的影響都一樣大,應強調查詢相關的實體,弱化不相關的。

雙信號模型

final_score = α × relevance + (1 - α) × entity_coverage
  • relevance:Meilisearch _rankingScore(0~1)
  • entity_coverage:實體在文章中的出現程度(0~1)
  • α = 0.5(兩信號等權),查詢無實體時 α = 1.0(不介入)

Entity Coverage 計算

用 CKIP NER 字典匹配查詢中的實體,分兩級:

角色判定權重
主詞查詢中第一個 PERSON/ORG/LOC1.0
配角其餘實體0.3

每個實體的 match signal:出現在標題 = 1.0,出現在內容 = 0.6,完全不在 = 0.0。

entity_coverage = Σ(entity_weight × match_signal) / Σ(entity_weight)

關鍵規則:

  • 硬篩:主詞 match_signal = 0 → 直接踢掉(絕不出現主角不在的假陽性)
  • 共現加權:主詞 + 配角在同一段落共現 → coverage +0.15

實體別名展開

建立靜態別名表,搜尋時自動展開。從最初的 25 組擴充到 136 組(涵蓋各種暱稱、簡稱、中文譯名變體)。成本極低但對人名搜尋的召回率提升極大。

測試結果

人名搜尋(別名展開):搜尋暱稱時,系統自動展開為全名。10/10 結果全部正確,100% 精準。

雙實體查詢(Entity Scoring 前後對比)

改善前改善後
不含主角的文章74 分排前列硬篩踢掉
結果品質混雜不相關文章只保留提及主角的文章
50 篇 raw → filtered19 篇(去除 62% 假陽性)

無實體查詢(如「禱告的方法」):Entity scoring 不介入,α = 1.0 純用 Meilisearch 分數,結果和改善前完全相同。不會誤傷。


Chunking 與 Multi-scale RRF

為什麼需要 Chunking

一篇 3000 字的文章如果只有一個 embedding 向量,語意會被「稀釋」——同時提到宣教、財務、禱告的文章,向量變成三個主題的平均值,搜哪個主題都「有點像但不太像」。

把文章切成段落(chunk),每段各自 embedding,搜尋時匹配段落而非整篇文章,精度提升。

我選了 Paragraph-Based chunking:文章有明確的 \n\n 段落分隔、速度快、語意完整性遠高於固定大小切分、不需要額外 NLP 工具。

實作數據

指標數值
文章數7,647
Chunk 數59,409(avg ~303 chars/chunk)
EmbedderuserProvided(Python 預算,不經 Meilisearch 呼叫 embed-server)

架構教訓:Meilisearch 的 ollama embedder source 對大量 chunk 有不可接受的 timeout 風險(633 tasks 全 timeout)。改用 userProvided + Python batch 預算完全消除此問題。如果你要建大量 chunk 的 index,直接用 userProvided。

Multi-scale RRF:文章 + 段落混合搜尋

原本:articles-kw + articles-semantic → merge → rerank → entity scoring

升級:articles-kw + articles-semantic     ← 文章層級信號
     + chunks-kw  + chunks-semantic       ← 段落層級信號(新增)
            ↓ RRF(k=60) ↓
        rerank → entity scoring

RRF(Reciprocal Rank Fusion)把四路信號的排名合併:RRF(d) = 1/(k + rank(d)),k=60 是平滑常數。

實際效果

RRF merge: 34 article-signal + 23 chunk-signal → 17 candidates (chunk-only: 12)
Entity scoring: ['某組織(subject)'] → 10 articles survived

chunk-only: 12 代表有 12 篇文章只透過段落搜尋發現,文章層級搜尋完全遺漏。每次查詢大約有 10-15 篇這樣被補回。

Trade-off

段落命中但文章主題偏離的案例確實存在——某個段落提到「差傳」但整篇文章其實在講別的。緩解方案是調高 RRF_K(60 → 80),降低 chunk-only 信號的權重。


R5 到 R7 品質驗收

R5:Multi-scale RRF 上線

查詢R4 KWR5 KW變化
植堂360484+34%
社會關懷557762+37%

窄域主題改善最明顯。段落搜尋命中了文章搜尋遺漏的結果。

R6:跨來源知識庫(5,376 篇)

從 1,848 篇擴充到 5,376 篇。

查詢R5 KWR6 KW變化
植堂484819+69%
社會關懷7621000達上限

Top-5 相關度維持 ~95%,新來源完全可搜,既有結果未被取代。

R7:7,647 篇 + 80 題 Benchmark

最終版本的知識庫包含 7,647 篇文章、59,409 個 chunks。用 80 題涵蓋 16 個主題分類的標準問題集測試,對比新來源匯入前後:

指標數值
Top-1 不變48/80(60%)
Top-1 改善32/80(40%)

教義辨析類問題改善最大 —— 這正是新來源的強項。48/80 題 Top-1 不變,表示既有來源的優勢位置未被取代,新舊來源互補而非互搶。


Silent Fallback:最慘痛的教訓

R7 測試過程中,經過逐一確認六層 pipeline 的實際運行狀態,結果發現一個悲劇 …

R3-R6 期間多個機制存在 silent fallback !!!!

機制問題
Cross-encoder Rerankingembed-server 的 /api/rerank endpoint 未就緒,try-except 靜默退回原始排序
Entity Scoring模組載入失敗時靜默退回純 relevance 排序
Multi-scale RRFchunks index 為空時直接退回 article-only 排序
Hybrid Search(R1)前端未正確送出 hybrid 參數,只有 keyword search 在跑

根本原因:所有進階機制都採用 try-except + fallback 的防禦性設計,錯誤只寫 log.warning,不中斷服務,這在生產環境是正確的(graceful degradation),但開發期間導致「看似啟用但實際未運作」—— 我以為在調 reranking 權重,實際上 reranking 根本沒跑。

經驗教訓:pipeline 的每一層在首次部署後,必須有明確的 smoke test —— 檢查 log 中是否出現特定標記(如 “Reranked N → M articles”),而非依賴「沒有報錯 = 正常運作」的假設。之前有使用 QA 導入開發比較沒有這種問題,這次跑的太快,忘了嚴守自己的紀律,加上這陣子太信任 AI 開發(我主要用 Claude,同時使用 Gemini , Chatgpt 等同步 Debug ),結果就是自己挖坑自己跳 …

如果你也在建多層 pipeline,請在每一層加一個 health check ,Silent fallback 是生產環境的好設計,但一個沒注意,它會讓你浪費大量時間調整一個根本沒在跑的機制。


首頁定案:單一入口設計

前端原本提供三種搜尋模式(關鍵字 / 混合 / 語意)和兩種 AI 模式,但要理解每種模式的差異才能有效搜尋,實際使用後發現太複雜——大多數人只想輸入問題、找到文章。

定案:移除搜尋模式切換(固定 hybrid search)、移除 AI 模式切換、移除多輪對話 UI,保留標籤/年份篩選、收藏功能,先完全採用內建的 rag 語意搜尋,結果出來後再補上 AI LLM 彙整,有點像是 Notebooklm 的呈現方式,自己覺得效果不錯。

單一入口不代表功能縮水,而是把複雜度從使用者端移到後端。


誠實的投入產出檢討

回頭看,三個進階優化的投入產出比各不相同。

1. CKIP 全量跑 content —— 做過頭了

R2 的數據已經清楚顯示:bigram 在召回率上一直贏或持平。但我仍然花了 160 分鐘跑 content_ckip 全量。CKIP 的真正價值在查詢端分詞(人名/機構名不被切壞)和 NER 字典產生,不在 content 欄位的全量分詞,content_bigram 已經夠用。

如果重來,content 可能只跑 bigram 就夠用。

2. Cross-encoder Reranker —— 價值在排序精度,但目前無法量化

R3-R6 reranker 因 silent fallback 沒在跑,Top-5 相關度也沒掉回 91%,但這個推論有盲點 —— 同時期 Entity Scoring 也可能沒跑,三個變因無法分離。

更根本的問題是 Top-5 相關度太粗:它不區分「前 5 篇都相關但順序亂」和「最好的排第一」。Reranker 的價值在排序精度 —— RAG 場景中 LLM 只看 top-5,第一名放對放錯直接影響回答品質,需要 Top-1 精準度或 MRR 才能公平評估,不能從「沒跑也沒掉」就否定它。

2026-03-28 更新:Reranker 問題已解決。BGE-reranker-v2-m3 在 ARM64 上佔用 ~10GB RAM 導致 OOM,替換為 Qwen3-Reranker-0.6B ONNX INT8(YesNo 變體),記憶體降至 960MB,速度持平(~4s/10 docs),排序品質優秀(區分度:最相關 0.594 vs 最不相關 0.0006)。詳見第二篇更新。Cross-encoder reranking 從「理論有價值但實際跑不動」變成「穩定可用」。

3. Multi-scale RRF —— 長尾有價值,噪音有解法

Top-5 相關度跟沒加 RRF 時一樣 ~95%,但 RRF 的價值不在通用查詢的 Top-5,而在窄域主題。「小組化是什麼」直接段落命中、「以色列宣教」精準引用——這些是 article-only 搜尋找不到的。

噪音問題真實存在,但有已知解法(RRF_K 60→80 降低 chunk-only 權重)。建置時間成本不低(59,409 chunks 的 embedding + patch 機制),但對 RAG 問答需要精準引用段落的場景,這是必要的基礎設施。

總結:一個確認做過頭,兩個需要更好的指標來評估。


自建系統 vs NotebookLM

建置完成後回頭看 —— 這麼 ㄍㄟ ㄍㄨㄥ 搞這套系統到底是為什麼?什麼情境用 NotebookLM 反而更好?

面向自建 RAG PipelineNotebookLM
查詢品質高度客製化,六層 pipelineGoogle 原生多語言理解,黑箱
維護成本高:模型、服務、排程全自己管零:Google 維護
對外開放可做公開搜尋 URL不可對外,個人使用限制
或共用模式
資料隱私資料在自己的伺服器上傳 Google
擴充彈性任意自訂source/notebook 上限
可透過 api 控制部份功能

自建的核心優勢:充分彈性 是 NotebookLM 無法取代的唯一場景,從介面到每個使用細節都可以自己刻想要的方式。

六層 pipeline、CKIP 繁中分詞、entity scoring —— 市面上 99% 套裝方案不會為你處理這些(可能也不需要就是了)。

NotebookLM 的定位:個人備課、快速問答 —— 上傳幾十個檔案,維護成本趨近於零,Audio Overview 還能轉 Podcast。

建議路徑:兩者並存,自建系統高客製化,NotebookLM 補足更深入的互動對話。


七輪演進總覽

項目R1 (516)R2 (1854)R3 (+RAG)R4 (全量)R5 (+RRF)R6 (5376)R7 (7647)
Bigram
CKIPtitle 有限追上 bigram查詢端content 全量字典 1208 條
Reranker加入(※)✅ 確認
Entity加入(※)✅ 確認
RRF加入(※)✅ 確認
Hybrid❌ 參數未送✅ 修復
總評機制缺失優秀卓越卓越卓越卓越全機制確認

※ = 程式碼已部署但有 silent fallback,可能未實際運作。R7 是第一次確認全層級正常運行的 benchmark,還是要說,實在笨慘了 …


關鍵發現

  1. 語料量 + 機制完整度是搜尋品質的兩大槓桿 — 兩者同時提升時效果最顯著
  2. ONNX INT8 讓 Cross-encoder 在 ARM64 CPU 上可用 — 從 timeout 到 3.8 秒
  3. Entity-Aware Scoring 消除了「主角不在」的假陽性 — 雙信號模型 + 硬篩是關鍵
  4. 實體別名表小成本大效果 — 136 組靜態對照,成本趨近零
  5. 三供應商 LLM fallback 確保可用性 — 單一免費供應商不夠穩定
  6. userProvided embedder 是批次建 index 的正確架構 — Meilisearch ollama source 對大量 chunk 不可靠
  7. Silent fallback 是開發期間的隱形殺手 — 每一層都需要 smoke test
  8. 做過頭的事誠實承認 — content_ckip 全量確實不見得需要

已知盲點

  • 缺乏真實使用者回饋:80 組測試題庫查詢是我透過 AI 設計的,使用者的查詢習慣可能有顯著差異
  • Embedding 更新成本未評估:BGE-M3 不會是永遠的最佳選擇,但換模型需全量重跑,時間成本有點高(2026-03-28 更新:實測 Qwen3-Embedding-0.6B 後確認 BGE-M3 在 ARM64 CPU + 量化場景仍然是最佳選擇——Qwen3 的 decoder 架構在 ONNX 量化後區分度嚴重退化,詳見第一篇更新
  • 失敗案例未系統化:缺乏完整的 error analysis

原則沒有變:先用 80 分方案上線,有明確痛點再追 90 分。 但「明確痛點」不一定要等使用者回報——在 RAG 問答場景中,排序精度和段落召回是可以預見的需求。


參考資源

  • DREQ (Edinburgh, ECIR 2024): Entity-centric document re-ranking
  • REGENT (Missouri, 2025): Relevance-guided attention with entities as “semantic skeleton”
  • AI21 Multi-scale chunking (2026-01): 多尺度 index + RRF 合併
  • FloTorch benchmark (2026-02): 固定 512 token chunking 打敗語意分塊
  • BGE-M3 / BGE-reranker-v2-m3
  • ONNX Runtime
  • ihower 繁體中文 Embedding 評測
作者 Jacobmei:帶領街口支付對接國際巨頭 Apple,推動台灣金融科技國際化實踐。

← 回文章列表