影響度の測定
今回は影響度の測定のために、Ranking Evaluation APIを利用します。
Ranking Evaluation APIは、検索クエリのランキング結果に基づき、品質を測るためのものです。
概要については、昨年のアドベントカレンダーで記事にしていますのでご覧ください。
acro-engineer.hatenablog.com
本来、Ranking Evaluation APIは、正解となるランキングデータを事前に用意し、それに基づいてクエリを評価します。
本記事では、便宜上「変更前クエリの検索結果」を正解データと位置付け、クエリの変更がどの程度影響を及ぼすかを確認していきます。
ここからは、ElasticsearchへのリクエストはPythonを用いる想定で記述します。
※ 個人的にはKibanaのConsoleも好きですが、レスポンスを次のクエリに渡して実行する場合は辛いものがあります。
さて、変更前のクエリを実行してみます。
ここではIDの並び順のみ取得します。
res = requests.post(ES_HOST + '/wikidata/_search', data=json.dumps(body), headers=headers).json()
org_result_set = [k['_id'] for k in res['hits']['hits']]
org_result_set
レスポンスは次の通り(size=20に設定しています)
['GMVlHm8BSg9IjoHR5Dek',
'3tRtHm8BSg9IjoHROBB0',
'SMpoHm8BSg9IjoHRXBsO',
'9shnHm8BSg9IjoHRj4XA',
'_MRlHm8BSg9IjoHRVgLP',
'z8ZmHm8BSg9IjoHRrsBq',
'BsVlHm8BSg9IjoHR-2i-',
'GtBrHm8BSg9IjoHRhpPG',
'D8hnHm8BSg9IjoHRuc3Z',
'O9VtHm8BSg9IjoHRwSNt',
'l8hnHm8BSg9IjoHRuctX',
'HdBrHm8BSg9IjoHRe30U',
'7tBrHm8BSg9IjoHRjpin',
'4cpoHm8BSg9IjoHRpsTn',
'esVmHm8BSg9IjoHRO8xl',
'0MZmHm8BSg9IjoHRw_PP',
'e9VtHm8BSg9IjoHRzTZ2',
'JsdnHm8BSg9IjoHRAFPl',
'HcdnHm8BSg9IjoHRAFGB',
'kddvHm8BSg9IjoHRKJCY']
この結果を使って、Ranking Evaluationのリクエストを作成します。
まずはqueryを変更せずに実行してみます。
body = {
"requests": [
{
"id": "1",
"request": {
"query": {
"match": {
"text": "日本の電車"
}
}
},
"ratings": []
}
],
"metric": {
"dcg": {
"k" : 20,
"normalize": True
}
}
}
for id in org_result_set:
body['requests'][0]['ratings'].append({
'_index':'wikidata',
'_id':id,
'rating':1
})
requests.post(ES_HOST + '/wikidata/_rank_eval', data=json.dumps(body), headers=headers).json()
今回は比較のためのmetricとして、nDCGを利用することにします。
検索結果のランキングに全く差分がなければ、nDCGの値は1になります。
逆に言えば、 元の結果から大きく外れるほど0に近づくことになります。
それでは、クエリを変更してみましょう。
textフィールドへの検索をおこなうだけでなく、text.ngramも検索対象に入れてみます。
body = {
"requests": [
{
"id": "1",
"request": {
"query": {
"bool": {
"should": [
{
"match": {
"text": "日本の電車"
}
},
{
"match": {
"text.ngram": "日本の電車"
}
}
]
}
}
},
"ratings": []
}
],
"metric": {
"dcg": {
"k" : 20,
"normalize": True
}
}
}
for id in org_result_set:
body['requests'][0]['ratings'].append({
'_index':'wikidata',
'_id':id,
'rating':1
})
requests.post(ES_HOST + '/wikidata/_rank_eval', data=json.dumps(body), headers=headers).json()
レスポンス
{'metric_score': 0.1856246546622184,
'details': {'1': {'metric_score': 0.1856246546622184,
'unrated_docs': [{'_index': 'wikidata', '_id': 'n8tpHm8BSg9IjoHRJMSw'},
{'_index': 'wikidata', '_id': '7dNtHm8BSg9IjoHRB67v'},
{'_index': 'wikidata', '_id': 'ttNtHm8BSg9IjoHRB62I'},
{'_index': 'wikidata', '_id': 'otJsHm8BSg9IjoHRUjhV'},
{'_index': 'wikidata', '_id': 'kdNsHm8BSg9IjoHR-pG5'},
{'_index': 'wikidata', '_id': '-s5qHm8BSg9IjoHRpc46'},
{'_index': 'wikidata', '_id': 'yM5qHm8BSg9IjoHRrNzE'},
{'_index': 'wikidata', '_id': 'KcpoHm8BSg9IjoHRSQgb'},
{'_index': 'wikidata', '_id': 'pNZuHm8BSg9IjoHRlo2o'},
{'_index': 'wikidata', '_id': 's9ZuHm8BSg9IjoHRl46t'},
{'_index': 'wikidata', '_id': 'QMloHm8BSg9IjoHRFpFY'},
{'_index': 'wikidata', '_id': '1s1pHm8BSg9IjoHRyyBV'},
{'_index': 'wikidata', '_id': 'eM1qHm8BSg9IjoHRAIyh'},
{'_index': 'wikidata', '_id': '5stpHm8BSg9IjoHRHraG'},
{'_index': 'wikidata', '_id': 'FdBrHm8BSg9IjoHRhpPG'},
{'_index': 'wikidata', '_id': 'adFrHm8BSg9IjoHR6VuY'},
{'_index': 'wikidata', '_id': '9dFsHm8BSg9IjoHRAZiH'}],
'hits': [{'hit': {'_index': 'wikidata',
'_type': '_doc',
'_id': 'n8tpHm8BSg9IjoHRJMSw',
'_score': 29.578623},
'rating': None},
{'hit': {'_index': 'wikidata',
'_type': '_doc',
'_id': 'GtBrHm8BSg9IjoHRhpPG',
'_score': 29.015862},
'rating': 1},
{'hit': {'_index': 'wikidata',
'_type': '_doc',
'_id': '7dNtHm8BSg9IjoHRB67v',
'_score': 28.828218},
'rating': None},
{'hit': {'_index': 'wikidata',
'_type': '_doc',
'_id': 'ttNtHm8BSg9IjoHRB62I',
'_score': 28.828218},
'rating': None},
{'hit': {'_index': 'wikidata',
'_type': '_doc',
'_id': '7tBrHm8BSg9IjoHRjpin',
'_score': 28.740217},
'rating': 1},
{'hit': {'_index': 'wikidata',
'_type': '_doc',
'_id': 'otJsHm8BSg9IjoHRUjhV',
'_score': 28.483252},
'rating': None},
{'hit': {'_index': 'wikidata',
'_type': '_doc',
'_id': 'kdNsHm8BSg9IjoHR-pG5',
'_score': 28.478014},
'rating': None},
{'hit': {'_index': 'wikidata',
'_type': '_doc',
'_id': '-s5qHm8BSg9IjoHRpc46',
'_score': 28.416624},
'rating': None},
{'hit': {'_index': 'wikidata',
'_type': '_doc',
'_id': 'yM5qHm8BSg9IjoHRrNzE',
'_score': 28.312298},
'rating': None},
{'hit': {'_index': 'wikidata',
'_type': '_doc',
'_id': '9shnHm8BSg9IjoHRj4XA',
'_score': 28.297285},
'rating': 1},
{'hit': {'_index': 'wikidata',
'_type': '_doc',
'_id': 'KcpoHm8BSg9IjoHRSQgb',
'_score': 28.2964},
'rating': None},
{'hit': {'_index': 'wikidata',
'_type': '_doc',
'_id': 'pNZuHm8BSg9IjoHRlo2o',
'_score': 28.217278},
'rating': None},
{'hit': {'_index': 'wikidata',
'_type': '_doc',
'_id': 's9ZuHm8BSg9IjoHRl46t',
'_score': 28.217278},
'rating': None},
{'hit': {'_index': 'wikidata',
'_type': '_doc',
'_id': 'QMloHm8BSg9IjoHRFpFY',
'_score': 28.217278},
'rating': None},
{'hit': {'_index': 'wikidata',
'_type': '_doc',
'_id': '1s1pHm8BSg9IjoHRyyBV',
'_score': 28.157154},
'rating': None},
{'hit': {'_index': 'wikidata',
'_type': '_doc',
'_id': 'eM1qHm8BSg9IjoHRAIyh',
'_score': 28.04702},
'rating': None},
{'hit': {'_index': 'wikidata',
'_type': '_doc',
'_id': '5stpHm8BSg9IjoHRHraG',
'_score': 27.978518},
'rating': None},
{'hit': {'_index': 'wikidata',
'_type': '_doc',
'_id': 'FdBrHm8BSg9IjoHRhpPG',
'_score': 27.97674},
'rating': None},
{'hit': {'_index': 'wikidata',
'_type': '_doc',
'_id': 'adFrHm8BSg9IjoHR6VuY',
'_score': 27.943295},
'rating': None},
{'hit': {'_index': 'wikidata',
'_type': '_doc',
'_id': '9dFsHm8BSg9IjoHRAZiH',
'_score': 27.888338},
'rating': None}],
'metric_details': {'dcg': {'dcg': 1.3068473871238868,
'ideal_dcg': 7.040268381923511,
'normalized_dcg': 0.1856246546622184,
'unrated_docs': 17}}}},
'failures': {}}
nDCGの値が「0.1856246546622184」と、変更前の検索結果とかなりの差分があることがわかります。
nDCGの値が低くなったのは、検索ワード固有のものかもしれないので、リクエストを複数の検索ワードで発行し、平均を取るなどすると良いでしょう。
これによって、クエリ変更した際、検索結果にどの程度の影響が出るかを大まかに知ることができます。
「影響度がある程度大きい」かつ「オフラインで検索結果の改善が見られた」クエリを選んでABテストにかけるという判断基準のひとつとして有用かと思います。