ElasticSearch之处理深度分页

ElasticSearch之处理深度分页

在ES中实现分页的方法有三种,我们逐个分析一下他们的优缺点。

一、常规分页

在ES中,我们可以给查询条件加fromsize达到分页的效果,比如:

get test_index/_doc/_search
{
  "query":{
    "match_all":{}
  },
  "from":1000,
  "size":3
}

但是这种使用方式效率是非常低的,比如上面那条语句,意味着ES要在每个分片上面匹配排序并得到1003条数据,协调节点拿到这些数据再进行排序处理,最终返回我们需要的3条数据。

其次,ES为了性能考虑,限制了分页的深度,目前ES支持最大的max_result_window = 10000,也就是说当我们指定分页个数超过10000时,ES就不支持了

比如

get test_index/_doc/_search
{
  "query":{
    "match_all":{}
  },
  "from":9998,
  "size":3
}
------------------------------

"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."

总结
缺点:常规分页性能低下,不支持深度分页,默认10000条数据后不支持查询,深度分页会给每个分片的内存造成一定程度的压力。

优点:实现起来比较简单

场景:适合数据量小、没有深度分页的场景下可以使用。

二、scroll分页

如果我们分页一次请求需要获取较大的数据集,scroll是一个非常好的解决方案。

如果想要使用,只需要在请求中加一个参数scroll=? ?是什么呢?

其实,es的滚动搜索,是先搜索一批数据,然后下次再搜索一批数据,以此类推,直到搜索出全部数据。scroll搜索会在第一次请求的时候,保存一个当时的试图快照,之后都只会基于该旧的试图快照来提供数据搜索,如果这个快照时间内数据进行了变更,用户是看不到的。而我们添加的参数scroll=?就是指定快照时间。

比如:

get test_index/_doc/_search?scroll=1m
{
  "query":{
    "match_all":{}
  },
  "from":0,
  "size":1
}
------------------------------
{
  "_scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFmZrX09mWXpvUjZPV2hJVlF3b0hGY0EAAAAAAAAQsxZyT3ZKTDlLZlJIeUdkSEdONmtnb0pB",
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 9,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "test_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "name" : {
            "uid" : 1,
            "name" : "程大帅",
            "address" : "上海市汤臣一品"
          }
        }
      }
    ]
  }
}

会发现和平常的搜索查询不同,本次响应中携带了参数"_scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFmZrX09mWXpvUjZPV2hJVlF3b0hGY0EAAAAAAAAQsxZyT3ZKTDlLZlJIeUdkSEdONmtnb0pB"

接下来我们可以使用这个scroll_id来进行滚动查询,因为我们上面指定了滚动的步长:1,所以我们再使用scroll命令的时候不用指定步长,会每次都给我们一条数据,直到快照内数据为空。

post _search/scroll
{
  "scroll":"5m",
  "scroll_id":"FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFmZrX09mWXpvUjZPV2hJVlF3b0hGY0EAAAAAAAAQsxZyT3ZKTDlLZlJIeUdkSEdONmtnb0pB"
}

快照内数据滚动完毕就会是下面的样子。

{
  "_scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFmZrX09mWXpvUjZPV2hJVlF3b0hGY0EAAAAAAAAQsxZyT3ZKTDlLZlJIeUdkSEdONmtnb0pB",
  "took" : 1,
  "timed_out" : false,
  "terminated_early" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 9,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [ ]
  }
}

总结
优点:解决了深度分页问题,性能相对来说还可以。

缺点:快照时间内数据修改,无法及时感应到。滚动查询时需要维护scroll_id。

场景:大数据量情况下,比如海量数据的导出。

三、search_after

上面我们介绍了常规分页和scroll滚动分页。

常规分页无法避免深度分页问题。
scroll能够解决深度分页问题但是无法实现较实时的查询。

这时候可以考虑使用第三种分页方式search_after,这其实是一个假分页,根据上一页最后一条数据来确定下一页的开始位置。但是使用它每个文档必须要有一个全局唯一的值,比如我们的业务id,或者官方自动生成的id:_uid等,只要能表示其唯一性即可。

示例1:
比如我们使用官方生成的uid来进行search_after查询。

get test_index/_doc/_search
{
  "query":{
    "match_all":{}
  },
  "from":0,
  "size":1,
  "sort":{
    "_id":{
      "order":"desc"
    }
  }
}
----------------------
{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 7,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [
      {
        "_index" : "test_index",
        "_type" : "_doc",
        "_id" : "xc7BBn4B1rLCL6KXklDg",
        "_score" : null,
        "_source" : {
          "name" : {
            "id" : 6,
            "name" : "程六帅",
            "address" : "上海市汤臣六品"
          }
        },
        "sort" : [
          "xc7BBn4B1rLCL6KXklDg"
        ]
      }
    ]
  }
}

下次分页需要将上述分页结果集的最后一条数据的排序值带上。

get test_index/_doc/_search
{
  "query":{
    "match_all":{}
  },
  "from":0,
  "size":1,
  "search_after":["xc7BBn4B1rLCL6KXklDg"],
  "sort":{
    "_id":{
      "order":"desc"
    }
  }
}
-----------------------------------------
{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 7,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [
      {
        "_index" : "test_index",
        "_type" : "_doc",
        "_id" : "xM7BBn4B1rLCL6KXdVCl",
        "_score" : null,
        "_source" : {
          "name" : {
            "id" : 5,
            "name" : "程五帅",
            "address" : "上海市汤臣五品"
          }
        },
        "sort" : [
          "xM7BBn4B1rLCL6KXdVCl"
        ]
      }
    ]
  }
}

示例2:
实际上ES为我们提供的uid是无序的,所以我们最好文档中要包含一个唯一键,比如我们现在对文档中的唯一键id来进行search_after。

get test_index/_doc/_search
{
  "query":{
    "match_all":{}
  },
  "from":0,
  "size":1,
  "sort":{
    "name.id":"desc"
  }
}
-----------------------
{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 7,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [
      {
        "_index" : "test_index",
        "_type" : "_doc",
        "_id" : "xc7BBn4B1rLCL6KXklDg",
        "_score" : null,
        "_source" : {
          "name" : {
            "id" : 6,
            "name" : "程六帅",
            "address" : "上海市汤臣六品"
          }
        },
        "sort" : [
          6
        ]
      }
    ]
  }
}

再根据返回值的sort来进行search_after的参数构建。

get test_index/_doc/_search
{
  "query":{
    "match_all":{}
  },
  "search_after":["6"],
  "from":0,
  "size":1,
  "sort":{
    "name.id":"desc"
  }
}
---------------------------
{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 7,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [
      {
        "_index" : "test_index",
        "_type" : "_doc",
        "_id" : "xM7BBn4B1rLCL6KXdVCl",
        "_score" : null,
        "_source" : {
          "name" : {
            "id" : 5,
            "name" : "程五帅",
            "address" : "上海市汤臣五品"
          }
        },
        "sort" : [
          5
        ]
      }
    ]
  }
}

总结
优点:性能强劲,根据唯一排序键来滚动查询,不存在深度分页问题,也能够避免scroll的数据非实时性。

缺点:

  1. 实现较为复杂,需要文档中存在全局唯一键。
  2. 每一次查询都要带上上一次查询的结果。
  3. 无法实现跳页请求。

场景:适用于海量数据分页场景。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
https://github.com/yiisoft/yii2-elasticsearch/model部分:

)">
< <上一篇
下一篇>>