CLOVER🍀

That was when it all began.

ElasticsearchのAggregationsでファセットっぽいことをする

Elasticsearchでファセットをやりたかったら、Aggregationsというものを使用するらしいですね。要は集計処理らしいです。

Aggregations - ファセットよりも柔軟な集計 - @johtaniの日記 2nd

Aggregations | Elasticsearch Reference [6.4] | Elastic

ここは押さえていた方がよいと思い、早速チャレンジ。それにしても、たくさんAggregationsが用意されているんですね…。

とりあえず、Apache Solrでやっていたみたいな、フィールドの値によるファセット、レンジファセット、クエリによるファセットくらいはやってみようかなと。

準備

まずは、インデックスのマッピング定義。

{
  "settings": {
    "index": {
      "number_of_shards" : 5,
      "number_of_replicas" : 1,
      "analysis": {
        "tokenizer": {
          "kuromoji_tokenizer_search": {
            "type": "kuromoji_tokenizer",
            "mode": "search",
            "discard_punctuation" : "true",
            "user_dictionary" : "userdict_ja.txt"
          }
        },
        "analyzer": {
          "kuromoji_analyzer": {
            "type": "custom",
            "tokenizer": "kuromoji_tokenizer_search",
            "filter": ["kuromoji_baseform",
                       "kuromoji_part_of_speech",
                       "cjk_width",
                       "stop",
                       "ja_stop",
                       "kuromoji_stemmer",
                       "lowercase"]
          }
        }
      }
    }
  },
  "mappings": {
    "mytype": {
      "_source": { "enabled": true },
      "_all": { "enabled": true },
      "properties": {
        "isbn": { "type": "string", "store": "yes", "index": "not_analyzed" },
        "title": { "type": "string", "store": "yes", "index": "analyzed", "analyzer": "kuromoji_analyzer" },
        "price": { "type": "integer", "store": "yes", "index": "not_analyzed" },
        "publish_date": { "type": "string", "store": "yes", "index": "not_analyzed" },
        "author": { "type": "string", "store": "yes", "index": "analyzed", "analyzer": "kuromoji_analyzer" },
        "tag": { "type": "string", "store": "yes", "index": "not_analyzed" },
        "summary": { "type": "string", "store": "yes", "index": "analyzed", "analyzer": "kuromoji_analyzer" }
      }
    }
  }
}

テーマは書籍で、タグ(tag)と価格(price)でファセットを作ることを考えてみます。よって、これらは「not_analyzed」に設定しています。

データは、こういう状態になっているものとします。

