ES度量聚合(ElasticSearch Metric Aggregations)

從本篇將開始進入ES系列的聚合部分(Aggregations)。

本篇重點介紹Elasticsearch Metric Aggregations(度量聚合)。

Metric聚合,主要針對數值類型的字段,類似於關係型數據庫中的sum、avg、max、min等聚合類型。

本例基於如下索引進行試驗:

public static void createMapping_agregations() {
        RestHighLevelClient client = EsClient.getClient();
        try {
            CreateIndexRequest request = new CreateIndexRequest("aggregations_index02");
            XContentBuilder jsonBuilder = XContentFactory.jsonBuilder()
                                            .startObject()
                                                .startObject("properties")
                                                    .startObject("orderId")
                                                        .field("type", "integer")
                                                    .endObject()
                                                    .startObject("orderNo")
                                                        .field("type", "keyword")
                                                    .endObject()
                                                    .startObject("totalPrice")
                                                        .field("type", "double")
                                                    .endObject()
                                                    .startObject("sellerId")
                                                        .field("type", "integer")
                                                    .endObject()
                                                    .startObject("sellerName")
                                                        .field("type", "keyword")
                                                    .endObject()
                                                    .startObject("buyerId")
                                                        .field("type", "integer")
                                                    .endObject()
                                                    .startObject("buyerName")
                                                        .field("type", "keyword")
                                                    .endObject()
                                                    .startObject("createTime")
                                                        .field("type", "date")
                                                        .field("format", "yyyy-MM-dd HH:mm:ss")
                                                    .endObject()
                                                    .startObject("status")
                                                        .field("type", "integer")
                                                    .endObject()
                                                    .startObject("reciveAddressId")
                                                        .field("type", "integer")
                                                    .endObject()
                                                    .startObject("reciveName")
                                                        .field("type", "keyword")
                                                    .endObject()
                                                    .startObject("phone")
                                                        .field("type", "keyword")
                                                    .endObject()
                                                    .startObject("skuId")
                                                        .field("type", "integer")
                                                    .endObject()
                                                    .startObject("skuNo")
                                                        .field("type", "keyword")
                                                    .endObject()
                                                    .startObject("goodsId")
                                                        .field("type", "integer")
                                                    .endObject()
                                                    .startObject("goodsName")
                                                        .field("type", "keyword")
                                                    .endObject()
                                                    .startObject("num")
                                                        .field("type", "integer")
                                                    .endObject()
                                                .endObject()
                                            .endObject();
            request.mapping("_doc", jsonBuilder);
            System.out.println(client.indices().create(request, RequestOptions.DEFAULT));
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            EsClient.close(client);
        }
    }

對應的SQL表結構如下:

