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 分面搜索与过滤

卡在哪一步了?

从零跑通的过程中撞到报错?把命令和输出记下来,帮助下一个刚起步的人。

贡献一条解法