開始使用Elasticsearch(3): 聚合與數據分析

本文內容來自 https://blog.csdn.net/UbuntuTouch/article/details/99621105 , 有修正和刪減。

在今天的這篇文章中,將學習如何運用 Elasticsearch 來對我們的數據進行分析及一些關於 Analyzer 的介紹。在學習這個之前,我們必須完成之前的練習:

開始使用Elasticsearch (1)-- 如何對文檔進行操作

開始使用Elasticsearch (2)-- 如何對數據進行搜索

我們使用前面兩個練習所建立的 index,並在這篇文章中進行使用。

分析數據對很多的企業非常重要。它可以幫我們很快地分析出生產,運營中出現的問題,並實時地進行糾正或報警。

Aggregation(聚合) 簡介

聚合框架有助於基於搜索查詢提供聚合數據。它基於稱爲聚合的簡單構建塊,它們可以組合以構建複雜的數據摘要。

聚合可以被視爲在一組文檔上構建分析信息的工作單元。執行的上下文定義了該文檔集的內容(例如,在執行的查詢的上下文中執行頂級聚合/搜索請求的過濾器)。

有許多不同類型的聚合,每個聚合都有自己的目的和輸出。爲了更好地理解這些類型,通常更容易將它們分爲四個主要方面:

    1. Bucketing 存儲桶

    構建存儲桶的一系列聚合,其中每個存儲桶與密鑰和文檔標準相關聯。執行聚合時,將在上下文中的每個文檔上評估所有存儲桶條件,並且當條件匹配時,文檔被視爲“落入”相關存儲桶。在聚合過程結束時,我們最終會得到一個桶列表 - 每個桶都有一組“屬於”它的文檔。

    1. Metric 度量標準

    聚合可跟蹤和計算一組文檔的指標。

    1. Martrix 矩陣

    一系列聚合,它們在多個字段上運行,並根據從請求的文檔字段中提取的值生成矩陣結果。與度量標準和存儲區聚合不同,此聚合系列尚不支持腳本。

    1. Pipeline 管道

    聚合其他聚合的輸出及其關聯度量的聚合。

接下來是有趣的部分。由於每個存儲桶(bucket)有效地定義了一個文檔集(屬於該 bucket 的所有文檔),因此可以在 bucket 級別上關聯聚合,並且這些聚合將在該存儲桶的上下文中執行。這就是聚合的真正力量所在:聚合可以嵌套

注意一:bucketing聚合可以具有子聚合(bucketing 或 metric)。 將爲其父聚合生成的桶計算子聚合。 嵌套聚合的級別/深度沒有硬性限制(可以在“父”聚合下嵌套聚合,“父”聚合本身是另一個更高級聚合的子聚合)。

注意二:聚合可以操作於 double 類型的上限的數據。 因此,當在絕對值大於2 ^ 53的 long 上運行時,結果可能是近似的。

Aggregation 請求是搜索 API 的一部分,它可以帶有一個 query 的結構或者不帶。

準備數據

爲了更好的展示,我們首先來把之前的 twitter 的數據做一點小小的修改。我們添加一個新的字段 DOB (date of birth),也就是生日的意思。同時,我們也對 province,city 及 country 字段的類型做了調整,並把它們作爲 keyword。我們來做如下的操作:

DELETE twitter

PUT twitter
{
  "mappings": {
    "properties": {
      "DOB":{
        "type":"date"
      },
      "address":{
        "type":"text",
        "fields": {
          "keyword":{
            "type":"keyword",
          "ignore_above":256
          }
        }
      },
      "age":{
        "type":"long"
      },
      "city":{
        "type":"keyword"
      },
      "ocuntry":{
        "type":"keyword"
      },
      "location":{
        "type":"geo_point"
      },
      "message":{
        "type":"text",
        "fields":{
          "keyword":{
            "type":"keyword",
            "ignore_above":256
          }
        }
      },
      "province":{
        "type":"keyword"
      },
      "uid":{
        "type":"long"
      },
      "user":{
        "type":"text",
        "fields":{
          "keyword":{
            "type":"keyword",
            "ignore_above":256
          }
        }
      }
    }
  }
}

然後,我們再次使用 bulk API 來把數據導入到 Elasticsearch 中:

