跟著這篇走完,你會在終端把 Meilisearch 的中文搜索跑通:看懂它到底怎麼把一句中文切成詞,再用 dictionary、分隔符設置、localizedAttributes 和 locales 四種手段,把「搜不到」「搜不準」的中文查詢修回來。整個過程 7 步,每步都有一條命令能驗證自己做對了沒有。
動手前先交代環境。localhost:7700 是 Meilisearch 的預設監聽地址,master key 固定用 aSampleMasterKey(與系列一致),索引固定用 books_zh(本篇新建,不動前兩篇的 movies 和 books),示例文檔是 6 條虛構中文書籍;你要是換了端口、master key 或索引名,把示例值替換掉就行。這篇預設你已經裝好並跑通了從零跑通 Meilisearch 本地搜索服務,安裝、啟動、異步任務輪詢的套路全部複用第一篇,沒跑起來先回去把服務起了再回來。
有一點得先說清楚,免得你對錯號:中文分詞在 v1.54.3 全新安裝下是開箱可用的——Charabia 內置 jieba,自動語言檢測預設開著,直接搜「三體」就能命中。所以這篇不是「教你讓中文能搜」,而是講清它怎麼切詞、搜不到時怎麼定位和修。後面第六節那個「必須重建索引」的坑,只對從 v1.15 之前升級上來、庫裡本來就有中文的讀者成立,全新裝的庫不會遇到。