$ curl 'http://localhost:9200/myindex/mytype/_search?pretty&q=*'
{
  "took" : 6,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 7,
    "max_score" : 1.0,
    "hits" : [ {
      "_index" : "myindex",
      "_type" : "mytype",
      "_id" : "AVLV31-iGY3NQuHOiPWM",
      "_score" : 1.0,
      "_source" : {
        "isbn" : "978-4048662024",
        "title" : "高速スケーラブル検索エンジン ElasticSearch Server",
        "price" : 2800.0,
        "publish_date" : "20140321",
        "author" : [ "Rafal Kuc", "Marek Rogozinski", "大岩 達也", "大谷 純", "兼山 元太", "水戸 祐介", "守谷 純之介", "株式会社リクルートテクノロジーズ" ],
        "tag" : [ "Java", "Lucene", "Elasticsearch", "全文検索" ]
      }
    }, {
      "_index" : "myindex",
      "_type" : "mytype",
      "_id" : "AVLV31-iGY3NQuHOiPWP",
      "_score" : 1.0,
      "_source" : {
        "isbn" : "978-4777518654",
        "title" : "はじめてのSpring Boot―「Spring Framework」で簡単Javaアプリ開発",
        "price" : 2500.0,
        "publish_date" : "20141101",
        "author" : [ "槇 俊明" ],
        "tag" : [ "Java", "Spring", "Spring Boot" ]
      }
    }, {
      "_index" : "myindex",
      "_type" : "mytype",
      "_id" : "AVLV31-iGY3NQuHOiPWN",
      "_score" : 1.0,
      "_source" : {
        "isbn" : "978-4774127804",
        "title" : "Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築",
        "price" : 3200.0,
        "publish_date" : "20060517",
        "author" : [ " 関口 宏司" ],
        "tag" : [ "Java", "Lucene", "全文検索" ]
      }
    }, {
      "_index" : "myindex",
      "_type" : "mytype",
      "_id" : "AVLV31-iGY3NQuHOiPWR",
      "_score" : 1.0,
      "_source" : {
        "isbn" : "978-4798131610",
        "title" : "実践ドメイン駆動設計",
        "price" : 5200.0,
        "publish_date" : "20150317",
        "author" : [ "ヴァーン・ヴァーノン", "高木 正弘" ],
        "tag" : [ "DDD" ]
      }
    }, {
      "_index" : "myindex",
      "_type" : "mytype",
      "_id" : "AVLV31-iGY3NQuHOiPWO",
      "_score" : 1.0,
      "_source" : {
        "isbn" : "978-4774127804",
        "title" : "Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava",
        "price" : 3200.0,
        "publish_date" : "20060517",
        "author" : [ "Antonio Goncalves", "日本オラクル株式会社", "株式会社プロシステムエルオーシー" ],
        "tag" : [ "Java", "Java EE" ]
      }
    }, {
      "_index" : "myindex",
      "_type" : "mytype",
      "_id" : "AVLV31-iGY3NQuHOiPWL",
      "_score" : 1.0,
      "_source" : {
        "isbn" : "978-4774161631",
        "title" : "[改訂新版] Apache Solr入門 〜オープンソース全文検索エンジン",
        "price" : 3600.0,
        "publish_date" : "20131129",
        "author" : [ "大谷 純", "阿部 慎一朗", "大須賀 稔", "北野 太郎", "鈴木 教嗣", "平賀 一昭", "株式会社リクルートテクノロジーズ" ],
        "tag" : [ "Java", "Lucene", "Solr", "全文検索" ]
      }
    }, {
      "_index" : "myindex",
      "_type" : "mytype",
      "_id" : "AVLV31-iGY3NQuHOiPWQ",
      "_score" : 1.0,
      "_source" : {
        "isbn" : "978-4798121963",
        "title" : "エリック・エヴァンスのドメイン駆動設計",
        "price" : 5200.0,
        "publish_date" : "20110409",
        "author" : [ "エリック・エヴァンス", "今関 剛", "和智 右桂", "牧野 祐子" ],
        "tag" : [ "DDD" ]
      }
    } ]
  }
}

Terms Aggregation

まずは、Terms Aggregationから。こちらで、フィールド(というかTermですが)ベースのファセットっぽものを作ることを考えてみます。

先ほどのインデックスに対して、こういうクエリを用意。「tag」に含まれるTermでAggregationsを作ります。

{
  "aggregations": {
    "tags": {
      "terms": {
        "field": "tag",
        "order" : { "_count" : "desc" }
      }
    }
  }
}

「aggregations」の部分は、「aggs」でもよいみたいですね。

このクエリをファイルに保存し、以降こんな感じで実行していきます。

$ curl -XGET 'http://localhost:9200/myindex/mytype/_search?pretty' -d [クエリを書いたファイル名]

結果。

{
  "took" : 47,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 7,
    "max_score" : 1.0,
    "hits" : [ {
      "_index" : "myindex",
      "_type" : "mytype",
      "_id" : "AVLV31-iGY3NQuHOiPWM",
      "_score" : 1.0,
      "_source" : {
        "isbn" : "978-4048662024",
        "title" : "高速スケーラブル検索エンジン ElasticSearch Server",
        "price" : 2800.0,
        "publish_date" : "20140321",
        "author" : [ "Rafal Kuc", "Marek Rogozinski", "大岩 達也", "大谷 純", "兼山 元太", "水戸 祐介", "守谷 純之介", "株式会社リクルートテクノロジーズ" ],
        "tag" : [ "Java", "Lucene", "Elasticsearch", "全文検索" ]
      }
    }, {
      "_index" : "myindex",
      "_type" : "mytype",
      "_id" : "AVLV31-iGY3NQuHOiPWP",
      "_score" : 1.0,
      "_source" : {
        "isbn" : "978-4777518654",
        "title" : "はじめてのSpring Boot―「Spring Framework」で簡単Javaアプリ開発",
        "price" : 2500.0,
        "publish_date" : "20141101",
        "author" : [ "槇 俊明" ],
        "tag" : [ "Java", "Spring", "Spring Boot" ]
      }
    }, {

〜省略〜

  },
  "aggregations" : {
    "tags" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [ {
        "key" : "Java",
        "doc_count" : 5
      }, {
        "key" : "Lucene",
        "doc_count" : 3
      }, {
        "key" : "全文検索",
        "doc_count" : 3
      }, {
        "key" : "DDD",
        "doc_count" : 2
      }, {
        "key" : "Elasticsearch",
        "doc_count" : 1
      }, {
        "key" : "Java EE",
        "doc_count" : 1
      }, {
        "key" : "Solr",
        "doc_count" : 1
      }, {
        "key" : "Spring",
        "doc_count" : 1
      }, {
        "key" : "Spring Boot",
        "doc_count" : 1
      } ]
    }
  }
}