POST _bulk
{"index":{"_index":"twitter","_id":1}}
{"user":"張三","message":"今兒天氣不錯啊,出去轉轉去","uid":2,"age":20,"city":"北京","province":"北京","country":"中國","address":"中國北京市海淀區","location":{"lat":"39.970718","lon":"116.325747"}, "DOB": "1999-04-01"}
{"index":{"_index":"twitter","_id":2}}
{"user":"老劉","message":"出發,下一站雲南!","uid":3,"age":22,"city":"北京","province":"北京","country":"中國","address":"中國北京市東城區臺基廠三條3號","location":{"lat":"39.904313","lon":"116.412754"}, "DOB": "1997-04-01"}
{"index":{"_index":"twitter","_id":3}}
{"user":"李四","message":"happy birthday!","uid":4,"age":25,"city":"北京","province":"北京","country":"中國","address":"中國北京市東城區","location":{"lat":"39.893801","lon":"116.408986"}, "DOB": "1994-04-01"}
{"index":{"_index":"twitter","_id":4}}
{"user":"老賈","message":"123,gogogo","uid":5,"age":30,"city":"北京","province":"北京","country":"中國","address":"中國北京市朝陽區建國門","location":{"lat":"39.718256","lon":"116.367910"}, "DOB": "1989-04-01"}
{"index":{"_index":"twitter","_id":5}}
{"user":"老王","message":"Happy BirthDay My Friend!","uid":6,"age":26,"city":"北京","province":"北京","country":"中國","address":"中國北京市朝陽區國貿","location":{"lat":"39.918256","lon":"116.467910"}, "DOB": "1993-04-01"}
{"index":{"_index":"twitter","_id":6}}
{"user":"老吳","message":"好友來了都今天我生日,好友來了,什麼 birthday happy 就成!","uid":7,"age":28,"city":"上海","province":"上海","country":"中國","address":"中國上海市閔行區","location":{"lat":"31.175927","lon":"121.383328"}, "DOB": "1991-04-01"}

聚合操作

簡單地說,聚合的語法是這樣的:

"aggregations" : {
    "<aggregation_name>" : {
        "<aggregation_type>" : {
            <aggregation_body>
        }
        [,"meta" : {  [<meta_data_body>] } ]?
        [,"aggregations" : { [<sub_aggregation>]+ } ]?
    }
    [,"<aggregation_name_2>" : { ... } ]*
}

通常,我們也可以使用 aggs 來代替上面的 aggregations

下面,我們針對上面添加的數據來進行一些簡單的操作,這樣可以使得大家更加明白一些。

range聚合

我們可以把用戶按年齡進行分段,得到不同年齡段的用戶數量:

GET twitter/_search
{
  "size":0,
  "aggs":{
    "age":{
      "range":{
        "field":"age",
        "ranges":[
          {"from":20, "to":30},
          {"from":30, "to":40},
          {"from":40, "to":50}
        ]
      }
    }
  }
}

在這裏,我們使用了 range 類型的聚合。在上面我們定義了不同的年齡段。通過上面的查詢,我們可以得到不同年齡段的 bucket。執行結果是:

{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 6,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "age" : {
      "buckets" : [
        {
          "key" : "20.0-30.0",
          "from" : 20.0,
          "to" : 30.0,
          "doc_count" : 5
        },
        {
          "key" : "30.0-40.0",
          "from" : 30.0,
          "to" : 40.0,
          "doc_count" : 1
        },
        {
          "key" : "40.0-50.0",
          "from" : 40.0,
          "to" : 50.0,
          "doc_count" : 0
        }
      ]
    }
  }
}

注意: 上面的查詢代碼中, 我們把 size 設置爲0, 這時不會返回文檔數據, 而只返回統計數據。 假如設置爲大於1的數(比如設置爲2),會在hits.hits結果中返回文檔數據:

date_range 聚合

我們可以使用 date_range 來統計在某個時間段裏的文檔數:

GET twitter/_search
{
  "size":0,
  "aggs":{
    "birth_range":{
      "date_range":{
        "field":"DOB",
        "format": "yyyy-MM-dd",
        "ranges":[
          {"from":"1989-01-01", "to":"1990-01-01"},
          {"from":"1991-01-01", "to":"1992-01-01"}
        ]
      }
    }
  }
}

在上面我們查詢出生年月(DOB)從1989-01-01到1990-01-01及從1991-01-01到1992-01-01的文檔。顯示的結果是:

{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 6,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "birth_range" : {
      "buckets" : [
        {
          "key" : "1989-01-01-1990-01-01",
          "from" : 5.99616E11,
          "from_as_string" : "1989-01-01",
          "to" : 6.31152E11,
          "to_as_string" : "1990-01-01",
          "doc_count" : 1
        },
        {
          "key" : "1991-01-01-1992-01-01",
          "from" : 6.62688E11,
          "from_as_string" : "1991-01-01",
          "to" : 6.94224E11,
          "to_as_string" : "1992-01-01",
          "doc_count" : 1
        }
      ]
    }
  }
}

