この記事の内容
本記事では、Elasticsearchのscript query について、クエリ例を列挙しています。
script queryの使い所(と筆者が思うところ)
Elasticsearchは(全文)検索エンジン寄りのソフトですので、入力語に対して、各ドキュメントがマッチするのかという検索ユースケース中心で物事をみることが多いと思います。
ただ、もちろん、検索のモデルに、特に絞り込みにおいては、「洋室より和室の多いホテル」のようにそのドキュメントのフィールド間の関係が特定の関係を満たすものに絞り込みたいというケースもあります。
このような場合に、script queryが力を発揮します。
なお、script queryは、filterコンテキストのpainless scriptが記載できるみたいなので、その範囲でいろいろできそうです。
script query
実例
それでは実例です。
以下は、Elasticssearch6.8 での確認です。 上位バージョンでもエッセンスは変わらない範囲に絞ったつもりですが、7系がメジャーになった昨今では、オプションなど変わっている可能性があるのでご注意願います。
また、kibanaのDev Toolsへの貼り付けイメージで記載しています。
◆インデックスの設定
PUT /sqtest PUT /sqtest/_doc/_mapping { "properties": { "C": { "type": "keyword" }, "D": { "type": "keyword" } } }
◆サンプルデータ登録
POST sqtest/_doc/ { "A":100, "B":50, "C":"あああ", "D":"あああ"} POST sqtest/_doc/ { "A":100, "B":100, "C":"あああ", "D":"いいい"} POST sqtest/_doc/ { "A":100, "B":200, "C":"あああ", "D":""} POST sqtest/_doc/ { "A":100, "B":100, "C":"うううえ", "D":"えええ"} POST sqtest/_doc/ { "A":900, "B":900, "C":"1234567890", "D":""}
◆クエリ例
注: script queryは、filterコンテキストを基本とするので本来はfilterの中にリーフクエリDSL(この場合はscript)を記載するべきでしょうが、ここでは簡単のため、queryプロパティに直接ぶら下げています。
(1) Aが1より大きいもの
GET sqtest/_search { "query": { "script": { "script": { "source": "doc['A'].value > 1", "lang": "painless" } } } }
この使い方であれば、rangeクエリのシンプル版としても用いることができる。
要件によるが、性能やその他の条件ではなく、クエリの見栄えがビジネスルールやドメインをより適切に表しているかを重視して良いのであれば、rangeクエリよりもこちらの方がいい感じになることもある。 もちろん、そうでないこともある。
(2) AがBより大きいもの: ※複数フィールド間の条件
GET sqtest/_search { "query": { "script": { "script": { "source": "doc['A'].value > doc['B'].value", "lang": "painless" } } } }
例) 女性医師が男性医師より多い病院を検索(絞り込み)
【ひとりごと】クエリビルダー機能設計の観点でいうと、フィールド間の関係をElasticsearchのクエリの外に追い出ししやすくなるというメリットもあるかもしれない。
(3) AとBの文字列の内容が同じもの
GET sqtest/_search { "query": { "script": { "script": { "source": "doc['C'].value == doc['D'].value", "lang": "painless" } } } }
(4) Cの文字列の桁数が3より大きいもの
GET sqtest/_search { "query": { "script": { "script": { "source": "doc['C'].value.length() > 3", "lang": "painless" } } } }
(5) painless scriptなので、複合条件も記述できる
Cの文字列の長さが3より大きくかつ Aの値が100より大きい
または
Bは50以下
GET sqtest/_search { "query": { "script": { "script": { "source": """ ( doc['C'].value.length() > 3 && doc['A'].value > 100 ) || ( doc['B'].value <= 50 ) """, "lang": "painless" } } } }
(6) filterコンテキストなのでpost_filterにもかける
条件はひとつ前のものと同じなので特に目新しいものではないが、post_filterでも記述できるということであれば、ユースケースによっては少しトリックを仕込めるかもしれないという期待はある。
GET sqtest/_search { "query": { "match_all": {} }, "post_filter": { "script": { "script": { "source": """ ( doc['C'].value.length() > 3 && doc['A'].value > 100 ) || ( doc['B'].value <= 50 ) """, "lang": "painless" } } } }
(7) function_scoreでも使えた!
GET sqtest/_search?filter_path=**._score,**._source { "query": { "function_score": { "query": { "match_all": {} }, "functions": [ { "filter":{ "script": { "script": { "source": "doc['C'].value.length() > 3" , "lang": "painless" } }}, "weight" : 100 } ], "score_mode":"sum", "boost_mode":"sum" } } }
↓ 検索結果
{ "hits" : { "hits" : [ { "_score" : 101.0, "_source" : { "A" : 900, "B" : 900, "C" : "1234567890", "D" : "" } }, { "_score" : 101.0, "_source" : { "A" : 100, "B" : 100, "C" : "うううえ", "D" : "えええ" } }, { "_score" : 2.0, "_source" : { "A" : 100, "B" : 200, "C" : "あああ", "D" : "" } }, { "_score" : 2.0, "_source" : { "A" : 100, "B" : 50, "C" : "あああ", "D" : "あああ" } }, { "_score" : 2.0, "_source" : { "A" : 100, "B" : 100, "C" : "あああ", "D" : "いいい" } } ] } }