はてだBlog(仮称)

私的なブログど真ん中のつもりでしたが、気づけばWebサイト系のアプリケーション開発周りで感じたこと寄りの自分メモなどをつれづれ述べています。2020年6月現在、Elasticsearch、pandas、CMSなどに関する話題が多めです。...ですが、だんだんとより私的なプログラムのスニペット置き場になりつつあります。ブログで述べている内容は所属組織で販売している製品などに関するものではなく、また所属する組織の見解を代表するものではありません。

Elasticsearch script query によるフィールド間の関係による検索絞り込み

この記事の内容

本記事では、Elasticsearchのscript query について、クエリ例を列挙しています。

script queryの使い所(と筆者が思うところ)

Elasticsearchは(全文)検索エンジン寄りのソフトですので、入力語に対して、各ドキュメントがマッチするのかという検索ユースケース中心で物事をみることが多いと思います。

ただ、もちろん、検索のモデルに、特に絞り込みにおいては、「洋室より和室の多いホテル」のようにそのドキュメントのフィールド間の関係が特定の関係を満たすものに絞り込みたいというケースもあります。

このような場合に、script queryが力を発揮します。

f:id:azotar:20200127003739p:plain

なお、script queryは、filterコンテキストのpainless scriptが記載できるみたいなので、その範囲でいろいろできそうです。

script query

www.elastic.co

www.elastic.co

実例

それでは実例です。

以下は、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クエリのシンプル版としても用いることができる。

www.elastic.co

要件によるが、性能やその他の条件ではなく、クエリの見栄えがビジネスルールやドメインをより適切に表しているかを重視して良いのであれば、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" : "いいい"
        }
      }
    ]
  }
}