1. ElasticSearch分頁查詢的3個(gè)坑

        共 12974字,需瀏覽 26分鐘

         ·

        2022-08-03 06:04

        你知道的越多,不知道的就越多,業(yè)余的像一棵小草!

        你來,我們一起精進(jìn)!你不來,我和你的競爭對(duì)手一起精進(jìn)!

        編輯:業(yè)余草

        推薦:https://www.xttblog.com/?p=5351

        ES支持的三種分頁查詢方式

        • From + Size 查詢
        • Scroll 遍歷查詢
        • Search After 查詢
        Scroll

        「說明:」

        官方已經(jīng)不再推薦采用Scroll API進(jìn)行深度分頁。如果遇到超過 10000 的深度分頁,推薦采用search_after + PIT。

        官方文檔地址:https://www.elastic.co/guide/en/elasticsearch/reference/7.14/paginate-search-results.html

        分布式系統(tǒng)中的深度分頁問題

        「為什么分布式存儲(chǔ)系統(tǒng)中對(duì)深度分頁支持都不怎么友好呢?」

        首先我們看一下分布式存儲(chǔ)系統(tǒng)中分頁查詢的過程。

        下面是重點(diǎn)。。。


        假設(shè)在一個(gè)有 4 個(gè)主分片的索引中搜索,每頁返回10條記錄。

        當(dāng)我們請求結(jié)果的第1頁(結(jié)果從 1 到 10 ),每一個(gè)分片產(chǎn)生前 10 的結(jié)果,并且返回給 協(xié)調(diào)節(jié)點(diǎn) ,協(xié)調(diào)節(jié)點(diǎn)對(duì) 40 個(gè)結(jié)果排序得到全部結(jié)果的前 10 個(gè)。

        當(dāng)我們請求第 99 頁(結(jié)果從 990 到 1000),需要從每個(gè)分片中獲取滿足查詢條件的前1000個(gè)結(jié)果,返回給協(xié)調(diào)節(jié)點(diǎn), 然后協(xié)調(diào)節(jié)點(diǎn)對(duì)全部 4000 個(gè)結(jié)果排序,獲取前10個(gè)記錄。

        當(dāng)請求第10000頁,每頁10條記錄,則需要先從每個(gè)分片中獲取滿足查詢條件的前100010個(gè)結(jié)果,返回給協(xié)調(diào)節(jié)點(diǎn)。然后協(xié)調(diào)節(jié)點(diǎn)需要對(duì)全部(100010 * 分片數(shù)4)的結(jié)果進(jìn)行排序,然后返回前10個(gè)記錄。

        可以看到,在分布式系統(tǒng)中,對(duì)結(jié)果排序的成本隨分頁的深度成指數(shù)上升。

        這就是 web 搜索引擎對(duì)任何查詢都不要返回超過 10000 個(gè)結(jié)果的原因。

        分布式系統(tǒng)中的深度分頁問題

        From + Size 查詢

        準(zhǔn)備數(shù)據(jù)

        PUT user_index
        {
          "mappings": {
             "properties": {
                "id":  {"type""integer"},
                "name":   {"type""keyword"}
             }
          }
        }

        POST user_index/_bulk
        "create":  {  "_id""1" }}
        "id":1,"name":"老萬"}
        "create":  {  "_id""2" }}
        "id":2,"name":"老王"}
        "create":  {  "_id""3" }}
        "id":3,"name":"老劉"}
        "create":  {  "_id""4" }}
        "id":4,"name":"小明"}
        "create":  {  "_id""5" }}
        "id":5,"name":"小紅"}

        查詢演示

        「無條件查詢」

        POST user_index/_search

        默認(rèn)返回前10個(gè)匹配的匹配項(xiàng)。其中:

        • from:未指定,默認(rèn)值是 0,注意不是1,代表當(dāng)前頁返回?cái)?shù)據(jù)的起始值。
        • size:未指定,默認(rèn)值是 10,代表當(dāng)前頁返回?cái)?shù)據(jù)的條數(shù)。

        「指定from+size查詢」

        POST user_index/_search
        {
            "from"0
            "size"10,
            "query": {
                "match_all": {}
            },
            "sort": [
                {"id""asc"}    
            ]
        }

        max_result_window

        es 默認(rèn)采用的分頁方式是 from+ size 的形式,在深度分頁的情況下,這種使用方式效率是非常低的。

        比如 from = 5000,size=10, es 需要在各個(gè)分片上匹配排序并得到5000*10條有效數(shù)據(jù),然后在結(jié)果集中取最后 10條數(shù)據(jù)返回,這種方式類似于 mongo 的 skip + size。

        除了效率上的問題,還有一個(gè)無法解決的問題是,es 目前支持最大的 skip 值是 「max_result_window ,默認(rèn)為 10000」。也就是當(dāng) from + size > max_result_window 時(shí),es 將返回錯(cuò)誤。

        POST user_index/_search
        {
            "from": 10000, 
            "size": 10,
            "query": {
                "match_all": {}
            },
            "sort": [
                {"id""asc"}    
            ]
        }

        這是ElasticSearch最簡單的分頁查詢,但以上命令是會(huì)報(bào)錯(cuò)的。

        報(bào)錯(cuò)信息,指window默認(rèn)是10000。

        "root_cause": [
              {
                "type""illegal_argument_exception",
                "reason""Result window is too large, from + size must be less than or equal to: [10000] but was [10001]. 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 setting."
              }
            ],
            "type""search_phase_execution_exception",

        怎么解決這個(gè)問題,首先能想到的就是調(diào)大這個(gè) window。

        PUT user_index/_settings

            "index" : { 
                "max_result_window" : 20000
            }
        }

        然后這種方式只能暫時(shí)解決問題,當(dāng)es 的使用越來越多,數(shù)據(jù)量越來越大,深度分頁的場景越來越復(fù)雜時(shí),如何解決這種問題呢?

        「官方建議:」

        避免過度使用 from 和 size 來分頁或一次請求太多結(jié)果。

        不推薦使用 from + size 做深度分頁查詢的核心原因:

        • 搜索請求通??缭蕉鄠€(gè)分片,每個(gè)分片必須將其請求的命中內(nèi)容以及任何先前頁面的命中內(nèi)容加載到內(nèi)存中。
        • 對(duì)于翻頁較深的頁面或大量結(jié)果,這些操作會(huì)顯著增加內(nèi)存和 CPU 使用率,從而導(dǎo)致性能下降或節(jié)點(diǎn)故障。

        Search After 查詢

        search_after 參數(shù)使用上一頁中的一組排序值來檢索下一頁的數(shù)據(jù)。

        使用 search_after 需要具有相同查詢和排序值的多個(gè)搜索請求。如果在這些請求之間發(fā)生刷新,結(jié)果的順序可能會(huì)發(fā)生變化,從而導(dǎo)致跨頁面的結(jié)果不一致。為防止出現(xiàn)這種情況,您可以創(chuàng)建一個(gè)時(shí)間點(diǎn) (PIT) 以保留搜索中的當(dāng)前索引狀態(tài)。

        時(shí)間點(diǎn)Point In Time(PIT)保障搜索過程中保留特定事件點(diǎn)的索引狀態(tài)。

        「注意??:」

        es 給出了 search_after 的方式,這是在 >= 5.0 版本才提供的功能。

        Point In Time(PIT)是 Elasticsearch 7.10 版本之后才有的新特性。

        「PIT的本質(zhì):存儲(chǔ)索引數(shù)據(jù)狀態(tài)的輕量級(jí)視圖?!?/strong>

        如下示例能很好的解讀 PIT 視圖的內(nèi)涵。

        #1、給索引user_index創(chuàng)建pit
        POST /user_index/_pit?keep_alive=5m

        #2、統(tǒng)計(jì)當(dāng)前記錄數(shù) 5
        POST /user_index/_count

        #3、根據(jù)pit統(tǒng)計(jì)當(dāng)前記錄數(shù) 5
        GET /_search
        {
          "query": {
                "match_all": {}
            },
          "pit": {
             "id":  "i6-xAwEKdXNlcl9pbmRleBZYTXdtSFRHeVJrZVhCby1OTjlHMS1nABZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3AAAAAAAAIODBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EBFlhNd21IVEd5UmtlWEJvLU5OOUcxLWcAAA=="
             "keep_alive""5m"
          },
          "sort": [
                {"id""asc"}    
            ]
        }
        #4、插入一條數(shù)據(jù)
        POST user_index/_bulk
        "create":  {  "_id""6" }}
        "id":6,"name":"老李"}

        #5、數(shù)據(jù)總量 6
        POST /user_index/_count

        #6、根據(jù)pit統(tǒng)計(jì)數(shù)據(jù)總量還是 5 ,說明是根據(jù)時(shí)間點(diǎn)的視圖進(jìn)行統(tǒng)計(jì)。
        GET /_search
        {
          "query": {
                "match_all": {}
            },
          "pit": {
             "id":  "i6-xAwEKdXNlcl9pbmRleBZYTXdtSFRHeVJrZVhCby1OTjlHMS1nABZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3AAAAAAAAIODBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EBFlhNd21IVEd5UmtlWEJvLU5OOUcxLWcAAA=="
             "keep_alive""5m"
          },
          "sort": [
                {"id""asc"}    
            ]
        }

        有了 PIT,search_after 的后續(xù)查詢都是基于 PIT 視圖進(jìn)行,能有效保障數(shù)據(jù)的一致性。

        search_after 分頁查詢可以簡單概括為如下幾個(gè)步驟。

        獲取索引的pit

        POST /user_index/_pit?keep_alive=5m

        根據(jù)pit首次查詢

        說明:根據(jù)pit查詢的時(shí)候,不用指定索引名稱。

        GET /_search
        {
          "query": {
                "match_all": {}
            },
          "pit": {
             "id":  "i6-xAwEKdXNlcl9pbmRleBZYTXdtSFRHeVJrZVhCby1OTjlHMS1nABZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3AAAAAAAAIODBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EBFlhNd21IVEd5UmtlWEJvLU5OOUcxLWcAAA=="
             "keep_alive""1m"
          },
          "sort": [
                {"id""asc"}    
            ]
        }

        查詢結(jié)果:返回的sort值為2.

        hits" : [
              {
                "
        _index" : "user_index",
                "
        _type" : "_doc",
                "
        _id" : "2",
                "
        _score" : null,
                "
        _source" : {
                  "
        id" : 2,
                  "
        name" : "老王"
                },
                "
        sort" : [
                  2
                ]
              }
            ]

        根據(jù)search_after和pit進(jìn)行翻頁查詢

        說明:

        search_after指定為上一次查詢返回的sort值。

        要獲得下一頁結(jié)果,請使用最后一次命中的排序值(包括 tiebreaker)作為 search_after 參數(shù)重新運(yùn)行先前的搜索。如果使用 PIT,請?jiān)?pit.id 參數(shù)中使用最新的 PIT ID。搜索的查詢和排序參數(shù)必須保持不變。如果提供,則 from 參數(shù)必須為 0(默認(rèn)值)或 -1。

        GET /_search
        {
          "size": 1, 
          "query": {
                "match_all": {}
            },
          "pit": {
             "id":  "i6-xAwEKdXNlcl9pbmRleBZYTXdtSFRHeVJrZVhCby1OTjlHMS1nABZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3AAAAAAAAIOJ7FmdBWEd2UmFVVGllZldNdnhPZDJmX0EBFlhNd21IVEd5UmtlWEJvLU5OOUcxLWcAAA=="
             "keep_alive""5m"
          },
          "sort": [
                {"id""asc"}    
            ],
          "search_after": [                                
            2
          ]
        }

        search_after 優(yōu)缺點(diǎn)分析

        search_after 查詢僅支持向后翻頁。

        不嚴(yán)格受制于max_result_window,可以無限制往后翻頁。

        單次請求值不能超過 max_result_window;但總翻頁結(jié)果集可以超過。

        面試題思考

        1. 為什么采用search_after查詢能解決深度分頁的問題?
        2. search_after + pit 分頁查詢過程中,PIT 視圖過期怎么辦?
        3. search_after查詢,如果需要回到前幾頁怎么辦?

        Scroll 遍歷查詢

        ES 官方不再推薦使用Scroll API進(jìn)行深度分頁。如果您需要在分頁超過 10000 個(gè)點(diǎn)擊時(shí)保留索引狀態(tài),請使用帶有時(shí)間點(diǎn) (PIT) 的 search_after 參數(shù)。

        相比于 From + size 和 search_after 返回一頁數(shù)據(jù),Scroll API 可用于從單個(gè)搜索請求中檢索大量結(jié)果(甚至所有結(jié)果),其方式與傳統(tǒng)數(shù)據(jù)庫中游標(biāo)(cursor)類似。

        Scroll API 原理上是對(duì)某次查詢生成一個(gè)游標(biāo) scroll_id, 后續(xù)的查詢只需要根據(jù)這個(gè)游標(biāo)去取數(shù)據(jù),直到結(jié)果集中返回的 hits 字段為空,就表示遍歷結(jié)束。scroll_id 的生成可以理解為建立了一個(gè)臨時(shí)的歷史快照,在此之后的增刪改查等操作不會(huì)影響到這個(gè)快照的結(jié)果。

        所有文檔獲取完畢之后,需要手動(dòng)清理掉 scroll_id。雖然es 會(huì)有自動(dòng)清理機(jī)制,但是 srcoll_id 的存在會(huì)耗費(fèi)大量的資源來保存一份當(dāng)前查詢結(jié)果集映像,并且會(huì)占用文件描述符。所以用完之后要及時(shí)清理。使用 es 提供的 CLEAR_API 來刪除指定的 scroll_id

        首次查詢,并獲取_scroll_id

        POST /user_index/_search?scroll=1m
        {
          "size": 1,
          "query": {
                "match_all": {}
            }
        }

        返回結(jié)果:

        {
          "_scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EAAAAAACDlQBZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3",
          "took" : 0,
          "timed_out" : false,
          "_shards" : {
            "total" : 1,
            "successful" : 1,
            "skipped" : 0,
            "failed" : 0
          },
          "hits" : {
            "total" : {
              "value" : 6,
              "relation" : "eq"
            },
            "max_score" : 1.0,
            "hits" : [
              {
                "_index" : "user_index",
                "_type" : "_doc",
                "_id" : "1",
                "_score" : 1.0,
                "_source" : {
                  "id" : 1,
                  "name" : "老萬"
                }
              }
            ]
          }
        }

        根據(jù)scroll_id遍歷數(shù)據(jù)

        POST /_search/scroll                                                               
        {
          "scroll" : "1m",      
          "scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EAAAAAACDlKxZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3" 
        }

        刪除游標(biāo)scroll

        DELETE /_search/scroll
        {
            "scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EAAAAAACDlKxZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3"
        }

        優(yōu)缺點(diǎn)

        scroll查詢的相應(yīng)數(shù)據(jù)是非實(shí)時(shí)的,這點(diǎn)和 PIT 視圖比較類似,如果遍歷過程中插入新的數(shù)據(jù),是查詢不到的。

        并且保留上下文需要足夠的堆內(nèi)存空間。

        適用場景

        全量或數(shù)據(jù)量很大時(shí)遍歷結(jié)果數(shù)據(jù),而非分頁查詢。

        「官方文檔強(qiáng)調(diào):」

        不再建議使用scroll API進(jìn)行深度分頁。如果要分頁檢索超過 Top 10000+ 結(jié)果時(shí),推薦使用:PIT + search_after。

        業(yè)務(wù)層面優(yōu)化

        很多時(shí)候,技術(shù)解決不了的問題,可以通過業(yè)務(wù)層面變通下來解決!

        比如,針對(duì)分頁場景,我們可以采用如下優(yōu)化方案。

        增加默認(rèn)的篩選條件

        通過盡可能的增加默認(rèn)的篩選條件,如:時(shí)間周期和最低評(píng)分,減少滿足條件的數(shù)據(jù)量,避免出現(xiàn)深度分頁的情況。

        采用滾動(dòng)增量顯示

        典型場景比如手機(jī)上面瀏覽微博,可以一直往下滾動(dòng)加載。

        示例:

        如下列表展示中,取消了分頁按鈕,通過滾動(dòng)條增量加載數(shù)據(jù)。

        滾動(dòng)分頁

        小范圍跳頁

        通過對(duì)分頁組件的設(shè)計(jì),禁止用戶直接跳轉(zhuǎn)到非常大的頁碼中。比如直接跳轉(zhuǎn)到最后一頁這種操作。

        示例:google搜索的小范圍跳頁。

        谷歌搜索小范圍跳頁

        總結(jié)

        分布式存儲(chǔ)引擎的深度分頁目前沒有完美的解決方案。

        比如針對(duì)百度、google這種全文檢索的查詢,通過From+ size返回Top 10000 條數(shù)據(jù)完全能滿足使用需求,末尾查詢評(píng)分非常低的結(jié)果一般參考意義都不大。

        • From+ size:需要隨機(jī)跳轉(zhuǎn)不同分頁(類似主流搜索引擎)、Top 10000 條數(shù)據(jù)之內(nèi)分頁顯示場景。
        • search_after:僅需要向后翻頁的場景及超過Top 10000 數(shù)據(jù)需要分頁場景。
        • Scroll:需要遍歷全量數(shù)據(jù)場景 。
        • max_result_window:調(diào)大治標(biāo)不治本,不建議調(diào)過大。
        • PIT:本質(zhì)是視圖。
        分布式存儲(chǔ)引擎的深度分頁目前沒有完美的解決方案
        百度搜索分頁

        百度搜索的分頁最多只能到 76 頁,不管你搜索的結(jié)果匹配了多少內(nèi)容,只能翻到第 76 頁,而且也只能小范圍跳頁。

        搜索引擎都不能無限的翻頁下去
        es深度分頁問題
        淘寶搜索只有100頁

        分布式存儲(chǔ)引擎的搜索,有天然的缺陷存在,沒有完美的方案。當(dāng)存在技術(shù)解決不了的問題,那就從產(chǎn)品層面解決它。

        瀏覽 138
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. 韩国伦理片女教师 | 欧美 亚洲另类性爱 | 黄色搞逼视频 | 综合五月天 | 欧美理伦|