Elasticsearchで任意のキーワードを含んだ集計を行う


全文検索エンジンとして名高いElasticsearchですが、昨今はログの蓄積先として利用されていることも多いように思われます。
今回そんなログの中で特定のワードを含む場合を集計したい、というニーズが発生しました。
その中でwildcard, filter, aggsをどのように組み合わせるのかについて調査したので、備忘録としてまとめます。

wildcardクエリとは

wildcardはsqlではlikeに相当するクエリで、

wildcard.json
GET /hoge/_search
{
  "query": {
    "wildcard": {
      "name": {
        "value": "ta*"
      }
    }
  }
}

like.sql
SELECT * FROM hoge WHERE name LIKE 'ta%'

が同義となるようなものです。

この際*の位置や個数はLIKE句の%同様に、任意の場所に対し挿入することが可能です。
また、*では0文字以上の文字列にヒットしますが、?を用いることで1文字にのみヒットさせることも可能となります。

パターン 該当する文字列
ta* ta, tao, taro, taros, ...
ta? tao, tas, ...

aggregationとfilterの組み合わせ

次に集計関数であるaggsfilterの組み合わせです。
aggsが2重に書かれていますが、こちらは公式のドキュメントにを参考に書いています。

aggs+filter.json
GET /hoge/_search
{
  "aggs": {
    "group_by_name": {
      "aggs": {
        "aggregation": {
          "terms": {
            "field": "name"
          }
        }
      },
      "filter": {
        "range": {
          "created_at": {
            "gte": 20190401,
            "lt": 20190501
          }
        }
      }
    }
  },
  "size": 0
}

ここで、検索パラメータにsize:0を指定していますが、これはaggs句を実行した際に、hitしたドキュメントが戻り値として与えられる為で、今回のケースでは集計結果のみ必要な為指定しています。

aggregation + filter + wildcard

今回の本題である、これら三つを組み合わせた集計です。

aggs+filter(wildcard).json
GET /hoge/_search
{
  "aggs": {
    "group_by_name": {
      "aggs": {
        "aggregation": {
          "terms": {
            "field": "name"
          }
        }
      },
      "filter": {
        "wildcard": {
          "name": {
            "value": "ta*"
          }
        }
      }
    }
  },
  "size": 0
}

これで、名前の前方がtaに一致するデータの集計を取ることができました。

おまけ: aggregation + filter(wildcard AND range)

また、これに先ほどのrange等のフィルタを加えたい場合は、

aggs+filter(wildcard&&range).json
GET /hoge/_search
{
  "aggs": {
    "group_by_name": {
      "aggs": {
        "aggregation": {
          "terms": {
            "field": "name"
          }
        }
      },
      "filter": {
        "bool": {
          "must":[
            {
              "wildcard": {
                "name": {
                  "value": "ta*"
                }
              }
            },
            {
              "range": {
                "created_at": {
                  "gte": 20190401,
                  "lt": 20190501
                }
              }
            }
          ]
        }
      }
    }
  },
  "size": 0
}

とすることで集計可能です。

さらに、複数区間rangeを用いて指定したい場合は

aggs+filter(wildcard&&(range1||range2)).json
GET /hoge/_search
{
  "aggs": {
    "group_by_timestamp": {
      "aggs": {
        "aggregation": {
          "terms": {
            "field": "name"
          }
        }
      },
      "filter": {
        "bool": {
          "must":[
            {
              "wildcard": {
                "name": {
                  "value": "ta*"
                }
              }
            },
            {
              "bool":{
                "should":[
                  {
                    "range": {
                      "created_at": {
                        "gte": 20190401,
                        "lt": 20190501
                      }
                    }
                  },
                  {
                    "range": {
                      "created_at": {
                        "gte": 20180401,
                        "lt": 20180501
                      }
                    }
                  }
                ]
              }
            }
          ]
        }
      }
    }
  },
  "size": 0
}