StackTraces
Meilisearch從零跑通

從零跑通 Meilisearch 中文搜索:分詞與本地化 7 步,外加 3 個容易卡住的地方

@debug_master2026-10-05

跟著這篇走完,你會在終端把 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:從零跑通中文搜索的 7 個步驟

前置條件

項 要求 說明
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 任務返回給客戶端。

Meilisearch 中文分詞與本地化調用鏈路架構圖

概念 是什麼 類比
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 就搜索,用的還是舊設定。

相關文章:Meilisearch 分詞設置改完不生效

invalid_search_locales:locales 傳了非法值

現象:搜索請求 body 裡帶 "locales":["chinese"](或 "cn")返回 400,錯誤碼 invalid_search_locales。

原因:locales 只接受官方 ISO-639 取值(中文是 zh/zho/cmn,不是 cn/chinese);另外 locales 優先級高於索引側的 localizedAttributes,寫錯的 locales 會覆蓋你調好的索引設置。

相關文章:Meilisearch search locales 參數報 invalid_search_locales

中文查詢在已有中文的老庫上完全搜不到、像被忽略

現象:從 v1.15 之前的版本升級上來,庫裡本來就有中文文檔,升級後中文查詢和文檔卻像沒被索引一樣,搜不到。

原因:v1.15 改進了 Charabia 的中文分詞,官方要求含中文的數據集必須重建索引(reindex),否則中文查詢和文檔不會被正確處理、可能在搜索時被忽略。觸發重建的方式包括把 cmn 加入 localizedAttributes、改動 stop words 或 dictionary、或刷新 searchable attributes。

相關文章:Meilisearch 中文搜不到:升級後必須重建索引

前提說明:這條只對「從 v1.15 之前升級上來且庫裡已有中文」的讀者成立——本篇從零新建的索引天然是新分詞,不會遇到。

下一步

異步任務等待與輪詢

本篇第四步之後每一處設置變更都會觸發重索引、返回異步 task——任務沒 succeeded 就搜索,拿到的還是舊設定。把任務輪詢的套路單獨過一遍,後面所有寫操作都受益。

Meilisearch 添加文檔後立即搜索返回空結果

系列回顧:從零跑通 Meilisearch 本地搜索服務

本篇預設你已經裝好並跑通第一篇(安裝、啟動、建索引、導文檔、基礎搜索、異步任務)。沒跑過或想回顧 CRUD 操作,回去看系列第一篇。

從零跑通 Meilisearch 本地搜索服務

系列第二篇:分面搜索與過濾

中文搜索解決「能不能搜到、搜得準不準」;分面搜索解決「搜完怎麼按分類/價格/日期收窄」。想給中文數據集加可篩選的分面,接第二篇。

從零跑通 Meilisearch 分面搜索與過濾

卡在哪一步了?

從零跑通的過程中撞到報錯?把命令和輸出記下來,幫助下一個剛起步的人。

貢獻一條解法