通常の検索結果の後に、Aggregationsの結果が現れます。

多段にもできます。意味があるかどうかはさておき、「tag」の次に「price」でAggregationsを作ります。Pivot Facet的な感じですね。

{
  "aggregations": {
    "tags": {
      "terms": { "field": "tag" },
      "aggregations": {
        "prices": {
          "terms": { "field": "price" }
        }
      }
    }
  }
}

Aggregationsの結果部分。

  "aggregations" : {
    "tags" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [ {
        "key" : "Java",
        "doc_count" : 5,
        "prices" : {
          "doc_count_error_upper_bound" : 0,
          "sum_other_doc_count" : 0,
          "buckets" : [ {
            "key" : 3200,
            "doc_count" : 2
          }, {
            "key" : 2500,
            "doc_count" : 1
          }, {
            "key" : 2800,
            "doc_count" : 1
          }, {
            "key" : 3600,
            "doc_count" : 1
          } ]
        }
      }, {
        "key" : "Lucene",
        "doc_count" : 3,
        "prices" : {
          "doc_count_error_upper_bound" : 0,
          "sum_other_doc_count" : 0,
          "buckets" : [ {
            "key" : 2800,
            "doc_count" : 1
          }, {
            "key" : 3200,
            "doc_count" : 1
          }, {
            "key" : 3600,
            "doc_count" : 1
          } ]
        }
      }, {
        "key" : "全文検索",
        "doc_count" : 3,
        "prices" : {
          "doc_count_error_upper_bound" : 0,
          "sum_other_doc_count" : 0,
          "buckets" : [ {
            "key" : 2800,
            "doc_count" : 1
          }, {
            "key" : 3200,
            "doc_count" : 1
          }, {
            "key" : 3600,
            "doc_count" : 1
          } ]
        }
      }, {
        "key" : "DDD",
        "doc_count" : 2,
        "prices" : {
          "doc_count_error_upper_bound" : 0,
          "sum_other_doc_count" : 0,
          "buckets" : [ {
            "key" : 5200,
            "doc_count" : 2
          } ]
        }
      }, {
        "key" : "Elasticsearch",
        "doc_count" : 1,
        "prices" : {
          "doc_count_error_upper_bound" : 0,
          "sum_other_doc_count" : 0,
          "buckets" : [ {
            "key" : 2800,
            "doc_count" : 1
          } ]
        }
      }, {
        "key" : "Java EE",
        "doc_count" : 1,
        "prices" : {
          "doc_count_error_upper_bound" : 0,
          "sum_other_doc_count" : 0,
          "buckets" : [ {
            "key" : 3200,
            "doc_count" : 1
          } ]
        }
      }, {
        "key" : "Solr",
        "doc_count" : 1,
        "prices" : {
          "doc_count_error_upper_bound" : 0,
          "sum_other_doc_count" : 0,
          "buckets" : [ {
            "key" : 3600,
            "doc_count" : 1
          } ]
        }
      }, {
        "key" : "Spring",
        "doc_count" : 1,
        "prices" : {
          "doc_count_error_upper_bound" : 0,
          "sum_other_doc_count" : 0,
          "buckets" : [ {
            "key" : 2500,
            "doc_count" : 1
          } ]
        }
      }, {
        "key" : "Spring Boot",
        "doc_count" : 1,
        "prices" : {
          "doc_count_error_upper_bound" : 0,
          "sum_other_doc_count" : 0,
          "buckets" : [ {
            "key" : 2500,
            "doc_count" : 1
          } ]
        }
      } ]
    }
  }
}

Queryも一緒に使うことで、絞り込んだ結果からAggregationsを作ることもできます。

