ES面试高频题:filter与query的区别图解说明
在 Elastic Stack 的实际应用中,Elasticsearch(ES)作为核心的分布式搜索与分析引擎,承担着日志检索、实时监控、商品搜索等关键任务。面对海量数据和高并发查询需求,如何写出高效、低延迟的 DSL 查询语句,成为开发者必须掌握的核心技能。
而在技术面试中,“filter 和 query 有什么区别?”这个问题几乎成了每一场涉及 ES 的岗位必考题。它看似简单,实则层层深入——不仅能看出你是否“背过文档”,更能检验你对 Lucene 底层机制、性能优化策略以及缓存原理的真实理解。
今天我们就从一个工程师的视角出发,彻底讲清楚这个问题:不只是告诉你“是什么”,更要让你明白“为什么这么设计”、“怎么用才最有效”。
一、一句话说清本质区别
Query 计算相关性得分
_score,Filter 只判断“是或否”,不打分、可缓存。
这是所有讨论的起点。但如果你只记住这一句,在面试里可能还是会被追问到哑口无言。我们得一层层拆开来看。
二、两种上下文:query context vs filter context
Elasticsearch 并不是简单地把query和filter当作两个并列字段来处理,而是通过上下文(context)来决定它们的行为方式。
1. Query Context:我在找“最相关的”
当你使用match、multi_match、wildcard等查询时,你就处于query context下。
此时 ES 关心的是:
- 这个文档有多匹配我的关键词?
- 它应该排第几位?
为了回答这些问题,ES 会调用底层 Lucene 的评分模型(默认是 BM25),综合考虑词频(TF)、逆文档频率(IDF)、字段长度归一化等因素,最终为每个命中文档计算出一个_score。
举个例子:
"query": { "match": { "title": "无线降噪耳机" } }这段代码意味着:
- 对title字段做分词;
- 找出包含 “无线”、“降噪”、“耳机” 的文档;
- 给每个文档打分,比如完全匹配的得 8.2 分,部分匹配的得 4.5 分;
- 最后按_score倒序返回结果。
这个过程需要大量计算,而且每次请求都可能因用户输入不同而无法复用结果 —— 所以,默认情况下,query 不会被缓存。
2. Filter Context:我只关心“符不符合条件”
当你把查询放进bool.filter或者constant_score.filter中时,你就进入了filter context。
这时候 ES 完全不关心“有多像”,只问一个问题:“这个文档满不满足条件?” 回答只有两个:是 or 否。
例如:
"filter": [ { "term": { "brand.keyword": "Sony" } }, { "range": { "price": { "gte": 500 } } } ]它的逻辑是:
- 找出品牌等于 Sony 的文档;
- 再从中筛选价格 ≥500 的;
- 不打分,只保留符合条件的文档 ID。
更重要的是:这种布尔判断的结果可以被缓存!
三、底层机制揭秘:为什么 filter 更快?
要真正理解性能差异,就得看到 Lucene 层面发生了什么。
Query:逐文档打分 → 耗 CPU
在 query 模式下,Lucene 需要遍历倒排索引找到候选文档,然后对每一个文档执行复杂的数学公式(BM25)来计算_score。
这就像考试阅卷:不仅要看答案对不对,还要根据步骤给分。工作量大,还不能偷懒。
Filter:构建 BitSet → 可缓存 + 快速交集运算
而在 filter 模式下,Lucene 使用一种叫BitSet(位集)的数据结构。
假设你的索引有 100 万篇文档,Lucene 就创建一个长度为 100 万的二进制数组:
[1, 0, 1, 1, 0, 0, ..., 1] ↑ ↑ ↑ ↑ doc0 doc2 doc3 doc999999每一位代表对应文档是否满足当前 filter 条件。比如"brand=Sony"匹配了第 0、2、3、… 篇文档,那就把这些位置设为 1。
这个 BitSet 一旦生成,就可以存入Query Cache。下次再有人查"brand=Sony",直接从内存读取这个数组,跳过整个索引扫描过程。
更妙的是,多个 filter 条件之间可以通过位运算(AND / OR)快速求交集或并集。比如:
- A:
brand=Sony→ BitSet_A - B:
price >= 500→ BitSet_B - 结果 = BitSet_A & BitSet_B (按位与)
这种操作极快,几乎是常数时间完成。
✅ 实测数据显示:重复使用的 filter 查询,响应时间可从 60ms 降至 <10ms。
四、什么时候该用 query?什么时候用 filter?
| 场景 | 推荐用法 | 原因 |
|---|---|---|
| 用户输入关键词搜索 | query.match | 需要相关性排序 |
| 商品分类筛选(如 category=phone) | filter.term | 精确匹配,可缓存 |
| 时间范围过滤(如最近7天) | filter.range | 高频共用条件,适合缓存 |
| 用户权限控制(user_id=123) | filter.term | 每次都一样,缓存友好 |
| 是否上架(in_stock=true) | filter.term | 布尔值,天然适合 filter |
| 模糊匹配、同义词扩展 | query | 必须依赖评分机制 |
记住一条黄金法则:
🔑文本相关性 → query;结构化条件筛选 → filter
五、实战 DSL 设计:如何组合使用?
示例 1:电商商品搜索(典型场景)
GET /products/_search { "query": { "bool": { "must": [ { "match": { "title": "蓝牙耳机" } } ], "filter": [ { "term": { "brand.keyword": "Apple" } }, { "range": { "price": { "gte": 800, "lte": 2000 } } }, { "term": { "in_stock": true } } ] } }, "sort": [ { "_score": "desc" }, { "sales_count": "desc" } ] }解析一下执行流程:
- 先跑 filter 阶段:
- 加载或生成brand=Apple的 BitSet;
- 加载或生成price ∈ [800,2000]的 BitSet;
- 求交集,得到初步候选集; - 再跑 query 阶段:
- 在 filter 缩小后的范围内,对title字段做全文匹配;
- 计算_score; - 最终排序:先看相关性,再看销量。
这样做的好处是:
- 减少了参与打分的文档数量,节省 CPU;
- 多数用户的筛选条件相同,filter 缓存命中率高;
- 整体 QPS 提升明显。
示例 2:日志告警查询(只关心“有没有”)
有时候我们根本不在乎哪个日志“更相关”,只想知道:“过去一小时有没有 ERROR 日志?”
这时可以用constant_score强制进入 filter context:
GET /logs/_search { "query": { "constant_score": { "filter": { "bool": { "must": [ { "term": { "level": "ERROR" } }, { "range": { "@timestamp": { "gte": "now-1h/h" } } } ] } }, "boost": 1.0 } } }效果是:
- 所有匹配的日志统一返回_score=1.0;
- 查询完全走 filter 流程,极致高效;
- 支持缓存,非常适合定时轮询任务。
六、缓存机制详解:哪些 filter 会被缓存?
别以为所有 filter 都能自动进缓存!Elasticsearch 很聪明,但也有限制。
✅ 默认会被缓存的 filter 类型:
| 查询类型 | 是否缓存 |
|---|---|
term,terms | ✔️ |
range(数值/日期) | ✔️ |
exists | ✔️ |
prefix(前缀匹配) | ✔️ |
geo_bounding_box | ✔️ |
❌ 不会被缓存的情况:
- 查询中包含脚本(
script); - 使用了
wildcard、regexp等高成本查询(即使放在 filter 中); - 单次请求中动态生成的条件(如带时间戳的 range);
- 高基数字段(如 uid、session_id),容易导致 BitSet 过大,系统主动禁用缓存。
⚙️ 缓存配置项
# elasticsearch.yml indices.queries.cache.size: 10% # 默认占 JVM heap 的 10% indices.queries.cache.expire: # 已废弃,现在基于 LRU 自动淘汰你可以通过以下 API 查看缓存状态:
GET /_nodes/stats/indices/query_cache?pretty关键指标包括:
-hit_count:缓存命中次数;
-miss_count:未命中次数;
-evictions:被淘汰的条目数;
-memory_size_in_bytes:当前占用内存。
💡 如果发现
evictions频繁,说明缓存太小或查询太分散,建议调整大小或优化 filter 设计。
七、常见误区与避坑指南
❌ 错误做法 1:把 term 放进 query
"query": { "term": { "status": "active" } }虽然语法没错,但这是浪费!term查询本身不需要评分,放query里会导致:
- 多余的_score计算;
- 无法利用缓存;
- 性能下降。
✅ 正确做法:
"filter": [ { "term": { "status": "active" } } ]❌ 错误做法 2:在 filter 中使用 match
"filter": [ { "match": { "title": "error occurred" } } ]match是文本分析型查询,必然涉及分词和评分逻辑,即便放在filter中也无法缓存!
✅ 应改为:
"query": { "match": { "title": "error occurred" } }或者如果你真的只需要“包含关键词”的布尔判断,可以用query_string+constant_score包裹:
"constant_score": { "filter": { "query_string": { "query": "title:error AND title:occurred" } } }但注意:query_string一般也不会被缓存。
八、架构设计启示:搜索系统的分层思维
成熟的搜索服务通常采用如下分层结构:
用户输入 ↓ ┌──────────────────────┐ │ Query Layer │ ← 文本相关性匹配(match) │ - 关键词分词 │ │ - 相关性打分 │ └──────────┬───────────┘ ↓ ┌──────────────────────┐ │ Filter Layer │ ← 结构化条件过滤 │ - brand, price_range │ │ - in_stock, category │ │ - BitSet 缓存复用 │ └──────────┬───────────┘ ↓ 最终文档集合(取交集) ↓ ┌──────────────────────┐ │ Sort & Highlight │ │ - 按 _score 排序 │ │ - 高亮关键词 │ └──────────────────────┘这就是所谓的“主搜靠 query,过滤靠 filter”原则。
将高频、稳定的筛选条件下沉到 filter 层,既能提升性能,又能增强系统可扩展性。
九、面试加分回答:如何证明你真懂?
当面试官问完“区别是什么”之后,往往会追加一句:“那你在项目里是怎么用的?”
这时候不要只说概念,展示一点工程思维:
“我们在做一个电商平台的商品搜索功能时,最初把品牌、价格区间都放在
must里做match查询,结果大促期间集群负载飙升。后来我们重构了 DSL,把这些结构化字段全部移到bool.filter中,并启用了 Query Cache。监控显示缓存命中率达到 75% 以上,平均响应时间从 90ms 降到 22ms,QPS 提升了近 3 倍。”
这样的回答,既有理论深度,又有实战价值,远超单纯背诵定义的人。
写在最后
“filter 和 query 的区别”这个问题,表面上是个语法题,实际上是一道关于性能优化、缓存机制、系统设计的综合性考察。
掌握它的关键,不在于死记硬背表格对比,而在于理解背后的哲学:
Elasticsearch 是一个“相关性引擎”,但它也必须是一个“高性能数据库”。
query解决“多相关”,filter解决“快筛选”。
把两者合理分工,才能让搜索既准又快。
下次遇到这个面试题,不妨笑着回答:
“它们的区别,就像一个是阅卷老师,一个是安检门——一个打分,一个过筛。”
相信我,面试官一定会眼前一亮。
如果你正在准备 ES 相关的技术面试,欢迎收藏本文,反复阅读,真正做到“知其然,更知其所以然”。