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=?就是指定快照时间。

注意:ES默认限制存活的scrollId个数为500个,超出报错。

比如:

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。ES默认限制存活的scrollId个数为500个,不适用开放于客户端使用。

场景:大数据量情况下,比如海量数据的导出。或者后台统计聚合任务等等。

三、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. 无法实现跳页请求。

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

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Elasticsearch 中使用深度分页功能需要注意以下几点: 1. 尽量避免使用深度分页功能,因为它会增加网络和计算开销,可能导致性能问题。 2. 深度分页功能是通过设置 from 和 size 参数来实现的。from 参数表示从哪个位置开始查询,size 参数表示每页返回的文档数量。 3. Elasticsearch 默认最多只能返回 10000 条记录,如果需要查询更多的记录,需要设置 index.max_result_window 参数。但是设置太大会占用过多的内存,影响性能。 下面是一个 Java 实现 Elasticsearch 分页查询的示例代码: ``` import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.SearchType; import org.elasticsearch.client.Client; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHits; import org.elasticsearch.search.sort.SortBuilders; import org.elasticsearch.search.sort.SortOrder; public class ESQuery { private Client client; public ESQuery(Client client) { this.client = client; } public void search(String index, String type, int from, int size) { SearchResponse response = client.prepareSearch(index) .setTypes(type) .setQuery(QueryBuilders.matchAllQuery()) .addSort(SortBuilders.fieldSort("_id").order(SortOrder.DESC)) .setSearchType(SearchType.DFS_QUERY_THEN_FETCH) .setFrom(from) .setSize(size) .execute() .actionGet(); SearchHits hits = response.getHits(); for (SearchHit hit : hits) { System.out.println(hit.getSourceAsString()); } } } ``` 调用示例: ``` ESQuery esQuery = new ESQuery(client); esQuery.search("my_index", "my_type", 0, 10); // 查询第一页,每页10条记录 esQuery.search("my_index", "my_type", 10, 10); // 查询第二页,每页10条记录,从第11条记录开始 ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程大帅气

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值