elasticsearch查询性能优化

1.指定_source,进行结果过滤

默认情况下,elasticsearch在搜索的结果中,会把文档中保存在 _source 的所有字段都返回。
如果我们只想获取其中的部分字段,我们可以添加 _source 字段进行过滤

1.1 包含字段查询(includes)
{
  "query": {
    "match_all": {}
  },
  "_source": {
    "includes": ["title","price"]
  }
}

##结果
{
  "took": 4,
  "timed_out": false,
  "_shards": {
    "total": 3,
    "successful": 3,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 4,
    "max_score": 1,
    "hits": [
      {
        "_index": "shop",
        "_type": "goods",
        "_id": "P4EGtmgBBdkQnU_d8b7I",
        "_score": 1,
        "_source": {
          "price": 8599,
          "title": "联想(Lenovo)拯救者Y7000P英特尔酷睿 i7 15.6英寸游戏笔记本电脑(i7-8750H 8G 512G SSD GTX1060 144Hz黑)"
        }
      },
      {
        "_index": "shop",
        "_type": "goods",
        "_id": "QIEGtmgBBdkQnU_d-r6T",
        "_score": 1,
        "_source": {
          "price": 109,
          "title": "TP-LINK TL-WDR5620 1200M 5G双频智能无线路由器 四天线智能wifi 稳定穿墙高速家用路由器"
        }
      },
      {
        "_index": "shop",
        "_type": "goods",
        "_id": "PYEGtmgBBdkQnU_d4b4m",
        "_score": 1,
        "_source": {
          "price": 2299,
          "title": "小米8 全面屏游戏智能手机 6GB+64GB 黑色 全网通4G 双卡双待"
        }
      },
      {
        "_index": "shop",
        "_type": "goods",
        "_id": "PoEGtmgBBdkQnU_d6b4Q",
        "_score": 1,
        "_source": {
          "price": 4999,
          "title": "OPPO Find X曲面全景屏 波尔多红 8GB+128GB 全网通 移动联通电信全网通4G 双卡双待手机"
        }
      }
    ]
  }
}
1.2 排除字段查询(excludes)
{
  "query": {
    "match_all": {}
  },
  "_source": {
    "excludes": "price"
  }
}

##结果
{
  "took": 3,
  "timed_out": false,
  "_shards": {
    "total": 3,
    "successful": 3,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 4,
    "max_score": 1,
    "hits": [
      {
        "_index": "shop",
        "_type": "goods",
        "_id": "P4EGtmgBBdkQnU_d8b7I",
        "_score": 1,
        "_source": {
          "title": "联想(Lenovo)拯救者Y7000P英特尔酷睿 i7 15.6英寸游戏笔记本电脑(i7-8750H 8G 512G SSD GTX1060 144Hz黑)",
          "stock": 1900
        }
      },
      {
        "_index": "shop",
        "_type": "goods",
        "_id": "QIEGtmgBBdkQnU_d-r6T",
        "_score": 1,
        "_source": {
          "title": "TP-LINK TL-WDR5620 1200M 5G双频智能无线路由器 四天线智能wifi 稳定穿墙高速家用路由器",
          "stock": 9970
        }
      },
      {
        "_index": "shop",
        "_type": "goods",
        "_id": "PYEGtmgBBdkQnU_d4b4m",
        "_score": 1,
        "_source": {
          "title": "小米8 全面屏游戏智能手机 6GB+64GB 黑色 全网通4G 双卡双待",
          "stock": 8800
        }
      },
      {
        "_index": "shop",
        "_type": "goods",
        "_id": "PoEGtmgBBdkQnU_d6b4Q",
        "_score": 1,
        "_source": {
          "title": "OPPO Find X曲面全景屏 波尔多红 8GB+128GB 全网通 移动联通电信全网通4G 双卡双待手机",
          "stock": 5600
        }
      }
    ]
  }
}

java api

public static HttpEntity makeScrollStartHttpEntity(QueryBuilder qb, int fetchSize, String sortField,
        SearchOrder order) {
    JSONObject query = new JSONObject();
    query.put("query", JSONObject.parseObject(qb.toString()));
    query.put("size", fetchSize);
    SortBuilder sortBuilder = ElasticSearchManager.getSortBuilder(sortField, order);
    query.put("sort", JSONObject.parseObject(sortBuilder.toString()));

    JSONObject _source = new JSONObject();
    JSONArray includes = new JSONArray();

    includes.add("td_id");
    includes.add("s_channel");
    includes.add(EventIndexConstant.BUSINESS_FIELD);
    includes.add(EventIndexConstant.SCENE_NAME_FIELD);
    includes.add(EventIndexConstant.SCENE_FIELD);
    includes.add(EventIndexConstant.EVENT_TIMESTAMP_FIELD);
    includes.add(EventIndexConstant.STRATEGY_RULE_EXECUTE);
    includes.add(EventIndexConstant.STRATEGY_RULE);
    JSONArray excludes = new JSONArray();
    _source.put("includes", includes);
    _source.put("excludes", excludes);
    query.put("_source", _source);
   
    HttpEntity httpEntity = new NStringEntity(query.toJSONString(), ContentType.APPLICATION_JSON);
    return httpEntity;
}

