zoukankan      html  css  js  c++  java
  • 游标 深度分页 deep paging

    Solr Deep Paging(solr 深分页) - ickes的专栏 - CSDN博客 https://blog.csdn.net/xl_ickes/article/details/42772521

    问题
    深分页的问题是很清楚。Solr必须为返回的搜索结果准备一个列表,并返回它的一部分。如果该部分来源于该列表的前面并不难。但如果我们想返回第10000页(每页20条记录)的数据,Solr需要准备一个包含大小为200000(10000 * 20)的列表。这样,它不仅需要时间,还需要内存。像我们现在生产上的历史数据达到了6个亿的数据,如果直接跳转到最后一页,必定内存溢出。

    solr4.7是怎么解决这个问题的?
    答:Solr 4.7的发布改变了这一状况,引入了游标的概念。游标是一个动态结构,不需要存储在服务器上。游标包含了查询的结果的偏移量,因此,Solr的不再需要每次从头开始遍历结果直到我们想要的记录,游标的功能可以大幅提升深翻页的性能。

    用法
    游标的使用非常简单。在第一个查询中,我们需要传递一个额外的参数- cursorMark = *,告诉Solr返回游标。在返回中除了搜索结果,我们还可以得到nextCursorMark信息。看看下面这个例子。
    ————————————————
    版权声明:本文为CSDN博主「xl_ickes」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/xl_ickes/article/details/42772521

    常见深度分页方式 from+size
    es 默认采用的分页方式是 from+ size 的形式,在深度分页的情况下,这种使用方式效率是非常低的,比如

    from = 5000, size=10, es 需要在各个分片上匹配排序并得到5000*10条有效数据,然后在结果集中取最后10条

    数据返回,这种方式类似于mongo的 skip + size。

    除了效率上的问题,还有一个无法解决的问题是,es 目前支持最大的 skip 值是 max_result_window ,默认

    为 10000 。也就是当 from + size > max_result_window 时,es 将返回错误

    [root@dnsserver ~]# curl -XGET 127.0.0.1:9200/custm/_settings?pretty
    {
    "custm" : {
    "settings" : {
    "index" : {
    "max_result_window" : "50000",
    ....
    }
    }
    }
    }
     
    最开始的时候是线上客户的es数据出现问题,当分页到几百页的时候,es 无法返回数据,此时为了恢复正常使用,我们采用了紧急规避方案,就是将 max_result_window 的值调至 50000。

    [root@dnsserver ~]# curl -XPUT "127.0.0.1:9200/custm/_settings" -d
    '{
    "index" : {
    "max_result_window" : 50000
    }
    }'
     
    然后这种方式只能暂时解决问题,当es 的使用越来越多,数据量越来越大,深度分页的场景越来越复杂时,如何解决这种问题呢?

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

    使用 curl 进行分页读取过程如下:

    先获取第一个 scroll_id,url 参数包括 /index/_type/ 和 scroll,scroll 字段指定了scroll_id 的有效生存期,以分钟为单位,过期之后会被es 自动清理。如果文档不需要特定排序,可以指定按照文档创建的时间返回会使迭代更高效。
    [root@dnsserver ~]# curl -XGET 200.200.107.232:9200/product/info/_search?pretty&scroll=2m -d
    '{"query":{"match_all":{}}, "sort": ["_doc"]}'

    # 返回结果
    {
    "_scroll_id": "cXVlcnlBbmRGZXRjaDsxOzg3OTA4NDpTQzRmWWkwQ1Q1bUlwMjc0WmdIX2ZnOzA7",
    "took": 1,
    "timed_out": false,
    "_shards": {
    "total": 1,
    "successful": 1,
    "failed": 0
    },
    "hits":{...}
    }
     
    后续的文档读取上一次查询返回的scroll_id 来不断的取下一页,如果srcoll_id 的生存期很长,那么每次返回的 scroll_id 都是一样的,直到该 scroll_id 过期,才会返回一个新的 scroll_id。请求指定的 scroll_id 时就不需要 /index/_type 等信息了。每读取一页都会重新设置 scroll_id 的生存时间,所以这个时间只需要满足读取当前页就可以,不需要满足读取所有的数据的时间,1 分钟足以。
    [root@dnsserver ~]# curl -XGET '200.200.107.232:9200/_search/scroll?scroll=1m&scroll_id=cXVlcnlBbmRGZXRjaDsxOzg4NDg2OTpTQzRmWWkwQ1Q1bUlwMjc0WmdIX2ZnOzA7'

    #返回结果
    {
    "_scroll_id": "cXVlcnlBbmRGZXRjaDsxOzk1ODg3NDpTQzRmWWkwQ1Q1bUlwMjc0WmdIX2ZnOzA7",
    "took": 106,
    "_shards": {
    "total": 1,
    "successful": 1,
    "failed": 0
    },
    "hits": {
    "total": 22424,
    "max_score": 1.0,
    "hits": [{
    "_index": "product",
    "_type": "info",
    "_id": "did-519392_pdid-2010",
    "_score": 1.0,
    "_routing": "519392",
    "_source": {
    ....
    }
    }
    ]
    }
    }
     
    所有文档获取完毕之后,需要手动清理掉 scroll_id 。虽然es 会有自动清理机制,但是 srcoll_id 的存在会耗费大量的资源来保存一份当前查询结果集映像,并且会占用文件描述符。所以用完之后要及时清理。使用 es 提供的 CLEAR_API 来删除指定的 scroll_id
    ## 删掉指定的多个 srcoll_id
    [root@dnsserver ~]# curl -XDELETE 127.0.0.1:9200/_search/scroll -d
    '{"scroll_id" : ["cXVlcnlBbmRGZXRjaDsxOzg3OTA4NDpTQzRmWWkwQ1Q1bUlwMjc0WmdIX2ZnOzA7"]}'

    ## 删除掉所有索引上的 scroll_id
    [root@dnsserver ~]# curl -XDELETE 127.0.0.1:9200/_search/scroll/_all

    ## 查询当前所有的scroll 状态
    [root@dnsserver ~]# curl -XGET 127.0.0.1:9200/_nodes/stats/indices/search?pretty
    {
    "cluster_name" : "200.200.107.232",
    "nodes" : {
    "SC4fYi0CT5mIp274ZgH_fg" : {
    "timestamp" : 1514346295736,
    "name" : "200.200.107.232",
    "transport_address" : "200.200.107.232:9300",
    "host" : "200.200.107.232",
    "ip" : [ "200.200.107.232:9300", "NONE" ],
    "indices" : {
    "search" : {
    "open_contexts" : 0,
    "query_total" : 975758,
    "query_time_in_millis" : 329850,
    "query_current" : 0,
    "fetch_total" : 217069,
    "fetch_time_in_millis" : 84699,
    "fetch_current" : 0,
    "scroll_total" : 5348,
    "scroll_time_in_millis" : 92712468,
    "scroll_current" : 0
    }
    }
    }
    }
    }
     
    scroll + scan
    当 scroll 的文档不需要排序时,es 为了提高检索的效率,在 2.0 版本提供了 scroll + scan 的方式。随后又在 2.1.0 版本去掉了 scan 的使用,直接将该优化合入了 scroll 中。由于moa 线上的 es 版本是2.3 的,所以只简单提一下。使用的 scan 的方式是指定 search_type=scan

    # 2.0-beta 版本禁用 scroll 的排序,使遍历更加高效
    [root@dnsserver ~]# curl '127.0.0.1:9200/order/info/_search?scroll=1m&search_type=scan' -d '{"query":{"match_all":{}}'
    1
    2
    search_after 的方式
    上述的 scroll search 的方式,官方的建议并不是用于实时的请求,因为每一个 scroll_id 不仅会占用大量的资源(特别是排序的请求),而且是生成的历史快照,对于数据的变更不会反映到快照上。这种方式往往用于非实时处理大量数据的情况,比如要进行数据迁移或者索引变更之类的。那么在实时情况下如果处理深度分页的问题呢?es 给出了 search_after 的方式,这是在 >= 5.0 版本才提供的功能。

    search_after 分页的方式和 scroll 有一些显著的区别,首先它是根据上一页的最后一条数据来确定下一页的位置,同时在分页请求的过程中,如果有索引数据的增删改查,这些变更也会实时的反映到游标上。

    为了找到每一页最后一条数据,每个文档必须有一个全局唯一值,这种分页方式其实和目前 moa 内存中使用rbtree 分页的原理一样,官方推荐使用 _uid 作为全局唯一值,其实使用业务层的 id 也可以。

    第一页的请求和正常的请求一样,
    curl -XGET 127.0.0.1:9200/order/info/_search
    {
    "size": 10,
    "query": {
    "term" : {
    "did" : 519390
    }
    },
    "sort": [
    {"date": "asc"},
    {"_uid": "desc"}
    ]
    }
     
    第二页的请求,使用第一页返回结果的最后一个数据的值,加上 search_after 字段来取下一页。注意,使用 search_after 的时候要将 from 置为 0 或 -1
    curl -XGET 127.0.0.1:9200/order/info/_search
    {
    "size": 10,
    "query": {
    "term" : {
    "did" : 519390
    }
    },
    "search_after": [1463538857, "tweet#654323"],
    "sort": [
    {"date": "asc"},
    {"_uid": "desc"}
    ]
    }
     
    总结:search_after 适用于深度分页+ 排序,因为每一页的数据依赖于上一页最后一条数据,所以无法跳页请求。

    且返回的始终是最新的数据,在分页过程中数据的位置可能会有变更。这种分页方式更加符合moa的业务场景。

    es 库 scroll search 的实现
    由于当前服务端的 es 版本还局限于 2.3 ,所以无法使用的更高效的 search_after 的方式,在某些场景中为了能取得所有的数据,只能使用 scroll 的方式代替。以下基于 scroll_search 实现的 c API:

    es_cursor * co_es_scroll_search(char* esindex, char* estype,
    cJSON* query, cJSON* sort, cJSON* fields, int size, char* routing);
    BOOL es_scroll_cursor_next(es_cursor* cursor);
    void es_cursor_destroy(es_cursor* cursor);
    1
    2
    3
    4
    具体业务的使用场景如下:

    // 1. 获取第一个 scroll_id 和部分数据
    es_cursor *cursor = co_es_scroll_search((char*)index_name,(char*)type_name,
    queryJ, sortJ, fieldJ, size , routing);
    // 2. 迭代处理每一项数据,当前页的数据处理完毕之后会自动根据 scroll_id 去请求下一页,无需业务层关心
    while (es_scroll_cursor_next(cursor))
    {
    cJSON* data = es_cursor_json(cursor); //获取一项数据
    ....
    }
    // 3. 销毁游标,同时会清除无效的 scroll_id ,无需业务层关心
    es_cursor_destroy(cursor);
     
    附:es 版本变更记录如下

    2.0 -> 2.1 -> 2.2 -> 2.3 -> 2.4 -> 5.0 -> 5.1 -> 5.2 -> 5.3 -> 5.4 -> 5.5 -> 5.6 -> 6.0 -> 6.1

    https://www.elastic.co/guide/en/elasticsearch/reference/7.4/search-request-body.html#request-body-search-search-after

    Pagination of results can be done by using the from and size but the cost becomes prohibitive when the deep pagination is reached. The index.max_result_window which defaults to 10,000 is a safeguard, search requests take heap memory and time proportional to from + size. The Scrollapi is recommended for efficient deep scrolling but scroll contexts are costly and it is not recommended to use it for real time user requests. The search_after parameter circumvents this problem by providing a live cursor. The idea is to use the results from the previous page to help the retrieval of the next page.

    search_after is not a solution to jump freely to a random page but rather to scroll many queries in parallel. It is very similar to the scroll API but unlike it, the search_after parameter is stateless, it is always resolved against the latest version of the searcher. For this reason the sort order may change during a walk depending on the updates and deletes of your index.

  • 相关阅读:
    JQ插件
    jQuery radio的取值与赋值
    Js获取当前日期时间
    jquery 整理之一
    2014-9月收集整理之二(原生)
    Codeforces Round #279 (Div. 2) E-Restoring Increasing Sequence
    HDU-4431 麻将
    hdu-2222 AC自动机模板题
    Codeforces Round #460 (Div. 2) E. Congruence Equation
    Codeforces Round #459 (Div. 2) D. MADMAX
  • 原文地址:https://www.cnblogs.com/rsapaper/p/6781992.html
Copyright © 2011-2022 走看看