はてだBlog(仮称)

私的なブログど真ん中のつもりでしたが、気づけばWebサイト系のアプリケーション開発周りで感じたこと寄りの自分メモなどをつれづれ述べています。2020年6月現在、Elasticsearch、pandas、CMSなどに関する話題が多めです。...ですが、だんだんとより私的なプログラムのスニペット置き場になりつつあります。ブログで述べている内容は所属組織で販売している製品などに関するものではなく、また所属する組織の見解を代表するものではありません。

ElasticsearchのBucket aggregationsのさわり

ElasticsearchのAggregationsは種類が豊富ゆえに、嬉しい悲鳴なのですが、私のようなメモリが小さい人間はどれがどれというところの理解が大変です。

実際は、似た演算は、クエリの形とパラメータ、得られるバケット(演算結果)の形式が似ているので、同じような用途の内容違いのものを形から入って理解しましょうということで、記事をまとめてみました。

今回は、Bucket aggregatios、Metrics ...、Pipeline ...、でいうところのBucket aggregationsの俯瞰照会です。

www.elastic.co

事前準備:サンプルデータのインポート

クエリの説明用に、a_ex というインデックスにデータをインポートします。 他の方法でもかまいませんが、ここではkibanaにペタペタ貼り付け想定で示しています。

mapping設定

PUT a_ex/
{
  "mappings": {
    "mapping": {
      "properties": {
        "date": {
          "type": "date"
        },
        "loc": {
          "type": "geo_point"
        }
      }
    }
  }
}

データ(25件)のPOST

POST a_ex/_doc/
{"a": "a10", "b": 10, "c": 20, "weight": 1, "loc": {"lat": 35.5, "lon": 145.1}, "date": "2021-07-01"}
POST a_ex/_doc/
{"a": "a10", "b": 10, "c": 20, "weight": 1, "loc": {"lat": 35.5, "lon": 145.1}, "date": "2021-07-01"}
POST a_ex/_doc/
{"a": "a10", "b": 10, "c": 20, "weight": 1, "loc": {"lat": 35.5, "lon": 145.1}, "date": "2021-07-01"}
POST a_ex/_doc/
{"a": "a10", "b": 10, "c": 20, "weight": 1, "loc": {"lat": 35.5, "lon": 145.1}, "date": "2021-07-01"}
POST a_ex/_doc/
{"a": "a10", "b": 10, "c": 20, "weight": 1, "loc": {"lat": 35.5, "lon": 145.1}, "date": "2021-07-01"}
POST a_ex/_doc/
{"a": "a100", "b": 100, "c": 200, "weight": 2, "loc": {"lat": 35.5, "lon": 145.2}, "date": "2021-07-02"}
POST a_ex/_doc/
{"a": "a100", "b": 100, "c": 200, "weight": 2, "loc": {"lat": 35.5, "lon": 145.2}, "date": "2021-07-02"}
POST a_ex/_doc/
{"a": "a100", "b": 100, "c": 200, "weight": 2, "loc": {"lat": 35.5, "lon": 145.2}, "date": "2021-07-02"}
POST a_ex/_doc/
{"a": "a100", "b": 100, "c": 200, "weight": 2, "loc": {"lat": 35.5, "lon": 145.2}, "date": "2021-07-02"}
POST a_ex/_doc/
{"a": "a100", "b": 100, "c": 200, "weight": 2, "loc": {"lat": 35.5, "lon": 145.2}, "date": "2021-07-02"}
POST a_ex/_doc/
{"a": "a1000", "b": 1000, "c": 2000, "weight": 3, "loc": {"lat": 35.5, "lon": 145.3}, "date": "2021-07-03"}
POST a_ex/_doc/
{"a": "a1000", "b": 1000, "c": 2000, "weight": 3, "loc": {"lat": 35.5, "lon": 145.3}, "date": "2021-07-03"}
POST a_ex/_doc/
{"a": "a1000", "b": 1000, "c": 2000, "weight": 3, "loc": {"lat": 35.5, "lon": 145.3}, "date": "2021-07-03"}
POST a_ex/_doc/
{"a": "a1000", "b": 1000, "c": 2000, "weight": 3, "loc": {"lat": 35.5, "lon": 145.3}, "date": "2021-07-03"}
POST a_ex/_doc/
{"a": "a1000", "b": 1000, "c": 2000, "weight": 3, "loc": {"lat": 35.5, "lon": 145.3}, "date": "2021-07-03"}
POST a_ex/_doc/
{"a": "a10000", "b": 10000, "c": 20000, "weight": 4, "loc": {"lat": 35.5, "lon": 145.4}, "date": "2021-07-04"}
POST a_ex/_doc/
{"a": "a10000", "b": 10000, "c": 20000, "weight": 4, "loc": {"lat": 35.5, "lon": 145.4}, "date": "2021-07-04"}
POST a_ex/_doc/
{"a": "a10000", "b": 10000, "c": 20000, "weight": 4, "loc": {"lat": 35.5, "lon": 145.4}, "date": "2021-07-04"}
POST a_ex/_doc/
{"a": "a10000", "b": 10000, "c": 20000, "weight": 4, "loc": {"lat": 35.5, "lon": 145.4}, "date": "2021-07-04"}
POST a_ex/_doc/
{"a": "a10000", "b": 10000, "c": 20000, "weight": 4, "loc": {"lat": 35.5, "lon": 145.4}, "date": "2021-07-04"}
POST a_ex/_doc/
{"a": "a100000", "b": 100000, "c": 200000, "weight": 5, "loc": {"lat": 35.5, "lon": 145.5}, "date": "2021-07-05"}
POST a_ex/_doc/
{"a": "a100000", "b": 100000, "c": 200000, "weight": 5, "loc": {"lat": 35.5, "lon": 145.5}, "date": "2021-07-05"}
POST a_ex/_doc/
{"a": "a100000", "b": 100000, "c": 200000, "weight": 5, "loc": {"lat": 35.5, "lon": 145.5}, "date": "2021-07-05"}
POST a_ex/_doc/
{"a": "a100000", "b": 100000, "c": 200000, "weight": 5, "loc": {"lat": 35.5, "lon": 145.5}, "date": "2021-07-05"}
POST a_ex/_doc/
{"a": "a100000", "b": 100000, "c": 200000, "weight": 5, "loc": {"lat": 35.5, "lon": 145.5}, "date": "2021-07-05"}

