聚合(aggregations)可以讓我們極其方便的實現對數據的統計、分析、運算。例如:
- 什么品牌的手機最受歡迎?
- 這些手機的平均價格、最高價格、最低價格?
- 這些手機每月的銷售情況如何?
實現這些統計功能的比數據庫的sql要方便的多,而且查詢速度非常快,可以實現近實時搜索效果。
1.1.聚合的種類
聚合常見的有三類:
- 桶(Bucket)聚合:用來對文檔做分組
- TermAggregation:按照文檔字段值分組,例如按照品牌值分組、按照國家分組
- Date Histogram:按照日期階梯分組,例如一周為一組,或者一月為一組
- 度量(Metric)聚合:用以計算一些值,比如:最大值、最小值、平均值等
- Avg:求平均值
- Max:求最大值
- Min:求最小值
- Stats:同時求max、min、avg、sum等
- 管道(pipeline)聚合:其它聚合的結果為基礎做聚合
注意:參加聚合的字段必須是keyword、日期、數值、布爾類型
1.2.DSL實現聚合
現在,我們要統計所有數據中的酒店品牌有幾種,其實就是按照品牌對數據分組。此時可以根據酒店品牌的名稱做聚合,也就是Bucket聚合。
1.2.1.Bucket聚合語法
語法如下:
GET /hotel/_search
{
"size": 0, // 設置size為0,結果中不包含文檔,只包含聚合結果
"aggs": { // 定義聚合
"brandAgg": { //給聚合起個名字
"terms": { // 聚合的類型,按照品牌值聚合,所以選擇term
"field": "brand", // 參與聚合的字段
"size": 10 // 希望獲取的聚合結果數量
}
}
}
}
結果如圖:
1.2.2.聚合結果排序
默認情況下,Bucket聚合會統計Bucket內的文檔數量,記為_count,並且按照_count降序排序。
我們可以指定order屬性,自定義聚合的排序方式:
GET /hotel/_search
{
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"order": {
"_count": "asc" // 按照_count升序排列
},
"size": 10
}
}
}
}
1.2.3.限定聚合范圍
默認情況下,Bucket聚合是對索引庫的所有文檔做聚合,但真實場景下,用戶會輸入搜索條件,因此聚合必須是對搜索結果聚合。那么聚合必須添加限定條件。
我們可以限定要聚合的文檔范圍,只要添加query條件即可:
GET /hotel/_search
{
"query": {
"range": {
"price": {
"lte": 200 // 只對200元以下的文檔聚合
}
}
},
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"size": 20
}
}
}
}
這次,聚合得到的品牌明顯變少了:
1.2.4.Metric聚合語法
上節課,我們對酒店按照品牌分組,形成了一個個桶。現在我們需要對桶內的酒店做運算,獲取每個品牌的用戶評分的min、max、avg等值。
這就要用到Metric聚合了,例如stat聚合:就可以獲取min、max、avg等結果。
語法如下:
GET /hotel/_search
{
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"size": 20
},
"aggs": { // 是brands聚合的子聚合,也就是分組后對每組分別計算
"score_stats": { // 聚合名稱
"stats": { // 聚合類型,這里stats可以計算min、max、avg等
"field": "score" // 聚合字段,這里是score
}
}
}
}
}
}
這次的score_stats聚合是在brandAgg的聚合內部嵌套的子聚合。因為我們需要在每個桶分別計算。
另外,我們還可以給聚合結果做個排序,例如按照每個桶的酒店平均分做排序:
1.2.5.小結
aggs代表聚合,與query同級,此時query的作用是?
- 限定聚合的的文檔范圍
聚合必須的三要素:
- 聚合名稱
- 聚合類型
- 聚合字段
聚合可配置屬性有:
- size:指定聚合結果數量
- order:指定聚合結果排序方式
- field:指定聚合字段
1.3.RestAPI實現聚合
1.3.1.API語法
聚合條件與query條件同級別,因此需要使用request.source()來指定聚合條件。
聚合條件的語法:
聚合的結果也與查詢結果不同,API也比較特殊。不過同樣是JSON逐層解析:
1.3.2.業務需求
還是在上一節的基礎之上進行迭代
需求:搜索頁面的品牌、城市等信息不應該是在頁面寫死,而是通過聚合索引庫中的酒店數據得來的:
分析:
目前,頁面的城市列表、星級列表、品牌列表都是寫死的,並不會隨着搜索結果的變化而變化。但是用戶搜索條件改變時,搜索結果會跟着變化。
例如:用戶搜索“東方明珠”,那搜索的酒店肯定是在上海東方明珠附近,因此,城市只能是上海,此時城市列表中就不應該顯示北京、深圳、杭州這些信息了。
也就是說,搜索結果中包含哪些城市,頁面就應該列出哪些城市;搜索結果中包含哪些品牌,頁面就應該列出哪些品牌。
如何得知搜索結果中包含哪些品牌?如何得知搜索結果中包含哪些城市?
使用聚合功能,利用Bucket聚合,對搜索結果中的文檔基於品牌分組、基於城市分組,就能得知包含哪些品牌、哪些城市了。
因為是對搜索結果聚合,因此聚合是限定范圍的聚合,也就是說聚合的限定條件跟搜索文檔的條件一致。
查看瀏覽器可以發現,前端其實已經發出了這樣的一個請求:
請求參數與搜索文檔的參數完全一致。
返回值類型就是頁面要展示的最終結果:
結果是一個Map結構:
- key是字符串,城市、星級、品牌、價格
- value是集合,例如多個城市的名稱
1.3.3.業務實現
在cn.itcast.hotel.web
包的HotelController
中添加一個方法,遵循下面的要求:
- 請求方式:
POST
- 請求路徑:
/hotel/filters
- 請求參數:
RequestParams
,與搜索文檔的參數一致 - 返回值類型:
Map<String, List<String>>
代碼:
@PostMapping("filters")
public Map<String, List<String>> getFilters(@RequestBody RequestParams params){
return hotelService.getFilters(params);
}
這里調用了IHotelService中的getFilters方法,尚未實現。
在cn.itcast.hotel.service.IHotelService
中定義新方法:
Map<String, List<String>> filters(RequestParams params);
在cn.itcast.hotel.service.impl.HotelService
中實現該方法:
還是在上一節的基礎上進行功能的新增
@Override
public Map<String, List<String>> filters(RequestParams params) {
try {
// 1.准備Request
SearchRequest request = new SearchRequest("hotel");
// 2.准備DSL
// 2.1.query,使用的是相同的查詢條件
buildBasicQuery(params, request);
// 2.2.設置size
request.source().size(0);
// 2.3.聚合
buildAggregation(request);
// 3.發出請求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析結果
Map<String, List<String>> result = new HashMap<>();
Aggregations aggregations = response.getAggregations();
// 4.1.根據品牌名稱,獲取品牌結果
List<String> brandList = getAggByName(aggregations, "brandAgg");
result.put("brand", brandList);
// 4.2.根據品牌名稱,獲取品牌結果
List<String> cityList = getAggByName(aggregations, "cityAgg");
result.put("city", cityList);
// 4.3.根據品牌名稱,獲取品牌結果
List<String> starList = getAggByName(aggregations, "starAgg");
result.put("starName", starList);
return result;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void buildAggregation(SearchRequest request) {
request.source().aggregation(AggregationBuilders
.terms("brandAgg")
.field("brand")
.size(100)
);
request.source().aggregation(AggregationBuilders
.terms("cityAgg")
.field("city")
.size(100)
);
request.source().aggregation(AggregationBuilders
.terms("starAgg")
.field("starName")
.size(100)
);
}
private List<String> getAggByName(Aggregations aggregations, String aggName) {
// 4.1.根據聚合名稱獲取聚合結果
Terms brandTerms = aggregations.get(aggName);
// 4.2.獲取buckets
List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
// 4.3.遍歷
List<String> brandList = new ArrayList<>();
for (Terms.Bucket bucket : buckets) {
// 4.4.獲取key
String key = bucket.getKeyAsString();
brandList.add(key);
}
return brandList;
}