前言:在接口设计上,对数据进行查询时,往往会采用分页查询的形式进行数据的拉取,主要是为了避免一次性返回过大的结果导致对网络,内存,客户端应用程序,集群服务等产生过大的压力,导致出现性能问题。在elasticsearch中分页查询主要有两种方式,from size分页查询与scroll深度分页查询。
一.from size分页查询
使用from
和size
参数来进行分页查询。from
参数用于指定查询结果的起始位置,size
参数用于指定每页返回的文档数量。
操作样例
代码语言:javascript复制#Python
from elasticsearch import Elasticsearch
# 创建 Elasticsearch 客户端
es = Elasticsearch()
# 定义查询条件
query = {
"query": {
"match_all": {} # 这里可以替换为其他查询条件
},
"size": 10, # 每页返回的文档数量
"from": 0 # 查询结果的起始位置
}
# 执行查询
result = es.search(index="your_index_name", body=query)
# 处理查询结果
total_hits = result["hits"]["total"]["value"] # 总命中数
hits = result["hits"]["hits"] # 查询命中的文档列表
for hit in hits:
# 处理每个文档的数据
doc_id = hit["_id"]
source = hit["_source"]
# 输出分页信息
print(f"Total hits: {total_hits}")
print(f"Returned hits: {len(hits)}")
#Java
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.builder.SearchSourceBuilder;
public class PaginationExample {
public static void main(String[] args) {
// 创建 Elasticsearch 客户端
RestHighLevelClient client = new RestHighLevelClient();
// 定义查询请求
SearchRequest searchRequest = new SearchRequest("your_index_name");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchAllQuery()); // 这里可以替换为其他查询条件
sourceBuilder.from(0); // 查询结果的起始位置
sourceBuilder.size(10); // 每页返回的文档数量
searchRequest.source(sourceBuilder);
try {
// 执行查询
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
// 处理查询结果
long totalHits = response.getHits().getTotalHits().value; // 总命中数
SearchHit[] hits = response.getHits().getHits(); // 查询命中的文档列表
for (SearchHit hit : hits) {
// 处理每个文档的数据
String docId = hit.getId();
Map<String, Object> source = hit.getSourceAsMap();
// ...
}
// 输出分页信息
System.out.println("Total hits: " totalHits);
System.out.println("Returned hits: " hits.length);
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭 Elasticsearch 客户端
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
使用限制与分析
当我们在使用from size这种方式对elasticsearch返回的数据进行分页时,使用方式上类似于关系型数据库的limit offset,offset
;
在日常搜索场景下,我们可以通过对结果进行评分的排序,来提高搜索结果的相关性,使用该方式将最相关的数据返回给客户端。设置from
参数来指定查询结果的起始位置,size
参数来指定每页返回的文档数量。
当我们使用这种方式进行分页查询时,elasticsearch默认上限为10000条数据。虽然我们可以通过设置该参数index.max_result_window
来提高分页查询返回条数的上限,但提高该上限可能会造成以下问题,影响集群稳定运行。
- 内存消耗:较大的窗口大小意味着 Elasticsearch 需要为查询结果保留更多的内存空间。如果查询结果非常庞大,可能会导致 Elasticsearch 集群的内存消耗增加,从而影响性能和稳定性。
- 查询性能下降:当查询结果窗口较大时,Elasticsearch 需要处理更多的数据并返回更多的结果。这可能导致查询的响应时间增加,因为 Elasticsearch 需要更多的时间来处理和返回结果。
- 网络传输开销:如果查询结果窗口较大,将会返回更多的数据量。这可能会增加网络传输的开销,尤其是在分布式环境中跨节点传输结果时。
- 客户端资源消耗:较大的查询结果窗口可能会导致客户端应用程序需要处理更多的数据。如果客户端不具备足够的资源来处理大量的查询结果,可能会影响客户端的性能和稳定性。
当分页的数据超过10000条时,我们又需要返回大量的结果,我们可以通过search_after的方式。其操作步骤如下: 1. 首先我们获取一个pit,并设置有效时间为1分钟,其作用为创建一个时间点,保留索引当前的搜索状态,以避免多次搜索后,结果不一致。
代码语言:javascript复制POST /my-index-000001/_pit?keep_alive=1m
然后elasticsearch会返回一个ID给我们。
代码语言:javascript复制{
"id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA=="
}
2. 在查询时,携带pit。此时我们在搜索时,搜索的结果均为该时间点的索引状态内的数据。搜索请求命中的数据会自动添加至携带了pit的搜索请求中。_shard_doc作为索引分片与文档在lucene内部的id的组合生成的唯一值,在我们的搜索请求中,我们可以自定义对齐排序。
代码语言:javascript复制GET /_search
{
"size": 10000,
"query": {
"match" : {
"user.id" : "elkbee"
}
},
"pit": {
"id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==",
"keep_alive": "1m"
},
"sort": [
{"@timestamp": {"order": "asc", "format": "strict_date_optional_time_nanos", "numeric_type" : "date_nanos" }}
]
}
3. 当我们需要获取下一页结果时,只需要将上一次命中的排序值,作为参数,重新执行一次search_after请求即可。
代码语言:javascript复制GET /_search
{
"size": 10000,
"query": {
"match" : {
"user.id" : "elkbee"
}
},
"pit": {
"id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==",
"keep_alive": "1m"
},
"sort": [
{"@timestamp": {"order": "asc", "format": "strict_date_optional_time_nanos"}}
],
"search_after": [
"2021-05-20T05:30:04.832Z",
4294967298
],
"track_total_hits": false
}
4. 在使用完成后,我们还需要将pit进行删除。以结束该时间点的索引状态。
代码语言:javascript复制DELETE /_pit
{
"id" : "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA=="
}
二.scroll深度分页查询
通过scroll游标在索引中对数据进行滚动请求,每次只需要携带_scroll_id,就在多个请求之间保持查询上下文,并逐步滚动结果集,以获取更多的文档。
操作步骤与样例
- 发起初始查询:您需要执行初始查询来获取第一页的结果集。在查询请求中,需要设置
scroll
参数来指定滚动请求的有效期,并设置size
参数来指定每次滚动请求返回的文档数量。 - 处理初始查询结果:初始查询会返回一批文档结果。您可以遍历这些结果并处理每个文档的数据。
- 发起滚动请求:使用上一步返回的滚动 ID(scroll_id),您可以发起滚动请求来获取下一页的文档。在每个滚动请求中,需要设置相同的
scroll
参数和使用上一个请求返回的滚动 ID。 - 处理滚动请求结果:滚动请求会返回下一页的文档结果。您可以遍历这些结果并处理每个文档的数据。
- 重复步骤 3 和步骤 4:您可以重复发起滚动请求并处理结果,直到没有更多的文档返回为止。
#发起一个scroll查询,游标id的有效时间为一分钟。
POST /my-index-000001/_search?scroll=1m
{
"size": 100,
"query": {
"match": {
"message": "foo"
}
}
}
使用限制与分析
在scroll查询中,scroll_id的有效时间默认为1分钟,我们在进行大量数据查询,或进行大量数据导出时,为了方便可能会将有效时间设置的很大,如果keep alive时间设置过大可能会造成以下问题:
- 资源占用:大数据量级的Scroll 查询会占用集群资源,包括内存、CPU 和网络带宽。如果将有效时间设置得非常大,那么服务器需要保持滚动查询的上下文信息,并且需要为每个滚动查询保留足够的资源。这可能导致集群资源的过度消耗,降低整体性能和稳定性。
- 内存泄漏:如果滚动查询的有效时间过长,可能导致内存泄漏问题。因为elasticsearch需要在内存中维护滚动查询的上下文信息,如果这些信息无法及时释放,可能会导致内存占用不断增加,最终耗尽服务器的可用内存。
- 查询一致性:滚动查询的有效时间过长可能会导致查询结果的一致性问题。如果在滚动查询期间有新的文档被索引,而滚动查询的有效时间仍在进行中,那么这些新文档将不会包含在滚动查询的结果中。这可能会导致查询结果不准确或不完整。
因此,我们需要根据业务需求与集群资源负载,合理的设置keep alive的有效时间范围,将有效时间设置为适当的范围,以便集群能够一定时间内能够处理滚动查询,并及时释放资源。
在新版本的elasticsearch中,已经引入了Search_after API与Cursor API来逐步替代Scroll API,我们将在后续的文章中进行讨论。
我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!