🔍

nestedクエリにヒットした件数でソートしたい

2024/12/12に公開

はじめに

業務でnestedクエリを使用し、ヒットした件数で並び替える実装をしました。
SQLとは異なる部分で少し手間取ったので備忘録としてまとめます。

やりたいこと

以下のような親子関係を持つデータが存在するとします。

  • : 著者 (authors)
  • : 記事 (articles)

ここで、記事を検索し、ヒットした記事の件数で著者を並び替えたいとします。

SQLでは以下のようなクエリで実現できます。

SELECT authors.*, COUNT(articles.id) AS article_count
FROM authors
INNER JOIN articles
ON authors.id = articles.author_id
WHERE articles.title = "elasticsearch"
ORDER BY article_count DESC;

この記事では、これをElasticsearchで実現します。


検証用インデックスの準備

業務ではnestedフィールドを使用して親子関係を構築しました。
この記事でもauthorsインデックスにarticlesというnestedフィールドを持たせ、親子関係を再現します。

Kibanaにアクセスする

記事用の開発環境は以下の記事を参考に構築しました。
https://www.elastic.co/jp/blog/getting-started-with-the-elastic-stack-and-docker-compose

インデックスの作成

PUT authors
{
  "mappings": {
    "properties": {
      "articles": {
        "type": "nested"
      }
    }
  }
}

ドキュメントの追加

以下のように著者3人と、それぞれ異なる件数の記事を追加します。
また、articles内にcounterフィールドを持たせ、全ての値を1に設定します。
このcounterフィールドは、ヒットした件数で並び替える際に使用します。

POST authors/_bulk
{"index":{}}
{"name":"著者その1","description":"記事を1件投稿してます。","articles":[{"title":"記事その1","counter":1}]}
{"index":{}}
{"name":"著者その2","description":"記事を2件投稿してます。","articles":[{"title":"記事その1","counter":1},{"title":"記事その2","counter":1}]}
{"index":{}}
{"name":"著者その3","description":"記事を3件投稿してます。","articles":[{"title":"記事その1","counter":1},{"title":"記事その2","counter":1},{"title":"記事その3","counter":1}]}

検索の実行

sortクエリにnestedフィルターを設定し、ヒットした記事のcounterフィールドの合計値で降順ソートします。
これにより、ヒットした記事の件数で著者を並び替えます。

検索クエリ

GET authors/_search
{
  "query": {
    "nested": {
      "path": "articles",
      "inner_hits": {
        "_source": ["articles.title"]
      },
      "query": {
        "bool": {
          "filter": [
            {
              "terms": {
                "articles.title.keyword": ["記事その1", "記事その2", "記事その3"]
              }
            }
          ]
        }
      }
    }
  },
  "sort": [
    {
      "articles.counter": {
        "mode": "sum",
        "order": "desc",
        "nested": {
          "path": "articles",
          "filter": {
            "terms": {
              "articles.title.keyword": ["記事その1", "記事その2", "記事その3"]
            }
          }
        }
      }
    }
  ],
  "_source": ["name", "description"]
}

検索結果

※ 一部不要な箇所は省略

{
  "hits": {
    "total": { "value": 3 },
    "hits": [
      {
        "_source": {
          "name": "著者その3",
          "description": "記事を3件投稿してます。"
        },
        "sort": [3],
        "inner_hits": {
          "articles": {
            "hits": {
              "total": { "value": 3 },
              "hits": [
                { "_source": { "title": "記事その1" } },
                { "_source": { "title": "記事その2" } },
                { "_source": { "title": "記事その3" } }
              ]
            }
          }
        }
      },
      {
        "_source": {
          "name": "著者その2",
          "description": "記事を2件投稿してます。"
        },
        "sort": [2],
        "inner_hits": {
          "articles": {
            "hits": {
              "total": { "value": 2 },
              "hits": [
                { "_source": { "title": "記事その1" } },
                { "_source": { "title": "記事その2" } }
              ]
            }
          }
        }
      },
      {
        "_source": {
          "name": "著者その1",
          "description": "記事を1件投稿してます。"
        },
        "sort": [1],
        "inner_hits": {
          "articles": {
            "hits": {
              "total": { "value": 1 },
              "hits": [
                { "_source": { "title": "記事その1" } }
              ]
            }
          }
        }
      }
    ]
  }
}

クエリの条件を変更する

例えば、"記事その1""記事その3"に絞り込む場合、以下のようにクエリを変更します。

{
  "terms": {
-   "articles.title.keyword": ["記事その1", "記事その2", "記事その3"]
+   "articles.title.keyword": ["記事その1", "記事その3"]
  }
}

"記事その3"を持っているのは"著者その3"だけなので、"著者その3"が一番上に来ます。

検索結果
{
  "hits": {
    "total": { "value": 3 },
    "hits": [
      {
        "_source": {
          "name": "著者その3",
          "description": "記事を3件投稿してます。"
        },
        "sort": [2],
        "inner_hits": {
          "articles": {
            "hits": {
              "total": { "value": 2 },
              "hits": [
                { "_source": { "title": "記事その1" } },
                { "_source": { "title": "記事その3" } }
              ]
            }
          }
        }
      },
      {
        "sort": [1],
        "inner_hits": {
          "articles": {
            "hits": {
              "total": { "value": 1, },
              "hits": [
                { "_source": { "title": "記事その1" } }
              ]
            }
          }
        }
      },
      {
        "_source": {
          "name": "著者その2",
          "description": "記事を2件投稿してます。"
        },
        "sort": [1],
        "inner_hits": {
          "articles": {
            "hits": {
              "total": { "value": 1 },
              "hits": [
                { "_source": { "title": "記事その1" } }
              ]
            }
          }
        }
      }
    ]
  }
}

参考文献

https://www.elastic.co/guide/en/elasticsearch/reference/current/sort-search-results.html#nested-sorting

Discussion