前置條件
| 項 | 要求 | 說明 |
|---|---|---|
| Meilisearch | v1.54.3 或更高 | 2026-10-01 發佈的當前 stable,與系列一致;低於此版本中文分詞行為可能有差異 |
| 前置文章 | 已跑通 meilisearch-local-search |
本文複用其安裝/啟動/master key/異步任務輪詢套路;沒跑通先回去補 |
| curl | 任意版本 | 全文用 curl 調 REST API;jq 可選,用於美化輸出 |
| 資源 | 2 GB 記憶體、1 GB 磁盤空閒 | 與系列一致,本篇 books_zh 數據集極小,無額外負擔 |
先跑這條,確認第一篇的服務還在。
curl http://localhost:7700/health
{"status":"available"}
返回 {"status":"available"} 就滿足條件。要是返回 curl: (7) Failed to connect to localhost port 7700,說明服務沒起——回第一篇第一步用 meilisearch --master-key="aSampleMasterKey" 重新啟動,再回來。
第一步:創建中文索引 books_zh(不動前兩篇的 movies/books)
本篇換一套獨立數據集 books_zh,跟前兩篇的 movies、books 互不干擾。索引走顯式創建,直接把主鍵字段指定為 id——自動推斷失敗會報 index_primary_key_no_candidate_found,顯式聲明最穩。
curl -X POST http://localhost:7700/indexes \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
-d '{"uid":"books_zh","primaryKey":"id"}'
{
"taskUid": 0,
"indexUid": "books_zh",
"status": "enqueued",
"type": "indexCreation"
}
創建索引是異步任務,返回體裡的 taskUid 就是這次任務的編號,後面靠它查進度。全新環境任務號從 0 開始;要是你的環境已經跑過其他任務,以實際返回的 taskUid 為準。
驗證一下。
curl http://localhost:7700/tasks/0 -H "Authorization: Bearer aSampleMasterKey"
{
"uid": 0,
"indexUid": "books_zh",
"status": "succeeded",
"type": "indexCreation",
"details": { "primaryKey": "id" }
}
返回裡 status 是 succeeded、type 是 indexCreation,details.primaryKey 是 id,這一步就成了。萬一任務 failed 且錯誤裡帶 primary_key,多半是主鍵推斷出了問題,排查入口見Meilisearch primary key 自動推斷失敗;返回 401 則是 auth 頭沒帶或 master key 寫錯,見Meilisearch production 模式不設 master key 拒絕啟動。
第二步:灌一批「故意能踩到坑」的中文文檔
往 books_zh 寫 6 條虛構中文書籍。字段是刻意設計的,別亂改——後面幾步的驗證結果都依賴它們:title 裡既有短中文詞(三体、平凡的世界),也有帶符號的編程書名(C++、C#);publisher 裡有複合專名「未來科技出版社」。
curl -X POST http://localhost:7700/indexes/books_zh/documents \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
--data-binary '[
{"id": 1, "title": "三体", "author": "刘慈欣", "category": "科幻", "publisher": "重庆出版社"},
{"id": 2, "title": "平凡的世界", "author": "路遥", "category": "文学", "publisher": "北京十月文艺出版社"},
{"id": 3, "title": "C++ 编程入门", "author": "张伟", "category": "编程", "publisher": "机械工业出版社"},
{"id": 4, "title": "C# 高级编程", "author": "李娜", "category": "编程", "publisher": "人民邮电出版社"},
{"id": 5, "title": "冰激凌与夏天", "author": "林小满", "category": "美食", "publisher": "未来科技出版社"},
{"id": 6, "title": "未来科技简史", "author": "王强", "category": "科普", "publisher": "未来科技出版社"}
]'
{
"taskUid": 1,
"indexUid": "books_zh",
"status": "enqueued",
"type": "documentAdditionOrUpdate"
}
文檔添加同樣是異步任務,POST 返回的 taskUid 只表示入隊,文檔還沒被索引。等對應任務 succeeded 再往下走。
確認數據到位。
curl -X GET http://localhost:7700/indexes/books_zh/stats \
-H "Authorization: Bearer aSampleMasterKey"
{
"numberOfDocuments": 6,
"isIndexing": false,
"fieldDistribution": {
"author": 6, "category": 6, "id": 6, "publisher": 6, "title": 6
}
}
返回裡 numberOfDocuments 是 6、isIndexing 是 false,fieldDistribution 能看到 id/title/author/category/publisher 五個字段,說明文檔全部索引完。要是 numberOfDocuments 是 0 或 isIndexing 還是 true,多半是任務沒等完,見Meilisearch 添加文檔後立即搜索返回空結果。
第三步:驗證預設中文分詞已生效(建立基線)
先看一眼預設的三個分詞設置,確認它們都還空著,再直接搜一個短中文詞。
# 3.1 看默认分词设置(dictionary / separatorTokens / nonSeparatorTokens 都应为 [])
curl http://localhost:7700/indexes/books_zh/settings \
-H "Authorization: Bearer aSampleMasterKey"
# 3.2 直接搜中文,验证开箱可用
curl -X POST http://localhost:7700/indexes/books_zh/search \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
-d '{"q":"三体"}'
GET /settings 返回裡跟分詞相關的幾個字段(其餘字段省略):
{
"rankingRules": ["words", "typo", "proximity", "attributeRank", "sort", "wordPosition", "exactness"],
"nonSeparatorTokens": [],
"separatorTokens": [],
"dictionary": [],
"localizedAttributes": null,
"prefixSearch": "indexingTime"
}
三個分詞設置都是空陣列,localizedAttributes 是 null(注意不是空陣列),說明當前用的全是預設詞邊界。
搜「三體」:
{
"hits": [
{"id": 1, "title": "三体", "_matchesPosition": {"title": [{"start": 0, "length": 3}, {"start": 3, "length": 3}]}}
],
"estimatedTotalHits": 1
}
中,命中了。這裡要停下來看一眼 _matchesPosition:它是命中位置,start 和 length 按 UTF-8 字節算(一個漢字 3 字節)。「三體」被切成兩段,每段 length 都是 3,說明它在索引裡被拆成了兩個單字 token「三」「體」——jieba 詞表裡沒有「三體」這個新詞,就退化成單字了。
再搜一個詞對比一下:
curl -X POST http://localhost:7700/indexes/books_zh/search \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
-d '{"q":"冰激凌"}'
{
"hits": [
{"id": 5, "title": "冰激凌与夏天", "_matchesPosition": {"title": [{"start": 0, "length": 9}]}}
],
"estimatedTotalHits": 1
}
這次只有一段、length 是 9(三個字),說明「冰激凌」在 jieba 詞表裡是一個整詞。
兩條放一起就懂了:Meilisearch 用開源 Rust 庫 Charabia 做多語言分詞,它分兩步——先按書寫系統(script,比如漢字、拉丁字母)把字段切成段,再對每段跑對應的分詞 pipeline。中文走的是專用 pipeline,具體是 jieba 字典分詞,另外還做 decomposition 和 kvariant(異體字)歸一化。這一整套預設開啟、零設定,所以中文開箱能搜;但切多細,取決於 jieba 詞表——詞表裡沒有的詞會被切碎。
順便埋個錨點,它貫穿全文:查詢和文檔必須用同一種方式分詞,否則再正確的詞也可能命中不了。
驗證命令就是上面 3.2 那條。期望輸出:三個分詞設置為空陣列;搜「三體」命中 id 1。要是搜「三體」完全沒結果,先確認第二步的文檔任務已 succeeded;如果你確實是從 v1.15 之前升級上來、庫裡早有中文,看第六節。
第四步:用 dictionary 把專名鎖成整詞(改分詞粒度)
第三步結尾我們知道了根結所在:Meilisearch 用 jieba 按字典切中文,詞典裡沒有的詞會被切碎——「三體」就是這樣被拆成「三」「體」兩個單字的。可「三體」已經是書名了,領域裡的專名、人名、複合詞(未来科技、重庆出版社)不可能都收進 jieba 的詞表。每次遇到新詞都要被切碎,搜索時查詢和文檔的片段邊界就對不齊,表現出來就是「明明有這幾個字卻搜不到、或排到很後面」。這就是第四步要解決的事,先用預設切法看一個複合詞「未來科技」被切成什麼樣。
curl -X POST http://localhost:7700/indexes/books_zh/search \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
-d '{"q":"未来科技"}'
{
"hits": [
{"id": 5, "title": "冰激凌与夏天", "_matchesPosition": {"publisher": [{"start": 0, "length": 6}, {"start": 6, "length": 6}]}},
{"id": 6, "title": "未来科技简史", "_matchesPosition": {"publisher": [{"start": 0, "length": 6}, {"start": 6, "length": 6}], "title": [{"start": 0, "length": 6}, {"start": 6, "length": 6}]}}
],
"estimatedTotalHits": 2
}
命中 id 5、id 6 兩條,publisher 裡都是兩段、每段 6 字節——也就是「未來科技」被切成了「未來」「科技」兩個 token。
現在把它鎖成一個整詞。
curl -X PUT http://localhost:7700/indexes/books_zh/settings/dictionary \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
--data-binary '["未来科技"]'
{
"taskUid": 2,
"indexUid": "books_zh",
"status": "enqueued",
"type": "settingsUpdate"
}
dictionary 的作用是把指定字符串強制解析成一個 term,不改詞邊界,供匹配和近鄰排名使用。官方把它定位成「數據集含大量領域專有詞、或不靠空白分詞的語言時尤其有用」,官方示例裡還放人名("J. R. R. Tolkien")。中文的情況就是:jieba 按字典切詞,詞表裡沒有的專名、新詞會被切成更小的片段,查詢和文檔的片段邊界一旦對不齊,就會表現為「明明文檔裡有這幾個字卻搜不到,或者排到很後面」。把整詞寫進 dictionary 能把它固定成一個 term。
dictionary 是索引側設置,PUT 是異步 settingsUpdate 任務、會重索引全部文檔,必須等 succeeded 再驗證。
等任務成功後,回讀設置、再搜一次:
curl http://localhost:7700/tasks/2 -H "Authorization: Bearer aSampleMasterKey"
curl http://localhost:7700/indexes/books_zh/settings/dictionary -H "Authorization: Bearer aSampleMasterKey"
回讀是 ["未来科技"]。再搜「未來科技」:
{
"hits": [
{"id": 5, "title": "冰激凌与夏天", "_matchesPosition": {"publisher": [{"start": 0, "length": 12}]}},
{"id": 6, "title": "未来科技简史", "_matchesPosition": {"publisher": [{"start": 0, "length": 12}], "title": [{"start": 0, "length": 12}]}}
],
"estimatedTotalHits": 2
}
注意這裡:命中還是兩條,沒變,變的是 publisher 從「兩段各 6 字節」變成「一段 12 字節」——「未來科技」現在是一個整詞了。所以我得誠實說一句:dictionary 改變的是分詞粒度,進而影響 proximity(近鄰)和 exactness(精確度)這類排名打分,它不負責把 0 命中變成命中。別把它當成「搜不到的解藥」,那個場景看下一節。
驗證命令:任務 succeeded、type 是 settingsUpdate,GET /settings/dictionary 回讀 ["未来科技"]。要是改完 dictionary 搜索行為完全沒變化,多半是任務沒等完(異步 + 重索引),見Meilisearch 分詞設置改完不生效。
第五步:用 separatorTokens / nonSeparatorTokens 定製詞邊界(兩個真實案例)
第四步我們學到一個重要邊界:dictionary 只能改「分詞粒度」,它救不了「搜不到」。可這裡還有個更微妙的層級坑——dictionary 連 C++、C# 這種帶符號的詞都鎖不了。為什麼?因為 +、# 是預設分隔符,C++/C# 在 token 化時被拆成了更小的片段(C),這時候 dictionary 壓根拿不到完整的 C++ 字符串,自然無從鎖起。換句話說:dictionary 是在「詞已經切好之後」鎖粒度,管不了「邊界把詞切開」這一步。邊界歸邊界管——這就是這對設置登場的原因。它倆是一對反向操作:separatorTokens 在預設分隔符之外新增詞邊界字符(預設空陣列),nonSeparatorTokens 把字符從預設分隔符集合裡移除(預設空陣列)。一個用來「讓本該斷開的斷開」,一個用來「讓本不該斷的不斷」。
先看後者的真實場景。《C++ 編程入門》和《C# 高級編程》兩本書都在庫裡,直接搜 C# 會發生什麼:
curl -X POST http://localhost:7700/indexes/books_zh/search \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
-d '{"q":"C#"}'
{
"hits": [
{"id": 3, "title": "C++ 编程入门", "_matchesPosition": {"title": [{"start": 0, "length": 1}]}},
{"id": 4, "title": "C# 高级编程", "_matchesPosition": {"title": [{"start": 0, "length": 1}]}}
],
"estimatedTotalHits": 2
}
兩本都被命中,length 還都只有 1——說明 + 和 # 被當成了分隔符,C++ 和 C# 都被切成了單個字符 C,你搜 C# 時它其實只匹配到那個 C。這就是典型的「搜不準」。
把這兩個符號聲明成「詞的一部分」:
curl -X PUT http://localhost:7700/indexes/books_zh/settings/non-separator-tokens \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
--data-binary '["+","#"]'
{
"taskUid": 3,
"indexUid": "books_zh",
"status": "enqueued",
"type": "settingsUpdate"
}
等 task 3 succeeded 後再搜:
# 搜 C#
curl -X POST http://localhost:7700/indexes/books_zh/search \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
-d '{"q":"C#"}'
# 搜 C++
curl -X POST http://localhost:7700/indexes/books_zh/search \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
-d '{"q":"C++"}'
# C# → {"hits":[{"id":4,"title":"C# 高级编程","_matchesPosition":{"title":[{"start":0,"length":2}]}}],"estimatedTotalHits":1}
# C++ → {"hits":[{"id":3,"title":"C++ 编程入门","_matchesPosition":{"title":[{"start":0,"length":3}]}}],"estimatedTotalHits":1}
C# 現在整詞命中 id 4(length 2),C++ 整詞命中 id 3(length 3),互不串門了。
再看「新增分隔符」的場景。假設有個字段存的是豎線連接的標籤,先加一條文檔:
curl -X POST http://localhost:7700/indexes/books_zh/documents \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
--data-binary '[{"id":7,"title":"色彩手册","keywords":"red|green|blue"}]'
{
"taskUid": 4,
"indexUid": "books_zh",
"status": "enqueued",
"type": "documentAdditionOrUpdate"
}
等 task 4 succeeded 後搜 green:
curl -X POST http://localhost:7700/indexes/books_zh/search \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
-d '{"q":"green"}'
{
"hits": [],
"query": "green",
"estimatedTotalHits": 0
}
0 命中。因為 | 不是預設分隔符,red|green|blue 在索引裡是一個 token,任何一小段都匹配不到。把它聲明成分隔符:
curl -X PUT http://localhost:7700/indexes/books_zh/settings/separator-tokens \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
--data-binary '["|"]'
{
"taskUid": 5,
"indexUid": "books_zh",
"status": "enqueued",
"type": "settingsUpdate"
}
等 task 5 succeeded 後再搜:
{
"hits": [
{"id": 7, "title": "色彩手册", "keywords": "red|green|blue", "_matchesPosition": {"keywords": [{"start": 4, "length": 5}]}}
],
"estimatedTotalHits": 1
}
命中 id 7,green 從第 4 字節起、佔 5 字節(前面的 red| 正好 4 字節)。這才叫「搜到了」。
關於預設分隔符再補兩句。預設分隔符分軟分隔符和硬分隔符兩類,它們在文檔裡佔的位置距離不同,直接影響 proximity(近鄰)排名。官方 known limitations 裡有個好記的例子:搜 Hello World 時 Hello 在位置 0、World 在位置 1;換成 Hello, World,逗號佔的位置是 8。所以定製分隔符不只影響「能不能搜到」,也會改變排序。完整字符清單這裡不展開,需要時查 Charabia 源碼或官方文檔。
要分清 dictionary 和這兩個設置的語義:dictionary 是「鎖整詞、不改邊界」,這兩個是「改邊界」。
驗證命令:GET /settings/non-separator-tokens 回讀 ["#","+"]、GET /settings/separator-tokens 回讀 ["|"](注意回讀順序可能被規範化);搜 C# 只命中 id 4、搜 green 命中 id 7。改完設置沒變化同樣是任務沒等完,見Meilisearch 分詞設置改完不生效。
第六步:用 localizedAttributes 聲明字段語言(會觸發重索引)
第四、五步改的都是「怎麼切」——詞鎖多整、哪裡斷開。可還有個決定中文切得準不準的因素沒碰:用哪套語言的切法。例子就在前面——「三體」裡那一串漢字,到底是按中文 jieba 切,還是按日文假名規則切,結果完全不一樣。而 Meilisearch 預設靠自動檢測猜語言,長文檔判得準,但短片段、邊輸邊搜(比如邊打字邊搜的中文輸入框)經常判錯,中日這種同屬 CJK、判定更是極易混淆。猜錯了語言,前面調好的詞粒度全白搭。第六步就是把「這些字段是哪種語言」白紙黑字寫死,不再靠猜。執行方式是給按語言命名的字段聲明語言;本數據集字段名沒帶語言後綴,這裡演示的是 attributePatterns 的通配結構;真實的多語言數據集裡,字段通常命名成 title_zh、title_ja 這類,正好拿來匹配。
curl -X PUT http://localhost:7700/indexes/books_zh/settings/localized-attributes \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
--data-binary '[{"locales":["cmn"],"attributePatterns":["*_zh"]},{"locales":["jpn"],"attributePatterns":["*_ja"]}]'
{
"taskUid": 6,
"indexUid": "books_zh",
"status": "enqueued",
"type": "settingsUpdate"
}
先看預設值。全新索引裡 GET /settings/localized-attributes 返回的是 null,不是空陣列——這點容易記錯。
localizedAttributes 是索引側設置,結構是陣列,每項形如 {"locales":["jpn"],"attributePatterns":["*_ja"]}。attributePatterns 支持 * 通配(["title_*","description"] 能匹配 title_en、title_fr 和 description),locales 用 ISO-639 值(中文可以用 zh、zho 或 cmn)。官方說明它會影響 searchable、filterable、sortable 這幾類屬性。
這裡有個坑必須講清楚。官方在 v1.15 的更新裡改進了中文分詞,並要求含中文的數據集在升級後重建索引(reindex),否則中文查詢和文檔不會被正確處理、可能在搜索時被忽略。觸發重建的方式包括把中文 locale 加進 localizedAttributes、改動 stop words 或 dictionary、刷新 searchable attributes。本篇是全新索引,本步的 PUT 本身就是一次正常重索引,不會踩這個坑——這個坑只對從 v1.15 之前升級上來、庫裡已有中文的讀者成立。
多語言組織上還有一條建議:不同語言推薦各自獨立建索引,中日不要混在同一個字段裡,跨語言搜索用 federated/multi-search。
驗證命令(輪詢重索引任務 + 回讀設置):
curl http://localhost:7700/tasks/6 -H "Authorization: Bearer aSampleMasterKey"
curl http://localhost:7700/indexes/books_zh/settings/localized-attributes -H "Authorization: Bearer aSampleMasterKey"
回讀(鍵序會被規範化):
[
{"attributePatterns": ["*_zh"], "locales": ["cmn"]},
{"attributePatterns": ["*_ja"], "locales": ["jpn"]}
]
期望輸出:任務 type 是 settingsUpdate、status 是 succeeded(本步會重索引),回讀是上面的陣列。要是中文查詢在已有中文的老庫上完全搜不到、像被忽略,見Meilisearch 中文搜不到:升級後必須重建索引。
第七步:用 locales 查詢參數覆蓋自動檢測
第六步把語言寫死在了索引設置裡——一旦配好,整個庫就用固定語言切。但這有個不靈活的地方:索引側聲明是靜態、一次就全局生效,前端 UI 要切語言(中文 ↔ 日文)、或某一次短查詢恰好被自動檢測判錯時,不可能為改一次查詢去重改索引設置再等重索引。所以需要一種只在單次請求裡生效、能臨時覆蓋的開關——這就是 7 步裡收尾的 locales 查詢參數。這一步讓單次請求顯式說明查詢語言,再故意傳一個非法值看報錯。
# 7.1 显式声明查询语言为 cmn
curl -X POST http://localhost:7700/indexes/books_zh/search \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
-d '{"q":"三体","locales":["cmn"]}'
# 7.2 传入非法 locale 值,观察报错
curl -X POST http://localhost:7700/indexes/books_zh/search \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
-d '{"q":"三体","locales":["chinese"]}'
7.1 正常命中:
{
"hits": [{"id": 1, "title": "三体"}],
"estimatedTotalHits": 1
}
7.2 返回 400:
{
"message": "Unknown value `chinese` at `.locales[0]`: expected one of `af`, `ak`, `am`, `ar`, `az`, `be`, `bn`, `bg`, `ca`, `cs`, `da`, `de`, `cy`, `el`, `en`, `eo`, `et`, `fi`, `fr`, `gu`, `he`, `hi`, `hr`, `hu`, `hy`, `id`, `it`, `jv`, `ja`, `kn`, `ka`, `km`, `ko`, `la`, `lv`, `lt`, `ml`, `mr`, `mk`, `my`, `ne`, `nl`, `nb`, `or`, `pa`, `fa`, `pl`, `pt`, `ro`, `ru`, `si`, `sk`, `sl`, `sn`, `es`, `sr`, `sv`, `ta`, `te`, `tl`, `th`, `tk`, `tr`, `uk`, `ur`, `uz`, `vi`, `yi`, `zh`, `zu`, `afr`, ... `zho`, `zul`, `cmn`",
"code": "invalid_search_locales",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid_search_locales"
}
(message 裡的語言清單很長,這裡截斷了中間部分,但報錯結構和 code 是原樣。)
locales 是查詢側參數(類型是 enum<string>[] | null,不傳就走自動檢測),顯式指定這次 query 的語言,覆蓋自動檢測;取值同樣是 ISO-639,中文的合法值只有 zh、zho、cmn——不是 cn,也不是 chinese,所以 7.2 才會報 invalid_search_locales。
優先級是這裡的關鍵行為:查詢側的 locales 優先於索引側的 localizedAttributes。官方原文是:如果 locales 搜索參數和 localizedAttributes 索引設置衝突,locales 勝出。好處是 UI 切語言時很靈活,代價是寫錯的 locales 會覆蓋你精心調好的索引設置。非法值統一返回 {message, code, type, link} 四個字段,type 是 invalid_request。
驗證命令就是 7.1 和 7.2 兩條。期望輸出:7.1 命中 id 1,7.2 返回 400、code 是 invalid_search_locales。要是搜中文明明詞沒錯卻時好時壞,多半是查詢和文檔分詞方式不一致(自動檢測對短查詢判錯),顯式加 locales 收窄;傳了非法值報錯,見Meilisearch search locales 參數報 invalid_search_locales。
核心概念
這張圖是整條調用鏈路的走向,從左到右是一次請求的完整路徑:curl 發出設置/搜索請求,經過 API 層進入 Meilisearch Server,Server 內的 Charabia 分詞器把中文切詞後交給索引 books_zh,再把 hits 或 settingsUpdate 任務返回給客戶端。