terms聚合

通過 term 聚合來查詢某一個關鍵字出現的頻率。在如下的 term 聚合中,我們想尋找在所有的文檔出現 ”Happy birthday” 裏按照城市進行分類的一個聚合。

GET twitter/_search
{
  "query":{
    "match":{
      "message":"Happy birthday"
    }
  },
  "size":0,
  "aggs":{
    "city":{
      "terms":{
        "field":"city",
        "size":10
      }
    }
  }
}

注意這裏的"size":10指的是前10名的城市。聚合的結果是:

{
  "took" : 9,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "city" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "北京",
          "doc_count" : 2
        },
        {
          "key" : "上海",
          "doc_count" : 1
        }
      ]
    }
  }
}

從上面的結果可以看出,在所有的含有 "Happy birthday" 的文檔中,有兩個是來自北京的,有一個是來自上海。

在正常的情況下,聚合是按照 doc_count 降序排列的。如果你想按照 key 進行排序的話,你可以嘗試如下的方法:

GET twitter/_search
{
  "query":{
    "match":{
      "message":"Happy birthday"
    }
  },
  "size":0,
  "aggs":{
    "city":{
      "terms":{
        "field":"city",
        "size":10,
        "order":{
          "_key":"asc"
        }
      }
    }
  }
}

執行結果是:

{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "city" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "上海",
          "doc_count" : 1
        },
        {
          "key" : "北京",
          "doc_count" : 2
        }
      ]
    }
  }
}

上面的結果顯示的是和之前不一樣的排序。doc_count 爲 1 的 “上海” 反而排到前面去了,雖然這個和我們之前的 asc (上升)排序是有點不太一致的地方 (按照拼音 shanghai 應該比 beijing 要大)。這個和語言的處理有關,但是我們確實看到我們是可以控制這個排序的。你也可以使用 _count 來進行升序的排列:

GET twitter/_search
{
  "query":{
    "match":{
      "message":"Happy birthday"
    }
  },
  "size":0,
  "aggs":{
    "city":{
      "terms":{
        "field":"city",
        "size":10,
        "order":{
          "_count":"asc"
        }
      }
    }
  }
}

你也可以通過在 term 聚合中指定順序來使用 nested aggreation(嵌套聚合) 的結果進行排序,而不是按計數對結果進行排序:

GET twitter/_search
{
  "query":{
    "match":{
      "message":"Happy birthday"
    }
  },
  "size":0,
  "aggs":{
    "top_cities":{
      "terms":{
        "field":"city",
        "order":{
          "avg_age":"desc"
        }
      },
      "aggs":{
        "avg_age":{
          "avg":{
            "field": "age"
          }
        }
      }
    }
  }
}

上面的代碼是按照 nested aggregation(嵌套聚合) 的結果 avg_age 降序排列的。顯示的結果爲:

{
  "took" : 4,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "top_cities" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "上海",
          "doc_count" : 1,
          "avg_age" : {
            "value" : 28.0
          }
        },
        {
          "key" : "北京",
          "doc_count" : 2,
          "avg_age" : {
            "value" : 25.5
          }
        }
      ]
    }
  }
}

我們也可以使用 script 來生成一個在索引裏沒有的術語來進行統計。比如,我們可以通過如下的 script 來生成一個對文檔人出生年份的統計:

GET twitter/_search
{
  "size":0,
  "aggs":{
    "birth_year":{
      "terms":{
        "script":{
          "source":"2020 - doc['age'].value"
        },
        "size":10
      }
    }
  }
}

在上面的代碼中, 我們通過腳本:

	"script":{
          "source":"2019 - doc['age'].value"
        }

根據年齡計算出生年份 並進行統計:

  "aggregations" : {
    "birth_year" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "1990",
          "doc_count" : 1
        },
        {
          "key" : "1992",
          "doc_count" : 1
        },
        {
          "key" : "1994",
          "doc_count" : 1
        },
        {
          "key" : "1995",
          "doc_count" : 1
        },
        {
          "key" : "1998",
          "doc_count" : 1
        },
        {
          "key" : "2000",
          "doc_count" : 1
        }
      ]
    }
  }

在上面我們可以看到 key 爲1990,1992,1994等。這些 key 在我們原有的字段中根本就不存在。

Histogram Aggregation 直方圖(柱狀圖)聚合

