Aggregations - ファセットよりも柔軟な集計

Posted by johtani on Wednesday, May 7, 2014

目次

こんなツイートを見つけたので、Aggregationのサンプルでも書こうかなと。(前から書こうと思ってたんですが。。。)

ちなみに、Aggregationは1.0.0から導入された機能なので、ElasticSearch Server日本語版には掲載されていない機能になります。(ごめんなさい)

公式ガイドのAggregationsのページはこちらになりますが、実例があったほうがいいかなと。

@yoshi_ken さんから実例のサンプルの指定もいただいたので、ブログを書くのが非常に楽です。ありがとうございます。

問題

元ネタ(gist)

次のような不動産系のデータがあるとします。

  • id
  • 物件名
  • 都道府県(東京、神奈川、…..)
  • 物件種別(賃貸、売買、…..)

この時、都道府県別に、物件種別ごとの件数を取得したいという趣旨です。

  • 東京
    • 賃貸: xxx件
    • 売買: yyy件
  • 神奈川
    • 賃貸: xxx件
    • 売買: yyy件 …

これを、Elasticsearchでどうやって取得するかという問題です。

インデックスとデータの登録

まずは、インデックスを作ります。 あくまでもサンプルなので、全部not_analyzedにしてますが、そのへんは適宜変更してください。

# create index
PUT /pref_aggs
{
  "settings": {
    "number_of_shards": 2
  },
  "mappings": {
    "japan" : {
      "_id" : {
        "path" : "id"
      },
      "properties": {
        "id": {"type": "string", "index": "not_analyzed"},
        "name": {"type": "string", "index": "not_analyzed"},
        "pref": {"type": "string", "index": "not_analyzed"},
        "type": {"type": "string", "index": "not_analyzed"}
      }
    }
  }
}

_idを使用して、データ登録時にidフィールドにある文字列をそのままIDとして登録できるように指定してあります。

登録するデータは次のようなものを適当に100件程度作ってりました。

{"id": "id0", "name": "name0", "pref": "01_北海道", "type": "売買"}
{"id": "id1", "name": "name1", "pref": "09_栃木県", "type": "売買"}
{"id": "id2", "name": "name2", "pref": "38_愛媛県", "type": "賃貸"}
{"id": "id3", "name": "name3", "pref": "40_福岡県", "type": "賃貸"}
{"id": "id4", "name": "name4", "pref": "35_山口県", "type": "売買"}
{"id": "id5", "name": "name5", "pref": "12_千葉県", "type": "賃貸"}
...

データの登録には、前に紹介した方法「stream2esと複数データの登録」を用いました。

ファセット

このようなデータがある場合に、まず思いつくのはファセットによる取得です。 いささか強引ですが。。。

GET /pref_aggs/japan/_search
{
  "size": 0,
  "query": {
    "match_all": {}
  },
  "facets": {
    "type_賃貸": {
      "terms": {
        "order": "term",
        "field": "pref",
        "size": 50
      }, "facet_filter": {"term": {"type": "賃貸" }}
    },
    "type_売買": {
      "terms": {
        "order": "term",
        "field": "pref",
        "size": 50
      }, "facet_filter": {"term": {"type": "売買" }}
    }

  }
}

facet_filterを使用して、typeフィールドによる個別の絞込を行っています。 あとは、prefフィールドのファセットを取得すれば、出力は次のようになります。

