ElasticSearch 分页问题分析

Posted 爱上口袋的天空

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ElasticSearch 分页问题分析相关的知识,希望对你有一定的参考价值。

1、简介

ElasticSearch是搜索引擎,从搜索的意义上来说,如果筛选条件或前几页都找不到需要的数据,继续深度分页也不会找到想要的数据。

ElasticSearch不要做深度分页和随机深度跳页。

2、ES 分页建议

  • 增加默认的筛选条件,尽量减少数据量的展示,比如:最近一个月;
  • 限制总分页数,比如:淘宝、京东仅显示100页查询结果,百度仅显示76页;
  • 修改跳页的展现方式,改为滚动显示,或小范围跳页,比如:谷歌、百度的小范围跳页。

3、ES 三种分页比较

  • from+size:适用于浅分页(数据量小于max_result_window),在增大max_result_window情况下,也可实现深度分页,但效率低下,可能出现 OOM。
  • scroll:适用于数据导出,基于生成的历史快照查询,对于数据的变更不会反映到快照上。
  • search_after:适用于实时请求和高并发场景(深度分页+排序),由于每一页的数据依赖于上一页最后一条数据,所以无法做到随机跳页(滚动显示)。

4、from+size方式

         使用 from and size 进行分页。但是es官方并不建议用这种方法进行大数据的查询尤其是深度分页,具体原因我们要知道它是如何查询的。

         size为返回的结果个数,默认为10;from表示从第几行开始,不包含在内。

        假设我们在有5个主分片的索引中搜索,查询第一页数据,即前10条数据,那么es会从每个分片中生成排序好的结果,取出前10条,然后返回给请求节点,请求节点再将这50条记录再次排序选出前10条。

        但是如果请求的是第1000页的数据,即第10001至第10010条数据。那么es会从5个分片中取出各个分片顶端的10010条数据,然后将这总共50050条数据返回给请求节点,请求节点再次对这50050条数据进行一次全局排序,取出第50041至50050条数据,抛弃50040条数据,这就造成了很大的浪费。

          在分布式系统中排序的花费会随着分页的深度而成倍增长,如果数据特别大对CPU和内存的消耗会非常巨大甚至会导致OutOfMemory,所以这也是为什么网络搜索引擎不能返回多余1000个结果的原因

案例:

GET test_dev/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "age": 28
          }
        }
      ]
    }
  },
  "size": 10,
  "from": 20,
  "sort": [
    {
      "timestamp": {
        "order": "desc"
      },
      "_id": {
        "order": "desc"
      }
    }
  ]
}

5、scroll分页方式

        为了满足深度分页的场景,es 提供了 scroll 的方式进行分页读取。原理上是对某次查询生成一个游标 scroll_id , 后续的查询只需要根据这个游标去取数据,直到结果集中返回的 hits 字段为空,就表示遍历结束。scroll_id 的生成可以理解为建立了一个临时的历史快照,在此之后的增删改查等操作不会影响到这个快照的结果。

       可以把 scroll 理解为关系型数据库里的 cursor,因此,scroll 并不适合用来做实时搜索,而更适用于后台批处理任务,比如群发。

      可以把 scroll 分为初始化和遍历两步,初始化时将所有符合搜索条件的搜索结果缓存起来,可以想象成快照,在遍历时,从这个快照里取数据,也就是说,在初始化后对索引插入、删除、更新数据都不会影响遍历结果,并且维护这个快照需要占用很多资源。

      使用scroll,每次只能获取一页的内容,然后会返回一个scrollid,根据scrollid可以不断地获取下一页的内容,所以scroll并不适用于有跳页的情景。但是在真正的使用场景中,第10000条数据已经是很后面的数据了,可以“折衷”一下,不提供跳转页面功能,只能下一页的翻页。

    可以看出scroll不适合支持那种实时的和用户交互的前端分页工作,其主要用途用于从ES集群分批拉取大量结果集的情况,一般都是offline的应用场景。  比如需要将非常大的结果集拉取出来,存放到其他系统处理,或者需要做大索引的reindex等等。

案例:

GET test_dev/_search?scroll=5m
{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "age": 28
          }
        }
      ]
    }
  },
  "size": 10,
  "from": 0,
  "sort": [
    {
      "timestamp": {
        "order": "desc"
      },
      "_id": {
        "order": "desc"
      }
    }
  ]
}
  1. scroll=5m表示设置scroll_id保留5分钟可用。
  2. 使用scroll必须要将from设置为0。
  3. size决定后面每次调用_search搜索返回的数量