1.時系列バケット(date系)

  • auto_date_histogram
  • date_histogram
  • date_range

まずは、時系列バケットキラーコンテンツならぬキラークエリですね。

Date系タイプのフィールドを指定して、それをグループにしたバケット分類します。

そのグループや刻み方のオプションによって何種類かあります。

並べてみると相対化されてわかりやすくなる(と思う)のですが、

というものになっているようです。

同列に語れるものは3種類あり、これを仕様を見比べしやすいように、JavaScriptのアロー関数で擬似的にシンタックスの要点を示したものが次のものです。 (似た形のものをまとめて、かつ対比することで、具体的な説明を見なくとも、名前から何ができるかなんとなくわかりません? 今回は対比することで察しやすくなるよね、という考えで、具体的な説明は極力省かせてもらっています。)

const date = field => ({
    "aggs": {
        "my_num": {
            "auto_date_histogram": {
                field,
                "buckets": 10
                //, "format": "yyyy-MM-dd"
            }
        },
        "my_interval": {
            "date_histogram": {
                field,
                "calendar_interval": "2d"
            }
        },
        "my_range": {
            "date_range": {
                field,
                "format": "MM-yyyy",
                "ranges": [
                    { "from": "01-2015", "to": "03-2015", "key": "quarter_01" },
                    { "from": "03-2015", "to": "06-2015", "key": "quarter_02" }
                ],
                "keyed": true
            }
        }
    }
})

実際に、上記の一つを試してみます。