基於多桶值源的彙總,可以應用於從文檔中提取的數值或數值範圍值。 它根據值 動態構建固定大小(也稱爲間隔)的存儲桶。

GET twitter/_search
{
  "size":0,
  "aggs":{
    "age_distribution":{
      "histogram":{
        "field":"age",
        "interval":2
      }
    }
  }
}

執行結果:

 "aggregations" : {
    "age_distribution" : {
      "buckets" : [
        {
          "key" : 20.0,
          "doc_count" : 1
        },
        {
          "key" : 22.0,
          "doc_count" : 1
        },
        {
          "key" : 24.0,
          "doc_count" : 1
        },
        {
          "key" : 26.0,
          "doc_count" : 1
        },
        {
          "key" : 28.0,
          "doc_count" : 1
        },
        {
          "key" : 30.0,
          "doc_count" : 1
        }
      ]
    }
  }

上面顯示從20-22年齡段 有一個文檔。從22-24也有一個文檔。

date_histogram 日期直方圖

這種聚合類似於正常的直方圖,但只能與日期或日期範圍值一起使用。 由於日期在 Elasticsearch 中內部以長值表示,因此也可以但不準確地對日期使用正常的直方圖。

GET twitter/_search
{
  "size":0,
  "aggs":{
    "age_distribution":{
      "date_histogram":{
        "field":"DOB",
        "interval":"year"
      }
    }
  }
}

上面我們使用 DOB (出生日期) 作爲 date_histogram 的字段來進行聚合統計。我們按照每隔一年這樣的時間間隔來進行。顯示結果:

 "aggregations" : {
    "age_distribution" : {
      "buckets" : [
        {
          "key_as_string" : "1989-01-01T00:00:00.000Z",
          "key" : 599616000000,
          "doc_count" : 1
        },
        {
          "key_as_string" : "1990-01-01T00:00:00.000Z",
          "key" : 631152000000,
          "doc_count" : 0
        },
        {
          "key_as_string" : "1991-01-01T00:00:00.000Z",
          "key" : 662688000000,
          "doc_count" : 1
        },
        {
          "key_as_string" : "1992-01-01T00:00:00.000Z",
          "key" : 694224000000,
          "doc_count" : 0
        },
        {
          "key_as_string" : "1993-01-01T00:00:00.000Z",
          "key" : 725846400000,
          "doc_count" : 1
        },
        {
          "key_as_string" : "1994-01-01T00:00:00.000Z",
          "key" : 757382400000,
          "doc_count" : 1
        },
        {
          "key_as_string" : "1995-01-01T00:00:00.000Z",
          "key" : 788918400000,
          "doc_count" : 0
        },
        {
          "key_as_string" : "1996-01-01T00:00:00.000Z",
          "key" : 820454400000,
          "doc_count" : 0
        },
        {
          "key_as_string" : "1997-01-01T00:00:00.000Z",
          "key" : 852076800000,
          "doc_count" : 1
        },
        {
          "key_as_string" : "1998-01-01T00:00:00.000Z",
          "key" : 883612800000,
          "doc_count" : 0
        },
        {
          "key_as_string" : "1999-01-01T00:00:00.000Z",
          "key" : 915148800000,
          "doc_count" : 1
        }
      ]
    }
  }

上面的結果顯示 DOB 從 1989-01-01 到 1990-01-01 有一個文檔。從 1990-01-01 到 1991-01-01 區間沒有一個文檔。

cardinality Aggregation 基數聚合

我們也可以使用 cardinality 聚合來統計到底有多少個城市:

GET twitter/_search
{
  "size": 0,
  "aggs": {
    "number_of_cities": {
      "cardinality": {
        "field": "city"
      }
    }
  }
}

注意: 原文(7.5版本)中是用的 "field": "city.keyword"

執行結果是:

{
  "took" : 5,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 6,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "number_of_cities" : {
      "value" : 2
    }
  }
}

之前維護的數據中有兩個城市:北京及上海。它們在文檔中雖然出現多次,但是從唯一性上,只有兩個城市。

Metric 聚合

我們可以使用 Metrics 來統計我們的數值數據,比如我們想知道所有用戶的平均年齡是多少?我們可以用下面的聚合:

GET twitter/_search
{
  "size": 0,
  "aggs": {
    "average_age": {
      "avg": {
        "field": "age"
      }
    }
  }
}

執行結果是:

{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 6,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "average_age" : {
      "value" : 25.166666666666668
    }
  }
}

所有人的平均年齡是 25.166666666666668 歲。

