UGA Boxxx

つぶやきの延長のつもりで、知ったこと思ったこと書いてます

【Elasticsearch】Field Collapsingについて

トリバゴのように一つのホテルの価格を複数のサービスで比較をすることを考えるとき
検索結果はホテルをあるソート順(おすすめ順)で並べた上で、さらに各ホテルの中で比較したいサービスがあるソート順(価格低い順)に並んでいるようにしたい

これを実現するために、ElasticsearchのField Collapsing という機能があることを知ったので調べてみた

https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html#request-body-search-collapse

Field Collapsing

Field Collapsing は指定されたフィールドで検索結果をグルーピングする機能

Field Collapsingを使うためには、クエリに collapse 句を追加し、collapse 句の中でグルーピングのキーとなるフィールドを指定する

キーに指定するフィールドは、keywordnumeric である必要がある

次のクエリは「elasticsearch」という文言を含むつぶやきを ユーザー毎にグルーピングし、いいねの数で並べ替えている

GET /twitter/_search
{
    "query": {
        "match": {
            "message": "elasticsearch"
        }
    },
    "collapse" : {
        "field" : "user" 
    },
    "sort": ["likes"], 
    "from": 10 
}

このときの、検索結果総数(total)は「elasticsearch」とつぶやいたツイート数であり、重複しないユーザの数などはわからないので注意が必要

Field Collapsingの展開

inner_hits オプションをつけると検索結果の中の各グループに検索結果に含めることができる

次のクエリは「elasticsearch」という文言を含むつぶやきを ユーザー毎にグルーピングし、 さらに、グループごとに日付の昇順にソートした上位5件をlast_tweetsという名前で取得している

max_concurrent_group_searches で内部クエリを何並列に実行するかを指定する
この値はデフォルトではノード数とスレッドプールサイズによって自動的に決定される

GET /twitter/_search
{
    "query": {
        "match": {
            "message": "elasticsearch"
        }
    },
    "collapse" : {
        "field" : "user", 
        "inner_hits": {
            "name": "last_tweets", 
            "size": 5, 
            "sort": [{ "date": "asc" }] 
        },
        "max_concurrent_group_searches": 4 
    },
    "sort": ["likes"]
}

複数のinner_hits を取得することも可能

GET /twitter/_search
{
    "query": {
        "match": {
            "message": "elasticsearch"
        }
    },
    "collapse" : {
        "field" : "user", 
        "inner_hits": [
            {
                "name": "most_liked",  
                "size": 3,
                "sort": ["likes"]
            },
            {
                "name": "most_recent", 
                "size": 3,
                "sort": [{ "date": "asc" }]
            }
        ]
    },
    "sort": ["likes"]
}

各グループごとにリクエストを送信することになるため、inner_hitsが多すぎる場合は処理が大幅に遅くなる可能性があるので注意

第2レベルのField Collapsing

第2レベルのField Collapsingもサポートされている

次のクエリは「elasticsearch」という文言を含むつぶやきを 国ごとにグルーピングし、 さらに、各国のユーザごとのツイートをグルーピングしたものを取得している

GET /twitter/_search
{
    "query": {
        "match": {
            "message": "elasticsearch"
        }
    },
    "collapse" : {
        "field" : "country",
        "inner_hits" : {
            "name": "by_location",
            "collapse" : {"field" : "user"},
            "size": 3
        }
    }
}

検索結果

{
    ...
    "hits": [
        {
            "_index": "twitter",
            "_type": "_doc",
            "_id": "9",
            "_score": ...,
            "_source": {...},
            "fields": {"country": ["UK"]},
            "inner_hits":{
                "by_location": {
                    "hits": {
                       ...,
                       "hits": [
                          {
                            ...
                            "fields": {"user" : ["user124"]}
                          },
                          {
                            ...
                            "fields": {"user" : ["user589"]}
                          },
                          {
                            ...
                             "fields": {"user" : ["user001"]}
                          }
                       ]
                    }
                 }
            }
        },
        {
            "_index": "twitter",
            "_type": "_doc",
            "_id": "1",
            "_score": ..,
            "_source": {...},
            "fields": {"country": ["Canada"]},
            "inner_hits":{
                "by_location": {
                    "hits": {
                       ...,
                       "hits": [
                          {
                            ...
                            "fields": {"user" : ["user444"]}
                          },
                          {
                            ...
                            "fields": {"user" : ["user1111"]}
                          },
                          {
                            ...
                             "fields": {"user" : ["user999"]}
                          }
                       ]
                    }
                 }
            }

        },
        ....
    ]
}