基于Lucene查询原理分析Elasticsearch的性能

前言

Elasticsearch是一个很火的分布式搜索系统,提供了非常强大而且易用的查询和分析能力,包括全文索引、模糊查询、多条件组合查询、地理位置查询等等,而且具有一定的分析聚合能力。因为其查询场景非常丰富,所以如果泛泛的分析其查询性能是一个非常复杂的事情,而且除了场景之外,还有很多影响因素,包括机型、参数配置、集群规模等等。本文主要是针对几种主要的查询场景,从查询原理的角度分析这个场景下的查询开销,并给出一个大概的性能数字,供大家参考。

Lucene查询原理

本节主要是一些Lucene的背景知识,了解这些知识的同学可以略过。

Lucene的数据结构和查询原理

Elasticsearch的底层是Lucene,可以说Lucene的查询性能就决定了Elasticsearch的查询性能。关于Lucene的查询原理大家可以参考以下这篇文章:

Lucene查询原理

Lucene中最重要的就是它的几种数据结构,这决定了数据是如何被检索的,本文再简单描述一下几种数据结构:

  1. FST:保存term字典,可以在FST上实现单Term、Term范围、Term前缀和通配符查询等。
  2. 倒排链:保存了每个term对应的docId的列表,采用skipList的结构保存,用于快速跳跃。
  3. BKD-Tree:BKD-Tree是一种保存多维空间点的数据结构,用于数值类型(包括空间点)的快速查找。
  4. DocValues:基于docId的列式存储,由于列式存储的特点,可以有效提升排序聚合的性能。

组合条件的结果合并

了解了Lucene的数据结构和基本查询原理,我们知道:

  1. 对单个词条进行查询,Lucene会读取该词条的倒排链,倒排链中是一个有序的docId列表。
  2. 对字符串范围/前缀/通配符查询,Lucene会从FST中获取到符合条件的所有Term,然后就可以根据这些Term再查找倒排链,找到符合条件的doc。
  3. 对数字类型进行范围查找,Lucene会通过BKD-Tree找到符合条件的docId集合,但这个集合中的docId并非有序的。

现在的问题是,如果给一个组合查询条件,Lucene怎么对各个单条件的结果进行组合,得到最终结果。简化的问题就是如何求两个集合的交集和并集。

1. 对N个倒排链求交集

上面Lucene原理分析的文章中讲过,N个倒排链求交集,可以采用skipList,有效的跳过无效的doc。

2. 对N个倒排链求并集

处理方式一:仍然保留多个有序列表,多个有序列表的队首构成一个优先队列(最小堆),这样后续可以对整个并集进行iterator(堆顶的队首出堆,队列里下一个docID入堆),也可以通过skipList的方式向后跳跃(各个子列表分别通过skipList跳)。这种方式适合倒排链数量比较少(N比较小)的场景。

处理方式二:倒排链如果比较多(N比较大),采用方式一就不够划算,这时候可以直接把结果合并成一个有序的docID数组。

处理方式三:方式二中,直接保存原始的docID,如果docID非常多,很消耗内存,所以当doc数量超过一定值时(32位docID在BitSet中只需要一个bit,BitSet的大小取决于segments里的doc总数,所以可以根据doc总数和当前doc数估算是否BitSet更加划算),会采用构造BitSet的方式,非常节约内存,而且BitSet可以非常高效的取交/并集。

3. BKD-Tree的结果怎么跟其他结果合并

通过BKD-Tree查找到的docID是无序的,所以要么先转成有序的docID数组,或者构造BitSet,然后再与其他结果合并。

查询顺序优化

如果采用多个条件进行查询,那么先查询代价比较小的,再从小结果集上进行迭代,会更优一些。Lucene中做了很多这方面的优化,在查询前会先估算每个查询的代价,再决定查询顺序。

结果排序

