TSUNAGU GROUP TECHNOLOGIES

TGT TechBlogTGT TechBlog

フロントエンドからバックエンドまでの技術ナレッジ

エラスティックサーチ DSL検索クエリ スコアリング計算


現在、shotworksでは、ElasticSearchという全文検索エンジンを使ってワークを表示しております。
そのElasticSearchを使って思うことでクエリが、見慣れないのもあり、わかりにくいものになっております。
なので、ここでは簡単なサンプルを紹介し、今後の開発のお役に立てればというのと自分の知識の整理として書きました。

一致検索

サンプルでは,CompanyNameがインディバルのものを検索しています。

curl -s http://localhost:9200/testindex/_search -XGET -d '
{
"query": {
"bool": {
"must": {
"match": {
"CompanyName": {
"query": "インディバル",
"type": "phrase"
}
}
}
}
}
}

指定の場所から指定の件数取得

fromが何件目から取得 サンプルだと2件目から
sizeが何件取得 サンプルだと3件取得となります
なので,CompanyNameがインディバルのもので2件目から3件取得します。
sizeの指定がない場合、デフォルトで10件返却されます。

curl -s http://localhost:9200/testindex/_search -XGET -d '
{
"query": {
"bool": {
"must": {
"match": {
"CompanyName": {
"query": "インディバル",
"type": "phrase"
}
}
}
}
},
"from" : "2",
"size" : "3"
}'

件数のみ取得したい場合

sizeを0にするとヒットした件数だけを返します。
下のサンプルでは、CompanyNameがインディバルのものが何件あるか見ています。

curl -s http://localhost:9200/testindex/_search -XGET -d '
{
"query": {
"bool": {
"must": {
"match": {
"CompanyName": {
"query": "インディバル",
"type": "phrase"
}
}
}
}
},
"size" : "0"
}'

指定のフィールドの値だけ取得したい
fields または _sourceを使う
下記ではともにqueryに引っかかったTestNameだけを取得する形となる

curl -s http://localhost:9200/testindex/_search -XGET -d '
{
"query": {
"bool": {
"must": {
"match": {
"TestId": {
"query": "999999999",
"type": "phrase"
}
}
}
}
},
"_source": {"TestName"},
"from" : "2",
"size" : "3"
}'
curl -s http://localhost:9200/testindex/_search -XGET -d '
{
"query": {
"bool": {
"must": {
"match": {
"TestId": {
"query": "999999999",
"type": "phrase"
}
}
}
}
},
"fields": "TestName",
"from" : "2",
"size" : "3"
}'

ソート検索

sort直下に並び替えたいフィールド名を書き、そのフィールドに
“order” : “desc” //降順
“order” : “asc” //昇順
などがある。

curl -s http://localhost:9200/testindex/_search -XGET -d '
{
"query": {
"bool": {
"must": {
"match": {
"TestId": {
"query": "999999999",
"type": "phrase"
}
}
}
}
},
"fields": "TestName",
"sort": {
"DateTime": {
"order" : "desc"
}
}
}'

平均の値の集計

aggsを使うことで,SQLでいうgroupbyのようなことが可能になります。
下記の例では、CompanyNameが企業名、salaryが給与とし
企業別の平均給与の集計を行っております。
aggs_companyとaggs_avg_salaryは任意の名前でいい場所となります。
なので必ずしもfieldにこの名前がある必要はありません。
返却される集計結果の値の名前として使われます。

curl -s http://localhost:9200/testindex/_search -XGET -d '
{
"size": 0,
"query": {
"match_all": {}
},
"aggs": {
"aggs_company": {
"terms": {
"field": "CompanyName"
},
"aggs": {
"aggs_avg_salary": {
"avg": {
"field": "salary"
}
}
}
}
}
}
'

alias設定状況確認

aliasなどの内容を確認する

curl -XGET localhost:9200/_aliases?pretty

_reindexによる現状のindexのコピー

TestIndexの構成などをNewTestIndexという別名のindexにそのままコピーする

