【ElasticSearch从入门到放弃系列 十】Elasticsearch深度分页查询方式讨论

本文涉及的产品
检索分析服务 Elasticsearch 版,2核4GB开发者规格 1个月
简介: 【ElasticSearch从入门到放弃系列 十】Elasticsearch深度分页查询方式讨论

在日常工作中,我们的方法提供两种接口用来分页批量的获取数据,第一种是普通的GetEntityList,另外一种是GetEntityIdListByScrollV2,第二种就是我们所说的Scroll方式查询数据。批量获取数据的时候为了性能总是推荐Scroll的方式,但是一直不明白这种方式是什么意思,今天来学习下几种不同的分页查询方式的适用场景。

浅度分页适用场景

一个搜索请求到来的时候,正如我在上篇blog【ElasticSearch从入门到放弃系列 九】Elasticsearch原理机制探索里谈到的,有一个请求的流程,我们举个例子来回顾下,如果我在ES集群的3个节点全部4个分片上共存储了400条人员信息数据,每个分片100条,接下来我要分页获取所有人员中【按年龄排序户籍地为乌拉特前旗】的每页10条的第3页的全部人员数据。也就是依据年龄去排序所有数据,然后取from为20,size为10的10条数据。

  • size:显示应该返回的结果数量,默认是10。
  • from:显示查询数据的偏移量,即应该跳过的初始结果数量,默认是0,我们这里取第三页的数据,则from应该设置为20

那么依照这样的需求我们请求发送到集群会怎么处理呢?

处理流程如下【请求会被随机转发主分片或副本分片,采取随机轮询的方式,我们这里假定都是主分片处理】:

  1. client发送分页查询请求到ES1(coordinating node)上,ES1上的【S2、S3】各建立一个大小为from+size(30)的优先级队列来存放查询结果;
  2. 协调节点将请求转发到ES2【S1】和ES3【S0】上,它们各建立一个大小为from+size(30)的优先级队列来存放查询结果;
  3. 每个shards在内部执行查询(搜索户籍地为乌拉特前旗,且按照年龄进行排序),把from+size(30)条记录存到内部的优先级队列(top N表)中;
  4. 每个shards把缓存的from+size(30)条记录返回给ES1;
  5. query phase:ES1获取到各个shards数据后,进行合并排序,选择30*4共120条记录里的前 from + size 30条数据以及用于排序的 _score 存到优先级队列即可,以便 fetch 阶段使用。
  6. fetch phase:协调节点ES1获取到整体的top30后,取其中的第20-30条也就是第三页的数据中的10个doc id,根据doc id去各个节点上拉取实际的document数据,最终返回给客户端

这样一个数据量在这种场景下还是可以hold的,但是如果查询量比较大呢?假设我们每个分片上存储了10万条数据,共计40万条数据,我们要取第1万页的数据,也就是from为10000,size为10,那么我们再看一遍流程如下:

  1. client发送分页查询请求到ES1(coordinating node)上,ES1上的【S2、S3】各建立一个大小为from+size(10010)的优先级队列来存放查询结果;内存、IO损耗
  2. 协调节点将请求转发到ES2【S1】和ES3【S0】上,它们各建立一个大小为from+size(10010)的优先级队列来存放查询结果;内存、IO损耗
  3. 每个shards在内部执行查询(按照年龄进行排序),把from+size(10010)条记录存到内部的优先级队列(top N表)中;CPU损耗
  4. 每个shards把缓存的from+size(10010)条记录返回给ES1;网络带宽损耗
  5. query phase:ES1获取到各个shards数据后,进行合并排序,选择10010*4共40040条记录里的前 from + size 10010条数据以及用于排序的 _score 存到优先级队列即可,以便 fetch 阶段使用。CPU损耗
  6. fetch phase:协调节点ES1获取到整体的top10010后,取其中的第10001-10010条也就是第10000页的数据中的10个doc id,根据doc id去各个节点上拉取实际的document数据,最终返回给客户端

以上的各个阶段可以看到,当页码很深的时候,我们拿10条数据是多么的不容易,性能损耗是多么严重,所以ES对这种获取方式的数据条数做了限制:

