在初始化查询阶段(query phase),查询被向索引中的每个分片副本(原本或副本)广播。每个分片在本地执行搜索并且建 立了匹配 document 的优先队列(priority queue)。
优先队列:一个优先队列(priority queue)只是一个存有前n个(top-n)匹配document的有序列表。这个优先队列的大小由分页参数 from + size 决定。

查询阶段
当一个搜索请求被发送到一个节点Node,这个节点就变成了协调节点。这个节点的工作是向所有相关的分片广播搜索请求并且把它们的响应整合成一个全局的有序结果集。对于后续请求,协调节点会轮询所有 的分片副本以分摊负载。每一个分片在本地执行查询和建立一个长度为 from+size 的有序优先队列——这个长度意味着它自己的结果数量就足够满 全局的请求要求。分片返回一个轻量级的结果列表给协调节点。只包含documentID值和排序需要用到的值,例如 _score 。
查询阶段辨别出那些满足搜索请求的document,但我们仍然需要取回那些document本身。这就是取回阶段的工作,如图分布式搜索的取回阶段所示。

取回阶段
协调节点为每个持有相关document的分片建立多点get请求然后发送请求到处理查询阶段的分片副本。 一旦协调节点收到所有结果,会将它们汇集到单一的回答响应里,这个响应将会返回给客户端。
我们看一下分布式存储系统中分页查询的过程:
可以看到,在分布式系统中,对结果排序的成本随分页的深度成指数上升。这就是 web 搜索引擎对任何查询都不要返回超过 10000 个结果的原因。
为了限制 from + size 分页的深度,ElasticSearch 的分页窗口默认最多允许 10000 条数据,即 在每页 20 条数据的情况,最多可以分 500 页,超过后报错
ES 支持的三种分页查询方式
说明: 官方已经不再推荐采用Scroll API进行深度分页。如果遇到超过10000的深度分页,推荐用search_after + PIT
index:shopping
@Test
public void saveAll(){
List<Product> productList = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
Product product = new Product();
product.setId(Long.valueOf(i));
product.setTitle("["+i+"]小米手机");
product.setCategory("手机");
product.setPrice(1999.0 + i);
product.setImages("http://www.test/xm.jpg");
productList.add(product);
}
productDao.saveAll(productList);
}
通过 from和 size是 ElasticSearch 最常用的分页方式,可以类比 MySQL 的 LIMIT start,limit
from:未指定,默认值是 0,注意不是1,代表当前页返回数据的起始值。
size: 未指定,默认值是 10,代表当前页返回数据的条数。
POST shopping/_search
{
"from": 0,
"size": 10,
"query": {
"match_all": {}
},
"sort": [
{"id": "asc"}
]
}
Java Client
/**
* 分页查询 FROM + SIZE 查询
* @param currentPage 当前页,第一页从 0 开始, 1 表示第二页
* @param pageSize 每页显示多少条
*/
public void findByPageable(int currentPage,int pageSize){
//设置排序(排序方式,正序还是倒序,排序的 id)
Sort sort = Sort.by(Sort.Direction.ASC,"id");
//设置查询分页
PageRequest pageRequest = PageRequest.of(currentPage, pageSize,sort);
//分页查询
Page<Product> productPage = productDao.findAll(pageRequest);
for (Product Product : productPage.getContent()) {
System.out.println(Product);
}
}
测试分页窗口限制
// 这是ElasticSearch最简单的分页查询,但以上命令是会报错的。
POST shopping/_search
{
"from": 10000,
"size": 10,
"query": {
"match_all": {}
},
"sort": [
{"id": "asc"}
]
}
// 因为size + from > 10000 所有导致报错
"error" : {
"root_cause" : [
{
"type" : "illegal_argument_exception",
"reason" : "Result window is too large, from + size must be less than or equal to: [10000] but was [10010]. 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",
}
怎么解决这个问题,首先能想到的就是调大这个window。
PUT user/_settings
{
"index" : {
"max_result_window" : 20000
}
}
然后这种方式只能暂时解决问题,当es 的使用越来越多,数据量越来越大,深度分页的场景越来越复杂时,如何解决这种问题呢?
官方建议:
避免过度使用 from 和 size 来分页或一次请求太多结果。
不推荐使用 from + size 做深度分页查询的核心原因:
使用search_after 进行分页 相比 from & size 的方式要更加高效,而且在不断有新数据入库的时候仅仅使用 from 和 size 分页会有重复的情况,相比使用 scroll 分页,search_after 可以进行实时的查询,不过 search_after 不适合跳跃式的分页。
使用 search_after 类比 SQL,相当于
# 查询shopping num > 0 的前5条数据 并且不会走索引
SELECT * FROM shopping WHERE ORDER BY num > 0 ASC LIMIT 10
# 优化:获取返回列表中的最后一个 num,即 最大的 num,定为 {before_max_num}
SELECT * FROM shopping WHERE num > {before_max_num} ORDER BY num ASC LIMIT 5
但是 search_after 参数使用上一页中的一组排序值来检索下一页的数据。(增加一个条件查询 排序值 > 上一页排序值 )使用 search_after 需要具有相同查询和排序值的多个搜索请求。 如果在这些请求之间发生刷新,结果的顺序可能会发生变化,从而导致跨页面的结果不一致。 为防止出现这种情况,您可以创建一个时间点 (PIT) 以保留搜索中的当前索引状态。
时间点 Point In Time(PIT)保障搜索过程中保留特定事件点的索引状态。
注意⚠️:
es 给出了 search_after 的方式,这是在 >= 5.0 版本才提供的功能。
Point In Time(PIT)是 Elasticsearch 7.10 版本之后才有的新特性。
PIT的本质:存储索引数据状态的轻量级视图。
如下示例能很好的解读 PIT 视图的内涵。
// 1.给索引user_index创建 pit
POST /shopping/_pit?keep_alive=5m
// 2. 统计当前记录数 5
POST /shopping/_count
// 3. 根据pit统计当前记录数 5
GET /_search
{
"query": {
"match_all": {}
},
"pit": {
"id": "i6-xAwEKdXNlcl9pbmRleBZYTXdtSFRHeVJrZVhCby1OTjlHMS1nABZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3AAAAAAAAIODBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EBFlhNd21IVEd5UmtlWEJvLU5OOUcxLWcAAA==",
"keep_alive": "5m"
},
"sort": [
{"id": "asc"}
]
}
// 4. 插入一条数据
POST shopping/_bulk
{ "create": { "_id": "6" }}
{ "id":6,"name":"奶瓶"}
// 5. 数据总量 6
POST /shopping/_count
// 6. 根据pit统计数据总量还是 5 ,说明是根据时间点的视图进行统计。
GET /_search
{
"query": {
"match_all": {}
},
"pit": {
"id": "i6-xAwEKdXNlcl9pbmRleBZYTXdtSFRHeVJrZVhCby1OTjlHMS1nABZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3AAAAAAAAIODBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EBFlhNd21IVEd5UmtlWEJvLU5OOUcxLWcAAA==",
"keep_alive": "5m"
},
"sort": [
{"id": "asc"}
]
}
有了 PIT,search_after 的后续查询都是基于 PIT 视图进行,能有效保障数据的一致性。
//1. 获取索引的pit
POST /shopping/_pit?keep_alive=5m
//2. 根据 pit 首次查询 根据 pit 查询的时候,不用指定索引名称。
GET /_search
{
"size": 1,
"from": 0,//注意from要从0开始
"query": {
"match_all": {}
},
"pit": {
"id": "i6-xAwEKdXNlcl9pbmRleBZYTXdtSFRHeVJrZVhCby1OTjlHMS1nABZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3AAAAAAAAIODBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EBFlhNd21IVEd5UmtlWEJvLU5OOUcxLWcAAA==",
"keep_alive": "1m"
},
"sort": [
{"id": "asc"}
]
}
//查询结果
"hits" : [
{
"_index" : "shopping",
"_type" : "_doc",
"_id" : "0",
"_score" : null,
"_source" : {
"_class" : "com.caffee.es.model.Product",
"id" : 0,
"title" : "[0]小米手机",
"category" : "手机",
"price" : 1999.0,
"images" : "http://www.test/xm.jpg"
},
"sort" : [
0,
4294967296
]
}
]
//3. 根据search_after和pit进行翻页查询: search_after指定为上一次查询返回的sort值。
GET /_search
{
"size": 1,
"query": {
"match_all": {}
},
"pit": {
"id": "i6-xAwEKdXNlcl9pbmRleBZYTXdtSFRHeVJrZVhCby1OTjlHMS1nABZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3AAAAAAAAIOJ7FmdBWEd2UmFVVGllZldNdnhPZDJmX0EBFlhNd21IVEd5UmtlWEJvLU5OOUcxLWcAAA==",
"keep_alive": "5m"
},
"sort": [
{"id": "asc"}
],
"search_after": [
0
]
}
//查询结果
"hits" : [
{
"_index" : "shopping",
"_type" : "_doc",
"_id" : "1",
"_score" : null,
"_source" : {
"_class" : "com.caffee.es.model.Product",
"id" : 1,
"title" : "[1]小米手机",
"category" : "手机",
"price" : 2000.0,
"images" : "http://www.test/xm.jpg"
},
"sort" : [
1
]
}
]
- 带有 pit 参数的搜索请求不得指定 index、routing 和 preference,因为这些参数是从时间点复制的。
- id 参数告诉 Elasticsearch 从这个时间点使用上下文执行请求。
- keep_alive 参数告诉 Elasticsearch 应该将时间点的生存时间延长多长时间。
Java Client
/**
* 分页查询 search_after + SIZE + PIT 查询
* @param indices 索引名用于创建PIT
* @param sortNum 排序值
* @param pageSize 页数
* @return
* @throws Exception
*/
public Integer findByPageableBySearchAfterPIT(String indices,int sortNum,int pageSize) throws Exception {
// 1. 创建时间点,过期时间5分钟
String pitId = createPit(indices,5);
// 2.结合 search after 和 PIT ID 进行深度分页
final PointInTimeBuilder pitBuilder = new PointInTimeBuilder(pitId);
// 3.创建搜索条件
final SearchSourceBuilder searchSourceBuilder = SearchSourceBuilder.searchSource()
.pointInTimeBuilder(pitBuilder) // 指定 pit
.from(0)
.size(pageSize)
.searchAfter(new Object[]{sortNum})
.sort("id", SortOrder.ASC);
SearchRequest searchRequest = new SearchRequest();//indices 无需指定索引名
searchRequest.source(searchSourceBuilder);
//4. 获取结果
SearchResponse search = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
Object[] arrays = new Object[1];
System.out.println(search.getHits().getHits());
for(SearchHit hit : search.getHits().getHits()){
Map<String, Object> map = hit.getSourceAsMap();
System.out.println(JSONObject.toJSONString(map));
System.out.println(hit.getSortValues()[0]);
arrays = hit.getSortValues();
}
System.out.println("sort值" + arrays[0]);
// 最后关闭 Point In Time
final ClosePointInTimeRequest closePointInTimeRequest = new ClosePointInTimeRequest(pitId);
restHighLevelClient.closePointInTime(closePointInTimeRequest,RequestOptions.DEFAULT);
if(arrays[0] == null) return null;
else return Integer.parseInt(arrays[0].toString());
}
/**
* 创建 PIT OpenPointInTimeRequest支持版本:highlevelclient7.16以上
* @param indices 索引名
* @param keep_alive 存活时间 单位:分钟
* @throws Exception
*/
private String createPit(String indices,int keep_alive) throws Exception{
// 构造 pit open Request
//1. 根据索引创建时间点
final OpenPointInTimeRequest pitRequest = new OpenPointInTimeRequest(indices);
//2. 设置存活时间
pitRequest.keepAlive(TimeValue.timeValueMinutes(keep_alive));
//打开 pit 获取 pitId
final OpenPointInTimeResponse pitResponse = restHighLevelClient.openPointInTime(pitRequest, RequestOptions.DEFAULT);
//3. 读取返回的时间点 id
final String pitId = pitResponse.getPointInTimeId();
return pitId;
}
思考 🤔
1、为什么采用 search_after 查询能解决深度分页的问题? 2、search_after + pit 分页查询过程中,PIT视图过期怎么办? 3、search_after 查询,如果需要回到前几页怎么办?
scroll api 的方式会创建一个快照,每次查询后,输入上一次的 scroll_id, 来实现 下一页 的功能
所有文档获取完毕之后,需要手动清理掉 scroll_id 。虽然es 会有自动清理机制,但是 srcoll_id 的存在会耗费大量的资源来保存一份当前查询结果集映像,并且会占用文件描述符。所以用完之后要及时清理。使用 es 提供的 CLEAR_API 来删除指定的 scroll_id。
注意:ES官方不再推荐使用
Scroll API进行深度分页。 如果您需要在分页超过 10,000 个点击时保留索引状态,请使用带有时间点 (PIT) 的 search_after 参数。
// 1. 首次查询,并获取_scroll_id
GET /shopping/_search?scroll=1m
{
"from" : 0,
"size" : 5,
"sort" : [
{
"id" : {
"order" : "asc"
}
}
]
}
// 查询结果
{
"_scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDnF1ZXJ5VGhlbkZldGNoAxZBeDZHc2tUTFFMS0tlbnRDTjhXcjVBAAAAAAAAD5wWWXpCaUVWSVNRZkNma2VLY1VEMUprQRZBeDZHc2tUTFFMS0tlbnRDTjhXcjVBAAAAAAAAD54WWXpCaUVWSVNRZkNma2VLY1VEMUprQRZBeDZHc2tUTFFMS0tlbnRDTjhXcjVBAAAAAAAAD50WWXpCaUVWSVNRZkNma2VLY1VEMUprQQ==",
"hits" : {
"total" : {
"value" : 10000,
"relation" : "eq"
},
"max_score" : null,
"hits" : [
{
"_index" : "shopping",
"_type" : "_doc",
"_id" : "0",
"_score" : null,
"_source" : {
"_class" : "com.caffee.es.model.Product",
"id" : 0,
"title" : "[0]小米手机",
"category" : "手机",
"price" : 1999.0,
"images" : "http://www.test/xm.jpg"
},
"sort" : [
0
]
}
]
}
}
//2. 根据scroll_id遍历数据
POST /_search/scroll
{
"scroll" : "1m",
"scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EAAAAAACDlKxZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3"
}
// 3.删除游标scroll
DELETE /_search/scroll
{
"scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EAAAAAACDlKxZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3"
}
Java Client
// 基本的查询条件不变
final SearchSourceBuilder sourceBuilder = SearchSourceBuilder.searchSource()
.from(0).size(5)
.sort(SortBuilders.fieldSort("num").order(SortOrder.ASC));
//
final SearchRequest searchRequest = new SearchRequest();
searchRequest.indices("test-paginate-index");
searchRequest.source(sourceBuilder);
// 指定 Scroll 方式 和 失效时间
final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1));
searchRequest.scroll(scroll);
SearchResponse response = highLevelClient.search(searchRequest);
// 从响应结果中获取 scrollId
final String scrollId = response.getScrollId();
// 接下来通过 滚动的方式 查询
final SearchScrollRequest searchScrollRequest = Requests.searchScrollRequest(scrollId);
searchScrollRequest.scroll(scroll);
// 这里从第2页开始再翻 10 页
for (int i = 0; i < 10; i++) {
response = highLevelClient.searchScroll(searchScrollRequest);
}
// 使用完别忘的清除资源
final ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
clearScrollRequest.addScrollId(scrollId);
highLevelClient.clearScroll(clearScrollRequest);
scroll 查询的相应数据是非实时的,这点和PIT视图比较类似,如果遍历过程中插入新的数据,是查询不到的。 并且保留上下文需要足够的堆内存空间。
全量或数据量很大时遍历结果数据,而非分页查询。
官方文档强调: 不再建议使用scroll API进行深度分页。如果要分页检索超过 Top 10,000+ 结果时,推荐使用:PIT + search_after。
如果我们需要根据查询结果去重,可以使用 ElasticSearch 的 Collapse 折叠功能,Collapse 同样也支持 from + size 分页,比如 我们这个实例中, id 是唯一,但是 price 都会有大量的重复,如果我们 根据 price 去重分页就可以通过 Collapse 实现
GET /shopping/_search
{
"from" : 0,
"size" : 5,
"collapse": {
"field": "price"
},
"sort" : [
{
"price" : {
"order" : "asc"
}
}
]
}
我正在用Ruby编写一个简单的程序来检查域列表是否被占用。基本上它循环遍历列表,并使用以下函数进行检查。require'rubygems'require'whois'defcheck_domain(domain)c=Whois::Client.newc.query("google.com").available?end程序不断出错(即使我在google.com中进行硬编码),并打印以下消息。鉴于该程序非常简单,我已经没有什么想法了-有什么建议吗?/Library/Ruby/Gems/1.8/gems/whois-2.0.2/lib/whois/server/adapters/base.
我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden
我知道我可以指定某些字段来使用pluck查询数据库。ids=Item.where('due_at但是我想知道,是否有一种方法可以指定我想避免从数据库查询的某些字段。某种反拔?posts=Post.where(published:true).do_not_lookup(:enormous_field) 最佳答案 Model#attribute_names应该返回列/属性数组。您可以排除其中一些并传递给pluck或select方法。像这样:posts=Post.where(published:true).select(Post.attr
华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o
C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.
MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO
深度学习部署:Windows安装pycocotools报错解决方法1.pycocotools库的简介2.pycocotools安装的坑3.解决办法更多Ai资讯:公主号AiCharm本系列是作者在跑一些深度学习实例时,遇到的各种各样的问题及解决办法,希望能够帮助到大家。ERROR:Commanderroredoutwithexitstatus1:'D:\Anaconda3\python.exe'-u-c'importsys,setuptools,tokenize;sys.argv[0]='"'"'C:\\Users\\46653\\AppData\\Local\\Temp\\pip-instal
遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg
我正在尝试查询我的Rails数据库(Postgres)中的购买表,我想查询时间范围。例如,我想知道在所有日期的下午2点到3点之间进行了多少次购买。此表中有一个created_at列,但我不知道如何在不搜索特定日期的情况下完成此操作。我试过:Purchases.where("created_atBETWEEN?and?",Time.now-1.hour,Time.now)但这最终只会搜索今天与那些时间的日期。 最佳答案 您需要使用PostgreSQL'sdate_part/extractfunction从created_at中提取小时
通常,数组被实现为内存块,集合被实现为HashMap,有序集合被实现为跳跃列表。在Ruby中也是如此吗?我正在尝试从性能和内存占用方面评估Ruby中不同容器的使用情况 最佳答案 数组是Ruby核心库的一部分。每个Ruby实现都有自己的数组实现。Ruby语言规范只规定了Ruby数组的行为,并没有规定任何特定的实现策略。它甚至没有指定任何会强制或至少建议特定实现策略的性能约束。然而,大多数Rubyist对数组的性能特征有一些期望,这会迫使不符合它们的实现变得默默无闻,因为实际上没有人会使用它:插入、前置或追加以及删除元素的最坏情况步骤复