注:私の手元のElasticsearch 6.8では、7系から対応のcalender_intervalパラメータには対応しておらず、deprecatedなintervalパラメータで代用しています。7系でcalender_interval対応後のバージョンをご利用の方は下記例についてご注意ください。

GET a_ex/_search
{
  "size": 0,
  "aggs": {
    "my_interval": {
      "date_histogram": {
        "field": "date",
        "interval": "1d"
      }
    }
  }
}

結果

{
  "aggregations" : {
    "my_interval" : {
      "buckets" : [
        {
          "key_as_string" : "2021-07-01T00:00:00.000Z",
          "doc_count" : 5
        },
        {
          "key_as_string" : "2021-07-02T00:00:00.000Z",
          "doc_count" : 5
        },
        {
          "key_as_string" : "2021-07-03T00:00:00.000Z",
          "doc_count" : 5
        },
        {
          "key_as_string" : "2021-07-04T00:00:00.000Z",
          "doc_count" : 5
        },
        {
          "key_as_string" : "2021-07-05T00:00:00.000Z",
          "doc_count" : 5
        }
      ]
    }
  }}

2.数値範囲(ヒストグラム的)バケット

  • variable_width_histogram
  • histogram
  • range

日付を巡る要件と数値(お金など)の階級化・グループ化は似たものが多いですね。 ということで、数値の値によるバケットのグループ化に対応するのが、rangeやhistogramアグリゲーションです。 こころなしか、時系列系と品揃えが似ていますね。

以下、JavaScriptのアロー関数で擬似的にシンタックスの要点です。

const hist = field => ({
    "aggs": {
        // variable_width_histogramが使えるのは7.9あたりから
        /* 
        "my_num": {
            "variable_width_histogram": {
                field,
                "buckets": 2
            }
        },
        */
        "my_interval": {
            "histogram": {
                field,
                "interval": 5
            }
        },
        "my_range": {
            "range": {
                field,
                "ranges": [
                    { "to": 100.0 },
                    { "from": 100.0, "to": 200.0 },
                    { "from": 200.0 }
                ]
            }
        }
    }
})

実際のクエリイメージ(histogram)

GET a_ex/_search
{
  "size": 0,
  "aggs": {
    "my_interval": {
      "histogram": {
        "field": "b",
        "interval": "1000"
      }
    }
  }
}

3. geo系バケット

  • geohash_grid
  • geotile_grid
  • geo_distance

時系列と数値の間ほどではありませんが、 座標(GEO)系にも似た要件が発生することが多いような気がしますが、それらにも対応しているようです。

geohashとグループ化の精細度のようなもの、geotileとグループ化の精細度のようなもの および geo_distacneとrangesの3種類ですね。

以下、JavaScriptのアロー関数で擬似的にシンタックスの要点です。

なお、クエリイメージと結果は割愛します。実際に試す場合は、最初に登録したデータのlocフィールドを使ってください。

const geo = field => ({
    "aggs": {
        "my_geohashGrid": {
            "geohash_grid": {
                field,
                "precision": 3
            }
        },
        "my_geotileGrid": {
            "geotile_grid": {
                field,
                "precision": 8
            }
        },
        "my_geoDistance": {
            "geo_distance": {
                field,
                "origin": "52.3760, 4.894",
                "ranges": [
                    { "to": 100000 },
                    { "from": 100000, "to": 300000 },
                    { "from": 300000 }
                ]
            }
        }
    }
})

4. terms(特定フィールドの値で分類)系バケット

  • terms
  • rare_terms

これはシンプルな例ですね。 特定のフィールドの値に応じて分類。 いわゆるカテゴリデータ的なものでのグループ分類。

説明順が真ん中あたりになりましたが、私見では、Bucket Aggsで一番最初にお世話になるのは、termsアグリゲーションではないでしょうか。

以下、JSアロー関数での擬似説明。

const aggByFieldVal = field => ({
    "aggs": {
        "vals": {
            "terms": { field }
        },
        /* ver 7系から対応(ver 6系では未対応) 
        "rareVals": {
            "rare_terms": { field, max_doc_count:1 }
        }
        */

    }
})

