一文搞懂 Elasticsearch 中的 Query 与 Filter:别再傻傻分不清了
你有没有过这样的经历?写了个 ES 查询,功能是实现了,但响应慢得像蜗牛。翻遍日志也没报错,最后发现——原来是把本该放进filter的条件塞进了query,白白浪费了一堆 CPU 去算根本不需要的_score。
这事儿太常见了。
尤其是在初学 Elasticsearch 的时候,很多人看着 DSL(领域特定语言)那层层嵌套的 JSON 就头大,更别说搞清楚什么时候用match、什么时候用term,又或者为什么有些查询能缓存、有些却每次都重新计算。
今天我们就来彻底讲明白一件事:Query 和 Filter 到底有什么区别?为什么这个区分如此重要?
不整虚的,直接上实战视角,带你从原理到代码,一步步看清它们的本质差异。
先问一个问题:你的搜索需要“打分”吗?
这是判断该用Query还是Filter的第一准则。
- 如果你需要知道“哪个文档更匹配我的关键词”,比如用户搜“苹果手机”,你想把标题里同时出现“苹果”和“手机”的排前面,那就得用Query—— 它会计算
_score。 - 但如果只是要“找出符合条件的文档”,比如“价格小于5000元”、“品牌是 Apple”、“库存为 true”,这些非黑即白的条件,压根不用打分,就该丢进Filter。
🔥 核心一句话总结:
Query 影响_score,Filter 不影响。前者用于排序相关性,后者用于高效过滤。
听起来简单?可现实中,90% 的性能问题都出在这一步选错了上下文。
深入底层:Query 是怎么工作的?
我们以一个典型的全文检索为例:
{ "query": { "match": { "title": "蓝牙耳机" } } }当你发起这样一个请求时,Elasticsearch 干了这几件事:
- 分词处理:将 “蓝牙耳机” 拆成 “蓝牙” 和 “耳机”(基于字段 mapping 设置的 analyzer)
- 倒排索引查找:去
title字段的倒排表中找包含这两个词的文档 ID - 相关性评分:对每个命中的文档,使用 BM25 算法计算得分(考虑词频 TF、逆文档频率 IDF、字段长度归一化等)
- 排序返回:按
_score降序排列,返回 top-N 结果
整个过程的核心在于第3步——打分。
而这个打分是有代价的。尤其是面对百万级数据时,每多一次无意义的评分,就意味着更多的 CPU 占用、更高的延迟。
所以问题来了:如果你其实并不关心排序,只是想筛出某些记录呢?
比如统计过去一小时支付服务的日志错误数,你还需要给每条日志打分吗?
显然不需要。
这时候就应该切换到Filter Context。
Filter 为什么快?不只是“不打分”那么简单
还是上面那个需求,我们改用 filter 写法:
GET /logs/_search { "size": 0, "query": { "bool": { "filter": [ { "term": { "service.name": "payment" } }, { "range": { "@timestamp": { "gte": "now-1h/h" } } } ] } }, "aggs": { "errors": { "terms": { "field": "status.keyword" } } } }这段查询做了什么?
- 只关心满足条件的日志条目,不做任何排序
- 在聚合前先通过 filter 快速缩小数据集范围
- 所有条件都在filter context下执行
关键来了:Filter 不仅不打分,还能被缓存!
BitSet 缓存:Filter 性能起飞的秘密武器
ES 内部会对 filter 条件的结果生成一个叫BitSet的结构——简单说就是一个位数组,每一位代表一个文档是否命中。
例如:
Doc ID: 0 1 2 3 4 5 6 ... BitSet: 1 0 1 1 0 1 0 ...表示只有 Doc 0、2、3、5 符合条件。
一旦这个 BitSet 被构建出来,并且条件没有变化,下次同样的 filter 请求就可以直接复用结果,跳过所有扫描和判断逻辑。
✅ 实测数据:在千万级索引上,已缓存的 term filter 查询响应时间可稳定在<1ms;而同等 query 查询仍在 10~50ms 区间波动。
而且这种缓存是自动的!只要你用了 filter context,ES 就会尝试缓存它(当然也有失效机制,后面会提)。
对比表格:一眼看懂 Query vs Filter
| 特性 | Query | Filter |
|---|---|---|
是否计算_score | 是 | 否 |
| 是否影响排序 | 是 | 否 |
| 是否支持缓存 | 弱(依赖上下文,不易命中) | 强(自动加入 BitSet 缓存) |
| 典型应用场景 | 关键词搜索、模糊匹配、高亮 | 条件筛选、聚合前置、权限控制 |
| 性能开销 | 高(涉及评分 + 分析) | 低(仅布尔判断,可缓存) |
记住这张表,以后写查询前先问自己一句:我真需要_score吗?
如果答案是否定的,请果断把它扔进filter。
实战案例:电商搜索是怎么设计的?
想象一下你在做一个电商平台的商品搜索功能。
用户输入“运动手环”,同时还设置了筛选条件:品牌=华为、价格区间 100-300 元、有货。
你怎么写这个查询?
❌ 错误写法:全塞进 must
{ "query": { "bool": { "must": [ { "match": { "name": "运动手环" } }, { "term": { "brand.keyword": "Huawei" } }, { "range": { "price": { "gte": 100, "lte": 300 } } }, { "term": { "in_stock": true } } ] } } }看起来没问题?功能也能跑通。
但性能差在哪?
brand、price、in_stock都是精确条件,不需要参与评分- 每次请求都要重新计算这些字段的相关性分数,CPU 白烧
- 无法利用 filter cache,重复请求无法加速
✅ 正确姿势:Query + Filter 分工协作
GET /products/_search { "from": 0, "size": 20, "query": { "bool": { "must": [ { "multi_match": { "query": "运动手环", "fields": ["name^2", "description"] }} ], "filter": [ { "term": { "brand.keyword": "Huawei" } }, { "range": { "price": { "gte": 100, "lte": 300 } } }, { "term": { "in_stock": true } } ], "must_not": [ { "term": { "status": "deleted" } } ] } } }这才是标准做法!
拆解一下执行流程:
- 先过 filter:用 BitSet 快速圈定候选文档集合(符合品牌、价格、库存条件的)
- 再跑 query:只在剩下的子集中做文本匹配和打分
- 排除 must_not:去掉已删除商品
- 最终排序返回 Top-20
这样做的好处是什么?
- 减少参与评分的文档数量 → 提升 query 执行效率
- 复用 filter 缓存 → 高频筛选条件秒级响应
- 整体响应速度提升可达3~10 倍
常见误区避坑指南
⚠️ 误区1:把match放进 filter
{ "filter": { "match": { "title": "hello world" } } }你以为加了个 filter 上下文就能提速?错!
match查询本身依赖文本分析和评分机制,在 filter 中虽然语法合法,但会跳过正常的分析流程,可能导致匹配失败或结果异常。
✅ 正确做法:
- 若需全文检索 → 放回querycontext 使用match
- 若需精确匹配 → 改用term查询,并确保字段类型为keyword
⚠️ 误区2:动态时间导致缓存失效
"range": { "@timestamp": { "gte": "now-1h" } }这个条件每分钟都在变(now 在动),BitSet 缓存几乎永远无法命中。
✅ 解决方案:对齐时间窗口
"range": { "@timestamp": { "gte": "now-1h/h" } }加上/h表示向下取整到小时边界。只要在同一小时内发起请求,条件就是一致的,缓存就能复用。
类似技巧也适用于/d(天)、/m(分钟)等单位。
⚠️ 误区3:字段类型没设对,filter 白写了
{ "term": { "brand": "Apple" } }如果brand是text类型,会被分词器处理,term查询将无法精确匹配。
✅ 必须使用.keyword子字段:
{ "term": { "brand.keyword": "Apple" } }这也是为什么建议建模时明确区分字段用途:
-text:用于全文检索(match)
-keyword:用于精确匹配(term/range)
高阶提示:如何验证你的查询是否高效?
ES 提供了一个强大的调试工具:Profile API
开启方式很简单,在查询中加入"profile": true:
{ "profile": true, "query": { ... } }返回结果会详细列出每个子查询的执行耗时、是否命中缓存、调用了哪些底层组件。
重点关注:
-breakdown.score是否过高?说明可能不该用 query
-type为CachedFilter?恭喜,filter 缓存生效了
-rewrite_time过长?可能是 too_many_clauses 导致的性能陷阱
有了 profile 数据,优化就有了依据,不再是凭感觉调参。
最佳实践清单:照着做就对了
| 场景 | 推荐做法 |
|---|---|
| 文本关键词搜索 | 用match或multi_match,放在must |
| 精确值匹配(品牌、状态、ID) | 用term,字段走.keyword,放入filter |
| 数值/日期范围 | 用range,放入filter |
| 聚合分析 | 外层 query 用filter缩小范围 |
| 排除某些记录 | 用must_not(属于 filter context) |
| 组合复杂条件 | 一律使用bool容器进行结构化组织 |
| 高频不变条件(如 region=CN) | 放入filter,最大化缓存利用率 |
| 动态参数拼接 | 注意避免破坏缓存一致性(如时间对齐策略) |
写在最后:理解上下文,才是掌握 ES 的起点
很多人觉得 Elasticsearch 难,其实是没抓住它的设计哲学。
它不是单纯的数据库,而是一个围绕“相关性”构建的搜索引擎。所有的机制——从倒排索引到评分模型,再到 filter 缓存——都是为了一个目标服务:在海量数据中快速找到最相关的那一部分。
而你要做的,就是学会区分:
- 哪些条件决定“相关性” → 交给 Query
- 哪些条件只是“硬性门槛” → 交给 Filter
一旦你掌握了这个思维模式,你会发现,不仅查询写得更快了,系统资源消耗也明显下降,甚至原来卡顿的聚合报表现在都能实时响应了。
未来随着向量检索(kNN)、混合查询(hybrid search)的发展,上下文管理只会越来越重要。今天的query和filter,就是明天vector与keyword协同的基础。
所以,别再把所有条件都往must里塞了。
合理分工,让 Query 专注“找得准”,让 Filter 负责“筛得快”。
这才是真正的es查询语法成长之路。
如果你在实际项目中遇到过类似的性能瓶颈,欢迎在评论区分享你的排查思路和解决方案,我们一起讨论!