ps:如果我们想要获得聚合后的结果,那么使用includes没有效果
includes是指定hits>hits>_source中返回的字段,而使用聚合时,我们会指定size=0,hits中为空数组
在这里插入图片描述
使用聚合时,设置size=0

在这里插入图片描述

2.查询时,使用query-bool-filter组合取代普通query

Elasticsearch filter和query的不同

  • 全文检索以及任何使用相关性评分的场景使用query检索。
  • 除此之外的其他使用filter过滤器过滤。

例子:

{
	"query": {
		"bool": {
			"filter": [{
				"range": {
					"td_ets": {
						"include_lower": true,
						"include_upper": true,
						"from": 1564655051000,
						"boost": 1.0,
						"to": 1571653451000
					}
				}
			}, {
				"term": {
					"td_b": {
						"boost": 1.0,
						"value": "ls1"
					}
				}
			}, {
				"term": {
					"td_s": {
						"boost": 1.0,
						"value": "ana1"
					}
				}
			}, {
				"bool": {
					"adjust_pure_negative": true,
					"should": [{
						"term": {
							"s_channel": {
								"boost": 1.0,
								"value": "wsyh"
							}
						}
					}, {
						"term": {
							"s_channel": {
								"boost": 1.0,
								"value": "hx"
							}
						}
					}],
					"boost": 1.0,
					"disable_coord": false
				}
			}],
			"adjust_pure_negative": true,
			"boost": 1.0,
			"disable_coord": false
		}
	}
}

java api

BoolQueryBuilder qb = QueryBuilders.boolQuery();
qb.filter(QueryBuilders.rangeQuery(EventIndexConstant.EVENT_TIMESTAMP_FIELD).from(fromDateTime).to(toDateTime));
qb.filter(QueryBuilders.termQuery(EventIndexConstant.BUSINESS_FIELD, business.getCode()));
qb.filter(QueryBuilders.termQuery(EventIndexConstant.SCENE_FIELD, scene.getCode()));
if (StringUtils.isNotBlank(code)) {
  BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
  String[] codeArray = code.split(",");
  for (String codeInfo : codeArray) {
      queryBuilder.should(QueryBuilders.termQuery(field, codeInfo));
  }
  qb.filter(queryBuilder);
}

JSONObject query = new JSONObject();
query.put("size", 0);
query.put("query", JSONObject.parseObject(qb.toString()));

2.Elasticsearch聚合性能优化:深度优先和广度优先

elasticsearch 里面桶的叫法和 SQL 里面分组的概念是类似的,一个桶就类似 SQL 里面的一个 group,多级嵌套的 aggregation, 类似 SQL 里面的多字段分组(group by field1,field2, ……),注意这里仅仅是概念类似,底层的实现原理是不一样的。

terms 桶基于我们的数据动态构建桶;它并不知道到底生成了多少桶。 大多数时候对单个字段的聚合查询还是非常快的, 但是当需要同时聚合多个字段时,就可能会产生大量的分组,最终结果就是占用 es 大量内存,从而导致 OOM 的情况发生。

假设我们现在有一些关于电影的数据集,每条数据里面会有一个数组类型的字段存储表演该电影的所有演员的名字。

{
  "actors" : [
    "Fred Jones",
    "Mary Jane",
    "Elizabeth Worthing"
  ]
}

如果我们想要查询出演影片最多的十个演员以及与他们合作最多的演员,使用聚合是非常简单的:

{
  "aggs" : {
    "actors" : {
      "terms" : {
         "field" : "actors",					//根据演员进行分桶
         "size" :  10							//只返回排名前10的演员
      },
      "aggs" : {
        "costars" : {
          "terms" : {
            "field" : "actors",			 		//子聚合中,继续根据演员进行分桶
            "size" :  5							//返回排名前5的桶
          }
        }
      }
    }
  }
}

这会返回前十位出演最多的演员,以及与他们合作最多的五位演员。这看起来是一个简单的聚合查询,最终只返回 50 条数据!

但是, 这个看上去简单的查询可以轻而易举地消耗大量内存,我们可以通过在内存中构建一个树来查看这个 terms 聚合。 actors 聚合会构建树的第一层,每个演员都有一个桶。然后,内套在第一层的每个节点之下, costar 聚合会构建第二层,每个联合出演一个桶,请参见 图 42 “Build full depth tree” 所示。这意味着每部影片会生成 n2 个桶!
在这里插入图片描述
用真实点的数据,设想平均每部影片有 10 名演员,每部影片就会生成 10*10 == 100 个桶。如果总共有 20,000 部影片,粗率计算就会生成 2,000,000 个桶。

