StackTraces
Meilisearch已驗證

Meilisearch 改了 dictionary / separatorTokens 後搜索行為沒變化

@debug_master6 天前更新閱讀 6 min00

給索引 PUT 了 dictionary、separatorTokens、nonSeparatorTokens、localizedAttributes 這類分詞設置,接口返回 200,但緊接著搜索行為一點沒變。原因是這些都是索引側設置:PUT 只是把變更排進隊列,返回一個 taskUid,真正幹活的是異步的 settingsUpdate 任務,而且它會重索引該索引的全部文檔。任務還沒 succeeded 就去搜,用的還是舊設定。輪詢到任務成功再搜才行。

Issue & Env(案發現場與環境)

  • 觸發場景:按文檔給 dictionary 或 separatorTokens 發了 PUT,命令行看到 200 和 taskUid,以為設置「已經生效」,馬上重搜,結果和設置前一模一樣。
  • 環境:Meilisearch v1.54.3,本機實跑復現,索引 books_zh。
  • 關鍵區分:PUT 返回 200 不代表設置生效,只代表任務入隊。這些設置不是查詢時讀取的即時參數,而是要重建倒排索引才能體現。
  • 直覺誤導:taskUid 這種一點點返回的值很容易被當成「操作完成」,實際上它對應的是一個還沒跑完的後臺任務。

StackTrace(堆疊日誌)

PUT dictionary 的返回,只有 taskUid 和 enqueued,沒有生效結果:

{"taskUid":2,"indexUid":"books_zh","status":"enqueued","type":"settingsUpdate","enqueuedAt":"2026-10-05T01:52:44.547909Z"}

輪詢到 GET /tasks/2 才看到它做完,details 裡帶著實際寫入的值:

{"uid":2,"batchUid":2,"indexUid":"books_zh","status":"succeeded","type":"settingsUpdate","canceledBy":null,"details":{"dictionary":["未来科技"]},"error":null,"duration":"PT0.024570S","enqueuedAt":"2026-10-05T01:52:44.547909Z","startedAt":"2026-10-05T01:52:44.556926Z","finishedAt":"2026-10-05T01:52:44.581496Z"}

separatorTokens 同理,任務號是 5:

{"taskUid":5,"indexUid":"books_zh","status":"enqueued","type":"settingsUpdate","enqueuedAt":"2026-10-05T01:52:53.876945Z"}

在這個任務跑完之前,行為完全不改。

Reproduce(復現程式碼)

用 red|green|blue 這段數據最能看清「設置沒生效」和「生效後」的差別:

# 1. 先放一条关键字里带 | 的文档
curl -s -X POST http://localhost:7700/indexes/books_zh/documents \
  -H 'Authorization: Bearer aSampleMasterKey' \
  -H 'Content-Type: application/json' \
  -d '[{"id":7,"title":"色彩手册","keywords":"red|green|blue"}]'

# 2. 设置前搜 green —— | 不是默认分隔符,整段是一个 token,命中 0
curl -s -X POST http://localhost:7700/indexes/books_zh/search \
  -H 'Authorization: Bearer aSampleMasterKey' \
  -H 'Content-Type: application/json' \
  -d '{"q":"green"}'
# 返回:{"hits":[],"query":"green","processingTimeMs":10,"limit":20,"offset":0,"estimatedTotalHits":0,...}

# 3. PUT separatorTokens,把 | 声明为分隔符
curl -s -X PUT http://localhost:7700/indexes/books_zh/settings/separator-tokens \
  -H 'Authorization: Bearer aSampleMasterKey' \
  -H 'Content-Type: application/json' \
  -d '["|"]'
# 返回:{"taskUid":5,"indexUid":"books_zh","status":"enqueued","type":"settingsUpdate",...}

# 4. 任务没跑完就搜,还是 0 命中;轮询到 succeeded 后再搜
curl -s http://localhost:7700/tasks/5 \
  -H 'Authorization: Bearer aSampleMasterKey'
# 返回:{"uid":5,...,"status":"succeeded","type":"settingsUpdate","details":{"separatorTokens":["|"]},...}

