1. 程式人生 > 其它 >解決 Elastic Search 的深分頁問題

解決 Elastic Search 的深分頁問題

Elastic Search 為了避免深分頁,不允許使用分頁(from + size)查詢 10000 條以後的資料,因此如果要查詢第 10000 條以後的資料,要使用 Elastic Search 提供的 scroll 遊標 來查詢

1. 為什麼不能使用 from + size 進行深分頁查詢?

之所以 Elastic Search 不支援使用 from + size 來查詢 10000 條以後的資料,是因為假設取的頁數較大時(深分頁),像是請求第 20 頁,Elastic Search 不得不取出所有分片上的第 1 頁到第 20 頁的所有文件,並做排序,最終再取出 from 後的 size 條結果作為最終的返回值

假設你有 16 個分片,則需要在 coordinate node 彙總到 shards * (from + size) 條記錄,即需要 16 * (20 + 10) 記錄後做一次全域性排序,而當索引非常非常大(千萬或億)時,是無法使用 from + size 做深分頁的,分頁越深則越容易 Out Of Memory,即使你運氣很好沒有發生 Out Of Memory,也會非常消耗 CPU 和記憶體資源

為了保護機器,Elastic Search 使用index.max_result_window:10000 這個設定作為保護措施 ,即預設 from + size 不能超過 10000,雖然這個引數可以動態修改,也可以在配置檔案配置,但是最好不要這麼做,應該改用 Elastic Search 提供的 scroll 方法來取得資料

2. scroll 遊標原理

可以把 scroll 理解為關係型資料庫裡的 cursor,因此,scroll 並不適合用來做實時搜尋,而更適用於後臺批處理任務,比如群發,使用 scroll 可以增加效能的原因,是因為如果做深分頁,每次搜尋都必須重新排序,非常浪費,而使用 scroll 就是一次把要用的資料都排完了,分批取出,因此比使用 from + size 還好

scroll 具體分為初始化和遍歷兩步

  • 初始化時將所有符合搜尋條件的搜尋結果快取起來,可以想象成快照
  • 在遍歷時,從這個快照裡取資料

也就是說,在初始化後對索引插入、刪除、更新資料都不會影響遍歷結果

3. 具體例項

初始化 - 請求

GET my_index/_search?scroll=1m
{
    "query":{
        "range":{
            "createTime": {
                "gte": 1522229999999
            }
        }
    },
    "size": 1000
}
  • 注意要在URL中的search後加上 scroll=1m,不能寫在
    request body
    中,其中 1m 表示這個遊標要保持開啟 1 分鐘
  • 可以指定 size 大小,就是每次回傳幾筆資料,當回傳到沒有資料時,仍會返回 200 成功,只是 hits 裡的 hits 會是空 list
  • 在初始化時除了回傳 _scroll_id,也會回傳前 100 筆(假設 size = 100)的資料
  • request body 和一般搜尋一樣,因此可以說在初始化的過程中,除了加上 scroll 設定遊標開啟時間之外,其他的都跟一般的搜尋沒有兩樣(要設定查詢條件,也會回傳前 size 筆的資料)

初始化 - 返回結果

{
    "_scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAfv5-FjNOamF0Mk1aUUhpUnU5ZWNMaHJocWcAAAAAAH7-gBYzTmphdDJNWlFIaVJ1OWVjTGhyaHFnAAAAAAB-_n8WM05qYXQyTVpRSGlSdTllY0xocmhxZwAAAAAAdsJxFmVkZTBJalJWUmp5UmI3V0FYc2lQbVEAAAAAAHbCcBZlZGUwSWpSVlJqeVJiN1dBWHNpUG1R",
    "took": 2,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 84,
        "max_score": 1,
        "hits": [
            {
                "_index": "video1522821719",
                "_type": "doc",
                "_id": "84056",
                "_score": 1,
                "_source": {
                    "title": "三個院子",
                    "createTime": 1522239744000
                }
            }
            ....99 data
        ]
    }
}

遍歷資料 - 請求

使用初始化返回的 _scroll_id 來進行請求,每一次請求都會繼續返回初始化中未讀完資料,並且會返回一個 _scroll_id,這個 _scroll_id 可能會改變,因此每一次請求應該帶上上一次請求返回的 _scroll_id,且每次傳送 scroll 請求時,都要在請求引數帶上 scroll=1m,重新重新整理這個 scroll 的開啟時間,以防不小心超時導致資料取得不完整