[root@localhost elasticsearch-5.7.4]# curl -XGET 'http://11.12.84.126:9200/_audit_0102/_log_0102/_search?size=2&from=10000&pretty=true'
{
  "error" : {
    "root_cause" : [ {
      "type" : "query_phase_execution_exception",
      "reason" : "Result window is too large, from + size must be less than or equal to: [10000] but was [10010]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level parameter."
    } ],
    "type" : "search_phase_execution_exception",
    "reason" : "all shards failed",
    "phase" : "query",
    "grouped" : true,
    "failed_shards" : [ {
      "shard" : 0,
      "index" : "_audit_0102",
      "node" : "f_CQitYESZedx8ZbyZ6bHA",
      "reason" : {
        "type" : "query_phase_execution_exception",
        "reason" : "Result window is too large, from + size must be less than or equal to: [10000] but was [10010]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level parameter."
      }
    } ]
  },
  "status" : 500
}

from+size最多限制10000,超过限制即报错,当然这个参数可以通过如下的方式调整,例如调整到50000

curl -XPUT "http://11.12.84.126:9200/_audit_0102/_settings" -d '{
        "index": {
            "max_result_window": 50000
        }
    }'

但就算调整了也只是一种临时方案,硬件极限承载能力并不是通过调整配置能解决的,需要更换策略。从报错信息也可以看出,ES推荐使用Scroll的方式:

See the scroll api for a more efficient way to request large data sets

深度分页适用场景

在进行深度分页时我们不推荐再使用from+size的方式,而是使用Scroll,当然Scroll细分也有几种,分别适用于不同的场景。

Scroll

scroll 类似于sql中的cursor,使用scroll,每次只能获取一页的内容(按照上边的场景,每次获取10条),然后会返回一个scroll_id。根据返回的这个scroll_id可以不断地获取下一页的内容,所以scroll并不适用于有跳页的情景。Scroll的流程分为两个步骤

  • 第一次搜索完成之后,将所有复合条件的搜索结果缓存起来,类似于对结果集做了一个快照;
  • 在需要返回数据时,从该快照中按照scroll返回数据;在scroll快照生成之后,在快照有效期范围内,对于该索引的增删改都不会影响快照的结果。

以下是具体操作:

初始化

初始化的时候请求接口还需要index和type【6版本后没有了】信息,初始化的作用时将所有复合条件的搜索结果缓存起来,类似于对结果集做了一个快照,之后的搜索就是游标在快照上的滚动了。

GET UserInfo/_search?scroll=5m
{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "home": "乌拉特前旗"
          }
        }
      ]
    }
  },
  "size": 10,
  "from": 0,
  "sort": [
    {
      "age": {
        "order": "desc"
      },
      "_id": {
        "order": "desc"
      }
    }
  ]
}

其中:scroll=5m表示设置scroll_id保留5分钟可用;使用scroll必须要将from设置为0【不允许跳页】;size决定后面每次调用_search搜索返回的数量【这里为10条】。需要注意:实际返回给协调节点的数量为:分片的数量*size,也就是每次返回40条,共进行10001次请求和返回行为

搜索

然后我们可以通过数据返回的_scroll_id读取下一页内容,每次请求将会读取下10条数据,直到数据读取完毕或者scroll_id保留时间截止,请求的接口不再使用索引名了,而是 _search/scroll,其中GET和POST方法都可以使用

GET _search/scroll
{
  "scroll_id": "DnF1ZXJ5VGhlbk【全网唯一Scrollid】",
  "scroll": "5m"
}

需要注意:每次都要传参数 scroll,刷新搜索结果的缓存时间,相对于流程我们再来看一下请求过程:

  1. client发送分页查询请求到ES1(coordinating node)上,ES1上的【S2、S3】各建立一个大小为from+size(10)的优先级队列来存放查询结果;内存无损耗、IO无损耗
  2. 协调节点将请求转发到ES2【S1】和ES3【S0】上,它们各建立一个大小为from+size(10)的优先级队列来存放查询结果;内存无损耗、IO无损耗
  3. 每个shards在内部执行查询(按照年龄进行排序),把from+size(10)条记录存到内部的优先级队列(top N表)中;CPU无损耗
  4. 每个shards把缓存的from+size(10)条记录返回给ES1;网络带宽无损耗
  5. query phase:ES1获取到各个shards数据后,发起下一次请求,直到轮询发起10001次请求,共获得40040条数据后执行合并排序,进行合并排序,选择10010*4共40040条记录里的前 from + size 10010条数据以及用于排序的 _score 存到优先级队列即可,以便 fetch 阶段使用。CPU无损耗、快照堆积
  6. fetch phase:协调节点ES1获取到整体的top100010后,取其中的第10001-10010条也就是第10000页的数据中的10个doc id,根据doc id去各个节点上拉取实际的document数据,最终返回给客户端

