压榨 ES,从 numeric 到 keyword,秒变闪电侠

之前有位小伙伴期望在不改代码的情况下优化 elasticsearch 查询,( 简化 ) 查询语句如下:

json 复制代码
{
 "query": {
   "term": {
     "status": 1
   }
 }
}

以下数据来自测试环境,文档数据量:135999,status 值分布: { "key" : 1, "doc_count" : 131371 }, { "key" : 3, "doc_count" : 206 }, { "key" : 2, "doc_count" : 33 } ,数据均为新建立 index。

Profile 该查询语句有如下:

从上图中可以看到 PointRangeQuery。了解到对该字段仅有等值配对场景 ( 类似于 where status = ? ),对字段类型从 long 改为 keyword ( 手动创建 mapping 然后 reindex,当然也就意味着语义的改变 ),有如下优化结果:

可以看到的性能有及其显著提升 3.943ms -> 0.137ms ( 数据量越大性能优势越明显 ),此时查询类型由之前 PointRangeQuery 变成 TermQuery,在BUILD_SCORER、NEXT_DOC 两部分有巨大的性能提升 。 关于如何选择数据类型,ES 的官方文档上有恰当说明:

Mapping numeric identifiers Not all numeric data should be mapped as a numeric field data type. Elasticsearch optimizes numeric fields, such as integer or long, for range queries. However, keyword fields are better for term and other term-level queries. Identifiers, such as an ISBN or a product ID, are rarely used in range queries. However, they are often retrieved using term-level queries. Consider mapping a numeric identifier as a keyword if:      You don't plan to search for the identifier data using range queries.      Fast retrieval is important. term query searches on keyword fields are often faster than term searches on numeric fields. If you're unsure which to use, you can use a multi-field to map the data as both a keyword and a numeric data type.

带着求知欲,接下来来看看为何会有如此显著的性能差距,结合图中的 PointRangeQuery / TermQuery 和 BUILD_SCORER、NEXT_DOC 关键字,可以搜索到如下源码:

先来看 PointRangeQuery

scss 复制代码
@Override
public Scorer get(long leadCost) throws IOException {
  if (values.getDocCount() == reader.maxDoc()
      && values.getDocCount() == values.size()
      && cost() > reader.maxDoc() / 2) {
    // If all docs have exactly one value and the cost is greater
    // than half the leaf size then maybe we can make things faster
    // by computing the set of documents that do NOT match the range
    final FixedBitSet result = new FixedBitSet(reader.maxDoc());
    result.set(0, reader.maxDoc());
    int[] cost = new int[] { reader.maxDoc() };
    values.intersect(getInverseIntersectVisitor(result, cost));
    final DocIdSetIterator iterator = new BitSetIterator(result, cost[0]);
    return new ConstantScoreScorer(weight, score(), scoreMode, iterator);
  }

  values.intersect(visitor);
  DocIdSetIterator iterator = result.build().iterator();
  return new ConstantScoreScorer(weight, score(), scoreMode, iterator);
}

上述方法方法中便对应的是 scorer 的构建逻辑,有两个逻辑点:

  1. PointValues-- Points represent numeric values and are indexed differently than ordinary text. Instead of an inverted index, points are indexed with datastructures such as KD-trees . These structures are optimized for operations such as range, distance, nearest-neighbor, and point-in-polygon queries.
csharp 复制代码
/** Finds all documents and points matching the provided visitor.
 *  This method does not enforce live documents, so it's up to the caller
 *  to test whether each document is deleted, if necessary. */
public abstract void intersect(IntersectVisitor visitor) throws IOException;

针对 numeric values,lucene 使用 KD-tree 进行组织,在查询的时候,通过索引数据读取 leaf block 进行过滤设置 ( 对应代码片段中的 values.intersect ) ,这其中的耗时便包括 DocIdSetIterator的构建和 PointValues 的处理 ( BUILD_SCORER )。

  1. BitSetIterator--A DocIdSetIterator which iterates over set bits in a bit set.
java 复制代码
private final BitSet bits;

@Override
public int nextDoc() {
  return advance(doc + 1);
}

@Override
public int advance(int target) {
  if (target >= length) {
    return doc = NO_MORE_DOCS;
  }
  return doc = bits.nextSetBit(target);
}

再来看 TermQuery

java 复制代码
@Override
public Scorer scorer(LeafReaderContext context) throws IOException {
  assert termStates == null || termStates.wasBuiltFor(ReaderUtil.getTopLevelContext(context)) : "The top-reader used to create Weight is not the same as the current reader's top-reader (" + ReaderUtil.getTopLevelContext(context);;
  final TermsEnum termsEnum = getTermsEnum(context);
  if (termsEnum == null) {
    return null;
  }
  LeafSimScorer scorer = new LeafSimScorer(simScorer, context.reader(), term.field(), scoreMode.needsScores());
  if (scoreMode == ScoreMode.TOP_SCORES) {
    return new TermScorer(this, termsEnum.impacts(PostingsEnum.FREQS), scorer);
  } else {
    return new TermScorer(this, termsEnum.postings(null, scoreMode.needsScores() ? PostingsEnum.FREQS : PostingsEnum.NONE), scorer);
  }
}

可以看到相对于之前的 PointRangeQuery,获取到 TermsEnum 便完成 scorer 的构建 ( BUILD_SCORER ),而这实际上就是简单从文件读取 posting list。而对于 NEXT_DOC 操作 ( BlockDocsEnum ),**相比之下:一方面是少了位操作;另一方面是 O(n) 最优时间复杂度 ( n 为满足条件的文档数 )。

ini 复制代码
private final long[] docBuffer = new long[BLOCK_SIZE+1];

@Override
public int nextDoc() throws IOException {
  if (docBufferUpto == BLOCK_SIZE) {
    refillDocs(); // we don't need to load freqBuffer for now (will be loaded later if necessary)
  }

  doc = (int) docBuffer[docBufferUpto];
  docBufferUpto++;
  return doc;
}

线上效果:

优化前:
优化后:
相关推荐
诚丞成1 小时前
ELK日志分析:Elasticsearch索引、Logstash接入与Kibana可视化
elk·elasticsearch·jenkins
Elasticsearch11 小时前
Elastic 被评为 2026 年 Gartner®《企业 AI 搜索魔力象限™》领导者
elasticsearch
ly768914 小时前
Elasticsearch 分片分配与再平衡的底层逻辑:从 allocation decider 到集群扩容抖动
大数据·elasticsearch·搜索引擎·集群扩容·分片分配·磁盘水位线
Elasticsearch19 小时前
使用 Lucene 搜索你的 beans —— Facets
elasticsearch
溪语流沙19 小时前
【Web全栈进阶】Alembic数据库迁移:改表结构不再删库
前端·数据库·python·elasticsearch·postgresql
ly76891 天前
Elasticsearch 写入链路的 refresh、flush 与 translog 边界:为什么 bulk 吞吐会突然塌陷
大数据·elasticsearch·搜索引擎·translog·写入链路·bulk调优
Elasticsearch1 天前
AWS 自动化根因分析:从 CloudWatch 告警到完成故障诊断,仅需 36 秒
elasticsearch
欢醉1 天前
基于日志的监控告警:我们是怎么用一套轻量方案把线上问题"盯"住的
elasticsearch·springcloud·日志
Elastic 中国社区官方博客2 天前
使用 Lucene 搜索你的 Bean — 索引
java·大数据·数据库·elasticsearch·搜索引擎·全文检索·lucene
Elasticsearch2 天前
FSCrawler 3.0 来了!你知道的,就是用来处理文件的!
elasticsearch