01

總覽與定位

全文檢索引擎解決什麼痛?與 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 則負責大規模聚合報表。三者職責不重疊。

座標Search Engine vs OLTP vs 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 在資料流中的位置。

一句話答法全文檢索引擎以倒排索引承擔模糊搜尋與相關性排序,通常作為 OLTP 的旁路索引;OLAP 負責大聚合,三者分工而非互相取代。

02

倒排索引(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 的倒排索引。

INDEX · products — 邏輯命名空間,查詢與寫入的目標
SHARD 0 · primary — 獨立 Lucene index
segment · immutable
📄 doc1 📄 doc4
segment · immutable
📄 doc7
SHARD 1 · primary — 獨立 Lucene index
segment · immutable
📄 doc2 📄 doc5
segment · immutable
📄 doc9
SHARD 0 · replica — primary 的副本(結構相同,以虛線表示),落在其他節點,提升讀吞吐與 HA。
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 上查倒排索引,再彙整結果。

白話為什麼 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 成本上升。

一句話答法倒排索引 = term → posting list(docId + TF);ES 的 shard/segment 不可變設計換取無鎖讀,更新靠 delete + reindex + background merge。

03

Analyzer 與 Tokenizer

文字如何變成 term?Index-time 與 Search-time 的分析管線必須一致。

使用者看到的「分散式系統」在寫入索引前,會經過 Analyzer 管線拆解成可索引的 term。ES 的 Analyzer 由三部分組成:

  1. Character Filters:預處理字元流(如 HTML strip、正規化全形半形)。
  2. Tokenizer:切分 token 流(如 standard 依標點切、ik_max_word 中文分詞)。
  3. 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 讓查詢仍能命中。

① INDEX-TIME · 寫入 doc 時 · analyzer = ik_max_word
內文「分散式系統」 → Char Filter
正規化(無變化)
→ Tokenizer
ik_max_word 最大粒度
→ Token Filter
lowercase / 同義詞
存入 6 個 term → 分散式 分散 式 系統 系 統
▼ 這些 term 寫進 INVERTED INDEX,等待被查 ▼
② SEARCH-TIME · 使用者查詢時 · analyzer = ik_smart
查詢「分散」 → Char Filter → Tokenizer
ik_smart 保守粒度
→ query term → 分散
✅ query term 分散 命中 index 中的同名 term(它來自 ik_max_word 對「分散式系統」的細粒度切分)→ 該 doc 進入候選並參與 BM25 計分。兩端 analyzer 不同,卻在 分散 這個 term 上會合。
✗ 反例:兩端「非故意地」不一致 → 0 結果
寫入 Elasticsearch 時 index analyzer 帶 lowercase filter,存成 term elasticsearch;若 search analyzer 漏了 lowercase,查詢 Elasticsearch 產生的 query term 仍是 Elasticsearch:
index term: elasticsearch ≠ query term: Elasticsearch → 對不上,0 hits
重點:analyzer 可以刻意不同(如上方 ik 範例),但每個處理步驟造成的 term 形態,兩端必須能對得上——這才是「成對設計」的真義。
陷阱改 Analyzer 不會自動重建索引

修改 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。

一句話答法Analyzer = Character Filter → Tokenizer → Token Filter;index/search analyzer 成對設計,改 analyzer 需 reindex,text 做全文、keyword 做精確 filter/agg。

04

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 vs Flush vs fsync

• 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 查詢優化的核心分界。

陷阱深分頁(Deep Pagination)

from=10000&size=10 需每 shard 收集 10010 筆再排序——記憶體與 CPU 爆炸。解法:search_after(cursor 分頁)、scroll(批次匯出,非即時 UI)、或 collapse 去重。

一句話答法Refresh 決定 NRT 可搜窗口(預設 1s);BM25 用 TF×IDF×欄位長度 norm 排序;filter 不計分可 cache,深分頁用 search_after。

05

互動 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 可見性語意。

一句話答法互動演示倒排索引查詢 = term 分詞 → posting list 查找 → BM25 計分排序;未 refresh 的 doc 對搜尋不可見。

06

面試考點與架構折衷

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 已有資料,如何加上全文搜尋?」

回答框架:
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)。

考點 2「Shard 數量怎麼估?」

• 單 shard 建議 10~50 GB(視 heap 與查詢複雜度)。
• Shard 過多 → 每 query 扇出過大、cluster state 膨脹;過少 → 無法水平擴展。
• 開始偏少、用 split / reindex 擴;Production 常預留 2~3 倍容量。
• Routing key(如 user_id)讓相關 doc 共 shard,減少 fan-out。

陷阱什麼時候不該用 ES?

• 強一致 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 的標準搜尋分析分層。

一句話答法ES 作旁路全文索引:CDC 同步、shard 按容量規劃、NRT refresh 換寫入吞吐、BM25 + boost 做排序;強一致與大聚合交還 OLTP / OLAP。