删除Scroll上下文快照

scroll的搜索上下文会在scroll的保留时间截止后自动清除,但是我们知道scroll是非常消耗资源的,所以一个建议就是当不需要scroll数据的时候,尽可能快的把scroll_id显式删除掉,因为每一个 scroll_id 不仅会占用大量的资源,而且会生成历史快照【在初始化请求时初始化数据快照,后续游标在快照上移动】,资源占用是很大的【5版本前scroll_id每次返回是变化的,5版本后就不变了】。又是一个经典的以空间换时间的例子

Scroll-Scan

一般来说,仅仅想要找到结果,不关心顺序。可以通过组合 scroll 和 scan 来关闭任何打分或者排序,以最高效的方式返回结果。需要做的就是将 search_type=scan 加入到查询的字符串中

GET UserInfo/_search?scroll=5m&search_type=scan
{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "home": "乌拉特前旗"
          }
        }
      ]
    }
  },
  "size": 10,
  "from": 0,
  "sort": [
    {
      "age": {
        "order": "desc"
      },
      "_id": {
        "order": "desc"
      }
    }
  ]
}

不算分,关闭排序,结果会按照在索引中出现的顺序返回

Scroll-after

scroll 的方式,官方的建议不用于实时的请求(一般用于数据导出),因为每一个 scroll_id 不仅会占用大量的资源,而且会生成历史快照,对于数据的变更不会反映到快照上。

search_after 分页的方式是根据上一页的最后一条数据来确定下一页的位置同时在分页请求的过程中,如果有索引数据的增删改查,这些变更也会实时的反映到游标上。但是需要注意,因为每一页的数据依赖于上一页最后一条数据,所以无法跳页请求。为了找到每一页最后一条数据,每个文档必须有一个全局唯一值,官方推荐使用 _uid 作为全局唯一值

GET UserInfo/_search?scroll=5m
{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "home": "乌拉特前旗"
          }
        }
      ]
    }
  },
  "size": 10,
  "from": 0,
  "sort": [
    {
      "age": {
        "order": "desc"
      },
      "_id": {
        "order": "desc"   //全局唯一值
      }
    }
  ]
}

接下来使用sort返回值搜索下一页

GET test_dev/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "home": "乌拉特前旗"
          }
        }
      ]
    }
  },
  "size": 10,
  "from": 0,
  "search_after": [
    "d0xH6GYBBtbwbQSP0j1A"
  ],
  "sort": [
    {
     "age": {
        "order": "desc"
      },
      "_id": {
        "order": "desc"
      }
    }
  ]
}

search_after及多个排序字段多个参数用逗号隔开,作为下一个检索search_after的参数。

总结

对比以上的几种分页查询方式我们来总结一下:

