从根上理解elasticsearch(lucene)查询原理(1)-lucece查询逻辑介绍
大家好,我是蓝胖子,最近在做一些elasticsearch 慢查询优化的事情,通常用分析elasticsearch 慢查询的时候可以通过profile api 去分析,分析结果显示的底层lucene在搜索过程中使用到的函数调用。所以要想彻底弄懂elasticsearch慢查询的原因,还必须将lucene的查询原理搞懂,今天我们就先来介绍下lucene的查询逻辑的各个阶段。
lucene 查询过程分析
先放上一张查询过程的流程图,下面的分析其实都是对这张图的更详细的介绍。

lucene的查询可以大致分为4个阶段,重写查询,创建查询weight对象,创建scorer对象准备计分,进行统计计分。
简单解释下这4个阶段;
1, 重写查询语句( rewrite query )
lucene提供了比较丰富的外部查询类型,像wildcardQuery,MatchQuery等等,但它们最后都会替换为比较底层的查询类型,例如wildcardQuery会被重写为MultiTermsQuery。
2, 创建查询weight对象( createWeight )
Query对象创建的权重对象, lucece的每个查询都会计算一个该查询占用的权重值,如果是不需要计分的,则权重值是一个固定常量,得到的文档结果是根据多个查询的权重值计算其得分的。下面是Weight 对象涉及的方法,

其中,scorer(LeafReaderContext context) 方法是个抽象方法,需要子类去实现的。
public abstract Scorer scorer(LeafReaderContext context) throws IOException;
方法返回的scorer对象拥有遍历倒排列表和统计文档得分的功能,下面会讲到实际上weight对象是创建BulkScore进行计分的,但BulkScore内部还是通过score对象进行计分。

再详细解释下Scorer对象中比较重要的方法;
- iterator() 方法返回的DocIdSetIterator 对象提供了遍历倒排列表的能力。如下是DocIdSetIterator 涉及的方法,其中docID()是为了返回当前遍历到的倒排列表的文档id,nextDoc()则是将遍历指针移动到下一个文档,并且返回文档id,advance 用于移动遍历指针。

- twoPhaseIterator 方法提供对文档二次精准匹配的能力,比如在matchPhrase查询中,不但要查出某个词,还要求查出的词之间相对顺序不变,那么这个相对顺序则是通过twoPhaseIterator的matches方法去进行判断。

3, 创建bulkScorer对象( weight.bulkScore)
weight 对象会调用BulkScore方法创建BulkScorer对象,bulkScorer 内部首先调用的是scorer抽象方法(需要由weight子类去实现的方法),得到的scorer对象再拿去构建DefaultBulkScorer 对象,所以说,实际上最后计分的还是通过scorer对象进行计分的。
public BulkScorer bulkScorer(LeafReaderContext context) throws IOException {
Scorer scorer = scorer(context);
if (scorer == null) {
// No docs match
return null;
}
// This impl always scores docs in order, so we can
// ignore scoreDocsInOrder:
return new DefaultBulkScorer(scorer);
}
bulkScorer类有如下方法,一个是提供对段所有文档进行计分,一个是可以在段的某个文档id范围内进行计分。

4, 进行统计计分
最后则是通过collector对象进行统计,这里提到了collecor对象,它其实是作为了上述bulkScorer的score方法参数传入的,在bulkScore.score方法内部,遍历文档时,对筛选出的文档会通过调用collector.collect(doc)方法进行收集,在collect方法内部,则是调用scorer对象对文档进行打分。

完整的搜索流程如下
public <C extends Collector, T> T search(Query query, CollectorManager<C, T> collectorManager)
throws IOException {
final C firstCollector = collectorManager.newCollector();
// 重写查询对象
query = rewrite(query, firstCollector.scoreMode().needsScores());
// 调用indexSearch的createWeight方法,本质上还是调用的Query的createWeight方法
final Weight weight = createWeight(query, firstCollector.scoreMode(), 1);
return search(weight, collectorManager, firstCollector);
}
// 简化了代码,保留了主流程,调用scorer.score 进行计分。
protected void search(List<LeafReaderContext> leaves, Weight weight, Collector collector){
// 得到每个segment段的收集器,源代码是可以在线程池中同时对几个segment进行搜索的,这里省略了。
leafCollector = collector.getLeafCollector(ctx);
BulkScorer scorer = weight.bulkScorer(ctx);
// 将收集器作为buklScore.score参数传入,对文档进行计分。
scorer.score(leafCollector, ctx.reader().getLiveDocs());
leafCollector.finish();
}

profile api 返回结果分析
理清楚了lucene的搜索逻辑,我们再来看看通过profile api返回的各个阶段耗时是统计的哪段逻辑。
在使用elasticsearch 的profile api 时,会返回如下的统计阶段