默认情况下,Lucene会按照Score排序,即算分后的分数值,如果指定了其他的Sort字段,就会按照指定的字段排序。那么,排序会非常影响性能吗?首先,排序并不会对所有命中的doc进行排序,而是构造一个堆,保证前(Offset+Size)个数的doc是有序的,所以排序的性能取决于(Size+Offset)和命中的文档数,另外就是读取docValues的开销。因为(Size+Offset)并不会太大,而且docValues的读取性能很高,所以排序并不会非常的影响性能。

各场景查询性能分析

上一节讲了一些查询相关的理论知识,那么本节就是理论结合实践,通过具体的一些测试数字来分析一下各个场景的性能。测试采用单机单Shard、64核机器、SSD磁盘,主要分析各个场景的计算开销,不考虑操作系统Cache的影响,测试结果仅供参考。

单Term查询

ES中建立一个Index,一个shard,无replica。有1000万行数据,每行只有几个标签和一个唯一ID,现在将这些数据写入这个Index中。其中Tag1这个标签只有a和b两个值,现在要从1000万行中找到一条Tag1=a的数据(约500万)。给出以下查询,那么它耗时如何呢:
请求:
{"query": {"constant_score": {"filter": {"term": {"Tag1": "a"}}}},"size": 1
}'
响应:
{"took":233,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":5184867,"max_score":1.0,"hits":...}

这个请求耗费了233ms,并且返回了符合条件的数据总数:5184867条。

对于Tag1="a"这个查询条件,我们知道是查询Tag1="a"的倒排链,这个倒排链的长度是5184867,是非常长的,主要时间就花在扫描这个倒排链上。其实对这个例子来说,扫描倒排链带来的收益就是拿到了符合条件的记录总数,因为条件中设置了constant_score,所以不需要算分,随便返回一条符合条件的记录即可。对于要算分的场景,Lucene会根据词条在doc中出现的频率来计算分值,并取分值排序返回。

目前我们得到一个结论,233ms时间至少可以扫描500万的倒排链,另外考虑到单个请求是单线程执行的,可以粗略估算,一个CPU核在一秒内扫描倒排链内doc的速度是千万级的。

我们再换一个小一点的倒排链,长度为1万,总共耗时3ms。

{"took":3,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":10478,"max_score":1.0,"hits":...}

Term组合查询

首先考虑两个Term查询求交集:

对于一个Term的组合查询,两个倒排链分别为1万和500万,合并后符合条件的数据为5000,查询性能如何呢?
请求:
{"size": 1,"query": {"constant_score": {"filter": {"bool": {"must": [{"term": {"Tag1": "a"  // 倒排链长度500万}},{"term": {"Tag2": "0" // 倒排链长度1万}}]}}}}
}
响应:
{"took":21,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":5266,"max_score":2.0,"hits":...}

这个请求耗时21ms,主要是做两个倒排链的求交操作,因此我们主要分析skipList的性能。

这个例子中,倒排链长度是1万、500万,合并后仍有5000多个doc符合条件。对于1万的倒排链,基本上不进行skip,因为一半的doc都是符合条件的,对于500万的倒排链,平均每次skip1000个doc。因为倒排链在存储时最小的单位是BLOCK,一个BLOCK一般是128个docID,BLOCK内不会进行skip操作。所以即使能够skip到某个BLOCK,BLOCK内的docID还是要顺序扫描的。所以这个例子中,实际扫描的docID数粗略估计也有几十万,所以总时间花费了20多ms也符合预期。

对于Term查询求并集呢,将上面的bool查询的must改成should,查询结果为:

{"took":393,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":5190079,"max_score":1.0,"hits":...}

花费时间393ms,所以求并集的时间是多于其中单个条件查询的时间。

字符串范围查询

RecordID是一个UUID,1000万条数据,每个doc都有一个唯一的uuid,从中查找0~7开头的uuid,大概结果有500多万个,性能如何呢?
请求:
{"query": {"constant_score": {"filter": {"range": {"RecordID": {"gte": "0","lte": "8"}}}}},"size": 1
}
响应:
{"took":3001,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":5185663,"max_score":1.0,"hits":...}查询a开头的uuid,结果大概有60多万,性能如何呢?请求:
{"query": {"constant_score": {"filter": {"range": {"RecordID": {"gte": "a","lte": "b"}}}}},"size": 1
}
响应:
{"took":379,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":648556,"max_score":1.0,"hits":...}

这个查询我们主要分析FST的查询性能,从上面的结果中我们可以看到,FST的查询性能相比扫描倒排链要差许多,同样扫描500万的数据,倒排链扫描只需要不到300ms,而FST上的扫描花费了3秒,基本上是慢十倍的。对于UUID长度的字符串来说,FST范围扫描的性能大概是每秒百万级。

字符串范围查询加Term查询

字符串范围查询(符合条件500万),加上两个Term查询(符合条件5000),最终符合条件数目2600,性能如何?
请求:
{"query": {"constant_score": {"filter": {"bool": {"must": [{"range": {"RecordID": {"gte": "0","lte": "8"}}},{"term": {"Tag1": "a"}},{"term": {"Tag2": "0"}}]}}}},"size": 1
}
结果:
{"took":2849,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":2638,"max_score":1.0,"hits":...}

这个例子中,查询消耗时间的大头还是在扫描FST的部分,通过FST扫描出符合条件的Term,然后读取每个Term对应的docID列表,构造一个BitSet,再与两个TermQuery的倒排链求交集。

数字Range查询

对于数字类型,我们同样从1000万数据中查找500万呢?
请求:
{"query": {"constant_score": {"filter": {"range": {"Number": {"gte": 100000000,"lte": 150000000}}}}},"size": 1
}
响应:
{"took":567,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":5183183,"max_score":1.0,"hits":...}

这个场景我们主要测试BKD-Tree的性能,可以看到BKD-Tree查询的性能还是不错的,查找500万个doc花费了500多ms,只比扫描倒排链差一倍,相比FST的性能有了很大的提升。地理位置相关的查询也是通过BKD-Tree实现的,性能很高。

数字Range查询加Term查询

这里我们构造一个复杂的查询场景,数字Range范围数据500万,再加两个Term条件,最终符合条件数据2600多条,性能如何?
请求:
{"query": {"constant_score": {"filter": {"bool": {"must": [{"range": {"Number": {"gte": 100000000,"lte": 150000000}}},{"term": {"Tag1": "a"}},{"term": {"Tag2": "0"}}]}}}},"size": 1
}
响应:
{"took":27,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":2638,"max_score":1.0,"hits":...}

这个结果出乎我们的意料,竟然只需要27ms!因为在上一个例子中,数字Range查询耗时500多ms,而我们增加两个Term条件后,时间竟然变为27ms,这是为何呢?

实际上,Lucene在这里做了一个优化,底层有一个查询叫做IndexOrDocValuesQuery,会自动判断是查询Index(BKD-Tree)还是DocValues。在这个例子中,查询顺序是先对两个TermQuery求交集,得到5000多个docID,然后读取这个5000多个docID对应的docValues,从中筛选符合数字Range条件的数据。因为只需要读5000多个doc的docValues,所以花费时间很少。

简单结论

  1. 总体上讲,扫描的doc数量越多,性能肯定越差。
  2. 单个倒排链扫描的性能在每秒千万级,这个性能非常高,如果对数字类型要进行Term查询,也推荐建成字符串类型。
  3. 通过skipList进行倒排链合并时,性能取决于最短链的扫描次数和每次skip的开销,skip的开销比如BLOCK内的顺序扫描等。
  4. FST相关的字符串查询要比倒排链查询慢很多(通配符查询更是性能杀手,本文未做分析)。
  5. 基于BKD-Tree的数字范围查询性能很好,但是由于BKD-Tree内的docID不是有序的,不能采用类似skipList的向后跳的方式,如果跟其他查询做交集,必须先构造BitSet,这一步可能非常耗时。Lucene中通过IndexOrDocValuesQuery对一些场景做了优化。

最后结尾再放一个彩蛋,既然扫描数据越多,性能越差,那么能否获取到足够数据就提前终止呢,下一篇文章我会介绍一种这方面的技术,可以极大的提高很多场景下的查询性能。

 

阿里云双十一1折拼团活动:已满6人,都是最低折扣了
【满6人】1核2G云服务器99.5元一年298.5元三年 2核4G云服务器545元一年 1227元三年
【满6人】1核1G MySQL数据库 119.5元一年
【满6人】3000条国内短信包 60元每6月
参团地址:http://click.aliyun.com/m/1000020293/

 

原文链接
本文为云栖社区原创内容,未经允许不得转载。

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

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

相关文章

首次落地中国大陆的OpenInfra:中国对于开源做出的贡献力量已不可忽视

戳蓝字“CSDN云计算”关注我们哦!作者 | 刘丹责编 | 阿秃一张标志着上海现代建筑地标的东方明珠海报,另一张展示着上海悠久历史的豫园景区海报,不仅向我们展示了这座城市浓厚的历史气息与现代化的繁荣,也让我们看到了OpenStack历经…

java类验证和装载顺序_Java类的加载机制和双亲委派模型

Java类的加载机制和双亲委派模型1类的加载机制类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括了:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(using)、和…

任正非:华为 5G 是瞎猫碰死老鼠

喜欢话糙理不糙的任正非,又飙金句。11月6日,在和彭博社记者对话时,谈到华为5G,他说:“回顾这个过程,我们也没有什么必胜的信心,有时候也是瞎猫碰上了死老鼠,刚好碰上世界是这个需求。…

html5游戏开发box2djs,Box2D.js简易示例

Box2dWeb example//在页面加载完毕后启动整个Box2D程序function init() {//简化缩写各个对象名称var b2Vec2 Box2D.Common.Math.b2Vec2;var b2AABB Box2D.Collision.b2AABB;var b2BodyDef Box2D.Dynamics.b2BodyDef;var b2Body Box2D.Dynamics.b2Body;var b2FixtureDef Bo…

七个不容易被发现的生成对抗网络(GAN)用例

像许多追随AI发展的人一样,我无法忽略生成建模的最新进展,尤其是图像生成中生成对抗网络(GAN)的巨大成功。看看下面这些样本:它们与真实照片几乎没有区别! 从2014年到2018年,面部生成的进展也非…

floquet端口x极化入射波_请问CST 2012 floquet中的模式设置

大家好,我用的是CST2012,我已经知道floquet中的TE00和TM00分别代表两种互相正交的线偏振的平面波,那如果我想模拟一束非偏振的平面波入射应该怎么设置?另外,如果我是用TE00的偏振光入射,那么在计算透射率的时候在透射…

端口占用8080

1. winr键输入cmd进入命令行:执行以下命令: netstat -ano 2. 找到8080端口对应的pid 3. 打开任务管理器:找到对应的pid,右击结束任务即可

华为人到底几点钟下班?

戳蓝字“CSDN云计算”关注我们哦!作者 | 程序猿责编 | 阿秃转自 | 鲜枣课堂近日,在职场论坛上有这样一个帖子:华为员工晒出7天的上班打卡记录。该员工晒出自己在 9 月份 23 号到 29 号的打卡记录。其中每天 9 点 30 之前打卡&am…

手把手教你数据不足时如何做深度学习NLP

作为数据科学家,你最重要的技能之一应该是为你的问题选择正确的建模技术和算法。几个月前,我试图解决文本分类问题,即分类哪些新闻文章与我的客户相关。 我只有几千个标记的例子,所以我开始使用简单的经典机器学习建模方法&#…

怎样判断一个网站是不是前后端分离的?

1.页面右击选择【检查】或者打开谷歌开发者模式 2.选择【NetWork】,重新刷新页面 3. 选择XHR 全称(xmlhttprequest),后,下面会有地址列表;查看页面的数据是从页面渲染的数据还是通过后端api接口获取的 4.左侧点击第一个链接&…

vue 圆形 水波_vue项目百度地图+echarts的涟漪水波效果

先看效果image.pngid"allmap"class"map"ref"map">import echarts from "echarts";import "echarts/extension/bmap/bmap";export default {data() {return {chart: echarts.ECharts,bmap: {},mapZoom: 6,};},mounted() {…

五年,时间告诉我只有自己强大才是真的强大!

曾经以为,阿里可能只是自己经过的一个小小驿站。 却没想到,一来就是五年。 当我们问起那些来了五年的阿里人:过去的这五年里,最“值得”的是什么? 他们这样说—— 回忆起和主管一起坐摩托车去拜访客户的兴奋&#x…

AI:为你写诗,为你做不可能的事

戳蓝字“CSDN云计算”关注我们哦! 最近,一档全程高能的神仙节目,高调地杀入了我们的视野:没错,就是撒贝宁主持,董卿、康辉等央视名嘴作为评审嘉宾,同时集齐央视“三大名嘴”同台的央视《主持人大…

计算机怎么远程桌面,电脑远程桌面如何连接 电脑远程桌面连接方法【详解】...

电脑远程桌面如何连接?利用远程控制软件办公的方式不仅大大缓解了城市交通状况,减少了环境污染,还免去了人们上下班路上奔波的辛劳,更可以提高企业员工的工作效率和工作兴趣。除此之外,可以突破时间空间的限制,大大提…

@RequestParam注解使用

当前端请求方式为:x-www-form-urlencoded 后端怎样接收呢?第一种场景: 当前端传递的参数和后端定义接收的变量一致 例如:前端 :username 后端接收定义的变量username#后端接收方式: RestController Reques…

word怎么改正错误单词_在word 里要怎么让电脑自动识别错误的英语单词?

word自动识别错误的英语单词:1、点击菜单栏的审阅-拼写和语法;2、在可能错误的单词下有红色波浪形,右侧可以看到提示:在word 里要怎么让电脑自动识别错误的英语单词?word自动识别错误的英语单词:1、点击菜单…

工程师如何“神还原”用户问题?闲鱼回放技术揭秘

我们透过系统底层来捕获ui事件流和业务数据的流动,并利用捕获到的这些数据通过事件回放机制来复现线上的问题。本文先介绍录制和回放的整体框架,接着介绍里面涉及到的3个关键技术点,也是这里最复杂的技术(模拟触摸事件&#xff0c…

html5怎么设置字体闪动,HTML最简单的文字闪烁代码

该楼层疑似违规已被系统折叠 隐藏此楼查看此楼Titlekeyframes blink{0%{opacity: 1;}50%{opacity: 1;}50.01%{opacity: 0;}100%{opacity: 0;}}-webkit-keyframes blink {0% { opacity: 1; }50% { opacity: 1; }50.01% { opacity: 0; }100% { opacity: 0; }}-moz-keyframes blin…

英特尔推出世界最大 FPGA 芯片;任正非表示华为尚未直接和美国公司商谈5G技术授权;OpenTitan开源……...

戳蓝字“CSDN云计算”关注我们哦! 嗨,大家好,重磅君带来的【云重磅】特别栏目,如期而至,每周五第一时间为大家带来重磅新闻。把握技术风向标,了解行业应用与实践,就交给我重磅君吧!重…

前端传递json,后端应该怎样接收呢?

Content-Type: application/json#后端接收方式 RestController RequestMapping("/user") Slf4j public class UserController {/*** param userForm*/PostMapping("/register")public ResponseVo register(RequestBody UserForm userForm) {log.info("…