你打開搜尋框,輸入「小籠包」。系統回傳所有包含「小」、「籠」、「包」的結果——小學、鳥籠、背包全跑出來。

這不是 bug。這是你的搜尋引擎不會「讀中文」。

英文有空格,dog 就是 dog,切詞天生容易。中文沒有這種天然分隔符。「台北市政府捷運站」到底是「台北市 / 政府 / 捷運站」,還是「台 / 北 / 市 / 政 / 府 / 捷 / 運 / 站」?Elasticsearch 預設的 standard analyzer 選了後者——逐字切,簡單粗暴,但搜尋品質慘不忍睹。

要讓搜尋引擎真正讀懂中文,得先搞懂 Analyzer 這條流水線。


洗衣店的三道工序

Analyzer 的運作像一家洗衣店。髒衣服進來,經過三道工序,變成乾淨的衣服掛上架。文字進來,經過三道處理,變成可搜尋的 token 存入倒排索引。

第一道:Char Filter(預處理)。衣服進門先掃一遍——有沒有口袋裡忘了掏的衛生紙、有沒有掛在上面的名牌。對文字來說,就是去掉 HTML 標籤、替換特殊字元、用正則做初步清理。可以有零到多個,依序執行。

第二道:Tokenizer(斷詞)。這是核心——把一整段文字切成一個個獨立的 token。每個 Analyzer 只能有一個 Tokenizer,它決定了切割的邏輯。Standard tokenizer 依 Unicode 規則切;whitespace 只認空格;keyword 完全不切(整串當一個 token)。中文搜尋的戰場就在這裡。

第三道:Token Filter(後處理)。切完之後的精修——轉小寫、移除停用詞(the、is、a 這些高頻但沒意義的詞)、詞幹還原(running → run)、同義詞擴展。可以有零到多個,依序執行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
原始文字: "<p>I LOVE Elasticsearch!!!</p>"

① Char Filter → 去 HTML 標籤

"I LOVE Elasticsearch!!!"

② Tokenizer → 依空格/標點切割

["I", "LOVE", "Elasticsearch"]

③ Token Filter → 轉小寫 + 去停用詞

["love", "elasticsearch"]

寫入 Inverted Index ✅

三道工序的組合就是一個 Analyzer。換不同的 Tokenizer、加不同的 Filter,搜尋的行為就完全不同。


內建的幾套「洗衣套餐」

Elasticsearch 預裝了幾個 Analyzer,不用設定就能用:

standard(預設值):用 Unicode 規則斷詞,加 lowercase filter。英文表現不錯,中文就是逐字切——「小籠包」變成 [“小”, “籠”, “包”]。90% 的情境先用這個看看效果。

simple:只留字母,數字會被直接吃掉。適合純文字內容,但處理含數字的技術文件會漏東西。

whitespace:只認空格,其他完全不管。”Hello, World!” 會切成 [“Hello,”, “World!”],連標點都保留。

keyword:完全不切。整個字串就是一個 token。適合 email、URL 這種不該被拆開的東西。

怎麼知道文字被切成什麼樣子?用 _analyze API 直接測:

1
2
3
4
5
6
POST _analyze
{
"analyzer": "standard",
"text": "台北市政府捷運站"
}
// 結果: ["台", "北", "市", "政", "府", "捷", "運", "站"]

八個沒有語義的單字。搜「台北」能命中,但搜「站」也會命中所有含「站」的文件。這就是為什麼中文需要專門的分詞器。


自己組裝一套 Analyzer

內建的不夠用?可以在建立 Index 的時候,從 Char Filter、Tokenizer、Token Filter 三個層級自己挑零件組裝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
PUT /my_custom_index
{
"settings": {
"analysis": {
"char_filter": {
"emoticon_filter": {
"type": "mapping",
"mappings": [":) => _happy_", ":( => _sad_"]
}
},
"filter": {
"my_synonyms": {
"type": "synonym",
"synonyms": ["小籠包,湯包,soup dumpling"]
}
},
"analyzer": {
"my_analyzer": {
"type": "custom",
"char_filter": ["emoticon_filter"],
"tokenizer": "standard",
"filter": ["lowercase", "my_synonyms"]
}
}
}
}
}

有個進階技巧值得一提:寫入和搜尋可以用不同的 Analyzer。寫入的時候用 ik_max_word 盡量多切(召回率高),搜尋的時候用 ik_smart 精準切(精確度高)。設定方式是在 Mapping 裡同時指定 analyzer(寫入用)和 search_analyzer(搜尋用)。

這就像圖書館的索引卡——建索引的時候多列幾種關鍵字,查的時候只用最精確的那個。


IK:中文搜尋的事實標準

IK Analyzer 是 Elasticsearch 中文分詞的老牌方案,生態最成熟、社群最活躍。它提供兩種模式:

ik_smart(智慧切):盡量切出最少、最大的詞組,不重疊。「台北市政府捷運站」→ [“台北市”, “政府”, “捷運站”]。適合搜尋端——你搜「台北市」就是要找台北市,不想被「台北」「北市」干擾。

