跟着这篇走完,你会在本地把 Meilisearch 的拼写容错(typo tolerance)跑通:先看默认配置下拼错单词能不能搜到结果,再把容错阈值拧松拧紧观察命中变化,最后用 disableOnWords、enabled 把容错定向关掉,亲眼看到“拼写容错到底在什么时候帮了你、什么时候反而碍事”。整个过程 6 步,每步都有一条命令能验证自己做对了没有。localhost:7700 是 Meilisearch 的默认监听地址,master key 固定用 aSampleMasterKey,索引建新的 books_typo——你要是换了端口、key 或索引名,把示例值替换掉就行。这篇默认你已经装好并跑通了从零跑通 Meilisearch 本地搜索服务,安装、启动、异步任务轮询的套路全部复用第一篇。跟系列其他篇的区别说在前面:这一篇不碰向量、不碰过滤,只玩拼错能不能搜到这一件事,索引数据里我加了 title_en(英文书名)字段——中文按词典切出来的词多是 1–2 个字的短词,按容错规则几乎没有容错空间,用英文书名演示才看得清阈值变化。

前置条件
| 项 | 要求 | 说明 |
|---|---|---|
| Meilisearch | v1.54.3 或更高 | typo tolerance 是长期稳定特性;v1.45 起改设置不再触发全量重索引,行为与老版本有差异 |
| 前置文章 | 已跑通 meilisearch-local-search |
复用其安装/启动/master key/异步任务轮询套路;没跑通先回去补 |
| curl | 任意版本 | 全文用 curl 调 REST API |
先跑这条,确认第一篇的服务还活着。
curl http://localhost:7700/health
{
"status": "available"
}
返回 {"status":"available"} 就满足条件。要是返回 curl: (7) Failed to connect to localhost port 7700,说明服务没起——回第一篇第一步用 meilisearch --master-key="aSampleMasterKey" 重新启动,再回来。
第一步:建 books_typo 索引,传 6 本带英文书名的书
容错要发生在“已索引的词”和“查询词”之间,所以第一步先把索引和数据准备好。索引走显式创建,指定主键 id,沿用系列惯例:
curl -X POST http://localhost:7700/indexes \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
-d '{"uid":"books_typo","primaryKey":"id"}'
{
"taskUid": 50,
"indexUid": "books_typo",
"status": "enqueued",
"type": "indexCreation",
"enqueuedAt": "2026-10-13T10:00:00.000Z"
}
接着传 6 本书。字段有 id、title、author、summary,外加系列其他篇没有的 title_en(英文书名,后面所有拼错演示都打在它身上):
curl -X POST http://localhost:7700/indexes/books_typo/documents \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
--data-binary '[
{"id":1,"title":"三体","author":"刘慈欣","summary":"仰望星空,写人类文明与三体文明星际博弈的科幻巨作","title_en":"The Three-Body Problem"},
{"id":2,"title":"球状闪电","author":"刘慈欣","summary":"以球状闪电为引的科幻小说,物理奇观与人生选择","title_en":"Ball Lightning"},
{"id":3,"title":"活着","author":"余华","summary":"写普通人在苦难中活下去的故事,农村家庭的伦理与温情","title_en":"To Live"},
{"id":4,"title":"许三观卖血记","author":"余华","summary":"写小人物靠卖血养家的故事,底层生活的坚韧与荒诞","title_en":"Chronicle of a Blood Merchant"},
{"id":5,"title":"明朝那些事儿","author":"当年明月","summary":"用通俗笔法写明朝三百年历史,官场斗争与王朝兴衰","title_en":"The Ming Dynasty Story"},
{"id":6,"title":"万历十五年","author":"黄仁宇","summary":"从万历十五年切入,剖析明朝制度的运转与深层困境","title_en":"1587, A Year of No Significance"}
]'
{
"taskUid": 51,
"indexUid": "books_typo",
"status": "enqueued",
"type": "documentAdditionOrUpdate",
"enqueuedAt": "2026-10-13T10:01:00.000Z"
}
上传是异步的,任务跑完才落库。等 taskUid: 51 变成 succeeded:
curl http://localhost:7700/tasks/51 -H "Authorization: Bearer aSampleMasterKey"
{
"uid": 51,
"indexUid": "books_typo",
"status": "succeeded",
"type": "documentAdditionOrUpdate",
"details": {
"receivedDocuments": 6,
"indexedDocuments": 6
}
}
看到 indexedDocuments: 6 就说明 6 条都落库了。搜一个正确拼写确认基线——problem 是《三体》英文书名里 The Three-Body Problem 的词:
curl -X POST http://localhost:7700/indexes/books_typo/search \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
-d '{"q":"problem"}'
{
"hits": [
{
"id": 1,
"title": "三体",
"title_en": "The Three-Body Problem"
}
],
"query": "problem",
"processingTimeMs": 2,
"estimatedTotalHits": 1
}
命中《三体》就对了。这一步只是基线,接下来才看容错。
第二步:拼错一个字母,默认配置下照样搜到
现在关键问题来了:搜错字会怎样。拼写容错(typo tolerance)是 Meilisearch 默认开启的检索特性——查询词和索引里的词有少量出入时,仍然能匹配到正确的词条。它内部用 Damerau-Levenshtein 距离计算“错几个字母”,再按词的字符数给不同的容错预算(后面第三步讲分档)。注意它不会给你“你是不是想找 problem?“这类拼写建议(没有 did-you-mean),只是让错拼的结果也能搜到。
搜索 problam(problem 第 6 个字母 e 打成 a,1 个错字):
curl -X POST http://localhost:7700/indexes/books_typo/search \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
-d '{"q":"problam","showRankingScore":true}'
{
"hits": [
{
"id": 1,
"title": "三体",
"title_en": "The Three-Body Problem",
"_rankingScore": 0.81
}
],
"query": "problam",
"processingTimeMs": 2,
"estimatedTotalHits": 1
}
problam 照样命中《三体》。这里 showRankingScore: true 让每条结果带上 _rankingScore(相关性分数,0.0 到 1.0,越高越相关)——它小于精确匹配的分值,说明这次是带着 1 个错字匹配上的。默认配置下这类单字母拼错是被容忍的,什么都不用配。
验证这一步成立:把容错关掉的命令还没用上,但可以先读一下当前设置,确认它确实是默认开的。
curl http://localhost:7700/indexes/books_typo/settings/typo-tolerance -H "Authorization: Bearer aSampleMasterKey"
{
"enabled": true,
"minWordSizeForTypos": {
"oneTypo": 5,
"twoTypos": 9
},
"disableOnWords": [],
"disableOnAttributes": []
}
enabled: true 就对了,这就是 typo tolerance 的默认设置对象。字段含义:enabled 总开关;minWordSizeForTypos 容错分档阈值;disableOnWords / disableOnAttributes 定向关闭开关(第五步用)。
第三步:阈值分档——词越短,容错越吝啬
默认的 oneTypo: 5, twoTypos: 9 意思是:1–4 字符的词允许 0 个错字;5–8 字符的词允许 1 个错字;9 字符及以上的词允许 2 个错字,每个词最多 2 个错字。另外两条规则记一下:首字符错字按 2 个错字计(防止只差第一个字母的假命中),相邻两字母交换位置(如 teh → the)只算 1 个错字。
拿两个查询对照就懂了。先搜 livi——live 第 4 个字母 e 打成 i,1 个错字,但词只有 4 个字符,按规则 0 容错:
curl -X POST http://localhost:7700/indexes/books_typo/search \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
-d '{"q":"livi"}'
{
"hits": [],
"query": "livi",
"processingTimeMs": 1,
"estimatedTotalHits": 0
}
4 字符的词错 1 个字母搜不到。再搜 lighning——lightning(Ball Lightning 里的词)少了个 t,8 个字符,有 1 个错字预算:
curl -X POST http://localhost:7700/indexes/books_typo/search \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
-d '{"q":"lighning"}'
{
"hits": [
{
"id": 2,
"title": "球状闪电",
"title_en": "Ball Lightning"
}
],
"query": "lighning",
"processingTimeMs": 2,
"estimatedTotalHits": 1
}
同样错 1 个字母,8 字符的词能容忍,4 字符的词不行——这就是 oneTypo: 5 的边界。再补充一条规则对首字符的惩罚:比如搜 ding(ming 首字母 d 换成 m),4 字符 0 容错之外,首字符错字本身还按 2 个错字计,双重不满足,搜不到。
curl -X POST http://localhost:7700/indexes/books_typo/search \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
-d '{"q":"ding"}'
{
"hits": [],
"query": "ding",
"processingTimeMs": 1,
"estimatedTotalHits": 0
}
这里顺带解释一个容易误解的点:容错只作用在分词后的词上,不是原始字符串。Meilisearch 的解析器 Charabia 先按语言把文本切词(英文按空格切、中文按词典切),容错计算发生在词和词之间。所以中文查询几乎不吃这层容错——中文词大多是 1–2 个字的短词,按上面的分档 0 容错,这就是为什么这篇要用 title_en 演示。验证这三条查询的输出:livi 和 ding 返回空、lighning 命中《球状闪电》,就说明你理解了分档规则。
第四步:PATCH 调阈值,验证改动真生效
默认的 oneTypo: 5 挡住了 4 字符词的 1 个错字。现在把它拧到 4,让 4 字符词也享受 1 个错字预算,然后搜刚才失败的 livi——如果改动生效,这次应该能命中《活着》。
配置端点用 PATCH(不是 PUT):PATCH /indexes/books_typo/settings/typo-tolerance,只改 minWordSizeForTypos。
curl -X PATCH http://localhost:7700/indexes/books_typo/settings/typo-tolerance \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
-d '{"minWordSizeForTypos":{"oneTypo":4,"twoTypos":9}}'
{
"taskUid": 52,
"indexUid": "books_typo",
"status": "enqueued",
"type": "settingsUpdate",
"enqueuedAt": "2026-10-13T10:10:00.000Z"
}
设置更新同样是异步任务,返回 202 + taskUid,等它 succeeded 再验证:
curl http://localhost:7700/tasks/52 -H "Authorization: Bearer aSampleMasterKey"
{
"uid": 52,
"indexUid": "books_typo",
"status": "succeeded",
"type": "settingsUpdate",
"enqueuedAt": "2026-10-13T10:10:00.000Z",
"startedAt": "2026-10-13T10:10:00.200Z",
"finishedAt": "2026-10-13T10:10:00.400Z"
}
再搜 livi:
curl -X POST http://localhost:7700/indexes/books_typo/search \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
-d '{"q":"livi"}'
{
"hits": [
{
"id": 3,
"title": "活着",
"title_en": "To Live"
}
],
"query": "livi",
"processingTimeMs": 2,
"estimatedTotalHits": 1
}
第三步搜不到、第四步搜到了——阈值改动真实生效。这里有个版本差异值得知道:v1.45 之前改 typo tolerance 设置会触发全量重索引(数据量大时要等很久);v1.45(2026-05-26)起 typo tolerance 设置改动免重索引,v1.46 起 disableOnWords 等也免。你本地是 v1.54.3,上面这个任务几百毫秒就完成了,就是免重索引的结果。验证:livi 从空命中变成命中《活着》,第四步就成立了。
第五步:disableOnWords / disableOnAttributes 定向关闭容错
阈值分档是“全局”的容错预算。有些词我们希望永远精确匹配——品牌名、型号、代码这类词,错一个字母就不该搜到。disableOnWords 就是干这个的:指定哪些查询词不吃容错。
把 problem 加进 disableOnWords,再搜刚才能命中的 problam:
curl -X PATCH http://localhost:7700/indexes/books_typo/settings/typo-tolerance \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
-d '{"disableOnWords":["problem"]}'
{
"taskUid": 53,
"indexUid": "books_typo",
"status": "enqueued",
"type": "settingsUpdate",
"enqueuedAt": "2026-10-13T10:15:00.000Z"
}
等任务 succeeded(轮询 GET /tasks/53,套路同第四步),再搜:
curl -X POST http://localhost:7700/indexes/books_typo/search \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
-d '{"q":"problam"}'
{
"hits": [],
"query": "problam",
"processingTimeMs": 1,
"estimatedTotalHits": 0
}
problem 被放进禁用名单后,problam 搜不到了。精确拼写不受影响,problem 本身照样能搜到《三体》。对照一下:第四步改的是“容错预算多给一点”,这一步是“某个词完全不给容错”——两种需求对应两个不同开关。
disableOnAttributes 是按字段关:指定某个字段名后,查这个词时对该字段里的内容不做容错匹配。适合 SKU、条码这类存数字字母串的字段。用法一样,{"disableOnAttributes":["title_en"]} 就把整个 title_en 字段的容错关掉。
验证这一步:problam 从命中变空、problem 仍命中。要是你把 disableOnWords 传成 ["Problem"] 这种带大小写的写法没生效,见常见问题第 3 条。
第六步:enabled:false 整体关闭,再重置回默认
最后一个开关是 enabled——整个索引的容错总闸。把第五步的 disableOnWords 恢复为空(传空数组),再关掉 enabled,这样就能纯粹看到“关闭容错”的效果:
curl -X PATCH http://localhost:7700/indexes/books_typo/settings/typo-tolerance \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
-d '{"disableOnWords":[],"enabled":false}'
{
"taskUid": 54,
"indexUid": "books_typo",
"status": "enqueued",
"type": "settingsUpdate",
"enqueuedAt": "2026-10-13T10:20:00.000Z"
}
等 taskUid: 54 变成 succeeded,把第二步和第三步的错拼查询都跑一遍——这次一个都不该命中:
curl -X POST http://localhost:7700/indexes/books_typo/search \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
-d '{"q":"problam"}'
{
"hits": [],
"query": "problam",
"processingTimeMs": 1,
"estimatedTotalHits": 0
}
curl -X POST http://localhost:7700/indexes/books_typo/search \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
-d '{"q":"lighning"}'
{
"hits": [],
"query": "lighning",
"processingTimeMs": 1,
"estimatedTotalHits": 0
}
enabled: false 之后错拼查询全军覆没。最后把它重置回默认——PATCH 传 null 表示“恢复该设置的默认值”:
curl -X PATCH http://localhost:7700/indexes/books_typo/settings/typo-tolerance \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
-d 'null'
{
"taskUid": 55,
"indexUid": "books_typo",
"status": "enqueued",
"type": "settingsUpdate",
"enqueuedAt": "2026-10-13T10:25:00.000Z"
}
等任务完成后读设置,确认回到默认:
curl http://localhost:7700/indexes/books_typo/settings/typo-tolerance -H "Authorization: Bearer aSampleMasterKey"
{
"enabled": true,
"minWordSizeForTypos": {
"oneTypo": 5,
"twoTypos": 9
},
"disableOnWords": [],
"disableOnAttributes": []
}
看到 enabled: true 和 oneTypo: 5 就完成了重置,problam 也恢复了命中。到这里你手上已经有三套旋钮:分档阈值(第四步)、定向关闭(第五步)、总开关(第六步)——按“宽松 / 精确 / 关闭”三个方向都能控住容错。
核心概念
下面这张图是拼写容错在一次查询里的位置:查询词先被分词,再与索引里的词做容错距离匹配,命中的文档按排序规则(其中就有 typo 这条)排出顺序。

