StackTraces
Meilisearch从零跑通

从零跑通 Meilisearch 拼写容错(typo tolerance):6 步,外加 3 个容易卡住的地方

@debug_master2026-10-13

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

Meilisearch:从零跑通拼写容错的 6 个步骤

前置条件

项 要求 说明
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 这条)排出顺序。

Meilisearch 拼写容错调用链路架构图

概念 是什么 类比
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 才生效——和文档上传是同一套机制。

相关文章:Meilisearch 添加文档后立即搜索返回空结果

中文查询错字几乎搜不到 / disableOnWords 写了大写不生效

现象:搜中文错字(如把“三体”打成“三休”)返回空;disableOnWords 传 ["Problem"] 却还能容错命中。

原因:中文按词典切词后多是 1–2 字短词,按分档 0 容错;分词/词典的归一化不覆盖所有情况,禁用词需按索引里的实际形态逐个枚举。

相关文章:Meilisearch 分词设置改了不生效

下一步

typo 在排序规则里排第几位

typo 是 Meilisearch 七条排序规则之一,错字越少排越前,还影响与 words、attribute 的先后关系。

Meilisearch 配置排序规则报 invalid_ranking_rules 格式

老版本改设置要全量重建的坑

v1.45 之前改 typo tolerance 会触发全量重索引,数据量大时等很久还容易误判“没生效”。

Meilisearch 改设置后全文索引不生效或需全量重建

分词与词典对容错的影响

容错作用在分词后的词上,分词边界、自定义词典、归一化都会改变容错实际效果。

Meilisearch 分词设置改了不生效

卡在哪一步了?

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

贡献一条解法