Elasticsearch使用terms聚合之后进行分页排序

引言

elasticsearch中实现聚合也非常常见,同时es的数据量一般比较大,因此聚合结果比较多,像terms聚合默认只返回10条聚合结果,所以聚合之后进行分页,也是非常常见的操作。
es的terms聚合只能传入需要聚合的field和需要聚合返回的条数size,可能一开始我们只能通过将size设置为较大一些,获取全部的聚合结果之后再进行代码手动分页,其实大可不必这样,es的管道聚合中就有bucket_sort针对聚合之后的桶进行分页排序
我们看官方的介绍:

父管道聚合,对其父多桶聚合之后的桶进行排序,可以指定零个或者多个排序字段以及相应的排序顺序,每个bucket可以根据_key或者_count对其子聚合进行排序,此外,可以设置from和size以截取存储的桶,也就算我们所需要的分页。

我们可能发现term聚合中包含参数sort,也可以对聚合之后的桶进行聚合,那么这个order与bucket_sort有什么区别了?官方的介绍:
与所有管道聚合一样,bucket_sort聚合在所有其他非管道聚合之后执行。这意味着排序仅适用于已从父聚合返回的任何bucket。例如,如果父聚合是terms,并且其大小设置为10,则bucket_sort将只对这10个返回的term bucket进行排序。
terms的order是对所有桶进行_key排序或者_count排序,bucket_sort只对terms的size范围内的数据进行_key排序或者_count排序。
参数如下:

sort排序字段

from截取桶起始位置,默认0

size 返回桶数(我们所需要的页数),默认值为父聚合的size

使用bucket_sort的DSL语句

{
  "from": 0,
  "size": 0,
  "query": {
    "bool": {
      "must": [
      ]
    }
  },
  "aggs": {
    "bucketAgg": {
      "terms": {
        "field": "extern.paragraph_id",
        "size": 1000
      },
      "aggs": {
        "toptop": {
          "top_hits": {
            "size": 1,
            "sort": []
          }
        },
        "bucketSort": {
          "bucket_sort": {
            "from": 0,
            "size": 10,
            "sort": []
          }
        }
      }
    }
  }
}

这里使用ElasticsearchRestTemplate作为第三方连接工具:
java代码使用如下,这里假设我们请求第一页page为2,每页10条size为10,所有from为 (2 - 1 )*10 = 10,size为10

// 桶排序聚合
		BucketSortPipelineAggregationBuilder bucketSortAggregation = PipelineAggregatorBuilders.bucketSort(
				paragraphBucketSortAggName, Lists.emptyList()).from((page - 1) * size).size(size);

terms聚合,添加子聚合bucketSortAggregation,因为我们已经知道需要的页数和条数,所以这个地方的size我们不必设置为较大的默认值,而是当前页数的最后一条记录,为(page - 1) * size + size = page * size

TermsAggregationBuilder termsAggregationBuilder =
				terms(chapterTermsAggName).field("extern.paragraph_id").size(page * size).order(bucketOrders)
				.subAggregation(bucketSortAggregation)

最后需要分页的时候我们发现总条数即总的桶数没法获取呀,这时候我们可以使用指标聚合cardinality

因为cardinality可以统计query不同字段的值,我们只需要将cardinality的field与terms聚合的字段相同就可以得到所有的桶数,
因为一个桶聚合可以同时添加多个指标聚合,java代码实现如下:

CardinalityAggregationBuilder cardinalityAggregation = cardinality(paragraphCardinalityAggName)
				.field("extern.paragraph_id");
ArrayList<BucketOrder> bucketOrders = Lists.newArrayList(BucketOrder.count(false), BucketOrder.key(true));
		TermsAggregationBuilder termsAggregationBuilder =
				terms(chapterTermsAggName).field("extern.paragraph_id").size(page * size).order(bucketOrders)
				.subAggregation(bucketSortAggregation).subAggregation(topHitsAggregation);

最后通过分页工具就可以实现分页之后返回的数据

NativeSearchQuery query = new NativeSearchQueryBuilder()
				.withQuery(boolQueryBuilder)
				.withPageable(Pageable.unpaged())
				.addAggregation(termsAggregationBuilder)
				.addAggregation(cardinalityAggregation)
				.build();

terms聚合并且分页的DSL语句如下:

{
  "from": 0,
  "size": 0,
  "query": {
    "match_all": {}
  },
  "aggs": {
    "bucketAgg": {
      "terms": {
        "field": "extern.paragraph_id",
        "size": 8
      },
      "aggs": {
        "bucketSort": {
          "bucket_sort": {
            "from": 1,
            "size": 4,
            "sort": []
          }
        }
      }
    },
    "countAgg": {
      "cardinality": {
        "field": "extern.paragraph_id"
      }
    }
  }
}


聚合之后的结果如图

至此分页就完成了。
这里如果需要查询出桶内经过排序之后的最新几条记录,可以使用指标聚合top_hits,

返回桶内经过排序的最近几条记录,比如这里我们需要最新的一条记录,java代码实现

TopHitsAggregationBuilder topHitsAggregation = AggregationBuilders.topHits(topHitsAggName).size(1);
		topHitsAggregation.sort(getAuthorSortBuilder(SortOrder.DESC));
		topHitsAggregation.sort(getTopicLikeSortBuilder(SortOrder.DESC));
		topHitsAggregation.sort(getTopicLikeSortBuilder(SortOrder.DESC));

这里经过tophits之后返回的数据不能直接转为实体,所有我们写一个公用工具类进行转换

package com.bukeneng.ugc.utiils;

import cn.hutool.json.JSONUtil;
import org.elasticsearch.search.SearchHits;

/**
 *
 * @author liufuqiang
 */
public class SearchHitsUtil {
	private SearchHitsUtil() {}

	public static long getTotalCount(SearchHits searchHits) {
		return searchHits.getTotalHits().value;
	}

	public static <T> T getSourceAsEntity(String sourceAsString, Class<T> clazz) {
		return JSONUtil.toBean(sourceAsString, clazz, true);
	}
}

调用:
获取对象
T t = SearchHitsUtil.getSourceAsEntity(topHit.getSourceAsString, T.class);
获取总条数
long totalCount = SearchHitsUtil.getTotalCount(hits);

参考文档
https://www.elastic.co/guide/en/elasticsearch/reference/8.4/search-aggregations-bucket.html

posted @ 2022-10-14 20:59  木马不是马  阅读(5727)  评论(4编辑  收藏  举报