5. filters

バケットの分類条件を検索クエリで指定しようというのがfilters。

他のBucket Aggregations でもそのようなものがないわけではないのですが、あるドキュメントが、複数のバケット両方でカウントされるということも可能です。

const filters = {
    "size": 0,
    "aggs": {
        "messages": {
            "filters": {
                "filters": {
                    "overRank1": { "prefix": { "a.keyword": "a10" } },
                    "onlyOverRank2": { "prefix": { "a.keyword": "a1000" } }
                }
            }
        }
    }
}

6. adjacency_matrix

あらかじめ定義したカギとなる分類条件(検索クエリ風の記法・条件で指定)のうち、同時に当てはまったものの組み合わせ自体をバケット分類基準とするのがadjacency_matrix。 ただし、マトリックスという名のとおり、どれか一つに当てはまるもの、同時に2つの条件に当てはまるものに分類されるので、3つの条件を設定すると、最大で3 + (3 * 2 )/2 = 6バケットに分類されます。

前項のfiltersと似ていますね。

filtersだと無意味に難しくなるような例に対してもっと宣言的にバケット分類できると言えるような気がするが、そこそこリアルでかつメリットを示しやすい良い例が思い当たらないところが悩ましい。

注:以下の例は、最初にインポートした例とは噛み合っていません。(公式Rの例が一番わかりやすいと思います。)


// 全体クエリで示しています。
const adj = {
    "size": 0,
    "aggs": {
        "interactions": {
            "adjacency_matrix": {
                "filters": {
                    "grpA": { "terms": { "accounts": ["hillary", "sidney"] } },
                    "grpB": { "terms": { "accounts": ["donald", "mitt"] } },
                    "grpC": { "terms": { "accounts": ["vladimir", "nigel"] } }
                }
            }
        }
    }
}

7. filter

filtersではなくて、filterです。

クエリで絞り込まなくても、絞り込みしたバケットを形成できる。 下記のようにアパレルの売り上げ平均とTシャツのみの売り上げ平均を比べてみるといった例に便利そうですし、 aggsを組み合わせなくとも、単純にこの分類のみのバケットをみてみたい...という使い方もそれなりに使えそうですね。

形から入る本記事ではこの場所になりましたが、実際は、termsと同じぐらいよく使われるAggsで、ゆえに理解は難しくないように思います。

// 全体クエリで示しています。

const filter = field => ({
    "size":0,
    "aggs": {
        "avg_price": { "avg": { field } },
        "t_shirts": {
            "filter": { "term": { "type": "t-shirt" } },
            "aggs": {
                "avg_price": { "avg": { field } }
            }
        }
    }
})

8. sampler

samplerは、shard数を指定して(つまりある程度shardを絞って)、バケット分類のある種の精度を落としつつその分、レスポンス確保するためのヒント的なaggsです。

分類というよりは、比較的ヘビーな処理であるAggregationsを近似的に行うための命令と思って良いでしょう。

const sampler = field => ({
    "aggs": {
        "sample": {
            "sampler": {
                "shard_size": 10
            },
            "aggs": {
                // value_countはちょっと変な組み合わせだが、やってることが分かる。
                "vc": {
                    "value_count": {
                        field
                    }
                }
            }
        }
    }
})

9. significant_terms(とsignificant_text)

このブログでは何度かでてます、significant_terms(とsignificant_text)。

const sig = (filterField, field) => ({
    "aggs": {
        "siggrp": {
            "terms": { "field": filterField },
            "aggs": {
                "sig": {
                    "significant_terms": { field }
                }
            }
        }
    }
})

itdepends.hateblo.jp

10. 機会があれば(残りのもの一覧)

ver 7.13のX-packを除いたBucket Aggregationsとしては他に次のようなものがあります。 これらはまた別途。 (ここまでご紹介したものと、形はだいぶ異なる...ということだけお伝えしておきます。)

  • global
  • nested
  • reverse_nested