现在,记住,聚合只是简单的希望得到前十位演员和与他们联合出演者,总共 50 条数据。为了得到最终的结果,我们创建了一个有 2,000,000 桶的树,然后对其排序,取 top10。 图 图 43 “Sort tree” 和图 图 44 “Prune tree” 对这个过程进行了阐述。
在这里插入图片描述
在这里插入图片描述
这时我们一定非常抓狂,在 2 万条数据下执行任何聚合查询都是毫无压力的。如果我们有 2 亿文档,想要得到前 100 位演员以及与他们合作最多的 20 位演员,作为查询的最终结果会出现什么情况呢?

可以推测聚合出来的分组数非常大,会使这种策略难以维持。世界上并不存在足够的内存来支持这种不受控制的聚合查询。

深度优先与广度优先(Depth-First Versus Breadth-First)

Elasticsearch 允许我们改变聚合的 集合模式 ,就是为了应对这种状况。 我们之前展示的策略叫做 深度优先 ,它是默认设置, 先构建完整的树,然后修剪无用节点。 深度优先 的方式对于大多数聚合都能正常工作,但对于如我们演员和联合演员这样例子的情形就不太适用。

为了应对这些特殊的应用场景,我们应该使用另一种集合策略叫做 广度优先 。这种策略的工作方式有些不同,它先执行第一层聚合, 再 继续下一层聚合之前会先做修剪。 图 图 45 “Build first level” 和图 图 47 “Prune first level” 对这个过程进行了阐述。

在我们的示例中, actors 聚合会首先执行,在这个时候,我们的树只有一层,但我们已经知道了前 10 位的演员!这就没有必要保留其他的演员信息,因为它们无论如何都不会出现在前十位中。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
因为我们已经知道了前十名演员,我们可以安全的修剪其他节点。修剪后,下一层是基于 它的 执行模式读入的,重复执行这个过程直到聚合完成,如图 图 48 “Populate full depth for remaining nodes” 所示。 这种场景下,广度优先可以大幅度节省内存。
在这里插入图片描述

{
  "aggs" : {
    "actors" : {
      "terms" : {
         "field" :        "actors",
         "size" :         10,
         "collect_mode" : "breadth_first" 
      },
      "aggs" : {
        "costars" : {
          "terms" : {
            "field" : "actors",
            "size" :  5
          }
        }
      }
    }
  }
}

广度优先仅仅适用于每个组的聚合数量远远小于当前总组数的情况下,因为广度优先会在内存中缓存裁剪后的仅仅需要缓存的每个组的所有数据,以便于它的子聚合分组查询可以复用上级聚合的数据。

广度优先的内存使用情况与裁剪后的缓存分组数据量是成线性的。对于很多聚合来说,每个桶内的文档数量是相当大的。 想象一种按月分组的直方图,总组数肯定是固定的,因为每年只有12个月,这个时候每个月下的数据量可能非常大。这使广度优先不是一个好的选择,这也是为什么深度优先作为默认策略的原因。

针对上面演员的例子,如果数据量越大,那么默认的使用深度优先的聚合模式生成的总分组数就会非常多,但是预估二级的聚合字段分组后的数据量相比总的分组数会小很多所以这种情况下使用广度优先的模式能大大节省内存,从而通过优化聚合模式来大大提高了在某些特定场景下聚合查询的成功率。

ps:广度优先在于对聚合结果进行筛选,如果我们想要获得所有的聚合结果,并不适用

java api

TermsAggregationBuilder sceneAgg = AggregationBuilders.terms("")
                                                              .field("td_s")
                                                              .size(10000)
                                                              .collectMode(
                                                                      Aggregator.SubAggCollectionMode.BREADTH_FIRST);

3.Elasticsearch聚合优化 | 聚合速度提升5倍

3.1 聚合为什么慢?

大多数时候对单个字段的聚合查询还是非常快的, 但是当需要同时聚合多个字段时,就可能会产生大量的分组,最终结果就是占用 es 大量内存,从而导致 OOM 的情况发生。
实践应用发现,以下情况都会比较慢:

  • 待聚合文档数比较多(千万、亿、十亿甚至更多);
  • 聚合条件比较复杂(多重条件聚合);
  • 全量聚合(翻页的场景用)。
3.2 聚合优化方案探讨

优化方案一:默认深度优先聚合改为广度优先聚合。

"collect_mode" : "breadth_first"
  • depth_first 直接进行子聚合的计算(默认,深度优先)
  • breadth_first 先计算出当前聚合的结果,针对这个结果在对子聚合进行计算。

优化方案二: 每一层terms aggregation内部加一个 “execution_hint”: “map”。

"execution_hint": "map"

国内解释最详细的版本来自Wood大叔:
在这里插入图片描述
Map方式的结论可简要概括如下:

  • 查询结果直接放入内存中构建map,在查询结果集小的场景下,速度极快;
  • 但如果待结果集合很大的情况,map方式不一定也快。
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值