一、回顾
上回简单写了ES在springboot项目中的简单使用,elasticsearch(ES)在SpringBoot中的基本使用 ,介绍了es的基本概念以及基本的查询等,但是针对多条件分页查询或者聚合查询等,使用ElasticSearchRepository可能就有些力不从心了,需要借助更为强大的elasticsearchTemplate,本文首先介绍多条件的分页查询,然后介绍项目中常用的聚合统计查询等。
二、ES复杂查询
1)多条件分页查询
由于是多条件的查询,再使用之前的TermQueryBuilder/MatchQueryBuilder等都不能满足要求了,需要使用多个条件混合的BoolQueryBuiler 进行查询,搜索查询也使用原生查询NativeSearchQuery进行查询。
// 多条件分页查询
public PageModel<Book> multiConditionQuery(BookDto bookDto){
String name = bookDto.getName();
String mainType = bookDto.getMainType();
List<Integer> pageNumList = bookDto.getPageNumList();
Date startTime = bookDto.getStartTime();
Date endTime = bookDto.getEndTime();
// 分页
Pageable page = PageRequest.of(bookDto.getPage()-1,bookDto.getPageSize());
// bool-查询
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
if(StringUtils.isNotBlank(name)){
boolQueryBuilder.filter(QueryBuilders.termQuery("name",name));
}
if(StringUtils.isNotBlank(mainType)){
boolQueryBuilder.filter(QueryBuilders.termQuery("mainType",mainType));
}
if(CollectionUtils.isNotEmpty(pageNumList)){
boolQueryBuilder.filter(QueryBuilders.termsQuery("pageNum",pageNumList));
}
if(startTime != null){
boolQueryBuilder.filter(QueryBuilders.rangeQuery("createTime").gte(DateUtil.formatESDate(startTime)));
}
if(endTime != null){
boolQueryBuilder.filter(QueryBuilders.rangeQuery("createTime").lte(DateUtil.formatESDate(endTime)));
}
// 排序
FieldSortBuilder createTimeSort = SortBuilders.fieldSort("createTime").order(SortOrder.DESC);
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
NativeSearchQuery build = nativeSearchQueryBuilder.withQuery(boolQueryBuilder)
.withPageable(page)
.withSort(createTimeSort)
.build();
AggregatedPage<Book> booksAgg = elasticsearchTemplate.queryForPage(build, Book.class);
PageModel<Book> bookPageModel =
new PageModel<>(booksAgg.getContent(),bookDto.getPage(),bookDto.getPageSize(),booksAgg.getTotalElements());
return bookPageModel;
}
2)聚合查询-带子聚合
项目中,我们经常有统计一个大类以及该大类下面的小类数量的需求,比如以我们的Book实体为例,我们可能需要统计库中"文学” 大类下面的“小说”、“散文”、“诗歌”等子类各有多少本。为了更好地应用该功能,我们模拟“造”一些数据存入到es中,插入数据的示例如下,我们使用random随机插入一些数据,只不过每一项的枚举我们事前固定在数组中。
@ApiOperation(value = "批量保存book数据")
@RequestMapping(value = "/batchSave", method = RequestMethod.PUT)
public String testSaveBook(@RequestParam Integer n) {
Random random = new Random();
String[] names = {"三国志", "三国演义", "三国", "红楼梦", "红楼", "西游记", "三国演义"};
Integer[] pageNums = {100, 200, 300, 400, 500};
String[] mainTypes = {"文学", "数学", "编程学", "法律学"};
String[] subType0s = {"小说", "散文", "诗歌", "话剧"};
String[] subType1s = {"代数", "几何"};
String[] subType2s = {"Java", "c++", "python", "go"};
String[] subType3s = {"民法", "刑法", "商法"};
String[] introductions = {"这是一本很好的故事", "这里面有非常动人的故事", "孙悟空三大白骨精"};
Double[] prices = {38.6, 35.8, 50.9, 40.7};
for (int i = 0; i < n; i++) {
Book b = new Book();
String id = UUID.randomUUID().toString().replace("_", "").substring(0, 10);
b.setId(id);
b.setName(names[random.nextInt(names.length)]);
b.setPageNum(pageNums[random.nextInt(pageNums.length)]);
b.setPrice(prices[random.nextInt(prices.length)]);
b.setIntroduction(introductions[random.nextInt(introductions.length)]);
Integer mainTypeIndex = random.nextInt(mainTypes.length);
b.setMainType(mainTypes[mainTypeIndex]);
// 子类型数据插入
switch (mainTypeIndex) {
case 0:
b.setSubType(subType0s[random.nextInt(subType0s.length)]);
break;
case 1:
b.setSubType(subType1s[random.nextInt(subType1s.length)]);
break;
case 2:
b.setSubType(subType2s[random.nextInt(subType2s.length)]);
break;
case 3:
b.setSubType(subType3s[random.nextInt(subType3s.length)]);
break;
default:
break;
}
b.setCreateTime(DateUtil.getOneDayOnset(random.nextInt(90), new Date()));
System.out.println("========save book ======= " + i + " ok!");
bookRepository.save(b);
}
return "ok";
}
然后我们就可以使用聚合查询统计出这些数据了,这里主要是在NativeSearchQuery中添加聚合查询,聚合查询类似MySQL中的group by 分组查询。
/**
* 聚合查询
*/
public void aggQueryByMainType(BookDto bookDto){
Date startTime = bookDto.getStartTime();
Date endTime = bookDto.getEndTime();
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
if(startTime != null){
boolQueryBuilder.filter(QueryBuilders.rangeQuery("createTime").gte(startTime));
}
if(endTime != null){
boolQueryBuilder.filter(QueryBuilders.rangeQuery("createTime").lte(endTime));
}
// 聚合查询再聚合查询,双次聚合
TermsAggregationBuilder fieldAgg = AggregationBuilders.terms("mainType").field("mainType");
fieldAgg.subAggregation(AggregationBuilders.terms("subType").field("subType"));
// 构建原生查询query
NativeSearchQuery build = new NativeSearchQueryBuilder()
.addAggregation(fieldAgg)
.withQuery(boolQueryBuilder)
.withPageable(PageRequest.of(0, 1))
.build();
AggregatedPage<Book> booksAgg = elasticsearchTemplate.queryForPage(build, Book.class);
Aggregations aggregations = booksAgg.getAggregations();
Terms terms = (Terms)aggregations.asMap().get("mainType");
List<? extends Terms.Bucket> buckets = terms.getBuckets();
// 取出桶内的数据
for(Terms.Bucket bucket :buckets){
String mainType = bucket.getKeyAsString();
System.out.println("mainType是:"+mainType+" 总数量是:"+bucket.getDocCount());
Aggregations subAgg = bucket.getAggregations();
Terms subTerms = (Terms)subAgg.asMap().get("subType");
List<? extends Terms.Bucket> subBbuckets = subTerms.getBuckets();
// 取出子桶中的内容
for (Terms.Bucket bucket1 : subBbuckets){
String subType = bucket1.getKeyAsString();
long subCount = bucket1.getDocCount();
System.out.println("=======对应的子级聚合的子类是:"+subType+" 子级数量是:"+subCount);
}
}
}
向es es_book_index 索引中插入500条数据,然后再统计,最后可以得到相应的统计结果:
mainType是:法律学mainType的总数量是:128
=======对应的子级聚合的子类是:民法 子级数量是:45
=======对应的子级聚合的子类是:刑法 子级数量是:43
=======对应的子级聚合的子类是:商法 子级数量是:40
mainType是:编程学mainType的总数量是:128
=======对应的子级聚合的子类是:python 子级数量是:42
=======对应的子级聚合的子类是:Java 子级数量是:31
=======对应的子级聚合的子类是:c++ 子级数量是:29
=======对应的子级聚合的子类是:go 子级数量是:26
mainType是:文学mainType的总数量是:127
=======对应的子级聚合的子类是:小说 子级数量是:40
=======对应的子级聚合的子类是:话剧 子级数量是:37
=======对应的子级聚合的子类是:散文 子级数量是:28
=======对应的子级聚合的子类是:诗歌 子级数量是:22
mainType是:数学mainType的总数量是:117
=======对应的子级聚合的子类是:几何 子级数量是:62
=======对应的子级聚合的子类是:代数 子级数量是:55
三、小结
借助elasticsearchTemplate 可以为我们提供强大的搜索统计功能,项目中我们经常以时间为横坐标,做各种增长趋势图等,这些需求我们都可以借助es的聚合查询轻松实现。后续将继续探讨es其他的强大功能。。。