curl -s -X POST http://localhost:7700/indexes/books_zh/search \
  -H 'Authorization: Bearer aSampleMasterKey' \
  -H 'Content-Type: application/json' \
  -d '{"q":"green"}'
# 任务成功后返回:{"hits":[{"id":7,"title":"色彩手册","keywords":"red|green|blue","_matchesPosition":{"keywords":[{"start":4,"length":5}]}}],"estimatedTotalHits":1,...}

Root Cause(根因分析)

分詞設置是索引側設置,不是查詢參數

dictionary、separatorTokens、nonSeparatorTokens、localizedAttributes 定義的是「文檔入庫時怎麼切詞」。文檔的 token 在索引階段就寫進了倒排索引,查詢時只按已經建好的 token 去匹配。所以要改變切詞行為,必須把已有文檔按新設置重新切一遍、重建索引——這一步不可能在搜索請求裡即時完成。

PUT 只是入隊,任務會重索引全部文檔

Meilisearch 把這類寫操作放進異步隊列。官方異步操作文檔原文:

Many operations in Meilisearch are processed asynchronously. These API requests are not handled immediately—instead, Meilisearch places them in a queue and processes them in the order they were received.

PUT /settings/... 返回的 taskUid 只表示「已入隊」,type 是 settingsUpdate。這個任務執行時會重索引該索引的全部文檔(本機 books_zh 只有 7 條,幾十毫秒跑完;數據量大時會明顯更久)。在它 succeeded 之前,倒排索引還是舊的,搜索自然沒變化。

各設置的表現差異

  • separatorTokens / nonSeparatorTokens:改變 token 邊界,直接決定一個詞能不能被切出來。red|green|blue 預設整段一個 token,加了 | 後 green 才成為獨立 token。
  • dictionary:把某個字符串鎖成一個整詞,改變的是分詞粒度和後續排名。本機把「未來科技」加進 dictionary 後,命中條數不變(仍是 2 條),但 _matchesPosition 從兩個 6 字節 token 變成一個 12 字節整詞——它影響的是切分與排名,不是「從搜不到變搜到」。
  • localizedAttributes:聲明字段語言,同樣會觸發重索引。

The Fix(解決方案)

第一步:拿到 taskUid 後輪詢任務

# PUT 返回 taskUid=5
curl -s http://localhost:7700/tasks/5 \
  -H 'Authorization: Bearer aSampleMasterKey'

第二步:等 status 變成 succeeded 再搜索

TASK_UID=5
while true; do
  STATUS=$(curl -s "http://localhost:7700/tasks/${TASK_UID}" \
    -H 'Authorization: Bearer aSampleMasterKey' | jq -r '.status')
  if [ "$STATUS" = "succeeded" ]; then
    echo "settings applied"
    break
  elif [ "$STATUS" = "failed" ]; then
    echo "settings update failed"
    curl -s "http://localhost:7700/tasks/${TASK_UID}" \
      -H 'Authorization: Bearer aSampleMasterKey' | jq '.error'
    break
  fi
  sleep 1
done

第三步:核對設置確實回讀了

任務成功後 GET 一下確認寫入的值(注意回讀時順序可能被規範化,比如 ["+","#"] 回讀成 ["#","+"]):

curl -s http://localhost:7700/indexes/books_zh/settings/separator-tokens \
  -H 'Authorization: Bearer aSampleMasterKey'

注意事項

  • 設置更新會重索引該索引全部文檔,大索引上耗時長,別在流量高峰做。
  • 多個設置可以一次 PATCH /settings 提交,但仍然是異步任務,同樣要等。
  • 用 SDK 時優先用其內置的 wait_for_task 一類方法,別自己搶跑。
  • 如果任務 failed,看 error 字段;比如索引側 locale 寫錯會以 invalid_settings_localized_attributes 失敗。

References(參考來源)

最後更新於 2026-10-05

這篇幫到你了嗎?

剛解決了一個棘手的報錯?花兩分鐘記錄下來,幫助下一個遇到同樣問題的開發者。

貢獻一條解法