我們也可以對只在北京的用戶文檔進行統計:

GET twitter/_search
{
  "size": 0,
  "query":{
    "match": {
      "city": "北京"
    }
  },
  "aggs": {
    "average_age": {
      "avg": {
        "field": "age"
      }
    }
  }
}

上面我們先查詢到所有在北京的用戶,然後再對這些文檔求年齡的平均值. 執行結果:

  "aggregations" : {
    "average_age" : {
      "value" : 24.6
    }
  }

聚合通常在查詢搜索結果上執行。 Elasticsearch 提供了一個特殊的 global(全局) 聚合,該全局聚合對所有文檔執行,而不受查詢的影響。

GET twitter/_search
{
  "size": 0,
  "query":{
    "match": {
      "city": "北京"
    }
  },
  "aggs": {
    "average_age": {
      "avg": {
        "field": "age"
      }
    },
    "average_age_all":{
      "global": {},
      "aggs":{
        "age_global_avg":{
          "avg":{
            "field":"age"
          }
        }
      }
    }
  }
}

在上面我們在 average_age_all 裏添加了一個 gobal 的聚合,這個平均值將會使用所有的6個文檔而不是限於在這個查詢的5個北京的文檔。執行結果是:

{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 5,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "average_age_all" : {
      "doc_count" : 6,
      "age_global_avg" : {
        "value" : 25.166666666666668
      }
    },
    "average_age" : {
      "value" : 24.6
    }
  }
}

我們也可以對整個年齡進行一個統計,比如:

GET twitter/_search
{
  "size":0,
  "aggs":{
    "age_stats":{
      "stats":{
        "field":"age"
      }
    }
  }
}

統計結果如下:

  "aggregations" : {
    "age_stats" : {
      "count" : 6,
      "min" : 20.0,
      "max" : 30.0,
      "avg" : 25.166666666666668,
      "sum" : 151.0
    }
  }

在這裏,我們可以看到到底有多少條數據,並且最大,最小的,平均值及加起來的合都在這裏一起顯示。

如果你想了解更多的細節,你可以使用 extended_stats:

GET twitter/_search
{
  "size":0,
  "aggs":{
    "age_stats":{
      "extended_stats":{
        "field":"age"
      }
    }
  }
}

執行結果是:

  "aggregations" : {
    "age_stats" : {
      "count" : 6,
      "min" : 20.0,
      "max" : 30.0,
      "avg" : 25.166666666666668,
      "sum" : 151.0,
      "sum_of_squares" : 3869.0,
      "variance" : 11.472222222222248,
      "std_deviation" : 3.3870669054835996,
      "std_deviation_bounds" : {
        "upper" : 31.940800477633868,
        "lower" : 18.392532855699468
      }
    }
  }

如果你想對多個指標進行統計並顯示它們之間的關係,你可以使用 matrix_stats

GET twitter/_search
{
  "size":0,
  "aggs":{
    "age_stats":{
      "matrix_stats":{
        "fields":["age","uid"]
      }
    }
  }
}

上面,我們把兩個整型字段 age 及 uid 進行統計。執行結果爲:

  "aggregations" : {
    "age_stats" : {
      "doc_count" : 6,
      "fields" : [
        {
          "name" : "uid",
          "count" : 6,
          "mean" : 4.5,
          "variance" : 3.5,
          "skewness" : 0.0,
          "kurtosis" : 1.7314285714285715,
          "covariance" : {
            "uid" : 3.5,
            "age" : 5.7
          },
          "correlation" : {
            "uid" : 1.0,
            "age" : 0.8211574455173661
          }
        },
        {
          "name" : "age",
          "count" : 6,
          "mean" : 25.166666666666668,
          "variance" : 13.76666666666667,
          "skewness" : -0.143450283024544,
          "kurtosis" : 1.8030533098042432,
          "covariance" : {
            "uid" : 5.7,
            "age" : 13.76666666666667
          },
          "correlation" : {
            "uid" : 0.8211574455173661,
            "age" : 1.0
          }
        }
      ]
    }
  }

我們也可以只得到這個年齡的最大值:

GET twitter/_search
{
  "size":0,
  "aggs":{
    "age_max":{
      "max":{
        "field":"age"
      }
    }
  }
}

執行結果:

  "aggregations" : {
    "age_max" : {
      "value" : 30.0
    }
  }

聚合通常適用於從聚合文檔集中提取的值。 可以使用聚合體內的字段鍵從特定字段提取這些值,也可以使用腳本提取這些值。我們可以通過 script 的方法來對我們的 aggregtion 結果進行重新計算:

