草庐IT

ElasticSearch 分页查询及深度分页原理与实现

白山’ 2023-10-27 原文

查询流程

查询阶段

在初始化查询阶段(query phase),查询被向索引中的每个分片副本(原本或副本)广播。每个分片在本地执行搜索并且建 立了匹配 document 的优先队列(priority queue)。

优先队列:一个优先队列(priority queue)只是一个存有前n个(top-n)匹配document的有序列表。这个优先队列的大小由分页参数 from + size 决定。

查询阶段

  1. 客户端发送一个 search(搜索) 请求给 Node 3 , Node 3 创建了一个长度为 from+size 的空优先级队列。
  2. Node 3 转发(根据ID选择路由) 这个搜索请求到索引中每个分片的原本或副本。每个分片在本地执行这个查询并且将结果到一个大小为 from+size 的有序本地优先队列里去。
  3. 每个分片返回 document 的ID和它优先队列里的所有 document 的排序值给协调节点 Node 3 。Node 3 把这些值合并到自己的优先队列里产生全局排序结果。

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

取回阶段

查询阶段辨别出那些满足搜索请求的document,但我们仍然需要取回那些document本身。这就是取回阶段的工作,如图分布式搜索的取回阶段所示。

取回阶段

  1. 协调节点辨别出哪个document需要取回,并且向相关分片发出 GET 请求。
  2. 每个分片加载document并且根据需要丰富(enrich)它们,然后再将document
  3. 一旦所有的document都被取回,协调节点会将结果返回给客户端。 协调节点先决定哪些document是实际(actually)需要取回的。例如,我们指定查询 { “from”: 90, “size”: 10} ,那么前90 条将会被丢弃,只有之后的10条会需要取回。这些document可能来自与原始查询请求相关的某个、某些或者全部分片。

协调节点为每个持有相关document的分片建立多点get请求然后发送请求到处理查询阶段的分片副本。 一旦协调节点收到所有结果,会将它们汇集到单一的回答响应里,这个响应将会返回给客户端。

分布式系统中深度分页问题

我们看一下分布式存储系统中分页查询的过程:

  1. 假设在一个有 4 个主分片的索引中搜索,每页返回10条记录。
  2. 当我们请求结果的第1页(结果从 1 到 10 ),每一个分片产生前 10 的结果,并且返回给 协调节点 ,协调节点对 40 个结果排序得到全部结果的前 10 个。
  3. 当我们请求第 99 页(结果从 990 到 1000),需要从每个分片中获取满足查询条件的前1000个结果,返回给协调节点, 然后协调节点对全部 4000 个结果排序,获取前10个记录。
  4. 当请求第10000页,每页10条记录,则需要先从每个分片中获取满足查询条件的前100010个结果,返回给协调节点。然后协调节点需要对全部(100010 * 分片数4)的结果进行排序,然后返回前10个记录。

可以看到,在分布式系统中,对结果排序的成本随分页的深度成指数上升。这就是 web 搜索引擎对任何查询都不要返回超过 10000 个结果的原因。

分页窗口限制

为了限制 from + size 分页的深度,ElasticSearch 的分页窗口默认最多允许 10000 条数据,即 在每页 20 条数据的情况,最多可以分 500 页,超过后报错

三种分页查询方式

ES 支持的三种分页查询方式

  • From + Size 查询
  • Scroll 遍历查询
  • Search After 查询

说明: 官方已经不再推荐采用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 查询

通过 fromsize是 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 做深度分页查询的核心原因:

    • 搜索请求通常跨越多个分片,每个分片必须将其请求的命中内容以及任何先前页面的命中内容加载到内存中。
    • 对于翻页较深的页面或大量结果,这些操作会显著增加内存和 CPU 使用率,从而导致性能下降或节点故障。

Search After + PIT 查询

使用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的本质:存储索引数据状态的轻量级视图。

如下示例能很好的解读 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
     ]
   }
 ]
  1. 带有 pit 参数的搜索请求不得指定 index、routing 和 preference,因为这些参数是从时间点复制的。
  2. id 参数告诉 Elasticsearch 从这个时间点使用上下文执行请求。
  3. 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;
 }

优缺点分析

  • 仅支持向后分页,不支持 跳页,上一页比较适合无限下拉的场景,比如:移动端的下拉列表
  • 不严格受制于 max_result_window,可以无限制往后翻页,单次请求值不能超过 max_result_window;但总翻页结果集可以超过。

思考 🤔

1、为什么采用 search_after 查询能解决深度分页的问题? 2、search_after + pit 分页查询过程中,PIT视图过期怎么办? 3、search_after 查询,如果需要回到前几页怎么办?

Scroll 遍历查询

scroll api 的方式会创建一个快照,每次查询后,输入上一次的 scroll_id, 来实现 下一页 的功能

所有文档获取完毕之后,需要手动清理掉 scroll_id 。虽然es 会有自动清理机制,但是 srcoll_id 的存在会耗费大量的资源来保存一份当前查询结果集映像,并且会占用文件描述符。所以用完之后要及时清理。使用 es 提供的 CLEAR_API 来删除指定的 scroll_id。

  1. 因为是在最开始分页的时候生成了一个快照,翻页的时候用的是快照数据,有新的数据写入以后,无法被查到,不会出现 跳过部分数据重复返回 的问题
  2. 因为快照的生成有一定的成本,这种方式比较适合离线的数据导出场景不适合实时的在线查询
  3. 在关闭 scroll 之前,segments 会一直被占用不会被 merge,这会导致 segments 和 文件句柄 的增加而不被释放

注意: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"
       }
     }
   ]
 }

有关ElasticSearch 分页查询及深度分页原理与实现的更多相关文章

  1. ruby - ECONNRESET (Whois::ConnectionError) - 尝试在 Ruby 中查询 Whois 时出错 - 2

    我正在用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.

  2. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  3. ruby-on-rails - 在 Rails 和 ActiveRecord 中查询时忽略某些字段 - 2

    我知道我可以指定某些字段来使用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

  4. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  5. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  6. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

  7. 深度学习部署:Windows安装pycocotools报错解决方法 - 2

    深度学习部署: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

  8. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

  9. sql - 查询忽略时间戳日期的时间范围 - 2

    我正在尝试查询我的Rails数据库(Postgres)中的购买表,我想查询时间范围。例如,我想知道在所有日期的下午2点到3点之间进行了多少次购买。此表中有一个created_at列,但我不知道如何在不搜索特定日期的情况下完成此操作。我试过:Purchases.where("created_atBETWEEN?and?",Time.now-1.hour,Time.now)但这最终只会搜索今天与那些时间的日期。 最佳答案 您需要使用PostgreSQL'sdate_part/extractfunction从created_at中提取小时

  10. ruby - Arrays Sets 和 SortedSets 在 Ruby 中是如何实现的 - 2

    通常,数组被实现为内存块,集合被实现为HashMap,有序集合被实现为跳跃列表。在Ruby中也是如此吗?我正在尝试从性能和内存占用方面评估Ruby中不同容器的使用情况 最佳答案 数组是Ruby核心库的一部分。每个Ruby实现都有自己的数组实现。Ruby语言规范只规定了Ruby数组的行为,并没有规定任何特定的实现策略。它甚至没有指定任何会强制或至少建议特定实现策略的性能约束。然而,大多数Rubyist对数组的性能特征有一些期望,这会迫使不符合它们的实现变得默默无闻,因为实际上没有人会使用它:插入、前置或追加以及删除元素的最坏情况步骤复

随机推荐