| 概念 | 是什么 | 类比 |
|---|---|---|
| typo tolerance(拼写容错) | 查询词拼写有出入时仍能匹配到正确词条的机制,默认开启 | 输入法把打错的拼音自动纠正成正确候选 |
| minWordSizeForTypos | 按词长分档的容错预算:词越长允许的错字越多 | 生僻字查字典时,笔画多的字允许多错一笔 |
| ranking rule(排序规则) | typo 容错是排序规则而非过滤器——错拼结果不会被排除,只是排在精确匹配后面 | 考试按卷面分排序,错别字多的排后面但不取消资格 |
| disableOnWords / disableOnAttributes | 定向关闭:指定查询词或字段不吃容错 | 报菜名必须一字不差,打错就重说 |
| Charabia 分词 | 容错作用在分词后的词上,不是原始字符串;中文按词典切词,多为短词 | 先断句再改错,句子都断错了就无从谈改错 |
常见问题
{"message":"Invalid value at \.typoTolerance.minWordSizeForTypos.oneTypo`: value: `1000` is too large … maximum value authorized is `255`“,“code”:“invalid_settings_typo_tolerance”}`
现象:PATCH typo tolerance 设置时返回 400,响应体带 code 而不是 taskUid。
原因:oneTypo / twoTypos 在服务端是 u8 类型,只能取 0–255;且 twoTypos 必须大于 oneTypo。这类字段校验在请求阶段同步报错,不会生成异步任务。
相关文章:Meilisearch 配置 typo tolerance 报 invalid_settings_typo_tolerance
PATCH 改完设置立刻搜索,结果还是旧行为
现象:第四步那种改阈值操作后马上搜,命中结果和改之前一模一样。
原因:设置更新是异步任务,PATCH 只代表任务入队(返回 taskUid),要轮询 GET /tasks/{taskUid} 到 succeeded 才生效——和文档上传是同一套机制。
中文查询错字几乎搜不到 / disableOnWords 写了大写不生效
现象:搜中文错字(如把“三体”打成“三休”)返回空;disableOnWords 传 ["Problem"] 却还能容错命中。
原因:中文按词典切词后多是 1–2 字短词,按分档 0 容错;分词/词典的归一化不覆盖所有情况,禁用词需按索引里的实际形态逐个枚举。