ik_max_word(最細切):窮舉所有可能的詞組,會重疊。同一句話 → [“台北市”, “台北”, “北市”, “政府”, “捷運站”, “捷運”, “站”]。適合索引端——多切幾種,使用者不管輸入「台北」還是「台北市」都能命中。

安裝要注意一件事:IK 的版本必須跟 Elasticsearch 完全一致。ES 8.17.0 配 IK 8.16.0?啟動直接失敗,錯誤訊息還不明顯,弄了半天才發現是版本不對。踩過這坑的人都記得。

1
2
3
4
5
6
# Docker 環境安裝
docker exec -it es01 bash
./bin/elasticsearch-plugin install \
https://get.infini.cloud/elasticsearch/analysis-ik/8.17.0
exit
docker restart es01

實戰設定:寫入 ik_max_word、搜尋 ik_smart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
PUT /menu_zh
{
"settings": {
"analysis": {
"analyzer": {
"ik_index": {
"type": "custom",
"tokenizer": "ik_max_word",
"filter": ["lowercase"]
},
"ik_search": {
"type": "custom",
"tokenizer": "ik_smart",
"filter": ["lowercase"]
}
}
}
},
"mappings": {
"properties": {
"dish_name": {
"type": "text",
"analyzer": "ik_index",
"search_analyzer": "ik_search"
},
"description": {
"type": "text",
"analyzer": "ik_index",
"search_analyzer": "ik_search"
},
"category": { "type": "keyword" },
"price": { "type": "integer" }
}
}
}

寫入測試資料、搜尋「小籠包」——IK 會精準命中包含這個詞組的文件,而不是把所有含「小」或「包」的東西都撈出來。


自定詞典:讓分詞器認識你的詞

IK 的內建詞典涵蓋了大部分常見中文詞彙,但你的業務用語它不一定認得。例如「鼎泰豐」如果不在詞典裡,會被切成 [“鼎”, “泰”, “豐”]——三個獨立的字,搜尋體驗直接崩壞。

解法是建立自定詞典:

1
2
3
4
5
# /config/analysis-ik/custom.dic
# 每行一個詞
鼎泰豐
小籠包
絲瓜蝦仁

修改 IKAnalyzer.cfg.xml 加入 <entry key="ext_dict">custom.dic</entry>,重啟 ES 生效。

每次加新詞都要重啟太痛苦?IK 支援遠端詞典熱更新——指向一個 HTTP URL,IK 每 60 秒檢查 Last-Modified / ETag,有變化就自動重載。不用重啟。

但有一個坑:熱更新只影響新寫入的文件。舊文件的倒排索引不會自動重建。要讓新詞對全量資料生效,得跑一次 _update_by_query_reindex


其他中文分詞方案

IK 不是唯一選擇,但對大多數場景來說是最穩的起手式。

jieba(結巴):Python 生態的主流分詞器,基於 HMM/Viterbi 演算法。如果你的專案已經大量用 Python 做 NLP,jieba 的整合性比 IK 好。

HanLP:功能最強大——命名實體辨識、句法分析、情感分析都包。適合企業級 NLP 應用,但設定複雜度也最高。

NGram / Edge NGram:暴力切割法,不依賴詞典。「elastic」→ [“e”, “el”, “ela”, “elas”, “elast”, “elasti”, “elastic”]。跟語言無關,專門用來做 autocomplete(自動完成)。

ICU Analyzer:基於 Unicode 標準的分割器,多語系混合內容的首選。如果你的資料裡同時有中文、日文、韓文、英文,ICU 比 IK 更適合當底層。


五個一定會踩的坑

版本不匹配:IK 版本跟 ES 差一個 minor version,啟動直接失敗。錯誤訊息不明顯,可能 debug 半天。永遠用完全一致的版本號。

寫入搜尋用同一個 analyzer:兩邊都用 ik_max_word,搜「台北」命中「台北市」、「台北區」、「台北車站」⋯⋯結果太廣。正確做法:寫入 ik_max_word、搜尋 ik_smart。

忘記繁簡體轉換:使用者輸入簡體但資料是繁體(或反過來)。需要在 Token Filter 加繁簡轉換(stconvert plugin),或在應用層統一。

自定詞典太大:超過 100MB 時 IK 載入時間明顯增加,拖慢 ES 啟動。控制詞典大小,或改用遠端詞典 + 熱更新。

更新詞典後舊資料搜不到:熱更新只影響新寫入。全量生效要 reindex。很多人忘了這一步,以為詞典更新就全自動。


Analyzer 的設計哲學其實就是一條流水線——原料進來(原始文字)、經過標準化的工序(Char Filter → Tokenizer → Token Filter)、產出標準化的成品(token)。

工序的組合決定了搜尋的品質。英文的工序簡單,空格切完、轉小寫、去停用詞就差不多了。中文的工序複雜,因為語言本身沒有天然分隔符,你得教機器怎麼「讀」。

IK 是目前最成熟的教法。但不管用什麼分詞器,最重要的 debug 工具就是 _analyze API——在設定 Mapping 之前,先拿你的實際資料測一輪,看看切出來的 token 是不是你預期的。搜尋品質的問題,八成出在 Analyzer 設定上。

參考來源:Elasticsearch 官方文件 — Text Analysis
參考來源:IK Analysis Plugin for Elasticsearch