filter字符串表达式里 AND 优先级高于 OR:A OR B AND C会被解析成A OR (B AND C)。跨条件组合时漏掉括号,结果与预期不符且不报任何错误(HTTP 200)。组合条件必须显式加括号:(A OR B) AND C。官方规范 BNF 明确or = and ("OR" and)*,AND 嵌套在 OR 之内,因此 AND 先结合。适用 Meilisearch 全版本。
Issue & Env(案发现场与环境)
- 业务场景:想表达“类型是恐怖片或喜剧片,并且上映时间晚于 1995-03-25(Unix 时间戳 795484800)”,把 filter 写成
genres = horror OR genres = comedy AND release_date > 795484800,结果返回了全部恐怖片(包含 1995 年前的老片),只有喜剧片分支正确地应用了日期限制。 - 环境:Meilisearch v1.54.3,curl / 任意 SDK 均可复现。
- 关键区分:这不是语法错误——表达式完全合法,请求返回 200,
estimatedTotalHits也正常。这是操作符优先级导致的语义错误,属于最隐蔽的一类坑:没有报错,只有结果不对。
StackTrace(堆栈日志)
官方 Getting started / filter expression reference 指南给出的括号强制示例原文:
(genres = horror OR genres = comedy) AND release_date > 795484800
官方明确警告:去掉括号后,该表达式会被解析为
genres = horror OR (genres = comedy AND release_date > 795484800)
观察到的现象:无任何报错(HTTP 200),但结果集与预期不同——所有 horror 电影都进来了(不再受日期约束),只有 comedy 分支受日期限制。
官方规范(specs 0118-search-api)中 filter 语法的 BNF 原文:
filter = expression EOF;
expression = or;
or = and ("OR" WS+ and)*;
and = not ("AND" WS+ not)*;
not = ("NOT" WS+ not) | primary;
primary = "(" WS* expression WS* ")" | ...
从文法结构看,or 的每个操作数是一个完整的 and 表达式:AND 先结合,OR 后结合。所以 A OR B AND C 等价于 A OR (B AND C),OR 组永远“逃逸”不出 AND 的约束。
Reproduce(复现代码)
# 目标:恐怖片或喜剧片,且 release_date > 795484800(约 1995-03-25)
# 错误写法——缺括号
curl -X POST 'http://localhost:7700/indexes/movies/search' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer aSampleMasterKey' \
--data-binary '{"q": "", "filter": "genres = horror OR genres = comedy AND release_date > 795484800"}'
# 结果:所有 horror 电影(含 1995 年前的老片)+ 1995 年后的 comedy
# 正确写法——跨条件组合加括号
curl -X POST 'http://localhost:7700/indexes/movies/search' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer aSampleMasterKey' \
--data-binary '{"q": "", "filter": "(genres = horror OR genres = comedy) AND release_date > 795484800"}'
# 结果:仅 1995 年后的 horror 或 comedy
Root Cause(根因分析)
AND 优先于 OR 是官方明确的设计
Meilisearch 的 filter 表达式解析采用与 SQL 一致的优先级约定:AND 高于 OR,括号用于覆盖默认结合顺序。官方规范 BNF 从文法层面固定了这一点(or = and ("OR" and)*),不是实现 bug。
前端动态拼 filter 字符串是最高频触发场景
分面导航的典型拼法:同一分面内多选值用 OR 连接(如 genres = horror OR genres = comedy),跨分面用 AND 连接(如 ... AND language = English)。一旦把两组条件直接拼接成 genres = horror OR genres = comedy AND language = English,OR 组就会脱离 AND 约束,喜剧片 + 语言条件的组合被“吞掉”。
排查方法
- 把 filter 字符串打印出来,按“AND 先结合”的规则人工还原解析树,对比预期。
- 或改用数组形式重写:外层数组元素间隐含 AND、内层数组元素间隐含 OR,层级一目了然,不用手写括号(见下)。
The Fix(解决方案)
规则:任何“多个 OR 组合再与其他条件 AND”的场景,OR 组必须整体加括号
curl -X POST 'http://localhost:7700/indexes/movies/search' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer aSampleMasterKey' \
--data-binary '{"q": "", "filter": "(genres = horror OR genres = comedy) AND release_date > 795484800"}'
等价数组写法(避免手写括号)
curl -X POST 'http://localhost:7700/indexes/movies/search' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer aSampleMasterKey' \
--data-binary '{
"q": "",
"filter": [["genres = horror", "genres = comedy"], "release_date > 795484800"]
}'
数组形式的语义:外层数组元素之间用 AND 连接,内层数组元素之间用 OR 连接,可内外混用;嵌套最大深度为 2,三层以上直接抛错。结构显式化之后,优先级不会再产生歧义。
注意事项
NOT同样受括号影响:release_date > 1577884550 AND (NOT director = "Tim Burton")中的括号也是强制的,去掉后NOT只作用于后一个条件。- 复杂表达式建议先对照官方 filter expression reference 核对优先级,再上线。
- 验证技巧:先不加括号请求一次,再加括号请求一次,对比两次
estimatedTotalHits,差异一眼可见。
References(参考来源)
- Filter expression reference — Meilisearch 官方文档meilisearch-docs
- Search API 规范(filter 语法 BNF)— Meilisearch 规范文档meilisearch-spec
- Getting started — Meilisearch 官方文档meilisearch-docs