{
  "query": {
    "query_string": {
      "query": "price: >=3500"
    }
  },
  "aggregations": {
    "tags": {
      "terms": { "field": "tag" }
    }
  }
}

結果。

  "aggregations" : {
    "tags" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [ {
        "key" : "DDD",
        "doc_count" : 2
      }, {
        "key" : "Java",
        "doc_count" : 1
      }, {
        "key" : "Lucene",
        "doc_count" : 1
      }, {
        "key" : "Solr",
        "doc_count" : 1
      }, {
        "key" : "全文検索",
        "doc_count" : 1
      } ]
    }
  }
}

検索結果は絞り込みたいけれど、Aggregationsは絞り込みたくない場合は「post_filter」を使えばよいそうです。

Post filter | Elasticsearch Reference [6.4] | Elastic

RailsでElasticsearch: アグリゲーション(ファセット)と Post Filter - Rails Webook

ちょっといろいろやった感じですが、Term Aggregationsはここまで。

Range Aggregation

続いて、Range Aggregation。範囲を指定してAggregationsを作ります。

クエリは、こんな感じで用意。

{
  "aggregations": {
    "prices_range": {
      "range": {
        "field": "price",
        "ranges": [
          { "to": 3000 },
          { "from": 3000, "to": 4000 },
          { "from": 4000, "to": 5000 },
          { "from": 5000 }
        ]
      }
    }
  }
}

なお、

Note that this aggregation includes the from value and excludes the to value for each range.

https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-range-aggregation.html

だそうなので、fromの値は含み(include)、toの値は含まない(exclude)ということみたいです。

結果。

  "aggregations" : {
    "prices_range" : {
      "buckets" : [ {
        "key" : "*-3000.0",
        "to" : 3000.0,
        "to_as_string" : "3000.0",
        "doc_count" : 2
      }, {
        "key" : "3000.0-4000.0",
        "from" : 3000.0,
        "from_as_string" : "3000.0",
        "to" : 4000.0,
        "to_as_string" : "4000.0",
        "doc_count" : 3
      }, {
        "key" : "4000.0-5000.0",
        "from" : 4000.0,
        "from_as_string" : "4000.0",
        "to" : 5000.0,
        "to_as_string" : "5000.0",
        "doc_count" : 0
      }, {
        "key" : "5000.0-*",
        "from" : 5000.0,
        "from_as_string" : "5000.0",
        "doc_count" : 2
      } ]
    }
  }
}

Filter Aggregation/Filters Aggregation

最後は、Filter AggregationおよびFilters Aggregation。検索結果をフィルタリングして、Aggregationsを作成します。クエリを使ってファセットを作る、みたいな感じですね。

クエリとして、このようなものを用意。

{
  "aggregations": {
    "java_books": {
      "filter": {
        "query_string": {
          "query": "tag:Java"
        }
      }
    }
  }
}

ここでは、Query String Queryを使っています。

結果。

  "aggregations" : {
    "java_books" : {
      "doc_count" : 5
    }
  }
}

複数定義してもOKです。

{
  "aggregations": {
    "java_books": {
      "filter": {
        "query_string": {
          "query": "tag:Java"
        }
      }
    },
    "ddd_books": {
      "filter": {
        "query_string": {
          "query": "tag:DDD"
        }
      }
    }
  }
}

結果。

  "aggregations" : {
    "ddd_books" : {
      "doc_count" : 2
    },
    "java_books" : {
      "doc_count" : 5
    }
  }
}

でも、こうするくらいならFilters Aggregationsを使うのかもしれません。

{
  "aggregations": {
    "tags": {
      "filters": {
        "filters": {
          "java": {
            "query_string": {
              "query": "tag:Java"
            }
          },
          "ddd": {
            "query_string": {
              "query": "tag:DDD"
            }
          }
        }
      }
    }
  }
}

複数のfilterをまとめて定義できます。

結果。

  "aggregations" : {
    "tags" : {
      "buckets" : {
        "java" : {
          "doc_count" : 5
        },
        "ddd" : {
          "doc_count" : 2
        }
      }
    }
  }
}

まとめ

ElasticsearchのAggregationsのうち、Term Aggregations、Range Aggregations、Filter Aggregations/Filters Aggregationsを使って、ファセットっぽいことをやってみました。

けっこうとっつきにくかったりするのかな?と恐る恐る思っていたのですが、とりあえずこの範囲ならなんとかなりそうな気がしました。