{
   "took": 6,
   "timed_out": false,
   "_shards": {
      "total": 2,
      "successful": 2,
      "failed": 0
   },
   "hits": {
      "total": 100,
      "max_score": 0,
      "hits": []
   },
   "facets": {
      "type_賃貸": {
         "_type": "terms",
         "missing": 0,
         "total": 52,
         "other": 0,
         "terms": [
            {
               "term": "00_北海道",
               "count": 1
            },
            {
               "term": "01_青森県",
               "count": 2
            },
            {
               "term": "03_宮城県",
               "count": 3
            },
            ...
      },
      "type_売買": {
         "_type": "terms",
         "missing": 0,
         "total": 48,
         "other": 0,
         "terms": [
            {
               "term": "00_北海道",
               "count": 2
            },
            {
               "term": "02_岩手県",
               "count": 1
            },
            {
               "term": "04_秋田県",
               "count": 1
            },
            ...
}

望んでいた形式とは少し異なりますが、facet_filterする回数を少なくするため、 ファセットは都道府県のフィールドを指定したためです。 アプリで頑張って入れ替えてください。。。

この場合、’type’の個数がわかっているので、頑張ってこのような記述ができました。 ただ、typeが増えた時にアプリの修正とかが必要になりますよね。

Aggregations

ということで、Aggregationsの出番です。 ファセットよりも柔軟に、検索結果に対していろいろな集計が行える機能になります。 一見に如かずということで、クエリを紹介します。

GET /pref_aggs/japan/_search
{
  "size": 0,
  "query": {
    "match_all": {}
  },
  "aggs": {
    "pref": {
      "terms": {
        "order": {
          "_term": "asc"
        },
        "field": "pref",
        "size": 50
      },
      "aggs": {
        "type": {
          "terms": {
            "field": "type",
            "size": 10
          }
        }
      }
    }
  }
}

ファセットよりもシンプルですし、賃貸といったような値を指定していません。 aggsというのがaggregations機能を指定している部分になります。 検索結果は次のように出力されます。

{
   "took": 4,
   "timed_out": false,
   "_shards": {
      "total": 2,
      "successful": 2,
      "failed": 0
   },
   "hits": {
      "total": 100,
      "max_score": 0,
      "hits": []
   },
   "aggregations": {
      "pref": {
         "buckets": [
            {
               "key": "00_北海道",
               "doc_count": 3,
               "type": {
                  "buckets": [
                     {
                        "key": "売買",
                        "doc_count": 2
                     },
                     {
                        "key": "賃貸",
                        "doc_count": 1
                     }
                  ]
               }
            },
            {
               "key": "01_青森県",
               "doc_count": 2,
               "type": {
                  "buckets": [
                     {
                        "key": "賃貸",
                        "doc_count": 2
                     }
                  ]
               }
            },
            ...

Aggregationsの結果は、望んでいた通りの出力になっています。

クエリの構成を見てみましょう。

"aggs": {
  "pref": { #1
    "terms": {
      "order": {
        "_term": "asc"
      },
      "field": "pref",
      "size": 50
    },
    "aggs": {  #2
      "type": {
        "terms": {
          "field": "type",
          "size": 10
        }
      }
    }
  }
}

最初の#1のprefは出力を扱いやすくするためにつけているラベルになります。好きな名前をつけることが可能です。 次のtermsがAggregationのタイプ(どのような集計をして欲しいか)になります。 今回は、prefフィールドにある単語(term)毎に、集計をしたいので、termsを指定します。 その他にどんなタイプがあるかは、公式ガイドをご覧ください。

次に、さらにtypeフィールドで集計したいので、#2の部分で後続のAggregationを指定しています。 都道府県同様、typeフィールドにある単語毎に集計するために、termsを指定します。

これで、先ほどのような結果が出力できます。 ちなみに、さらにtypeの中に他の種別で集計したいという場合は、さらにaggsを追加していけばOKです。

Aggregationは非常に柔軟な集計を可能にする機能です。ただし、検索結果に対して集計処理を行っているため、 メモリやCPUなどのリソースを消費するので注意が必要です。

Aggregationの説明については、こちらのFound.noのブログ(英語)がわかりやすかったので参考にしてみてください。

まとめ

非常に簡単ですが、Aggregationsについて紹介しました。 その他にもAggregationsでできることがあるので、後日別のサンプルを用意して説明しようかと思います。

100件のデータやここまでの操作については、gistにあるので、興味がある方はご覧いただければと。 stream2esの操作以外は、Marvelに付属のsenseを利用しています。


comments powered by Disqus

See Also by Hugo


Related by prelims-cli