Elasticsearchでネストされた子データのヒット件数で親データを並び替える


現場で、Elasticsearchのクエリ検証の機会があったのでアウトプットの意味を込めてメモ。

悩みポイント

悩みポイントは、ネストされた子データのヒット件数を使って親データを並び替えるというもの。ここでは大学の講義情報(親)と、講義を受ける学生(子)が存在する場合のクエリを例に挙げる。(講義:lectures、学生:studentsで記載。)

例えば、「田中」という学生の名前でヒットした学生の数を使用して、親データである講義を並び替えたい場合。

僕自身がまず初めに思い付いたのは、inner_hits内のtotal(ヒットした学生数)にアクセスして、並び替える手法でした。(クエリはあくまでイメージなのでご了承ください。)

クエリ(まずはinner_hitsでヒットした子データの件数をとるぞ〜):
GET lectures/_search
{
  "query": {
    "nested": {
      "path": "students",
      "query": {
        "bool": {
          "must": [
            {
               "match": { 
                 "students.name": "田中" 
               }
            }
          ]
        }
      },
      "inner_hits" : {}
    }
  }
}
検索結果:inner_hitsでヒットした学生数がとれたぞ〜
"hits": [
      {
        "_index": "lectures",
        "_type": "docs",
        "_id": "123456",
        "_score": 0.6360315,
        "_source": {
           // 「田中」にヒットする学生が受講している講義情報が返却される
        }
        "inner_hits": {
          "students": {
            "hits": {
              "total": 10, //「田中」にヒットする学生の件数(これを使いたい)
              "max_score": 0.5291085,
              "hits": [
                {
                  "_index": "lectures",
                  "_type": "docs",
                  "_id": "123456",
                  "_nested": {
                    "field": "students",
                    "offset": 1
                  },
                  "_score": 0.5291085,
                  "_source": {
                    "text": "田中太郎"
                  }
                },
                {
                  "_index": "lectures",
                  "_type": "docs",
                  "_id": "123456",
                  "_nested": {
                    "field": "students",
                    "offset": 1
                  },
                  "_score": 0.5291085,
                  "_source": {
                    "text": "田中太一"
                  }
                }
                ... 以下略
              ]
            }
          }
        }
      }
    ]

「これでinner_hits内のヒット件数(total)も取得できたし、後はtotalの値を使って親側からsortすればいけるんじゃね?」と安易に考えていたけれど、どうにも親要素(講義)からinner_hits内のtotalにはアクセスできないらしい。(親データの検索処理とinner_hitsの処理は別で分離されていて、アクセスが出来ないという記事をいくつか見かけました。もし出来た方がいれば教えてください。笑)

inner_hitsのtotal使えなかったらどうするよ?

絶望を感じながらも一旦考え方をリセットして悩み続けた結果、なんとか解決しました。
主に以下の記事を参考にしました。
Sort Parent Documents based on nested child Count in Elastic Search

具体的には、inner_hitsのtotalと同様の件数を親側で使用できるようにするため、studentsのキー直下に件数を表現するためのデータを保持させて、sortをかけようという方法。

studentsのデータ構造例:
students: [
   {
      "id": 1
      "name": "田中太郎",
      "phone_no": "111-1111-1111",
      "weight": 1 // ヒット件数を表現するためのキー
   },
   {
      "id": 2
      "name": "田中太一",
      "phone_no": "222-2222-2222",
      "weight": 1
   }
]

上記のようなキー(weight)を使うことで、ヒットした件数分の合計値を元に親データを並び替えることができる。

クエリ:
GET lectures/_search
{
  "query": {
    "nested": {
      "path": "students",
      "query": {
        "bool": {
          "must": [
            {
               "match": { 
                 "students.name": "田中" 
               }
            }
          ]
        }
      },
      "inner_hits" : {}
    }
  },
  "sort": [
     {
       "students.weight": {
         "mode": "sum", // ヒットした学生の合計
         "order": "desc", // ヒットした学生数の降順に講義(親)を並び替え
         "missing": "0", // ヒットしなかったデータは全て0件とする(カウントしない)
         "nested": {
            "path": "students",
            "filter": { // filter直下のクエリ情報はqueryの検索条件と同様
              "bool": {
                "must": [
                  {
                     "match": { 
                       "students.name": "田中" 
                     }
                  }
                ]
              }
            }
         }
       }
     }
   ]
}

これで仕様は満たせたので、ひとまず解決。