如果不了解源码可能会对这些统计指标比较疑惑,结合刚才对lucece 源码的了解来看下几个比较常见的统计指标。
next_doc 是取倒排链表中当前遍历到的文档id,并且把遍历的指针移动到下一个文档id消耗的时长。
score 是weight.scorer方法创建的score对象,进行文档计分的操作时消耗的时长。
match 是 twoPhaseIterator进行二次匹配判断时消耗的时长。
advance 是直接将遍历的指针移动到特定文档id处消耗的时长。
build_score 是weight对象在通过weight.scorer方法创建score对象时所耗费的时长。
create_weight 是query对象在调用其自身createWeight方法创建weight对象时耗费的时长。
set_min_competitive_score,compute_max_score,shallow_advance 我也还没彻底弄懂它们用到的所有场景,这里暂不做分析。
这里还要注意的一点是,像布尔查询是结合了多个子查询的结果,它内部会构造特别的scorer对象,比如ConjunctionScorer 交集scorer,它的next_doc 方法则是需要对其子查询的倒排链表求交集,所以你在用profile api 分析时,可能会看到布尔查询的next_doc 耗时较长,而其子查询耗时较长的逻辑则是advance,因为倒排列表合并逻辑会有比较多的advance移动指针的动作。
profile api 的实现原理
最后,我再来谈谈elasticsearch 是如何实现profile 的,lucene的搜索都是通过IndexSearcher对象来执行的,IndexSearcher在调用query对象自身的rewrite 方法重写query后,会调用IndexSearcher 的createWeight 方法来创建weight对象(本质上底层还是使用的query的createWeight方法)。
elasticsearch 继承了IndexSearcher ,重写了createWeight,在原本weight对象的基础上,封装了一个profileWeight对象。以下是关键代码。
public Weight createWeight(Query query, ScoreMode scoreMode, float boost) throws IOException {
if (profiler != null) {
// createWeight() is called for each query in the tree, so we tell the queryProfiler
// each invocation so that it can build an internal representation of the query // tree QueryProfileBreakdown profile = profiler.getQueryBreakdown(query);
Timer timer = profile.getNewTimer(QueryTimingType.CREATE_WEIGHT);
timer.start();
final Weight weight;
try {
weight = query.createWeight(this, scoreMode, boost);
} finally {
timer.stop();
profiler.pollLastElement();
}
return new ProfileWeight(query, weight, profile);
} else {
return super.createWeight(query, scoreMode, boost);
}
}
基于文章开头的lucene查询逻辑分析,可以知道,scorer对象最后也是通过weight对象的scorer方法得到的,所以创建出来的profileWeight的scorer方法通用也对返回的scorer对象封装了一层,返回的是profileScorer对象。
public Scorer scorer(LeafReaderContext context) throws IOException {
ScorerSupplier supplier = scorerSupplier(context);
if (supplier == null) {
return null;
}
return supplier.get(Long.MAX_VALUE);
}
@Override
public ScorerSupplier scorerSupplier(LeafReaderContext context) throws IOException {
final Timer timer = profile.getNewTimer(QueryTimingType.BUILD_SCORER);
timer.start();
final ScorerSupplier subQueryScorerSupplier;
try {
subQueryScorerSupplier = subQueryWeight.scorerSupplier(context);
} finally {
timer.stop();
}
if (subQueryScorerSupplier == null) {
return null;
}
final ProfileWeight weight = this;
return new ScorerSupplier() {
@Override
public Scorer get(long loadCost) throws IOException {
timer.start();
try {
return new ProfileScorer(weight, subQueryScorerSupplier.get(loadCost), profile);
} finally {
timer.stop();
}
}
@Override
public long cost() {
timer.start();
try {
return subQueryScorerSupplier.cost();
} finally {
timer.stop();
}
}
};
}
剩下的就好办了,在profileScore对象里对scorer对象的原生方法前后加上时间统计即可对特定方法进行计时了。比如下面代码中profileScore的advanceShallow方法。
public int advanceShallow(int target) throws IOException {
shallowAdvanceTimer.start();
try {
return scorer.advanceShallow(target);
} finally {
shallowAdvanceTimer.stop();
}
}
总结
通过本篇文章,应该可以对lucene的查询过程有了大概的了解,但其实对于elasticsearch的慢查询分析还远远不够,因为像布尔查询,wilcard之类的比较复杂的查询,我们还得弄懂,它们底层是究竟如何把一个大查询分解成小查询的。才能更好的弄懂查询耗时的原因,所以在下一节,我会讲解这些比较常见的查询类型的内部重写和查询逻辑。
从根上理解elasticsearch(lucene)查询原理(1)-lucece查询逻辑介绍的更多相关文章
- 从查询重写角度理解elasticsearch的高亮原理
一.高亮的一些问题 elasticsearch提供了三种高亮方式,前面我们已经简单的了解了elasticsearch的高亮原理; 高亮处理跟实际使用查询类型有十分紧密的关系,其中主要的一点就是muti ...
- 彻底理解MapReduce shuffle过程原理
彻底理解MapReduce shuffle过程原理 MapReduce的Shuffle过程介绍 Shuffle的本义是洗牌.混洗,把一组有一定规则的数据尽量转换成一组无规则的数据,越随机越好.MapR ...
- ElasticSearch学习总结(二):ES介绍与架构说明
本文主要从概念以及架构层面对Elasticsearch做一个简单的介绍,在介绍ES之前,会先对ES的"发动机"Lucene做一个简单的介绍 1. Lucene介绍 为了更深入地理解 ...
- 基于Lucene查询原理分析Elasticsearch的性能
前言 Elasticsearch是一个很火的分布式搜索系统,提供了非常强大而且易用的查询和分析能力,包括全文索引.模糊查询.多条件组合查询.地理位置查询等等,而且具有一定的分析聚合能力.因为其查询场景 ...
- Elasticsearch Lucene 数据写入原理 | ES 核心篇
前言 最近 TL 分享了下 <Elasticsearch基础整理>https://www.jianshu.com/p/e8226138485d ,蹭着这个机会.写个小文巩固下,本文主要讲 ...
- Lucene 查询原理 传统二级索引方案 倒排链合并 倒排索引 跳表 位图
提问: 1.倒排索引与传统数据库的索引相比优势? 2.在lucene中如果想做范围查找,根据上面的FST模型可以看出来,需要遍历FST找到包含这个range的一个点然后进入对应的倒排链,然后进行求并集 ...
- Elasticsearch原理学习--为什么Elasticsearch/Lucene检索可以比MySQL快?
转载于:http://vlambda.com/wz_wvS2uI5VRn.html 同样都可以对数据构建索引并通过索引查询数据,为什么Lucene或基于Lucene的Elasticsearch会比关系 ...
- Elasticsearch系列---聚合查询原理
概要 本篇主要介绍聚合查询的内部原理,正排索引是如何建立的和优化的,fielddata的使用,最后简单介绍了聚合分析时如何选用深度优先和广度优先. 正排索引 聚合查询的内部原理是什么,Elastich ...
- 从原理上理解MySQL的优化建议
从原理上理解MySQL的优化建议 预备知识 B+树索引 mysql的默认存储引擎InnoDB使用B+树来存储数据的,所以在分析优化建议之前,了解一下B+树索引的基本原理. 上图是一个B+树索引示意图, ...
- 读《深入理解Elasticsearch》点滴-查询评分
计算文档得分的因子: 文档权重(document boost):索引期赋予某个文档的权重值 字段权重(field boost):查询期赋予某个文档的权重值 协调因子(coord):基于文档中词项个数的 ...
随机推荐
- 【opencv】传统图像识别:hog+svm实现图像识别详解
图像识别技术是信息时代的一门重要的技术,其产生目的是为了让计算机代替人类去处理大量的物理信息.传统图像识别技术的过程分为信息的获取.预处理.特征抽取和选择.分类器设计和分类决策.本文也是从这四点出发进 ...
- 【升职加薪秘籍】我在服务监控方面的实践(7)-业务维度的redis监控
大家好,我是蓝胖子,关于性能分析的视频和文章我也大大小小出了有一二十篇了,算是已经有了一个系列,之前的代码已经上传到github.com/HobbyBear/performance-analyze,接 ...
- 动态规划二 & 贪心算法 实验三
一.贪心算法和动态规划法解决背包问题. 有一个背包其容积 C = 13.现有表格内的物品可以购买. 商品 价格 P 体积 V 啤酒 24 10 汽水 2 3 饼干 9 4 面包 10 5 牛奶 9 4 ...
- phpstudy搭建虚拟域名
phpstudy搭建虚拟域名 先使用phpstudy搭建好对应的环境 打开phpstudy控制面板,点击 其他选项菜单,选择 站点域名管理 输入网站虚拟域名,比如我这里DVWA靶场,我就写 www.d ...
- iframe标签下的通信
通常在页面中嵌套iframe的情况下还需要进行消息传递的通信需求.一般分为两种情况: 1.iframe里的链接与父页面链接是非跨域 这种情况处理比较简单,直接在父级页面下就可以写脚本控制iframe里 ...
- WASI support in Go
原文在这里. 由 Johan Brandhorst-Satzkorn, Julien Fabre, Damian Gryski, Evan Phoenix, and Achille Roussel 发 ...
- zxy 简单 dp 大讲堂
讲课讲得非常清楚啊,我绝赞膜拜.节奏可以,思路清晰,解法自然,为讲师点赞. 第一个题是 loj3282 / joisc2020 - Treatment Project.原问题由 \(\left(S, ...
- 递归解析Json,实现生成可视化Tree+快速获取JsonPath
内部平台的一个小功能点的实现过程,分享给大家: 递归解析Json,可以实现生成可视化Tree+快速获取JsonPath. 步骤: 1.利用JsonPath读取根,获取JsonObject 2.递归层次 ...
- 教育法学期末考试02MOOC
期末考试 返回 期末考试试卷为客观题,总分为100分,占课程成绩的40%.其中包含16道单选题,2道多选题.共18道题.单选题每道5分,多选题每道10分,限时90分钟完成. 倒计时: 01:21: ...
- .Net析构函数再论(CLR源码级的剖析)
前言 碰到一些问题,发觉依旧没有全面了解完全析构函数.本篇继续看下析构函数的一些引申知识. 概述 析构函数目前发现的总共有三个标记,这里分别一一介绍下.先上一段代码: internal class P ...