GET twitter/_search
{
  "size":0,
  "aggs":{
    "average_age":{
      "avg":{
        "field":"age",
        "script": {
          "source":"_value * params.correction",
          "params": {
            "correction":1.5
          }
        }
      }
    }
  }
}

上面的這個聚合可以幫我們計算平均值再乘以1.5倍的結果。執行結果:

  "aggregations" : {
    "average_age" : {
      "value" : 37.75
    }
  }

顯然現在的結果是之前的 25.166666666666668 的1.5倍。

我們也可以直接使用 script 的方法來進行聚合。在這種情況下,我們可以不指定特定的field。我們可能把很多項進行綜合處理,並把這個結果來進行聚合:

GET twitter/_search
{
  "size":0,
  "aggs":{
    "average_age":{
      "avg":{
        "field":"age",
        "script": {
          "source":"doc['age'].value * params.correction",
          "params": {
            "correction":2
          }
        }
      }
    }
  }
}

在這裏我們完全沒有使用 field 這個項, 直接使用 script 來形成我們的聚合, 結果如下:

  "aggregations" : {
    "average_age" : {
      "value" : 50.333333333333336
    }
  }

Percentile aggregation 百分位聚合

百分位數(percentile)表示觀察值出現一定百分比的點。 例如,第95個百分位數是大於觀察值的95%的值。該聚合針對從聚合文檔中提取的數值計算一個或多個百分位數。 這些值可以從文檔中的特定數字字段中提取,也可以由提供的腳本生成。

百分位通常用於查找離羣值。 在正態分佈中,第0.13和第99.87個百分位數代表與平均值的三個標準差。 任何超出三個標準偏差的數據通常被視爲異常。這在統計的角度是非常有用的。

我們現在來通過一個簡單的例子來展示 Percentile aggregation 的用法:

GET twitter/_search
{
  "size":0,
  "aggs":{
    "age_quartiles":{
      "percentiles":{
        "field":"age",
        "percents": [
          25, 50, 75, 100
        ]
      }
    }
  }
}

在上面,我們使用了一個叫做 age 的字段。它是一個數值的字段。我們通過 percentile aggregation 可以得到25%,50%及75%的人在什麼範圍。執行結果是:

  "aggregations" : {
    "age_quartiles" : {
      "values" : {
        "25.0" : 22.0,
        "50.0" : 25.5,
        "75.0" : 28.0,
        "100.0" : 30.0
      }
    }
  }

我們可以看到25%的人平均年齡是低於22.0歲,而50%的人的年齡是低於25.5歲,而所有的人的年齡都是低於30歲的。這裏的50%的年齡和我們之前計算的平均年齡(25.166666666666668)是不一樣的。

通過上面的代碼, 我們能夠查出來50%的人的年齡是多少,但是在實際的應用中,我們有時也很希望知道滿足我們的 SLA (Service Level Aggreement) 百分比是多少,這樣你可以找到自己服務的差距,比如達到一個標準的百分比是多少。針對演示數據,我們可以使用 Percentile Ranks Aggregation。我們使用如下的查詢:

GET twitter/_search
{
  "size":0,
  "aggs":{
    "age_40_percentage":{
      "percentile_ranks": {
        "field": "age",
        "values": [
          40
        ]
      }
    }
  }
}

執行結果:

  "aggregations" : {
    "age_40_percentage" : {
      "values" : {
        "40.0" : 100.0
      }
    }
  }

上面的結果表明,100%的人的年齡是在40歲以下的。在我們的實際應用中,比如我們輸入一個指標,我們可以看出來有多少比例是在那個指標以內的。這樣我們可以看出來我們的 SLA 是否滿足條件。

更爲複雜的聚合

我們可以結合上面的 bucket 聚合及 metric 聚合形成更爲複雜的搜索:

GET twitter/_search
{
  "size":0,
  "aggs":{
    "cities":{
      "terms": {
        "field": "city",
        "order":{
          "average_age":"desc"
        },
        "size": 5
      },
      "aggs":{
        "average_age":{
          "avg":{
            "field": "age"
          }
        }
      }
    }
  }
}

在上面,我們首先通過 terms 來生成每個城市的桶聚合,讓後在每個桶裏計算所有文檔的平均年齡。在正常的情況下,這個排序是按照每個城市裏文檔的多少降序排列的。在上面的搜索中,我們特意添加 average_age 來進行降序排序。執行結果如下:

  "aggregations" : {
    "cities" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "上海",
          "doc_count" : 1,
          "average_age" : {
            "value" : 28.0
          }
        },
        {
          "key" : "北京",
          "doc_count" : 5,
          "average_age" : {
            "value" : 24.6
          }
        }
      ]
    }
  }

