在使用 INFINI Easysearch 开发应用时,你是否遇到过这样的疑惑:
- “为什么加上了时间范围限制,查询反而变慢了?”
- “同样的查询,为什么第二次执行比第一次快那么多?”
- “在高并发场景下,CPU 占用率居高不下?”
很多时候,这些问题的答案都指向同一个优化点:合理使用 Filter Context(过滤上下文)。
在 Easysearch 中,并不是所有的搜索都需要计算相关性评分(Score)。学会区分“需要评分”和“不需要评分”的场景,并利用 Filter 缓存机制,是提升集群吞吐量最简单、最有效的手段。
1. 评分 vs. 过滤:两个平行世界 #
Easysearch 的 DSL 查询在执行时,会工作在两种不同的上下文中:
1.1 Query Context(查询上下文) #
- 核心问题:“这个文档与查询条件的匹配程度如何?”
- 行为:除了判断文档是否匹配外,还会计算
_score(相关性评分)。 - 成本:计算评分需要占用 CPU 资源。
- 典型场景:全文检索(
match)、模糊查询。
1.2 Filter Context(过滤上下文) #
- 核心问题:“这个文档是否匹配?”
- 行为:只回答“是(Yes)”或“否(No)”,不计算评分(Score 固定为 0 或 1)。
- 成本:极低。
- 优势:结果会被 Easysearch 自动缓存。
- 典型场景:状态筛选(
status: published)、时间范围(range)、分类标签、ID 查找。
2. 为什么 Filter 会快?揭秘 BitSet 缓存 #
当你使用 Filter 时,Easysearch 为了加速后续相同的查询,会使用一种被称为 BitSet(位图) 的数据结构来缓存结果。
什么是 BitSet? #
想象一个巨大的二进制数组,长度等于索引中的文档总数。
1代表匹配。0代表不匹配。
假设索引有 100 万个文档,我们在 status 字段上执行了一个 Filter 查询:term: { "status": "active" }。
Easysearch 会生成类似这样的 BitSet:[1, 0, 0, 1, 1, ...]。
BitSet 的魔力: #
- 内存占用极小:100 万个文档的状态,只需要 100 万个 bit,大约仅需 125 KB 内存。
- 加载速度极快:CPU 处理位运算的速度是纳秒级的。
- 复用性强:
- 当下一个用户查询“2023年发布的文章”且状态为“active”时,Easysearch 只需要分别取出“2023年”的 BitSet 和“active”的 BitSet。
- 执行一次快速的按位 AND 操作。
- 瞬间得出交集结果,完全不需要去读取磁盘上的原始索引数据。
这就是为什么 Filter 缓存能让搜索“飞起来”的底层逻辑。
3. 实战:如何将 Query 改造为 Filter #
让我们看一个经典的优化案例。
优化前:全部在 Query 中(性能较差) #
假设我们要搜索“手机”,且品牌必须是“Apple”,价格必须大于 5000,且必须有库存。
# term 浪费:我们在计算 "Apple" 的相关性
# range 浪费:价格不需要评分
GET /products/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"title": "手机"
}
},
{
"term": {
"brand": "Apple"
}
},
{
"range": {
"price": {
"gte": 5000
}
}
},
{
"term": {
"in_stock": true
}
}
]
}
}
}
在这个查询中,Easysearch 会尝试计算每个文档对“品牌是 Apple”的匹配得分——这完全没有意义,因为品牌要么是 Apple 要么不是,不存在“这个手机比那个手机更像 Apple”这种说法。
优化后:动静分离(性能极佳) #
我们将全文检索(需要评分)保留在 must 中,将精确匹配(不需要评分)移入 filter 中。
# match 计算评分
# filter 走缓存
GET /products/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"title": "手机"
}
}
],
"filter": [
{
"term": {
"brand": "Apple"
}
},
{
"range": {
"price": {
"gte": 5000
}
}
},
{
"term": {
"in_stock": true
}
}
]
}
}
}
优化收益:
- CPU 减负:跳过了 3 个条件的评分计算。
- 缓存加速:
brand: Apple和in_stock: true这种高频条件生成的 BitSet 会常驻内存。后续任何包含这些条件的查询都会受益。
4. 最佳实践总结 #
为了在 Easysearch 中获得最佳性能,请遵循以下原则:
- 能用 Filter 就别用 Query:
- 如果你不需要按相关度排序(例如日志查询、按时间排序的订单列表),甚至可以直接使用
bool: { "filter": [...] }包裹所有条件,这通常是最高效的查询方式。
- 精确匹配字段用 Keyword:
- 确保用于 Filter 的字段(如 ID、枚举值、Tag)在 Mapping 中映射为
keyword类型,或者是数值/日期类型。Text 类型字段上的 Filter 效率较低且消耗内存。
- 把最苛刻的过滤条件放在前面:
- 虽然 Easysearch 内部会自动优化执行顺序,但在编写 DSL 时,习惯性地将能过滤掉最多数据的条件放在前面是一个好习惯。
- 利用 Date Histogram:
- 对于时间范围查询(Range),Easysearch 只能缓存具体的范围(如
now-1h到now)。由于时间一直在变,缓存很难命中。 - 技巧:如果是历史数据分析,尽量将时间范围对齐到整点或整天,这样生成的 BitSet 可以被反复利用。
结语 #
Filter 缓存是 Easysearch/Lucene 架构中一项优雅且强大的设计。通过简单地将 DSL 中的 must 移动到 filter,你不仅能获得更快的响应速度(毫秒级),还能显著降低集群的 CPU 负载,从而节省硬件成本。
记住这句口诀:搜文本用 Match,筛数据用 Filter。





