どうしてこの記事を書こうと思ったのか
こんにちは、株式会社CINC開発本部です。
最近よく業務で Amazon OpenSearch Service ( ElasticSearch ) を利用しています。
今回は、SQLのサブクエリのような絞り込みが bucket_selector を使って実現できました。
Amazon OpenSearch Service ( ElasticSearch ) について基本的なクエリに関する情報は、公式ドキュメントはもちろん、いくつかのサイトでも見つけられましたが、今回のような高度な使い方に関しては、具体的な情報があまり見当たりませんでした。
そこで、自分が試行錯誤しながら辿り着いた解決策を備忘録として残したいと思いました。 この情報が同じように悩んでいる方々の参考になればと思います。
概要
特定の条件で絞り込んだ対象について、その中でも「グループAとそれ以外」という様な排他的な集計を行うクエリを書く際に、RDBであればサブクエリを利用します。
Amazon OpenSearch Service ( ElasticSearch ) では、現時点でサブクエリを実現する機能が提供されていないのですが、今回 bucket_selector を使うことで相当のデータ取得を行いましたので、その実装方法を記載します。
事例
次のような構造のインデックスにデータが入っている時
- キーワードID(kw_id)
- ドメイン(host)
- 検索Vol(sv)
対象インデックスに対して
- 自社のみ : 自社のドメインは含むが、競合のドメインを含まないキーワード
- 競合のみ : 自社のドメインは含まないが、競合のドメインを含むキーワード
- 自社+競合 : 自社と競合のどちらのドメインも含むキーワード
という条件で抽出を行いたい場合に
RDBであればサブクエリを利用する事で、それぞれの条件での抽出を行う事が可能です。
それに対して、 Amazon OpenSearch Service ( ElasticSearch ) でどの様に抽出したかをまとめていきます。
RDBでの実現方法
下記の様なクエリで抽出を行っています。
SELECT t02.host , COUNT(t02.cnt_own) AS cnt_own , sum(t02.sv_own) AS sv_own FROM ( SELECT host, kw_id , max(cnt_own) AS cnt_own , max(sv_own) AS sv_own FROM ( SELECT host, kw_id, 1 AS cnt_own, sv AS sv_own FROM data_table t01 WHERE host = :host_own AND kw_id NOT IN ( SELECT DISTINCT kw_id FROM data_table WHERE host = :host_competitor_1 ) ) GROUP BY host, kw_id ) t02 GROUP BY t02.host
サブクエリが入れ子になっていますが、自社が持つキーワードの中から、競合のホストの情報を持つkw_idをNOT IN で除外する事で、自社のみが持つキーワードを対象としています。
その後 host, kw_idでGROUP BY
して自社のみが持つキーワードについての集計を行っています。
(競合についての集計を行う際には1, svとしている部分については、CASE文を利用して集計用のデータを作ることになります。)
OpenSearchでの実現方法
下記の様なクエリで対応しました。
(JSONにコメントは付けられないので、実行時には // の行は削除が必要です。)
{ "size": 0, "from": 0, "_source": false, "query": { "bool": { "filter": [ { // ① 全体の絞り込み "bool": { "minimum_should_match": 1, "should": [ { "match": { "host": "test1.com" } }, { "match": { "host": "test2.com" } } ] } } ] } }, "aggs": { "kw_id_terms": { "aggs": { // ② キーワード単位でのグルーピング "terms":{ "field": "kw_id", "size": 100 }, // ③ 自社の絞り込み "own_filter": { "filter": { "match": { "host": "test1.com" } } }, // ③ 競合の絞り込み "competitors_filter": { "filter": { "bool": { "minimum_should_match": 1, "should": [ { "match": { "host": "test2.com" } } ] } } }, // ④ キーワードの絞り込み "keyword_bucket_selector": { "bucket_selector": { "buckets_path": { "competitors": "competitors_filter>_count", "own": "own_filter>_count" }, "script": "params.own > 0 && params.competitors == 0" } }, "sv_max": { "max": { "field": "sv" } } } }, "sv_sum_bucket": { "sum_bucket": { "buckets_path": "kw_id_terms>sv_max" } } } }
以下で①〜④のポイントについて詳細に解説します。
①全体の絞り込み
"bool": { "filter": [ { "bool": { "minimum_should_match": 1, "should": [ { "match": { "host": "test1.com" } }, { "match": { "host": "test2.com" } } ] } } ] }
filter
の中でshould
を利用しtest1.com と test2.comのいずれかに一致している結果を対象としています。
minimum_should_match
に1を設定する事でいずれか一つ以上にヒットしているもののみが対象となります。
(minimum_should_match
はデフォルトでは0なので、指定しない場合に他のfilter条件があると、それにマッチしているだけで検索に引っかかってしまうためです。)
②キーワード単位でのグルーピング
"terms":{ "field": "kw_id", "size": 100 },
terms
でkw_id毎にバケットを作成しています。
(なお、例ではsize:100としているが、結果キーワード数が十分入るsizeにしておく必要があります。場合によってはパーティションを設定して複数回に分けての取得が必要です。)
③自社・競合の絞り込み
"own_filter": { "filter": { "match": { "host": "test1.com" } } }, "competitors_filter": { "filter": { "bool": { "minimum_should_match": 1, "should": [ { "match": { "host": "test2.com" } } ] } } },
terms
のバケット内に更に
を用意しています。
競合用の filter では 複数の競合を指定する事 もあるため should
を利用しています。
④キーワードの絞り込み
"keyword_bucket_selector": { "bucket_selector": { "buckets_path": { "competitors": "competitors_filter>_count", "own": "own_filter>_count" }, "script": "params.own > 0 && params.competitors == 0" } },
bucket_selector
を利用することで 指定した条件に合致するバケットのみ を対象とする事が可能です。
例では「 自社ドメインを含まない(競合ドメインのみ) のフィルタのドキュメント数が0で、且つ 自社ドメインのみのフィルタのドキュメント数 が0より大きいバケット」を対象としています。
これにより terms
では、自社のみドキュメントがあるキーワードだけがバケットに含まれる形になり、 sv_sum_bucket
では、条件を満たしたバケットの sv_max の合計を集計しているため 自社のみが含まれるキーワード のSV合計が算出できます。