結果中有兩個城市:上海及北京。在上海城市中有1個文檔,而在北京城市裏有5個文檔。同時,我們也計算出來每個城市的平均年齡。由於我們使用了 average_age 來進行降排序,在結果中,我們可以看到“上海”排在前面,這是因爲上海城市的平均年齡比北京的平均年齡高。

Missing 聚合

我們可以通過這個聚合來統計出來確實某個字段的文檔個數。我們先添加如下的一個文檔:

PUT twitter/_doc/7
{
  "user": "張三",
  "message": "今兒天氣不錯啊,出去轉轉去",
  "uid": 2,
  "city": "北京",
  "province": "北京",
  "country": "中國",
  "address": "中國北京市海淀區",
  "location": {
    "lat": "39.970718",
    "lon": "116.325747"
  },
  "DOB": "1999-04-01"
}

在上面的文檔中,我們故意漏掉了 age 這個字段。使用如下的聚合來查詢有多少文檔缺少 age 這個字段:

GET twitter/_search
{
  "size":0,
  "aggs":{
    "total_missing_age":{
      "missing":{
        "field": "age"
      }
    }
  }
}

執行結果爲:

  "aggregations" : {
    "total_missing_age" : {
      "doc_count" : 1
    }

上面顯示一個文檔沒有 age 這個字段。

想了解更多關於 aggregation 的信息 請參閱:

Elasticsearch:aggregation介紹 Elasticsearch:pipeline aggregation 介紹 Elasticsearch:透徹理解Elasticsearch中的Bucket aggregation


Analyzer簡介

我們知道 Elasticsearch 可以實現秒級的搜索速度,其中很重要的一個原因就是: 當一個文檔被存儲的時候,同時它也對文檔的數據進行了索引(indexing)。這樣在以後的搜索中,就可以變得很快。簡單地說,當一個文檔進入到 Elasticsearch 時,它會經歷如下的步驟:

中間的那部分就叫做 Analyzer。我們可以看出來,它分爲三個部分:Char Filters, TokenizerToken Filter。它們的作用分別如下:

  • Char Filter: 字符過濾器的工作是執行清除任務,例如剝離HTML標記。

  • Tokenizer: 下一步是將文本拆分爲稱爲標記的術語。 這是由 tokenizer 完成的。 可以基於任何規則(例如空格)來完成拆分。 有關 tokennizer 的更多詳細信息,請參閱https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-tokenizers.html。

  • Token filter: 一旦創建了token,它們就會被傳遞給 token filter,這些過濾器會對 token 進行規範化。 Token filter 可以更改token,刪除術語或向 token 添加術語。

Elasticsearch 已經提供了比較豐富的 analyzer。我們可以自己創建自己的 token analyzer,甚至可以利用已經有的 char filter,tokenizer 及 token filter 來重新組合成一個新的 analyzer,並可以對文檔中的每一個字段分別定義自己的 analyzer。如果大家對analyzer 比較感興趣的話,請參閱https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-analyzers.html。

在默認配置下,standard analyzer 是 Elasticsearch 的默認分析器:

  • 沒有 Char Filter

  • 使用 standard tokonizer

  • 把字符串變爲小寫,同時有選擇地刪除一些 stop words 等。默認的情況下 stop words 爲 none,也即不過濾任何 stop words。

下面簡單地展示一下 analyzer 是如何實現的。

GET twitter/_analyze
{
  "text":[
    "Happy Birthday"
  ],
  "analyzer": "standard"
}

在上面的接口中,我們使用標準的 analyzer 來對字符串 "Happy birthday" 來分析,執行結果如下:

{
  "tokens" : [
    {
      "token" : "happy",
      "start_offset" : 0,
      "end_offset" : 5,
      "type" : "<ALPHANUM>",
      "position" : 0
    },
    {
      "token" : "birthday",
      "start_offset" : 6,
      "end_offset" : 14,
      "type" : "<ALPHANUM>",
      "position" : 1
    }
  ]
}

我們可以看到有兩個 token: happy 和 birthday。兩個token都變成小寫的了。同時我們也可以看到它們在文檔中的位置信息。

我們嘗試一下 english analyzer:

GET twitter/_analyze
{
  "text":[
    "Happy Birthday"
  ],
  "analyzer": "english"
}

執行結果是:

{
  "tokens" : [
    {
      "token" : "happi",
      "start_offset" : 0,
      "end_offset" : 5,
      "type" : "<ALPHANUM>",
      "position" : 0
    },
    {
      "token" : "birthdai",
      "start_offset" : 6,
      "end_offset" : 14,
      "type" : "<ALPHANUM>",
      "position" : 1
    }
  ]
}

我們可以看出來有完全不一樣的結果。這裏顯示的詞根。

同樣地,我們可以使用 whitespace analyzer:

GET twitter/_analyze
{
  "text":[
    "Happy Birthday"
  ],
  "analyzer": "whitespace"
}

執行結果:

{
  "tokens" : [
    {
      "token" : "Happy",
      "start_offset" : 0,
      "end_offset" : 5,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "Birthday",
      "start_offset" : 6,
      "end_offset" : 14,
      "type" : "word",
      "position" : 1
    }
  ]
}

在這裏,我們可以看出來沒有經過大小寫的處理。這對於精確的搜索是有用的,比如我們想區分字母的大小寫。

很多人很好奇,想知道中文字的切割時怎麼樣的。我們下面來做一個簡單的實驗:

GET twitter/_analyze
{
  "text":[
    "生日快樂"
  ],
  "analyzer": "standard"
}

執行結果:

{
  "tokens" : [
    {
      "token" : "生",
      "start_offset" : 0,
      "end_offset" : 1,
      "type" : "<IDEOGRAPHIC>",
      "position" : 0
    },
    {
      "token" : "日",
      "start_offset" : 1,
      "end_offset" : 2,
      "type" : "<IDEOGRAPHIC>",
      "position" : 1
    },
    {
      "token" : "快",
      "start_offset" : 2,
      "end_offset" : 3,
      "type" : "<IDEOGRAPHIC>",
      "position" : 2
    },
    {
      "token" : "樂",
      "start_offset" : 3,
      "end_offset" : 4,
      "type" : "<IDEOGRAPHIC>",
      "position" : 3
    }
  ]
}

我們可以看到有四個 token,並且它們的 type 也有所變化。

我們再看下用simple analyzer:

GET twitter/_analyze
{
  "text":[
    "Happy.Birthday"
  ],
  "analyzer": "simple"
}

執行結果是:

{
  "tokens" : [
    {
      "token" : "happy",
      "start_offset" : 0,
      "end_offset" : 5,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "birthday",
      "start_offset" : 6,
      "end_offset" : 14,
      "type" : "word",
      "position" : 1
    }
  ]
}

可以看到字符串中的 "." 也被正確認識,並作爲分隔符把 Happy.Birthday 切割爲兩個 token。

現在使用keyword 來拆分語句:

GET twitter/_analyze
{
  "text":[
    "Happy Birthday"
  ],
  "tokenizer": "keyword"
}

當我們使用 keyword 分析器時,字符串無論有多長,都被當做是一個 token。這個對我們的 term 相關的搜索及聚合是有很大的用途的。上面的分析結果顯示:

{
  "tokens" : [
    {
      "token" : "Happy Birthday",
      "start_offset" : 0,
      "end_offset" : 14,
      "type" : "word",
      "position" : 0
    }
  ]
}

我麼也可以使用 filter 處理我們的 token,比如:

GET twitter/_analyze
{
  "text":[
    "Happy Birthday"
  ],
  "tokenizer": "keyword",
  "filter": ["lowercase"]
}

經過上面的處理,我們的 token 變成爲:

{
  "tokens" : [
    {
      "token" : "happy birthday",
      "start_offset" : 0,
      "end_offset" : 14,
      "type" : "word",
      "position" : 0
    }
  ]
}

我們也可以使用單獨使用 tokenizer 來分析我們的文字:

standard tokenizer:

GET _analyze
{
  "tokenizer": "standard",
  "text":"Those who dare to fail miserably can achieve greatly."
}

生成如下的token:

Those, who, dare, to, fail, miserably, can, achieve, greatly

keyword tokenizer:

GET _analyze
{
  "tokenizer": "keyword",
  "text":"Los ANgeles"
}

執行結果:

{
  "tokens" : [
    {
      "token" : "Those who dare to fail miserably can achieve greatly.",
      "start_offset" : 0,
      "end_offset" : 53,
      "type" : "word",
      "position" : 0
    }
  ]
}

更多關於 analyzer 的資料: analysis-analyzers, Elasticsearch: analyzer學習文檔

至此,我們基本上已經完成了對 Elasticsearch 最基本的瞭解。

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