總覽與定位
全文檢索引擎解決什麼痛?與 OLTP / OLAP 資料庫在 System Design 版圖中的座標。
當使用者輸入「分散式 交易 一致性」這類模糊關鍵字,期望在毫秒內從數十億文件中找到最相關結果時,傳統 B-Tree 索引的 LIKE '%keyword%' 或全表掃描完全無法承受。搜尋與分析引擎(Search & Analytics Engine)的核心使命,是以倒排索引(Inverted Index)為基礎,提供全文檢索、相關性排序、聚合分析能力。
以 Elasticsearch(ES) 為代表的全文檢索引擎,在現代架構中幾乎總是旁路索引(Sidecar Index):authoritative data 仍存於 PostgreSQL / MongoDB 等 OLTP,透過 CDC 或雙寫同步到 ES 承擔跨 shard 搜尋;而 ClickHouse / BigQuery 等 OLAP 則負責大規模聚合報表。三者職責不重疊。
• OLTP:強一致、point lookup / 小範圍 transaction,不擅長模糊全文搜尋。
• Search Engine(ES):倒排索引 + 相關性評分,近即時可搜,最終一致。
• OLAP:Columnar 儲存、大 scan 聚合,延遲秒~分鐘級,不適合互動式搜尋框。
實務常見模式:MySQL (source of truth) → Debezium CDC → Elasticsearch (search) + ClickHouse (analytics)。
| 維度 | OLTP(如 PostgreSQL) | Search Engine(如 Elasticsearch) | OLAP(如 ClickHouse) |
|---|---|---|---|
| 索引結構 | B-Tree / Hash | Inverted Index + Doc Values | Columnar + Sparse Index |
| 典型查詢 | WHERE id = ? |
match "分散式 交易" |
GROUP BY date, SUM(revenue) |
| 一致性 | 強一致(ACID) | 近即時、最終一致 | 批次 / 準即時 |
| 延遲目標 | 毫秒級 point lookup | 毫秒~百毫秒級搜尋 | 秒~分鐘級聚合 |
面試高分:能說清楚「為什麼搜尋不直接打 OLTP」以及 ES 在資料流中的位置。
倒排索引(Inverted Index)
從「文件 → 詞彙」翻轉為「詞彙 → 文件列表」——全文檢索的資料結構基石。
正排索引(Forward Index)記錄「每份文件包含哪些詞」;倒排索引則反過來:每個 term(詞項) 對應一個 posting list(倒排列表),列出包含該 term 的所有 document ID,並附帶詞頻(TF)、位置(position)等 metadata 供評分與短語匹配使用。
ES 中的索引層次
- Index:邏輯命名空間(類似 database),如
products。 - Shard:Index 的水平切分,每個 shard 是獨立的 Lucene index,支援分散式搜尋與寫入。
- Segment:Shard 內不可變(immutable)的 Lucene 片段;寫入先進 memory buffer,
refresh後 flush 成新 segment。 - Document:JSON 文件,含
_id與 mapping 定義的 fields。
四個層次的包含關係
關鍵在於這四者不是平級,而是層層包含:一個 Index 水平切成多個 Shard,每個 Shard 是一份獨立的 Lucene index、內部由多個不可變 Segment 疊成,Segment 裡才真正存放各 Document 的倒排索引。
products — 邏輯命名空間,查詢與寫入的目標| ES 術語 | 關聯式 DB 類比 | 一句話職責 |
|---|---|---|
Index |
Database / Table | 邏輯命名空間;查詢的目標,本身不直接存資料 |
Shard |
水平分片(Partition) | 分散式與擴展的單位;每個是獨立 Lucene index |
Segment |
(無直接對應)不可變索引檔 | 倒排索引實際落地處;refresh 生成、merge 合併 |
Document |
Row | 一筆 JSON 文件,含 _id 與 mapping fields |
巢狀關係 Index ⊃ Shard ⊃ Segment ⊃ Document:搜尋時 coordinating node 把 query 扇出到各 shard,各 shard 在自己的 segment 上查倒排索引,再彙整結果。
Lucene 的 segment 一旦寫入磁碟就不修改——更新 = 標記舊 doc 為 deleted + 寫入新 doc 到新 segment。這讓並發讀取無鎖、合併(merge)可背景執行。代價是磁碟佔用與 merge I/O;Production 需監控 segment 數量與 merge 壓力。
Posting List 結構示意
假設三份文件經 Analyzer 後得到 term,倒排索引大致如下:
| Term | Posting List(docId : TF) | DF(Document Frequency) |
|---|---|---|
分散式 |
doc1:2, doc3:1 | 2 |
交易 |
doc1:1, doc2:3 | 2 |
一致性 |
doc1:1, doc2:1, doc3:2 | 3 |
雜湊 |
doc3:1 | 1 |
查詢「分散式 交易」= 取各 term 的 posting list 做交集(AND)或聯集(OR),再依評分排序。
• Range scan 全表:numeric range 可用 BKD-tree(doc values),但不如 columnar OLAP 高效。
• 高基數唯一值 point lookup:用 _id 或 routing key 查單 doc 可以,但 ES 不是 OLTP 替代品。
• 頻繁更新同一 doc:每次更新產生新 segment + 標記刪除,merge 成本上升。
Analyzer 與 Tokenizer
文字如何變成 term?Index-time 與 Search-time 的分析管線必須一致。
使用者看到的「分散式系統」在寫入索引前,會經過 Analyzer 管線拆解成可索引的 term。ES 的 Analyzer 由三部分組成:
- Character Filters:預處理字元流(如 HTML strip、正規化全形半形)。
- Tokenizer:切分 token 流(如 standard 依標點切、ik_max_word 中文分詞)。
- Token Filters:後處理 token(lowercase、stop words、stemming、同義詞)。
| Analyzer 類型 | 行為 | 典型用途 |
|---|---|---|
standard |
Unicode 分詞 + lowercase | 英文混合、通用預設 |
keyword |
整段文字當單一 term | SKU、UUID、enum 精確匹配 |
ik_max_word(外掛) |
中文最大粒度分詞 | 中文全文檢索(索引側) |
ik_smart(外掛) |
中文最細粒度分詞 | 中文搜尋側(較少過切) |
Mapping 中常見:title 用 text + analyzer;status 用 keyword 做 filter/aggregation。
Index Analyzer vs Search Analyzer
寫入時用 index analyzer,查詢時用 search analyzer。兩者可以不同(例如 index 用 ik_max_word 提高 recall,search 用 ik_smart 提高 precision),但必須在設計時成對定義,否則 query term 與 index term 對不上會零結果。
端到端追蹤:一個 term 如何從寫入走到命中
以中文欄位示範這條「刻意不對稱」的鏈:index 端用 ik_max_word 切到最細粒度以拉高 recall(存下最多可能被查到的 term),search 端用 ik_smart 切得保守以拉高 precision——兩條管線不同,但共享的 term 讓查詢仍能命中。
ik_max_word正規化(無變化) → Tokenizer
ik_max_word 最大粒度 → Token Filter
lowercase / 同義詞
ik_smartik_smart 保守粒度 → query term → 分散
分散 命中 index 中的同名 term(它來自 ik_max_word 對「分散式系統」的細粒度切分)→ 該 doc 進入候選並參與 BM25 計分。兩端 analyzer 不同,卻在 分散 這個 term 上會合。
Elasticsearch 時 index analyzer 帶 lowercase filter,存成 term elasticsearch;若 search analyzer 漏了 lowercase,查詢 Elasticsearch 產生的 query term 仍是 Elasticsearch:
修改 field 的 analyzer 只影響新寫入的文件。舊 doc 仍用舊 term 索引——需 reindex(新 index + alias 切換)才能生效。Production 變更 analyzer 必須規劃 reindex SOP 與 alias 零 downtime 切換。
text vs keyword 多欄位(Multi-field)同一欄位常同時需要全文搜尋與精確排序/聚合,用 multi-field:
"title": { "type": "text", "fields": { "raw": { "type": "keyword" } } }
搜尋打 title,排序 / terms agg 打 title.raw。
Refresh 與相關性評分
Near Real-time 的可搜尋性窗口,以及 BM25 如何把 posting list 變成排序分數。
Refresh:為什麼寫入後不能立刻搜到?
新 document 先寫入 shard 的 memory buffer(Indexing Buffer),此時對搜尋不可見。Refresh 將 buffer flush 成新的 Lucene segment,並開放給搜尋——這就是 ES「近即時(Near Real-time, NRT)」的來源。
- 預設
index.refresh_interval = 1s:最多約 1 秒延遲可搜。 - 大量 bulk 寫入時可暫設
-1關閉自動 refresh,寫完再手動 refresh,避免每秒建 segment 拖垮 merge。 refresh=wait_for:寫入 API 阻塞直到下次 refresh,適合「寫完立刻讀」但會拉高寫入延遲。
• Refresh:memory → 新 segment,可搜尋,資料可能仍在 OS cache。
• Flush:commit translog、fsync segment 到磁碟,持久化保證。
面試常混淆:refresh 管「搜尋可見性」,flush 管「宕機不丟資料」。
Relevance Scoring:BM25
ES 預設使用 BM25(Best Matching 25)計算 query 與 document 的相關性。核心直覺:
- TF(Term Frequency):term 在 doc 中出現越多次,分數越高(但有飽和,避免堆砌關鍵字)。
- IDF(Inverse Document Frequency):越稀有的 term 權重越高——「的」到處都有,「Raft」很稀有。
- Field Length Norm:短欄位(如 title)中命中 term 比長欄位(body)更有價值。
實務調參常用 boost:title 權重 ×3、tags ×2、body ×1。function_score 可疊加業務因子(銷量、距離、新鮮度 decay)。
| 機制 | 作用 | 面試一句話 |
|---|---|---|
match |
全文分詞匹配 + BM25 | 預設搜尋,OR 語意(可設 operator AND) |
term |
精確 term 匹配,不評分 | filter context,可 cache |
bool |
must / should / filter / must_not 組合 | filter 不計分但可 cache;must 計分 |
function_score |
自訂函數調整分數 | 電商:相關性 × 銷量 × 距離 decay |
Query context(計分)vs Filter context(yes/no + cache)是 ES 查詢優化的核心分界。
from=10000&size=10 需每 shard 收集 10010 筆再排序——記憶體與 CPU 爆炸。解法:search_after(cursor 分頁)、scroll(批次匯出,非即時 UI)、或 collapse 去重。
互動 Demo:倒排索引與 BM25 評分
模擬 Analyzer 分詞、posting list 交集、以及 refresh 前後的可搜尋性。
下方內建三份技術文章摘要。輸入查詢後,QUERY PIPELINE 會一步步演出「查詢字串 → 分詞 → 逐 term 查 posting list → 依操作符合併 docId → BM25 排序」的完整鏈條。切換 OR / AND 操作符可直接看到 posting list 取聯集還是交集;「寫入新 Doc」+「Refresh」則呈現 NRT 可見性——未 refresh 的 doc 不會出現在結果中。
Demo 簡化真實 BM25(無 field norm 飽和曲線),但保留 term 分詞 → posting list 合併(OR 聯集 / AND 交集)→ TF×IDF 排序的完整直覺與 refresh 可見性語意。
面試考點與架構折衷
ES 叢集怎麼設計?與 OLTP 如何同步?什麼時候不該用 Elasticsearch?
Elasticsearch 叢集核心概念
- Master-eligible nodes:叢集狀態管理(index/shard 分配),奇數節點防 split-brain(ES 7+ 用 quorum 選主)。
- Data nodes:持有 shard 資料,執行 indexing / search。
- Coordinating node:Scatter-gather——把查詢分發到各 shard,合併結果(query then fetch 兩階段)。
- Replica:shard 副本,提升讀吞吐與 HA;寫入需 primary + replica 確認(依
wait_for_active_shards)。
回答框架:
1. MySQL 保持 source of truth(ACID 寫入)。
2. CDC(Debezium / Maxwell)或應用雙寫同步到 ES。
3. ES 建 inverted index 承擔 match 查詢;精確 id lookup 仍走 MySQL 或 ES ids query。
4. 接受 eventual consistency;必要時 write → refresh=wait_for 或 read-your-writes 路由。
5. 全量 reindex SOP + alias 切換(products_v2 → alias products)。
• 單 shard 建議 10~50 GB(視 heap 與查詢複雜度)。
• Shard 過多 → 每 query 扇出過大、cluster state 膨脹;過少 → 無法水平擴展。
• 開始偏少、用 split / reindex 擴;Production 常預留 2~3 倍容量。
• Routing key(如 user_id)讓相關 doc 共 shard,減少 fan-out。
• 強一致 transaction、頻繁 row-level update → 用 OLTP。
• 大規模 GROUP BY / 報表 → 用 OLAP(ClickHouse / BigQuery)。
• 小資料量(< 百萬 row)簡單搜尋 → PostgreSQL tsvector / SQLite FTS 可能足夠,不必引入 ES 運維成本。
| Trade-off | 選 A | 選 B | 決策線索 |
|---|---|---|---|
| 一致性 | CDC 異步 | 雙寫 + 補償 | 可接受秒級 lag → CDC;需強保證 → 雙寫 + outbox |
| Refresh | 1s 預設 | -1 bulk 後手動 | 互動搜尋 vs 大量 ETL |
| 評分 | BM25 預設 | Learned to Rank | 有點擊日志 + ML 團隊 → LTR;否則 BM25 + boost |
| 同步 vs 分析 | ES 搜尋 | ClickHouse 分析 | 搜尋框 vs BI 報表,常並存 |
ES + OLTP + OLAP 三層是大型電商 / SaaS 的標準搜尋分析分層。