---
title: "繁體中文全文搜尋引擎實戰筆記（三）：進階優化與系統評估"
description: "前兩篇記錄了選型決策和基礎建置，這篇進入進階優化——Entity-Aware Scoring 如何消除「主角不在」的假陽性、Multi-scale RRF 如何用段落搜尋補回遺漏文章、七輪測試的完整品質演進，以及一段誠實的投入產出檢討：哪些做對了、哪些做過頭了、哪些至今無法量化。"
pubDate: 2026-03-20
author: "jacobmei"
category: "AI與科技"
tags: [資料庫, 教會, 聖經]
canonical: https://jacobmei.com/blog/2026/0320-7gwwf6/
lang: zh-TW
license: CC BY-NC 4.0
---

# 繁體中文全文搜尋引擎實戰筆記（三）：進階優化與系統評估

# 繁體中文全文搜尋引擎實戰筆記（三）：進階優化與系統評估

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

---

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

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

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

**參考文章：** [繁體中文搜尋預處理工具-trad-zh-search-開源](https://jacobmei.com/blog/2026/0323-ygw9rr/)

**GitHub**: [notoriouslab/trad-zh-search](https://github.com/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/LOC|1.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 → filtered|—|19 篇（去除 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）|
|Embedder|userProvided（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 KW|R5 KW|變化|
|---|---|---|---|
|植堂|360|484|+34%|
|社會關懷|557|762|+37%|

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

### R6：跨來源知識庫（5,376 篇）

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

|查詢|R5 KW|R6 KW|變化|
|---|---|---|---|
|植堂|484|**819**|**+69%**|
|社會關懷|762|1000|達上限|

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 Reranking|embed-server 的 `/api/rerank` endpoint 未就緒，try-except 靜默退回原始排序|
|Entity Scoring|模組載入失敗時靜默退回純 relevance 排序|
|Multi-scale RRF|chunks 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）。詳見[第二篇更新](#2026-03-28-更新qwen3-reranker-06b-實測bge-reranker-的替代方案)。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 Pipeline   | NotebookLM                           |
| ---- | ----------------- | ------------------------------------ |
| 查詢品質 | 高度客製化，六層 pipeline | Google 原生多語言理解，黑箱                    |
| 維護成本 | 高：模型、服務、排程全自己管    | 零：Google 維護                          |
| 對外開放 | 可做公開搜尋 URL        | 不可對外，個人使用限制<br>或共用模式                 |
| 資料隱私 | 資料在自己的伺服器         | 上傳 Google                            |
| 擴充彈性 | 任意自訂              | source/notebook 上限<br>可透過 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   | ✅        | ✅         | ✅         | ✅          | ✅         | ✅         | ✅         |
| CKIP     | title 有限 | 追上 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 量化後區分度嚴重退化，詳見[第一篇更新](#2026-03-28-更新qwen3-embedding-06b-vs-bge-m3-繁中實測)）
- **失敗案例未系統化**：缺乏完整的 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](https://huggingface.co/BAAI/bge-m3) / [BGE-reranker-v2-m3](https://huggingface.co/BAAI/bge-reranker-v2-m3)
- [ONNX Runtime](https://onnxruntime.ai/)
- [ihower 繁體中文 Embedding 評測](https://ihower.tw/blog/12167-embedding-models)
