federated search 的
distinct参数做的是跨索引去重,它要求去重属性名在每个参与查询的索引的filterableAttributes列表里都声明过。任一索引漏声明,整个 multi-search 请求 400 失败。这不是 bug 是设计——去重本质是跨索引的 filter 操作,而 filter 参数依赖 filterableAttributes 声明。
Issue & Env(案发现场与环境)
- 业务场景:三个索引
movies/movie_ratings/movie_people都有title字段。movies 已经 PATCH 过filterableAttributes: ["title"],另外两个索引漏了。发 federated search 带federation.distinct: "title"时返回 400。 - 环境:Meilisearch v1.54+,任意 federated multi-search 请求。
- 关键区分:这不是
invalid_multi_search_distinct(那个错误是 federation.distinct 和 query 级 distinct 同时存在),也不是普通的invalid_search_filter(那个是单索引 filter 语句里用了未声明的属性)。federation.distinct 触发的是跨索引 filter 校验,错误消息里的属性名是 federation 参数里的值。
StackTrace(错误响应)
{
"message": "Attribute \"title\" is not filterable.",
"code": "invalid_search_filter",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid_search_filter"
}
注:错误码是
invalid_search_filter(和单索引 filter 未声明的错误码一样),但触发路径是 federation.distinct 跨索引校验。具体 message 可能随 Meilisearch 版本略有差异,但「xxx is not filterable」这个结构是稳定的。
Reproduce(复现代码)
# 1. 三个索引都已建,movies 已声明 title 可过滤,movie_ratings 和 movie_people 没声明
curl -X PATCH 'http://localhost:7700/indexes/movies/settings/filterable-attributes' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer aSampleMasterKey' \
-d '["title"]'
# 2. 直接发带 federation.distinct 的请求 —— 400 失败
curl -X POST 'http://localhost:7700/multi-search' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer aSampleMasterKey' \
--data-binary '{
"federation": { "distinct": "title" },
"queries": [
{ "indexUid": "movies", "q": "batman" },
{ "indexUid": "movie_ratings", "q": "batman" },
{ "indexUid": "movie_people", "q": "batman" }
]
}'
{"message":"Attribute \"title\" is not filterable.","code":"invalid_search_filter","type":"invalid_request","link":"https://docs.meilisearch.com/errors#invalid_search_filter"}
Root Cause(根因分析)
federation.distinct 是跨索引的 filter 操作
Meilisearch 的 federated search 合并多索引结果后,要按 federation.distinct 指定的属性值去重。这个去重动作对每个参与索引来说,等价于在各自索引上做一次「只保留某个属性值唯一的文档」——而这个操作依赖该属性在索引里声明为 filterable。
不是「每个索引都要有这个字段」,是「每个索引都要声明它可过滤」
某个索引的文档里哪怕根本没有 title 这个字段,只要你在 federation.distinct 里写了它,Meilisearch 就会去每个参与索引的 filterableAttributes 列表里查——查不到就报错。这是一个声明层面的校验,不检查实际数据。
和 invalid_multi_search_distinct 的区别
Meilisearch 还有另一个 multi-search 相关错误码 invalid_multi_search_distinct——它的触发条件是 federation 级 distinct 和 query 级 distinct 同时存在。而本条 case 的触发条件是某个索引没在 filterableAttributes 里声明 distinct 属性,错误码是 invalid_search_filter。两者别混淆。
The Fix(解决方案)
第一步:给所有参与索引补声明
curl -X PATCH 'http://localhost:7700/indexes/movie_ratings/settings/filterable-attributes' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer aSampleMasterKey' \
-d '["title"]'
curl -X PATCH 'http://localhost:7700/indexes/movie_people/settings/filterable-attributes' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer aSampleMasterKey' \
-d '["title"]'
等两个 PATCH 任务都 succeeded 后重发 multi-search 请求。
第二步:验证 filterableAttributes 已生效
curl 'http://localhost:7700/indexes/movie_ratings/settings/filterable-attributes' \
-H 'Authorization: Bearer aSampleMasterKey'
["title"]
排查建议
- federation.distinct 的属性名提前拿个 checklist:凡是参与 federated search 的索引,都要在 filterableAttributes 里声明它。别假设「这个索引没这个字段所以不需要声明」——Meilisearch 不做这个推断。
- 如果某个索引真的不应该参与去重(比如它的文档结构里根本没有 title),那它就不应该出现在 queries 数组里,或者换一个所有索引共有的属性名做 distinct。
- PATCH filterableAttributes 是异步操作,任务 succeeded 之前别重发 multi-search——重发会得到同样的 400。
附带提醒:federation.distinct 按属性值去重还要求字段名跨索引一致。如果一个索引叫 title、另一个叫 movie_title,distinct 用哪个名字都会漏掉另一个——建模时就要统一去重字段的命名。
References(参考来源)
- Federated search — Meilisearch 官方文档meilisearch-docs
- Multi-search API reference — Meilisearch 官方文档meilisearch-docs
- Error codes — Meilisearch 官方文档meilisearch-docs
