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を使って、ファセットっぽいことをやってみました。
けっこうとっつきにくかったりするのかな?と恐る恐る思っていたのですが、とりあえずこの範囲ならなんとかなりそうな気がしました。