Elasticsearch Filter 缓存:Bitset 如何让查询速度飙升

用过 Elasticsearch 的同学大概率都听过 "把过滤条件放 filter 里更快",但很少有人说清背后到底快在哪。今天我们就用一个电商订单索引的真实场景,一步步拆解 Filter 的 Bitset 缓存工作原理,让你彻底搞懂这个性能优化的关键!

一、先搭个场景:电商订单索引

假设我们有一个orders索引,存了 10 条订单数据(文档 ID 1~10),字段结构很简单:

  • user_id:用户 ID(keyword 类型,精准匹配)
  • order_time:下单时间(date 类型,时间戳存储)
  • status:订单状态(keyword 类型:paid/unpaid/refunded)

现在要查 "用户 1001 在 2025 年 6 月之后下的订单",对应的 DSL 是:

json 复制代码
{
  "query": {
    "bool": {
      "filter": [
        { "term": { "user_id": "1001" } },
        { "range": { "order_time": { "gte": 1748419000 } } }
      ]
    }
  }
}

接下来,我们就跟着这个查询,看看 Bitset 缓存是怎么工作的。

二、第一次查询:从零生成 Bitset

当这个查询第一次执行时,ES 还没有任何缓存,需要走完整的 "扫描 - 生成 - 缓存" 流程。

1. 第一步:为user_id=1001生成 Bitset

Bitset(位图)是个超轻量的二进制结构 ------每一位对应一个文档 ID,匹配标 1,不匹配标 0

  • ES 先扫user_id的倒排索引,发现文档 2、5、7、9 属于用户 1001;
  • 生成 Bitset:0 1 0 0 1 0 1 0 1 0(第 2、5、7、9 位是 1);
  • 把这个 Bitset 存进内存缓存,贴上标签:user_id:1001

2. 第二步:为order_time>=1748419000生成 Bitset

同样的逻辑,处理时间范围条件:

  • order_time的倒排索引,找到文档 3、5、7、8、10 符合时间要求;
  • 生成 Bitset:0 0 1 0 1 0 1 1 0 1(第 3、5、7、8、10 位是 1);
  • 缓存这个 Bitset,标签:order_time:>=1748419000

3. 第三步:位运算组合出最终结果

两个 Bitset 都有了,ES 会做AND 位运算(只有两位都为 1 才保留):

plaintext 复制代码
user_id=1001的Bitset:0 1 0 0 1 0 1 0 1 0
order_time>=...的Bitset:0 0 1 0 1 0 1 1 0 1
AND运算结果:0 0 0 0 1 0 1 0 0 0

一眼就能看出,只有文档 5、7 同时满足两个条件 ------ 这比逐行检查文档快了不止一个量级!

三、第二次查询:直接复用缓存,速度起飞

如果几分钟后,你又执行了一模一样的过滤条件,事情就简单了:

  1. ES 直接从内存里拽出之前缓存的两个 Bitset,不用再扫倒排索引
  2. 重复 AND 位运算,瞬间得到结果;
  3. 返回文档 5、7。

整个过程跳过了最耗时的 "扫描索引" 步骤,响应时间可能从几十毫秒降到几毫秒 ------ 这就是缓存的威力!

四、数据变了怎么办?Bitset 的智能更新

如果这时候新增了一条文档 11(user_id=1001,order_time=1748500000),Bitset 会怎么处理?

  • ES 不会删掉旧缓存,而是增量更新 :把user_id:1001的 Bitset 第 11 位设为 1,order_time:>=...的 Bitset 第 11 位也设为 1;
  • 下次查询时,结果里就会自动包含文档 11,既保证了准确性,又没浪费缓存。

五、Bitset 为啥这么牛?核心优势拆解

  1. 极致轻量化:1000 万个文档的 Bitset 只占约 1.2MB 内存(10^7 位 = 10^7/8/1024 ≈ 1220KB),亿级文档也才几十 MB;
  2. 运算超快:位运算是 CPU 原生支持的操作,比遍历文档快几个数量级;
  3. 缓存复用率高:缓存的是 "条件规则" 而非 "查询结果",只要过滤条件相同,不管查什么字段都能复用。

六、缓存的生命周期:何时过期?占用多少内存?

1. 缓存的内存占用规则

Bitset 缓存并非独立存在,而是属于 ES 的Node Query Cache (节点级查询缓存),其内存上限由参数indices.queries.cache.size控制:

  • 默认值为 JVM 堆内存的 10%(比如 JVM 堆是 31GB,缓存上限约 3.1GB);
  • 也可手动设置固定值(如512mb),避免占用过多堆内存。

以 1000 万文档的 Bitset 为例,单个条件的 Bitset 仅占 1.2MB 左右,3.1GB 的缓存空间足以存储数百万个过滤条件的 Bitset,完全满足常规业务需求。

2. 缓存何时 "过期"?

Bitset 缓存没有固定过期时间 ,而是遵循LRU(最近最少使用)淘汰策略

  • 高频使用的过滤条件(如user_id=1001)会长期驻留内存,反复复用;
  • 低频使用的条件,当缓存空间不足时,会被优先淘汰;
  • 若索引数据发生变更(新增 / 删除 / 更新文档),ES 会增量更新 受影响的 Bitset,而非直接淘汰 ------ 比如修改了文档 5 的user_id,仅把user_id:1001的 Bitset 第 5 位从 1 改为 0,其余位保持不变。

只有当缓存达到内存上限,且该 Bitset 长期未被访问时,才会被 LRU 机制清理。

七、和 must 比,filter 到底快在哪?

很多人问 "为啥 must 里的条件不能用 Bitset?"------ 因为must要算相关性评分(_score),必须逐文档计算;而filter只需要 "是 / 否" 的判断,刚好适配 Bitset 的二进制特性。

简单说:

  • must:既要匹配,又要评分 → 慢;
  • filter:只匹配,不评分 → 能用 Bitset 缓存 → 快!

八、最后划重点:Filter 缓存的实用建议

  1. 过滤条件必放 filter :尤其是term/range/terms这类不影响评分的条件;
  2. 缓存不用手动管:ES 会用 LRU 策略自动管理内存,默认占 JVM 堆的 10%,无需手动清理;
  3. 高频条件收益最大:比如按用户 ID、时间范围的过滤,缓存命中率极高;低频条件则可能被淘汰,无需纠结;
  4. 手动调整缓存大小 :若业务有大量高频过滤条件,可适当调高indices.queries.cache.size(如设为 20%),提升缓存命中率。
相关推荐
用户849137175471636 分钟前
ThreadLocal 源码深度解析:JDK 设计者的“妥协”与“智慧”
java·后端
木木一直在哭泣38 分钟前
Java Stream.filter 全面解析:定义、原理与最常见使用场景
后端
用户03048059126338 分钟前
# 【Maven避坑】源码去哪了?一文看懂 Maven 工程与打包后的目录映射关系
java·后端
绫语宁1 小时前
以防你不知道LLM小技巧!为什么 LLM 不适合多任务推理?
人工智能·后端
q***18841 小时前
Spring Boot中的404错误:原因、影响及处理策略
java·spring boot·后端
用户69371750013841 小时前
17.Kotlin 类:类的形态(四):枚举类 (Enum Class)
android·后端·kotlin
h***34631 小时前
MS SQL Server 实战 排查多列之间的值是否重复
android·前端·后端
用户69371750013841 小时前
16.Kotlin 类:类的形态(三):密封类 (Sealed Class)
android·后端·kotlin
马卡巴卡1 小时前
MySQL权限管理的坑你踩了没有?
后端