curl -XPOST 'localhost:9200/_reindex?pretty' -d'
 {
 "source": {
 "index": "TestIndex"
 },
 "dest": {
 "index": "NewTestIndex"
 }
 }'

条件指定update

TestIdが1000の物に対して、TestDataを100に変更させる
SQLでいう
update testindex set TestData = 100 where TestId = 1000
みたいな処理となります。

curl -s http://localhost:9200/testindex/_update_by_query? -XPOST -d '
{
"script": { "inline": "ctx._source.TestData= 100"},
"query": {
"bool": {
"must": {
"match": {
"TestId": {
"query": "1000",
"type": "phrase"
}
}
}
}
}
}'

一定数未満のスコアを除外

min_scoreの値以下のスコアが返された場合は、query内の条件に引っかかってもヒットしません。
下記だと2以下のスコアになったものは取得できないことになります。
後述のスクリプトでのスコア計算を独自に扱うことをすることによって、より効果的に使える場面が増えます。

curl -s http://localhost:9200/testindex/_search -XGET -d '
{
"query": {
"bool": {
"must": {
"match": {
"CompanyName": {
"query": "インディバル",
"type": "phrase"
}
}
}
}
},
"min_score" : "2"
}'

数値型のフィールドをスコアに用いる

数値型のフィールドの値をそのままスコアとして使用できます。
下記のサンプルではNumberRateの値がそのままfuctionsのスコアになり
数値が多い順にヒットしていきます。

curl -s http://localhost:9200/testindex/_search -XGET -d '
{
"query": {
"function_score": {
"query": {
"match_all": {}
},
"functions": [
{
"field_value_factor": {
"field": "NumberRate"
}
}
]
}
}
}'

減少関数によるスコアリング

減少関数は、基準日、基準の数値、基準の位置に対して離れれば任意の値でスコアを減少させていく関数
0から1を返却する。
下記の項目設定を行う

項目説明
origin基準点となる(記述しない場合は現在の日時が入る)
offsetoriginの値から前後、offsetで定めた値までスコアを1返却する(デフォルトは0)
scaleoffsetで定めた値をすぎた際、減少させる範囲を決めるもの(日付であれば「1d,1h」など 位置であれば「1km」, 数値であれば「1」などになる)
decayscaleで定めた値を基準点からoffset+scale分ずれた時のスコアポイント

下記のサンプルのexpの場所は, gauss,exp,linの3つが選べる

項目説明
gaussガウス関数 (0か1付近では多少緩やかに減少するようになる)
exp指数関数 (離れるほど緩やかな減少率となる)
lin線形関数 1次関数 一直線の動きとなる
curl -s http://localhost:9200/testindex/_search -XGET -d '
{
  "query": {
    "function_score": {
      "query": {
        "match_all": {}
      },
      "functions": [
        {
          "exp": {
            "StartDate": {
              "origin": "2017-05-18",
              "scale": "5d",
              "offset": "1d",
              "decay": 0.6
            }
          }
        }
      ]
    }
  }
}'


上記の図のようなグラフとなリます。

スコアを独自に算出させる場合

数値型のフィールドに対して、script_scoreというものを用いれば独自の計算式を
使ってスコアを算出させることが可能です。
またサンプルのparamsはscript_scoreの演算内で使える定義値となります。
下記のサンプルは,queryで見つかった条件に対して、
hogeValueとhogeNumの値の掛け合わせに、param1という定義した20という値を掛け算した
結果をスコアとして返しております。
boost_modeについては後述します。

curl -s http://localhost:9200/testindex/_search -XGET -d '
{
  "query" : {
       "function_score" : {
        "query": {
          "match_all": {}
          },
        "boost_mode": "replace",
         "script_score": {
          "params": {
              "param1": 20
          },
           "script": "  return doc['hogeValue'].value*2*doc['hogeNum'].value* param1;"
         }
     }
  }
}'

下記のサンプルのような書き方もできるようです。
script_score内で過去の日付ほど、スコアを下げていく演算