| 概念 | 是什麼 | 類比 |
|---|---|---|
| Charabia | Meilisearch 用的開源 Rust 多語言分詞庫,先按書寫系統切段、再逐段跑語言 pipeline | 像流水線上的分揀加加工:先按語種分揀,再各用各的刀法切 |
| 書寫系統切分(script) | 把字段按「漢字/拉丁/假名」切成段,決定每段走哪套分詞管道 | 像先把混裝郵件按國家分堆,再交給對應語言的分揀員 |
| jieba 字典分詞 | 中文(CMN)的專用分詞方式:按 jieba 詞典切詞,配 decomposition 和異體字歸一化 | 像查詞典斷句,詞典裡沒有的新詞只能被拆成認識的字 |
| separatorTokens | 在預設分隔符基礎上新增的詞邊界字符,預設 [] |
像給斷句規則再加一個「見到它就斷開」的記號 |
| nonSeparatorTokens | 從預設分隔符集合裡移除的字符(使 C++/C# 成整詞),預設 [] |
像聲明「這個符號是詞的一部分,別在這斷」 |
| dictionary | 把指定字符串強制解析成單一 term(不改邊界),預設 [] |
像給詞典補錄一個專有名詞,讓整詞被認出 |
| localizedAttributes | 索引側按字段聲明語言的設置(locales + attributePatterns 通配),預設 null,影響 searchable/filterable/sortable |
像給每個字段貼語言標籤,告訴分詞器該用哪把刀 |
| locales(查詢參數) | 查詢側顯式指定 query 語言,覆蓋自動檢測,且優先於 localizedAttributes | 像查詞時手動選「中文詞典」,而不是讓系統猜 |
常見問題
改了分詞設置,搜索結果卻一點沒變
現象:PUT 了 dictionary、separatorTokens、nonSeparatorTokens 或 localizedAttributes 之後立刻搜索,行為跟改之前一模一樣。
原因:這些都是索引側設置,PUT 返回的只是 taskUid,實際是一次異步 settingsUpdate 任務、並會重索引全部文檔;任務沒 succeeded 就搜索,用的還是舊設定。
invalid_search_locales:locales 傳了非法值
現象:搜索請求 body 裡帶 "locales":["chinese"](或 "cn")返回 400,錯誤碼 invalid_search_locales。
原因:locales 只接受官方 ISO-639 取值(中文是 zh/zho/cmn,不是 cn/chinese);另外 locales 優先級高於索引側的 localizedAttributes,寫錯的 locales 會覆蓋你調好的索引設置。
中文查詢在已有中文的老庫上完全搜不到、像被忽略
現象:從 v1.15 之前的版本升級上來,庫裡本來就有中文文檔,升級後中文查詢和文檔卻像沒被索引一樣,搜不到。
原因:v1.15 改進了 Charabia 的中文分詞,官方要求含中文的數據集必須重建索引(reindex),否則中文查詢和文檔不會被正確處理、可能在搜索時被忽略。觸發重建的方式包括把 cmn 加入 localizedAttributes、改動 stop words 或 dictionary、或刷新 searchable attributes。
相關文章:Meilisearch 中文搜不到:升級後必須重建索引
前提說明:這條只對「從 v1.15 之前升級上來且庫裡已有中文」的讀者成立——本篇從零新建的索引天然是新分詞,不會遇到。
