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 分詞設置改了不生效

卡在哪一步了?

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

貢獻一條解法