curl -s http://localhost:9200/testindex/_search -XGET -d '
{
  "query" : {
       "function_score" : {
        "query": {
          "match_all": {}
          },
        "boost_mode": "replace",
        "score_mode": "multiply",
        "functions": [
            {
                "filter": {
                    "exists": {
                        "field": "date"
                    }
                },
                "script_score": {
                    "params": {
                        "now": 1409001061000
                     },
                    "script": "(5 / ((3.16*pow(10,-11)) * abs(now - doc['date'].date.getMillis()) + 0.2)) + 1.0"
                }
            }
        ],
     }
  }
}'

 boost_modeとscore_mode

boost_modeとscore_modeはスコア演算に使うものとなります。 boost_modeは、queryでの検索結果によるスコアとfuctions内のスコア合算計算の種類分け

minqueryのスコアとfunctionsのスコアの小さい方を結果として返す

boost_modeの種類説明
multiplyqueryのスコアとfunctionsのスコアの掛け算の結果をスコアとして返す (記述がない場合これがデフォルト設定になります)
replacequeryのスコアを破棄し、functionsのスコアのみを使用する
sumqueryのスコアとfunctionsのスコアを加算した結果をスコアとして返す
avgqueryのスコアとfunctionsのスコアの平均の結果をスコアとして返す
maxqueryのスコアとfunctionsのスコアの大きい方を結果として返す

score_modeは、functions内の各項目の計算方法決めです
minfunctions内の各項目のスコアの最小値を結果をスコアとして返す

score_modeの種類説明
multiplyfunctions内の各項目のスコアの掛け算の結果をスコアとして返す (記述がない場合これがデフォルト設定になります)
sumqueryのスコアを破棄し、functionsのスコアのみを使用する
avefunctions内の各項目のスコアの平均とした結果をスコアとして返す
first functions内の各項目の最初に計算されたものを返す
maxfunctions内の各項目のスコアの最大値を結果をスコアとして返す

複数のスクリプトを混ぜて演算


score_modeをmultiplyとし、functionsの
expとscript_scoreとfield_value_factorの結果を掛け算したスコアを返却する

curl -s http://localhost:9200/testindex/_search -XGET -d '
{
  "query": {
    "function_score": {
      "query": {
        "match_all": {}
      },
      "boost_mode": "replace",   //query直下のスコアを含めないでfunctionsだけのスコアを表示する
      "score_mode": "multiply",  //functions内の各項目の結果を掛け算でスコアに返す
      "functions": [
        {
          "exp": {
            "Date": {
              "origin": "2017-05-18",
              "scale": "5d",
              "offset": "1d",
              "decay": 0.6
            }
          },
          "script_score": {
              "script": " test = doc['TestValue'].value; hoge = doc['HogeValue'].value; if(test * hoge == 0){return 1}else{return test * hoge * 100000;} "
          },
          "field_value_factor": {
            "field": "NumberRate"
          }
        }
      ]
    }
  }
}'

SQLでクエリを叩く

https://github.com/NLPchina/elasticsearch-sql

上記のプラグインを入れることで,SQL形式にElasticSearchのクエリを投げることも可能です。
機能的にSQLだとできないこともありますが、入れることも検討してもいいものです。Elastic側でも、SQLでクエリを投げれるようにする検討をしているようです。今後のアップデートに期待です。

最後に

ElasticSearchのアップデートは早くこの前5系がでたと思えば

2017/5/9に6系のα版が発表されました。
https://www.elastic.co/blog/elasticsearch-6-0-0-alpha1-released

日々進化していくツールに対してついていくのは大変ですが、
手元にデータがあればそれを分析するツール、柔軟性を持った検索軸など
可能性がいくらでも広がるツールとなります。

これを読んだ方が少しでも、検索エンジンの導入、分析などにお役に立てれば幸いです。

執筆者プロフィール

インディバル入社4年目で主にショットワークス担当大学では紐の結ばれ方を研究するというマニアックなことをして過ごし、全く関係ないエンジニア職につき、今に至ります。