另外要注意一個小細節,返回的結果中的欄位返回的是 _scroll_id,但是放在請求裡的欄位則是 scroll_id,兩者拼寫上有不同

GET _search/scroll?scroll=1m
{
    "scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAdsMqFmVkZTBJalJWUmp5UmI3V0FYc2lQbVEAAAAAAHbDKRZlZGUwSWpSVlJqeVJiN1dBWHNpUG1RAAAAAABpX2sWclBEekhiRVpSRktHWXFudnVaQ3dIQQAAAAAAaV9qFnJQRHpIYkVaUkZLR1lxbnZ1WkN3SEEAAAAAAGlfaRZyUER6SGJFWlJGS0dZcW52dVpDd0hB"
}

遍歷資料 - 返回結果

如果沒有資料了,就會回傳空的 hits,可以用這個判斷是否遍歷完成了資料

{
    "_scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAdsMqFmVkZTBJalJWUmp5UmI3V0FYc2lQbVEAAAAAAHbDKRZlZGUwSWpSVlJqeVJiN1dBWHNpUG1RAAAAAABpX2sWclBEekhiRVpSRktHWXFudnVaQ3dIQQAAAAAAaV9qFnJQRHpIYkVaUkZLR1lxbnZ1WkN3SEEAAAAAAGlfaRZyUER6SGJFWlJGS0dZcW52dVpDd0hB",
    "took": 2,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 84,
        "max_score": null,
        "hits": []
    }
}

4. 優化 scroll 查詢

在一般場景下,scroll 通常用來取得需要排序過後的大筆資料,但是有時候資料之間的排序性對我們而言是沒有關係的,只要所有資料都能取出來就好,這時能夠對 scroll 進行優化

請求初始化

使用 _doc 去 sort 得出來的結果,這個執行的效率最快,但是資料就不會有排序,適合用在只想取得所有資料的場景

GET my_index/_search?scroll=1m
{
    "query": {
        "match_all" : {}
    },
    "sort": ["_doc"]
}

清除 scroll

雖然我們在設定開啟 scroll 時,設定了一個 scroll 的存活時間,但是如果能夠在使用完順手關閉,可以提早釋放資源,降低 ES 的負擔

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

5. scroll查詢指令碼

為了方便大家使用 scroll 進行深分頁查詢,我這邊寫了個bash指令碼,提供給大家使用

注意,因為 Elastic Search 改版的關係,本指令碼僅適用於 Elastic Search 7.x 以上

因指令碼需要,記得提前安裝 jq 套件

  • mac 環境使用 brew install jq 安裝
  • centos 環境使用 sudo yum install -y jq 安裝
  • ubuntu 環境使用 sudo apt install jq 安裝

使用方法

./es_scroll

指令碼 es_scroll.sh 內容如下

#!/bin/bash

# 本指令碼僅適用於 Elastic Search 7.x 以上

# 存放檔案路徑
FILE="/home/temp"

if [ -f $FILE ]; then
   rm $FILE
fi
touch "$FILE"

# Elastic Search 訪問地址
HOST=""

# Elastic Search index
INDEX=""

function scroll() {
    # 可以根據業務邏輯自行修改 query dsl
    search_dsl='
        {
            "query": {
                "match_all": {}
            },
            "size": 100
        }
        '

    json=$(curl -s -XGET "$HOST/$INDEX/_search?scroll=1m" -H "Content-Type: application/json" -d "$search_dsl")
    scroll_id=$(echo  $json | jq -c -r "._scroll_id")
    hits=$(echo  $json | jq -c ".hits.hits")

    length=$(echo  $hits | jq length)

    for i in $(seq $length)
    do
        index=$i-1
        hit=$(echo  $hits | jq -c ".[$index]._source")
        echo  $hit >> $FILE
    done

    scroll_dsl='{"scroll_id": "'"$scroll_id"'"}'

    while true
    do
        hits=$(curl -s -XGET "$HOST/_search/scroll?scroll=1m" -H "Content-Type: application/json" -d "$scroll_dsl" | jq -c ".hits.hits")

        if [ "$hits" == "" ];
        then
            echo  "exit"
            exit
        fi

        length=$(echo  $hits | jq length)

        for i in $(seq $length)
        do
            index=$i-1
            hit=$(echo  $hits | jq -c ".[$index]._source")
            echo  $hit >> $FILE
        done
    done
}

scroll

原文出自https://zhuanlan.zhihu.com/p/109068603