es分页查询原理
es分页查询1、page+sizeGET test_dev/_search{"query": {"bool": {"filter": [{"term": {"age": 28}}]}},"size": 10,"from": 20}和mysql类似,查询深分页时性能较差。当page*size过大时,会出现效率急剧下降的问
es分页查询
1、page+size
GET test_dev/_search
{
"query": {
"bool": {
"filter": [
{
"term": {
"age": 28
}
}
]
}
},
"size": 10,
"from": 20
}
和mysql类似,查询深分页时性能较差。当page*size过大时,会出现效率急剧下降的问题,同时其性能下滑相较于mysql会更加严重。
ES为什么深分页效率低?
这是由于es集群是分布式架构,对于一个查询实际上有两个阶段。即Query阶段和Fetch阶段。
Query阶段比较轻量级,通过查询倒排索引,获取满足查询结果的文档ID列表和其评分。 而Fetch阶段比较耗费资源,需要将每个shard的结果取回,在协调结点进行全局排序,最后通过doc_id获取全量数据。
举个例子,当page=1000,size=100时,请求会发送到所有的shard上去,每个Shard都会在本地存储一个大小为1000*100+100的priority queue,然后每个shard又会将priority queue发送给协调节点进行合并,最后将结果返回。也就是说,ES的深分页问题将会大大耗费每个节点的带宽、cpu和内存。
ES的排序方式
在query阶段就要进行排序,不全量查询的情况下怎么排序的?
- filter 查询 为 doc_id(Lucene 文件结构的当时索引时的先后顺序)
- 按照相关性得分排序( _score)
- 按照指定的字段排序 (term index中的顺序)
2、search_after
search_after 分页的方式是根据上一页的最后一条数据来确定下一页的位置,同时在分页请求的过程中,如果有索引数据的增删改查,这些变更也会实时的反映到游标上。但是需要注意,因为每一页的数据依赖于上一页最后一条数据,所以无法跳页请求。
其思想类似于mysql常用的方法:记录上一次访问的最后位置
select * from table where id > #max_id# order by id limit n;
这样实际上就避免了page过大导致的深分页问题。
唯一值id查询
为了找到每一页最后一条数据,每个文档必须有一个全局唯一值,官方推荐使用 _uid 作为全局唯一值,其实使用业务层的 id 也可以。 比如下面我们就使用了timestamp和 _id组成的唯一值。(分布式带来的问题)
GET test_dev/_search
{
"query": {
"bool": {
"filter": [
{
"term": {
"age": 28
}
}
]
}
},
"size": 10,
"from": 0,
"search_after": [
1541495312521,
"d0xH6GYBBtbwbQSP0j1A"
],
"sort": [
{
"timestamp": {
"order": "desc"
},
"_id": {
"order": "desc"
}
}
]
}
3、scroll游标查询
from+size查询在10000-50000条数据(1000到5000页)以内的时候还是可以的,但是如果数据过多的话,就会出现深分页问题。
为了解决上面的问题,elasticsearch提出了一个scroll滚动的方式。
GET test_dev/_search?scroll=5m
{
"query": {
"bool": {
"filter": [
{
"term": {
"age": 28
}
}
]
}
},
"size": 100
}
scroll如何进行查询的?
Query阶段:每个shard将命中的结果( doc_id和_score) 按照 _score 顺序在上下文中创建一个优先队列快照,并通过scroll_id指向它,lastEmittedDoc指向上次访问的位置,最后将TOP(size)的doc id返回给协调节点。
Fetch阶段:协调节点将各个shard返回的结果再进行合并排序,最后通过doc_id查找返回结果的全量数据。之后更新各个分片上的上下文。
GET _search/scroll
{
"scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAKP_RVFmFRbGp0Vm1pU01hM2M0eXlyUGVxRkEAAAAACeMUaxZSV1JjSnFtQVFDT094RUNsQjFFTF9BAAAAAAo_9FQWYVFsanRWbWlTTWEzYzR5eXJQZXFGQQAAAAAKP_RTFmFRbGp0Vm1pU01hM2M0eXlyUGVxRkEAAAAACeMUahZSV1JjSnFtQVFDT094RUNsQjFFTF9B",
"scroll": "5m"
}
如果对数据不要求排序,可以直接指定**_doc**来排序,不计算_score。
scroll_id如何根据上次查询找到对应的数据?
在Query阶段通过scroll_id找到对应的快照,然后用lastEmittedDoc将原来的查询语句添加bool查询条件**( >=lastEmitted.doc + 1)**,在快照中找数据。
scroll_id为什么有时候会变化?
scroll_id其中保留了shard信息,假如scroll查询语句需要路由到100个shard上查。scroll_id会比较长,记录这100个shard。有可能从开始到完成都需要路由到这100个shard,shard_id就不会变化。也有可能随着不断进行scroll,需要路由到的shard越来越少,shard_id也会越来越短。
scroll查询为什么要生成快照?
1.加速。
- 由于ES是分布式项目,数据通过 page+size和search_after(无唯一ID) 方法无法保证每次分页后返回的数据是上一页没有出现过的(想一想每个分片之间都有打分相同的数据,合并阶段会发生什么,每个分片内也有打分相同的数据)。
scroll的缺点
scroll 的方式,官方的建议不用于实时的请求(一般用于数据导出),因为每一个 scroll_id 不仅会占用大量的资源,而且会生成历史快照,对于数据的变更不会反映到快照上。
4、track_total_hits
在不进行集群配置的情况下,Elasticsearch 限制了最多的数值为10000,如果想要访问命中的数据中超过10000条,需要配置max_result_window。
从 Elasticsearch 7.0之后,为了提高搜索的性能,在 hits 字段中返回的文档数有时不是最精确的数值。
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 10000,
"relation" : "gte" //表示命中了一部分
},
...
}
可以通过将track_total_hits设置为true来显示全量数据,得到准确的命中数(但无法抓取超过一万条数据),各个分片返回准确的命中数量。也可以设置整数来表明想要返回数量。
当page*size分页查找的数量超过一万时也会无法查询。
track_total_hits 优化了什么
count 和 hit 是两种类型的查询,count代表精确统计(和关系型数据库的count一致),hit是从相关性的角度来统计的。hit表示,es检索到了和查询条件相关的文档,并按照相关性排序,返回topN,至于到底有多少文档和查询条件相关,其实是不重要的。
索引会优先查到_score高的数据,到一万就直接返回,不用算其他可能不需要的文件的相关性了。
5、总结
ES分页方式对比
分页方式 | 性能 | 优点 | 缺点 | 场景 |
---|---|---|---|---|
from + size | 低 | 灵活性好,实现简单 | 深度分页问题 | 数据量比较小,能容忍深度分页问题,一次性返回海量数据 |
scroll | 高 | 解决了深度分页问题 | 无法反应数据的实时性(快照)维护成本高,需要维护一个 scroll_id | 海量数据的导出需要查询海量结果集的数据,群发 |
search_after | 高 | 性能最好不存在深度分页问题能够反映数据的实时变更 | 实现复杂,需要有一个全局唯一的字段连续分页的实现会比较复杂,因为每一次查询都需要上次查询的结果 | 海量数据的分页 |
跳页
mysql可以通过索引进行较快的跳页查询
select * from table where id > (select id from table order by id limit m, 1) limit n;
es的倒排索引在深分页时跳页很低效!!!
需要有分页需求时,一般只开放前几十页的结果,页数过大时直接在业务层面报错。若需要显示命中的总条数,则设置track_total_hits=true。
更多推荐
所有评论(0)