分页查询方式 优点 缺点 适用场景
from+size 查询方式简单,只需一次请求 深度分页情况下,内存、IO、CPU、网络带宽损耗严重 10000条数据以内的分页查询和获取
Scroll 深度查询下不受影响,可以持续遍历进行深度分页查询 请求次数多,快照堆积多,查询过程中以快照模式存储,历史快照对于数据的变更不会反映到快照上 数据导出,穷举
Scroll-Scan 深度查询下不受影响,可以持续遍历进行深度分页查询,且由于无需排序,速度较Scroll快 较Scroll而言,无法排序 对排序没有要求的数据导出、穷举
Scroll-after 深度查询下不受影响,可以持续遍历进行深度分页查询,且因为不依赖Scroll_id,不依赖快照,所以数据变更可以体现在查询结果中 较Scroll而言,由于不生成快照,每条数据都必须有一个全局唯一id 动态的数据导出和穷举
相关实践学习
使用阿里云Elasticsearch体验信息检索加速
通过创建登录阿里云Elasticsearch集群,使用DataWorks将MySQL数据同步至Elasticsearch,体验多条件检索效果,简单展示数据同步和信息检索加速的过程和操作。
ElasticSearch 入门精讲
ElasticSearch是一个开源的、基于Lucene的、分布式、高扩展、高实时的搜索与数据分析引擎。根据DB-Engines的排名显示,Elasticsearch是最受欢迎的企业搜索引擎,其次是Apache Solr(也是基于Lucene)。 ElasticSearch的实现原理主要分为以下几个步骤: 用户将数据提交到Elastic Search 数据库中 通过分词控制器去将对应的语句分词,将其权重和分词结果一并存入数据 当用户搜索数据时候,再根据权重将结果排名、打分 将返回结果呈现给用户 Elasticsearch可以用于搜索各种文档。它提供可扩展的搜索,具有接近实时的搜索,并支持多租户。
相关文章
|
6月前
|
安全 Linux 开发工具
Elasticsearch 搜索入门技术之一
Elasticsearch 搜索入门技术之一
257 1
|
6月前
|
JSON 自然语言处理 数据库
数据库-ElasticSearch入门(索引、文档、查询)
数据库-ElasticSearch入门(索引、文档、查询)
386 0
|
存储 关系型数据库 数据库
ElasticSearch深度解析入门篇:高效搜索解决方案的介绍与实战案例讲解,带你避坑
ElasticSearch深度解析入门篇:高效搜索解决方案的介绍与实战案例讲解,带你避坑
ElasticSearch深度解析入门篇:高效搜索解决方案的介绍与实战案例讲解,带你避坑
|
1月前
|
存储 Java API
Elasticsearch 7.8.0从入门到精通
这篇文章详细介绍了Elasticsearch 7.8.0的安装、核心概念(如正排索引和倒排索引)、RESTful风格、各种索引和文档操作、条件查询、聚合查询以及在Spring Boot中整合Elasticsearch的步骤和示例。
128 1
Elasticsearch 7.8.0从入门到精通
|
2月前
|
数据可视化 Java Windows
Elasticsearch入门-环境安装ES和Kibana以及ES-Head可视化插件和浏览器插件es-client
本文介绍了如何在Windows环境下安装Elasticsearch(ES)、Elasticsearch Head可视化插件和Kibana,以及如何配置ES的跨域问题,确保Kibana能够连接到ES集群,并提供了安装过程中可能遇到的问题及其解决方案。
Elasticsearch入门-环境安装ES和Kibana以及ES-Head可视化插件和浏览器插件es-client
|
2月前
|
存储 关系型数据库 MySQL
浅谈Elasticsearch的入门与实践
本文主要围绕ES核心特性:分布式存储特性和分析检索能力,介绍了概念、原理与实践案例,希望让读者快速理解ES的核心特性与应用场景。
|
5天前
|
存储 JSON Java
ELK 圣经:Elasticsearch、Logstash、Kibana 从入门到精通
ELK是一套强大的日志管理和分析工具,广泛应用于日志监控、故障排查、业务分析等场景。本文档将详细介绍ELK的各个组件及其配置方法,帮助读者从零开始掌握ELK的使用。
|
3月前
|
JSON 搜索推荐 数据挖掘
ElasticSearch的简单介绍与使用【入门篇】
这篇文章是Elasticsearch的入门介绍,涵盖了Elasticsearch的基本概念、特点、安装方法以及如何进行基本的数据操作,包括索引文档、查询、更新、删除和使用bulk API进行批量操作。
ElasticSearch的简单介绍与使用【入门篇】
|
2月前
|
JSON 监控 Java
Elasticsearch 入门:搭建高性能搜索集群
【9月更文第2天】Elasticsearch 是一个分布式的、RESTful 风格的搜索和分析引擎,基于 Apache Lucene 构建。它能够处理大量的数据,提供快速的搜索响应。本教程将指导你如何从零开始搭建一个基本的 Elasticsearch 集群,并演示如何进行简单的索引和查询操作。
215 3
|
3月前
|
JSON 测试技术 API
黑马商城 Elasticsearch从入门到部署 RestClient操作文档
这篇文章详细介绍了如何使用Java的RestHighLevelClient客户端与Elasticsearch进行文档操作,包括新增、查询、删除、修改文档以及批量导入文档的方法,并提供了相应的代码示例和操作步骤。
下一篇
无影云桌面