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
