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

这篇帮到你了吗?

刚解决了一个棘手的报错?花两分钟记录下来,帮助下一个遇到同样问题的开发者。

贡献一条解法