CREATE TABLE `es_order_tmp` (
  `orderId` int(11) NOT NULL DEFAULT '0' COMMENT '主鍵',
  `orderNo` varchar(30) DEFAULT NULL COMMENT '訂單編號',
  `totalPrice` decimal(10,2) DEFAULT NULL COMMENT '訂單總價,跟支付中心返回金額相等,包括了雅豆,餘額,第三方支付的金額。運費包含在內,優惠券抵扣的金額不含在內',
  `sellerId` int(11) DEFAULT NULL COMMENT '商家ID',
  `selerName` varchar(50) DEFAULT NULL COMMENT '商家名稱',
  `buyerId` int(11) DEFAULT NULL COMMENT '創建者,購買者',
  `buyerName` varchar(255) DEFAULT NULL COMMENT '業主姓名',
  `createTime` varchar(22) DEFAULT NULL,
  `status` int(11) DEFAULT NULL COMMENT '訂單狀態,0:待付款,1:待發貨,2:待收貨,3:待評價,4:訂單完成,5:訂單取消,6:退款處理中,7:拒絕退貨,8:同意退貨,9:退款成功,10:退款關閉,11:訂單支付超時,12:半支付狀態',
  `reciveAddressId` int(11) DEFAULT NULL COMMENT '收貨地址ID',
  `reciveName` varchar(50) DEFAULT NULL,
  `phone` varchar(30) DEFAULT NULL COMMENT '聯繫號碼',
  `skuId` int(11) DEFAULT NULL COMMENT '貨品ID',
  `skuNo` varchar(100) DEFAULT NULL COMMENT 'SKU編號',
  `goodsId` int(11) DEFAULT NULL COMMENT '商品ID',
  `goodsName` varchar(100) DEFAULT NULL COMMENT '商品名稱',
  `num` int(11) DEFAULT NULL COMMENT '數量'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

avg 平均值

POST /exams/_search?size=0
{
    "aggs" : {
        "avg_grade" : { "avg" : { "field" : "grade" } }
    }
}

對字段grade取平均值。

對應的java示例如下:

public static void testMatchQuery() {
        RestHighLevelClient client = EsClient.getClient();
        try {
            SearchRequest searchRequest = new SearchRequest();
            searchRequest.indices("aggregations_index02");
            SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
            AggregationBuilder avg = AggregationBuilders.avg("avg-aggregation").field("num").missing(0);    // @1
            sourceBuilder.aggregation(avg);
            sourceBuilder.size(0);
            sourceBuilder.query(
                    QueryBuilders.termQuery("sellerId", 24)
            );
            searchRequest.source(sourceBuilder);
            SearchResponse result = client.search(searchRequest, RequestOptions.DEFAULT);
            System.out.println(result);
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            EsClient.close(client);
        }
    }

其中代碼@1:missing(0)表示如果文檔中沒有取平均值的字段時,則使用該值進行計算,本例中使用0參與計算。

其返回結果如下:

{
    "took":2,
    "timed_out":false,
    "_shards":{
        "total":5,
        "successful":5,
        "skipped":0,
        "failed":0
    },
    "hits":{
        "total":39,
        "max_score":0,
        "hits":[

        ]
    },
    "aggregations":{
        "avg#avg-aggregation":{
            "value":1.2820512820512822
        }
    }
}

Weighted Avg Aggregation 加權平均聚合

加權平均算法,∑(value * weight) / ∑(weight)。

加權平均(weghted_avg)支持的參數列表:

  • value
    提供值的字段或腳本的配置。例如定義計算哪個字段的平均值,該值支持如下子參數:
  • field
    用來定義平均值的字段名稱。
  • missing
    用來定義如果匹配到的文檔沒有avg字段,使用該值來參與計算。
  • weight
    用來定義權重的對象,其可選屬性如下:
  • field
    定義權重來源的字段。
  • missing
    如果文檔缺失權重來源字段,以該值來代表該文檔的權重值。
  • format
    數值類型格式化。
  • value_type
    用來指定value的類型,例如ValueType.DATE、ValueType.IP等。

示例如下:

POST /exams/_search
{
    "size": 0,
    "aggs" : {
        "weighted_grade": {
            "weighted_avg": {
                "value": {
                    "field": "grade"
                },
                "weight": {
                    "field": "weight"            // @2
                }
            }
        }
    }
}

從文檔中抽取屬性爲weight的字段的值來當權重值。
其JAVA示例如下:

public static void test_weight_avg_aggregation() {
        RestHighLevelClient client = EsClient.getClient();
        try {
            SearchRequest searchRequest = new SearchRequest();
            searchRequest.indices("aggregations_index02");
            SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
            WeightedAvgAggregationBuilder avg = AggregationBuilders.weightedAvg("avg-aggregation")
                    
                                        .value( 
                                                (new MultiValuesSourceFieldConfig.Builder())
                                                   .setFieldName("num")
                                                   .setMissing(0)
                                                   .build()
                                              )
                                        .weight(
                                                (new MultiValuesSourceFieldConfig.Builder())
                                                   .setFieldName("num")
                                                   .setMissing(1)
                                                   .build()
                                               )
    //                                    .valueType(ValueType.LONG)
                                        
                                       ;
            
            avg.toString();
            
            sourceBuilder.aggregation(avg);
            sourceBuilder.size(0);
            sourceBuilder.query(
                    QueryBuilders.termQuery("sellerId", 24)
            );
            searchRequest.source(sourceBuilder);
            SearchResponse result = client.search(searchRequest, RequestOptions.DEFAULT);
            System.out.println(result);
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            EsClient.close(client);
        }
    }

Cardinality Aggregation

基數聚合,先distinct,再聚合,類似關係型數據庫(count(distinct))。

示例如下:

POST /sales/_search?size=0
{
    "aggs" : {
        "type_count" : {
            "cardinality" : {
                "field" : "type"
            }
        }
    }
}

對應的JAVA示例如下:

public static void test_Cardinality_Aggregation() {
        RestHighLevelClient client = EsClient.getClient();
        try {
            SearchRequest searchRequest = new SearchRequest();
            searchRequest.indices("aggregations_index02");
            SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
            AggregationBuilder aggregationBuild = AggregationBuilders.cardinality("buyerid_count").field("buyerId");
            sourceBuilder.aggregation(aggregationBuild);
            sourceBuilder.size(0);
            sourceBuilder.query(
                    QueryBuilders.termQuery("sellerId", 24)
            );
            searchRequest.source(sourceBuilder);
            SearchResponse result = client.search(searchRequest, RequestOptions.DEFAULT);
            System.out.println(result);
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            EsClient.close(client);
        }
    }

返回結果如下:

{
    "took":30,
    "timed_out":false,
    "_shards":{
        "total":5,
        "successful":5,
        "skipped":0,
        "failed":0
    },
    "hits":{
        "total":39,
        "max_score":0,
        "hits":[

        ]
    },
    "aggregations":{
        "cardinality#type_count":{
            "value":11
        }
    }
}

上述實現與SQL:SELECT COUNT(DISTINCT buyerId) from es_order_tmp where sellerId=24; 效果類似,表示購買了商家id爲24的買家個數。

其核心參數如下:

  • precision_threshold
    精確度控制。在此計數之下,期望計數接近準確。在這個值之上,計數可能會變得更加模糊(不準確)。支持的最大值是40000,超過此值的閾值與40000的閾值具有相同的效果。默認值是3000。

上述示例中返回的11是精確值,如果改寫成下面的代碼,結果將變的不準確:

field("buyerId").precisionThreshold(5)

其返回結果如下:

{
    "took":5,
    "timed_out":false,
    "_shards":{
        "total":5,
        "successful":5,
        "skipped":0,
        "failed":0
    },
    "hits":{
        "total":39,
        "max_score":0,
        "hits":[

        ]
    },
    "aggregations":{
        "cardinality#buyerid_count":{
            "value":9
        }
    }
}
  • Pre-computed hashes
    一個比較好的實踐是需要對字符串類型的字段進行基數聚合的話,可以提前索引該字符串的hash值,通過對hash值的聚合,提高效率。
  • Missing Value
    missing參數定義了應該如何處理缺少值的文檔。默認情況下,它們將被忽略,但也可以將它們視爲具有一個值,通過missing value來設置。

Extended Stats Aggregation

stats聚合的擴展版本,示例如下:

GET /exams/_search
{
    "size": 0,
    "aggs" : {
        "grades_stats" : { "extended_stats" : { "field" : "grade" } }
    }
}

對應的JAVA示例如下:

public static void test_Extended_Stats_Aggregation() {
        RestHighLevelClient client = EsClient.getClient();
        try {
            SearchRequest searchRequest = new SearchRequest();
            searchRequest.indices("aggregations_index02");
            SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
            AggregationBuilder aggregationBuild = AggregationBuilders.extendedStats("extended_stats")
                                                     .field("num")
                                                  ;
            sourceBuilder.aggregation(aggregationBuild);
            sourceBuilder.size(0);
            sourceBuilder.query(
                    QueryBuilders.termQuery("sellerId", 24)
            );
            searchRequest.source(sourceBuilder);
            SearchResponse result = client.search(searchRequest, RequestOptions.DEFAULT);
            System.out.println(result);
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            EsClient.close(client);
        }
    }

返回的結果如下:

{
    "took":13,
    "timed_out":false,
    "_shards":{
        "total":5,
        "successful":5,
        "skipped":0,
        "failed":0
    },
    "hits":{
        "total":39,
        "max_score":0,
        "hits":[

        ]
    },
    "aggregations":{
        "extended_stats#extended_stats":{
            "count":39,                   //   @1
            "min":1,                        //   @2
            "max":11,                     //    @3
            "avg":1.2820512820512822,    // @4
            "sum":50,                                 // @5
            "sum_of_squares":162,           // @6
            "variance":2.5101906640368177,    // @7
            "std_deviation":1.5843581236692725,  // @8
            "std_deviation_bounds":{                       // @9
                "upper":4.450767529389827,
                "lower":-1.886664965287263
            }
        }
    }
}

將所能支持的聚合類型都返回。
@1:返回符合條件的總條數。
@2:該屬性在符合條件中的最小值。
@3:該屬性在符合條件中的最大值。
@4:該屬性在符合條件的文檔中的平均值。
@5:該屬性在符合條件的文檔中的sum求和。
@6-9:暫未理解其含義。

同樣支持missing屬性。

max Aggregation

求最大值,與avg Aggregation聚合類似,不再重複介紹。

min Aggregation

求最小值,與avg Aggregation聚合類似,不再重複介紹。

Percentiles Aggregation

百分位計算,ES提供的另外一種近似度量方式。主要用於展現以具體百分比下觀察到的數值,例如,第95個百分位上的數值,是高於 95% 的數據總和。百分位聚合通常用來找出異常,適用與使用統計學中正態分佈來觀察問題。
官方文檔:https://www.elastic.co/guide/cn/elasticsearch/guide/current/percentiles.html

例如:

GET latency/_search
{
    "size": 0,
    "aggs" : {
        "load_time_outlier" : {
            "percentiles" : {
                "field" : "load_time" 
            }
        }
    }
}

load_time,在官方文檔中的字段含義爲字段加載時間,其返回值如下:

{
    ...

   "aggregations": {
      "load_time_outlier": {
         "values" : {
            "1.0": 5.0,
            "5.0": 25.0,
            "25.0": 165.0,
            "50.0": 445.0,
            "75.0": 725.0,
            "95.0": 945.0,
            "99.0": 985.0
         }
      }
   }
}

默認的百分比key爲[ 1, 5, 25, 50, 75, 95, 99 ]。
按照官方的解讀,可以這樣理解上述返回結果:
"1.0": 5.0;表示(100-1)%的數據都大於5.0;也表示1%的數據小於5.0。
"5.0": 25.0 表示,95%的請求的加載時間大於等於25。
"99.0": 985.0 表示1%的請求的加載時間大於985.0。

  • percentile
    用來定義其百分比,例如percents:[10,50,95,99]
  • keyed
    默認情況下,keyed參數爲true,其結果的返回格式如上:
"values" : {
            "1.0": 5.0,
            "5.0": 25.0,
            "25.0": 165.0,
            "50.0": 445.0,
            "75.0": 725.0,
            "95.0": 945.0,
            "99.0": 985.0
         }

如果設置keyed=false,則返回值的格式如下:

"aggregations": {
        "load_time_outlier": {
            "values": [
                {
                    "key": 1.0,
                    "value": 5.0
                },
                {
                    "key": 5.0,
                    "value": 25.0
                },
           ...
            ]
        }
    }
  • 百分位使用場景
    百分位通常使用近似統計。

計算百分位數有許多不同的算法。簡單實現只是將所有值存儲在一個排序數組中。要找到第50個百分位,只需找到my_array[count(my_array) * 0.5]處的值。

顯然,這種簡單的實現沒有伸縮性——排序數組隨數據集中值的數量線性增長。爲了計算es集羣中可能存在的數十億個值的百分位數,兼顧性能的需求,故ES通常使用計算近似百分位數。近似百分位通常使用TDigest 算法。

在使用近似百分位時,通常需要考慮這些:

  1. 準確度與q(1-q)成正比。這意味着極端百分位數(如99%)比不那麼極端的百分位數(如中位數)更準確
  2. 對於較小的值集,百分位數是非常準確的(如果數據足夠小,可能是100%準確)。
  3. 當桶中值的數量增加時,算法開始近似百分位數。它有效地以準確性換取內存節省。準確的不準確程度很難一概而論,因爲它取決於您的數據分佈和聚合的數據量。
  • Compression
    近似算法必須平衡內存利用率和估計精度。這個平衡可以使用參數compression來控制。

TDigest算法使用許多“節點”來近似百分位數——可用節點越多,與數據量成比例的準確性(和大內存佔用)就越高。壓縮參數將節點的最大數量限制爲20 * compression。
因此,通過增加壓縮值,可以以增加內存爲代價來提高百分位數的準確性。較大的壓縮值也會使算法變慢,因爲底層樹數據結構的大小會增加,從而導致更昂貴的操作。默認壓縮值是100。
一個“節點”使用大約32字節的內存,因此在最壞的情況下(大量數據按順序到達),默認設置將產生大約64KB(32 20 100)大小的TDigest。實際上,數據往往更隨機,TDigest使用的內存更少。

HDR Histogram(直方圖)

HDR直方圖(High Dynamic Range Histogram,高動態範圍直方圖)是一種替代實現,在計算延遲度量的百分位數時非常有用,因爲它比t-digest實現更快,但需要更大的內存佔用。此實現維護一個固定的最壞情況百分比錯誤(指定爲有效數字的數量)。這意味着如果數據記錄值從1微秒到1小時(3600000000毫秒)直方圖設置爲3位有效數字,它將維持一個價值1微秒的分辨率值1毫秒,3.6秒(或更好的)最大跟蹤值(1小時)。

GET latency/_search
{
    "size": 0,
    "aggs" : {
        "load_time_outlier" : {
            "percentiles" : {
                "field" : "load_time",
                "percents" : [95, 99, 99.9],
                "hdr": { 
                  "number_of_significant_value_digits" : 3 
                }
            }
        }
    }
}
  1. hdr
    通過hdr屬性指定直方圖相關的參數。
  2. number_of_significant_value_digits
    指定以有效位數爲單位的直方圖值的分辨率。

注意:hdr直方圖只支持正值,如果傳遞負值,則會出錯。如果值的範圍是未知的,那麼使用HDRHistogram也不是一個好主意,因爲這可能會導致內存的大量使用。

  • Missing value
    missing參數定義了應該如何處理缺少值的文檔。默認情況下,它們將被忽略,但也可以將它們視爲具有一個值。

Percentiles Aggregation示例(Java Demo):

public static void test_Percentiles_Aggregation() {
        RestHighLevelClient client = EsClient.getClient();
        try {
            SearchRequest searchRequest = new SearchRequest();
            searchRequest.indices("aggregations_index02");
            SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
            AggregationBuilder aggregationBuild = AggregationBuilders.percentiles("percentiles")
                                                     .field("load_time")
                                                     .percentiles(75,90,99.9)
                                                     .compression(100)
                                                     .method(PercentilesMethod.HDR)
                                                     .numberOfSignificantValueDigits(3)
                                                  ;
            sourceBuilder.aggregation(aggregationBuild);
            sourceBuilder.size(0);
            sourceBuilder.query(
                    QueryBuilders.termQuery("sellerId", 24)
            );
            searchRequest.source(sourceBuilder);
            SearchResponse result = client.search(searchRequest, RequestOptions.DEFAULT);
            System.out.println(result);
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            EsClient.close(client);
        }
    }

Percentile Ranks Aggregation

百分位範圍表示觀察值低於某一值的百分比。例如,如果一個值大於或等於觀察值的95%,那麼它就屬於第95百分位。
假設您的數據包含網站加載時間。您可能有一個服務協議,95%的頁面加載完全在500ms內完成,99%的頁面加載完全在600ms內完成。

示例:

GET latency/_search
{
    "size": 0,
    "aggs" : {
        "load_time_ranks" : {                         // @1
            "percentile_ranks" : {                     // @2
                "field" : "load_time",                   // @3
                "values" : [500, 600]                  // @4
            }
        }
    }
}

代碼@1:聚合的名稱。
代碼@2:聚合的類型,這裏使用percentile_ranks。
代碼@3:用於聚合的字段。
代碼@5:設置觀察值。

其他的使用與1.7 Percentiles Aggregation類似,就不單獨給出JAVA示例了。

Stats Aggregation

返回的統計信息包括:min、max、sum、count和avg。
其示例如下:

POST /exams/_search?size=0
{
    "aggs" : {
        "grades_stats" : { "stats" : { "field" : "grade" } }
    }
}

對應的返回結果爲:

{
    ...
    "aggregations": {
        "grades_stats": {
            "count": 2,
            "min": 50.0,
            "max": 100.0,
            "avg": 75.0,
            "sum": 150.0
        }
    }
}

因爲與avg的使用類似,故JAVA示例就不重複給出。

Sum Aggregation

求和聚合。類似於關係型數據庫的sum函數,其使用與avg類似,故只是簡單羅列一下restful的使用方式:

POST /sales/_search?size=0
{
    "query" : {
        "constant_score" : {
            "filter" : {
                "match" : { "type" : "hat" }
            }
        }
    },
    "aggs" : {
        "hat_prices" : { "sum" : { "field" : "price" } }
    }
}

value count aggregation

值個數聚合,主要是統計一個字段有多少個不同的值,例如關係型數據庫中一張用戶表user中一個性別字段sex,其取值爲0,1,2,那不管這個表有多少行數據,sex的value count最多爲3。

示例如下:

POST /sales/_search?size=0
{
    "aggs" : {
        "types_count" : { "value_count" : { "field" : "type" } }
    }
}

其響應結果如下:

{
    ...
    "aggregations": {
        "types_count": {
            "value": 7
        }
    }
}

median absolute deviation aggregation

中位絕對偏差聚合。由於這部分內容與統計學關係密切,但這並不是我的特長,故對該統計的含義做深入解讀,在實際場景中,我們只需要知道ES提供了中位數偏差統計的功能,如果有這方面的需求,我們知道如何使用ES的中位數統計即可。

官方場景:
假設我們收集了商品評價數據(1星到5星之間的數值)。在實際使用過程中通常會使用平均值來展示商品的整體評價等級。中位絕對偏差聚合可以幫助我們瞭解評審之間的差異有多大。

在這個例子中,我們有一個平均評級爲3星的產品。讓我們看看它的評級的絕對偏差中值,以確定它們的變化有多大。按照我的理解,中位絕對偏差聚合 ,聚合的數據來源於(原始數據 - 所有原始數值的平均值 的絕對值進行聚合)。
例如評論原始數據如下:
1、2、5、5、4、3、5、5、5、5
其平均值:4
那中位數絕對偏差值聚合的數據爲:
3、2、1、1、0、1、1、1、1、1

其Restfull示例如下:

GET reviews/_search
{
  "size": 0,
  "aggs": {
    "review_average": {     // @1
      "avg": {                
        "field": "rating"
      }
    },
    "review_variability": {    // @2
      "median_absolute_deviation": {
        "field": "rating" 
      }
    }
  }
}

該聚合包含兩部分。
代碼@1:針對字段rating使用AVG進行聚合(平均聚合,求出中位數)
代碼@2:針對字段rating進行中位數絕對偏差聚合。

備註:在es high rest api中未封裝(median absolute deviation aggregation)聚合。

ES 關於 Metric聚合就介紹到這裏了,接下來將重點分析Es Buket聚合。


原文發佈時間爲:2019-03-10
本文作者:丁威,《RocketMQ技術內幕》作者。
本文來自中間件興趣圈,瞭解相關信息可以關注中間件興趣圈

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章