然后我们可以通过数据返回的_scroll_id读取下一页内容,每次请求将会读取下10条数据,直到数据读取完毕或者scroll_id保留时间截止:

GET _search/scroll
{
  "scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAJZ9Fnk1d......",
  "scroll": "5m"
}

注意:请求的接口不再使用索引名了,而是 _search/scroll,其中GET和POST方法都可以使用。

croll删除
根据官方文档的说法,scroll的搜索上下文会在scroll的保留时间截止后自动清除,但是我们知道scroll是非常消耗资源的,所以一个建议就是当不需要了scroll数据的时候,尽可能快的把scroll_id显式删除掉。

清除指定的scroll_id:

DELETE _search/scroll/DnF1ZXJ5VGhlbkZldGNo.....

清除所有的scroll:

DELETE _search/scroll/_all

6、search_after 

       上述的 scroll search 的方式,官方的建议并不是用于实时的请求,因为每一个 scroll_id 不仅会占用大量的资源(特别是排序的请求),而且是生成的历史快照,对于数据的变更不会反映到快照上。这种方式往往用于非实时处理大量数据的情况,比如要进行数据迁移或者索引变更之类的。那么在实时情况下如果处理深度分页的问题呢?es 给出了 search_after 的方式,这是在 >= 5.0 版本才提供的功能。

       searchAfter的方式通过维护一个实时游标来避免scroll的缺点,它可以用于实时请求和高并发场景。

       它的缺点是不能够随机跳转分页,只能是一页一页的向后翻,并且需要至少指定一个唯一不重复字段来排序(注:每个文档具有一个唯一值的字段应该用作排序规范的仲裁器。否则,具有相同排序值的文档的排序顺序将是未定义的。建议的方法是使用字段_id,它肯定包含每个文档的一个唯一值)。

       此外还有一个与scorll的不同之处是searchAfter的读取数据的顺序会受索引的更新和删除影响而scroll不会,因为searchAfter读取的并不是不可变的快照,而是依赖于上一页最后一条数据,所以无法跳页请求,用于滚动请求,于scroll类似,不同之处在于它使无状态的。 

案例:

POST twitter/_search
{
    "size": 10,
    "query": {
        "match" : {
            "title" : "es"
        }
    },
    "sort": [
        {"date": "asc"},
        {"_id": "desc"}
    ]
}

返回出的结果信息 :

{
      "took" : 29,
      "timed_out" : false,
      "_shards" : {
        "total" : 1,
        "successful" : 1,
        "skipped" : 0,
        "failed" : 0
      },
      "hits" : {
        "total" : {
          "value" : 5,
          "relation" : "eq"
        },
        "max_score" : null,
        "hits" : [
          {
            ...
            },
            "sort" : [
              ...
            ]
          },
          {
            ...
            },
            "sort" : [
              124648691,
              "624812"
            ]
          }
        ]
      }
    }

 上面的请求会为每一个文档返回一个包含sort排序值的数组。这些sort排序值可以被用于 search_after 参数里以便抓取下一页的数据。比如,我们可以使用最后的一个文档的sort排序值,将它传递给 search_after 参数:

GET twitter/_search
{
    "size": 10,
    "query": {
        "match" : {
            "title" : "es"
        }
    },
    "search_after": [124648691, "624812"],
    "sort": [
        {"date": "asc"},
        {"_id": "desc"}
    ]
}

注意:当我们使用search_after时,from值必须设置为0或者-1。

search_after缺点是不能够随机跳转分页,只能是一页一页的向后翻,并且需要至少指定一个唯一不重复字段来排序。它与滚动API非常相似,但与它不同,search_after参数是无状态的,它始终针对最新版本的搜索器进行解析。因此,排序顺序可能会在步行期间发生变化,具体取决于索引的更新和删除。

以上是关于ElasticSearch 分页问题分析的主要内容,如果未能解决你的问题,请参考以下文章

浅析Elasticsearch大数据下深度分页问题

elastic分页查询scroll

京东面试题:ElasticSearch深度分页解决方案

京东面试题:ElasticSearch深度分页解决方案

elasticsearch代码片段,及工具类SearchEsUtil.java

ElasticSearch学习问题记录——Invalid shift value in prefixCoded bytes (is encoded value really an INT?)(代码片段