📣 极限科技诚招搜索运维工程师(Elasticsearch/Easysearch)- 全职/北京 👉 : 立即申请加入

在使用 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 的魔力: #

  1. 内存占用极小:100 万个文档的状态,只需要 100 万个 bit,大约仅需 125 KB 内存。
  2. 加载速度极快:CPU 处理位运算的速度是纳秒级的。
  3. 复用性强
  • 当下一个用户查询“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
          }
        }
      ]
    }
  }
}

优化收益:

  1. CPU 减负:跳过了 3 个条件的评分计算。
  2. 缓存加速brand: Applein_stock: true 这种高频条件生成的 BitSet 会常驻内存。后续任何包含这些条件的查询都会受益。

4. 最佳实践总结 #

为了在 Easysearch 中获得最佳性能,请遵循以下原则:

  1. 能用 Filter 就别用 Query
  • 如果你不需要按相关度排序(例如日志查询、按时间排序的订单列表),甚至可以直接使用 bool: { "filter": [...] } 包裹所有条件,这通常是最高效的查询方式。
  1. 精确匹配字段用 Keyword
  • 确保用于 Filter 的字段(如 ID、枚举值、Tag)在 Mapping 中映射为 keyword 类型,或者是数值/日期类型。Text 类型字段上的 Filter 效率较低且消耗内存。
  1. 把最苛刻的过滤条件放在前面
  • 虽然 Easysearch 内部会自动优化执行顺序,但在编写 DSL 时,习惯性地将能过滤掉最多数据的条件放在前面是一个好习惯。
  1. 利用 Date Histogram
  • 对于时间范围查询(Range),Easysearch 只能缓存具体的范围(如 now-1hnow)。由于时间一直在变,缓存很难命中。
  • 技巧:如果是历史数据分析,尽量将时间范围对齐到整点或整天,这样生成的 BitSet 可以被反复利用。

结语 #

Filter 缓存是 Easysearch/Lucene 架构中一项优雅且强大的设计。通过简单地将 DSL 中的 must 移动到 filter,你不仅能获得更快的响应速度(毫秒级),还能显著降低集群的 CPU 负载,从而节省硬件成本。

记住这句口诀:搜文本用 Match,筛数据用 Filter。