从根上理解elasticsearch(lucene)查询原理(1)-lucece查询逻辑介绍

大家好,我是蓝胖子,最近在做一些elasticsearch 慢查询优化的事情,通常用分析elasticsearch 慢查询的时候可以通过profile api 去分析,分析结果显示的底层lucene在搜索过程中使用到的函数调用。所以要想彻底弄懂elasticsearch慢查询的原因,还必须将lucene的查询原理搞懂,今天我们就先来介绍下lucene的查询逻辑的各个阶段。

lucene 查询过程分析

先放上一张查询过程的流程图,下面的分析其实都是对这张图的更详细的介绍。

未命名文件 (2).jpg

lucene的查询可以大致分为4个阶段,重写查询,创建查询weight对象,创建scorer对象准备计分,进行统计计分。

简单解释下这4个阶段;

1, 重写查询语句( rewrite query )

lucene提供了比较丰富的外部查询类型,像wildcardQuery,MatchQuery等等,但它们最后都会替换为比较底层的查询类型,例如wildcardQuery会被重写为MultiTermsQuery。

2, 创建查询weight对象( createWeight )

Query对象创建的权重对象, lucece的每个查询都会计算一个该查询占用的权重值,如果是不需要计分的,则权重值是一个固定常量,得到的文档结果是根据多个查询的权重值计算其得分的。下面是Weight 对象涉及的方法,

Pasted image 20231207173540.png

其中,scorer(LeafReaderContext context) 方法是个抽象方法,需要子类去实现的。

public abstract Scorer scorer(LeafReaderContext context) throws IOException;

方法返回的scorer对象拥有遍历倒排列表和统计文档得分的功能,下面会讲到实际上weight对象是创建BulkScore进行计分的,但BulkScore内部还是通过score对象进行计分。

Pasted image 20231207175531.png

再详细解释下Scorer对象中比较重要的方法;

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

Pasted image 20231207180951.png

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

Pasted image 20231207180839.png

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范围内进行计分。

Pasted image 20231207181829.png

4, 进行统计计分

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

Pasted image 20231207175514.png
完整的搜索流程如下

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();
}

未命名文件 (2).jpg

profile api 返回结果分析

理清楚了lucene的搜索逻辑,我们再来看看通过profile api返回的各个阶段耗时是统计的哪段逻辑。

在使用elasticsearch 的profile api 时,会返回如下的统计阶段

Pasted image 20231208133407.png

如果不了解源码可能会对这些统计指标比较疑惑,结合刚才对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之类的比较复杂的查询,我们还得弄懂,它们底层是究竟如何把一个大查询分解成小查询的。才能更好的弄懂查询耗时的原因,所以在下一节,我会讲解这些比较常见的查询类型的内部重写和查询逻辑。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/259579.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

UDP群聊

客户端 import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader…

hbuilder + uniapp +vue3 开发微信云小程序

1、创建项目&#xff1a; 2、创建项目完成的默认目录结构&#xff1a; 3、在根目录新建一个文件夹cloudFns&#xff08;文件名字随便&#xff09;&#xff0c;存放云函数源码&#xff1a; 4、修改manifest.json文件&#xff1a;添加 小程序 appid和cloudfunctionRoot&#xff0…

Docker安装Mysql数据库

1. 前言 XXXXX 2. Docker中安装MySQL服务 以下以mysql8.2版本为例&#xff0c;mysql5.7的步骤也是一样的 2.1. 查看可用的MySQL版本 # 搜索镜像 docker search mysql2.2. 拉取MySQL镜像 # 拉取镜像 docker pull mysql# 或者 docker pull mysql:latest2.3. 查看本地镜像 …

关于IDEA中maven的作用以及如何配置MAVEN

关于IDEA中maven的作用以及如何配置MAVEN 1、Maven是什么2、Idea中对于Maven的配置3、下载依赖时&#xff0c;Idea下方的显示3.1、Maven中央仓库的下载显示界面3.2、阿里云仓库的下载显示界面 4、Maven在Idea中的使用4.1、clean4.2、validate4.3、compile4.4、test&#xff08;…

数据结构-线性表的链式存储结构

术语&#xff1a; 1.结点&#xff1a;数据元素的存储映像。有数据域和指针域两部分组成。 2.链表&#xff1a;n个结点由指针组成一个链表 3.结点只有一个指针域的链表&#xff0c;成为单链表或线性链表。 4.结点有两个指针域的链表&#xff0c;成为双链表。 5.首尾相接的链…

8 个顶级的 PDF 转 Word 转换器

PDF 是跨不同平台分发信息而不影响内容格式的好方法。但这种安全级别确实有其缺点。没有直接的方法来编辑 PDF 上的文本或内容。编辑 PDF 文档的唯一方法是将其转换为 Word 文档或其他可以编辑的文件类型。将 PDF 转换为 Word 是根据需要编辑 PDF 内容的最快方法。有许多免费的…

Python中的深拷贝和浅拷贝的区别

目录 一、深拷贝和浅拷贝的概念 二、Python中的深拷贝和浅拷贝实现 三、深拷贝和浅拷贝的区别及适用场景 四、如何选择深拷贝和浅拷贝 五、总结 在Python中&#xff0c;深拷贝和浅拷贝是非常重要的概念&#xff0c;它们在处理对象和数据结构时有着截然不同的行为。理解深拷…

MN316 OpenCPU丨HTTP使用介绍

HTTP&#xff08;Hyper Text Transfer Protocol&#xff09;即超文本传输协议&#xff0c;是一个简单的请求-响应协议&#xff0c;通常运行在TCP之上&#xff0c;它指定了客户端可能发送给服务器消息类型以及得到什么类型响应。HTTPS&#xff08;Hyper Text Transfer Protoc…

Windows 系统,TortoiseSVN 无法修改 Log 信息解决方法

使用SVN提交版本信息时&#xff0c;注释内容写的不全。通过右键TortoiseSVN的Show log看到提交的的注释&#xff0c;右键看到Edit log message的选项&#xff0c;然而提交后却给出错误提示&#xff1a; Repository has not been enabled to accept revision propchanges; ask …

炫酷不止一面:探索JavaScript动画的奇妙世界(下)

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

wps word中图片 一保存失真变糊

在wps中依次点击 文件-文字偏好设置-常规与保存 勾选不压缩文件中的图像 并 将默认目标输出设置为220ppi 即可

c# 中间件简说

一、什么是中间件 中间件是一种装配到应用管道以处理请求和响应的软件。是介于request与response处理过程之间的一个插件(一道处理过程)&#xff0c;相对比较轻量级&#xff0c;并且在全局上会影响到request对象和response对象的属性。因为改变的是全局&#xff0c;所以需要谨…