検索基盤部の内田です。私たちは、約1年前よりヤフー株式会社と協力し、検索機能の改善に取り組んでいます。現在、ZOZOTOWNのおすすめ順検索に用いている、ランキング学習を利用した検索機能も、その取り組みの一部です。
本記事では、Elasticsearch上で、ランキング学習により構築した機械学習モデルを用いた検索を行うためのプラグイン「Elasticsearch Learning to Rank」の簡単な使い方を紹介します。また、このプラグインをZOZOTOWNに導入し、実際に運用して得られた知見をご紹介します。ランキング学習の話題性が世の中で増していますが、検索エンジンを絡めた情報はまだ世の中に少ない印象があります。そのため、本記事が皆さんの参考になれば幸いです。
ランキング学習のイメージ
ランキング学習(Learning to Rank, LTR)とは、機械学習の枠組みのひとつであり、情報検索におけるランキングを予測するモデルを構築する手法です。イメージとしては「機械学習で、入力されたパラメータに基づいて賢く候補を並び替えるもの」と言えます。
Learning to Rankプラグインの使い方
ZOZOTOWNの検索システムは、Elasticsearchを利用して構築しています。弊社のElasticsearchに関する取り組みは、過去の記事でも紹介しているので、併せてご覧ください。
Elasticsearchには、ランキング学習で構築したモデルを利用して検索する機能がデフォルトでは用意されていません。Learning to Rankプラグイン(以下、LTRプラグイン)の利用は、その機能を導入するための比較的低コストな手段の1つです。本章では、LTRプラグインを使った検索の実行方法を簡単に紹介します。なお、Elasticsearchへのプラグインの導入方法は解説しません。プラグインの導入に関する情報はプラグインのREADMEやElasticsearchのドキュメントを参考ください。
LTRプラグインを用いて、ランキング学習で構築したモデルを利用した検索を実現するには、下記の3つの手順を踏む必要があります。
- Elasticsearchで計算する特徴量セットを定義する
- 機械学習モデルを構築してElasticsearchにアップロードする
- アップロードしたモデルを検索クエリで指定して検索する
なお、LTRプラグインを初めて利用する際は、feature store
の有効化が必要です。feature store
は、特徴量セットやモデルに関するメタデータを格納するインデックスに相当します。
PUT _ltr
1. Elasticsearchで計算する特徴量セットを定義する
本記事では、概要を説明します。詳細を確認したい場合は、公式ドキュメントを参照ください。
elasticsearch-learning-to-rank.readthedocs.io
機械学習モデルを用いたスコアリングでは、Elasticsearchが各特徴量の値を計算します。ただし、それらの特徴量の定義は、事前にElasticsearchへ登録しておく必要があります。そして、その特徴量の定義方法は、以下の3種類の方法が存在します。
mustache
- テンプレート言語mustacheを交えたElasticsearchのクエリDSLで記述する方法
derived_expression
- Luceneの式で記述する方法
- 基本的な演算子や算術関数が利用可能で、他の特徴量を参照する簡単な特徴量も定義可能
- なお、LTRプラグインの公式ドキュメントには
derived_expressions
と書かれている箇所があるが、正しくはderived_expression
script_feature
- Elasticsearchのスクリプト言語(Painlessなど)で記述する方法
- 複雑な特徴量を定義可能
上記の選択肢にある、script_feature
による特徴量定義は、検索の実行パフォーマンスを低下させる可能性が高いため、可能な限り避けることをおすすめします。基本的にはmustache
で特徴量を定義し、mustache
で定義した特徴量の値を参照する特徴量がある場合はderived_expression
の記法で定義します。
例えば、検索対象となる商品が「商品名(title
)」と「人気度(popularity
)」を持つとします。
PUT my_index { "mappings": { "properties": { "title": { "type": "text" }, "popularity": { "type": "integer" } } } }
ここで、下記の特徴量ベクトルを定義するとします。
- 商品名と検索キーワードのマッチ度
- 人気度の値
- それらの積
この場合、以下のように記述できます。
POST _ltr/_featureset/my_featureset { "featureset": { "features": [ { "name": "title_relevance", "params": ["keyword"], "template_language": "mustache", "template": { "match": { "title": "{{keyword}}" } } }, { "name": "popularity_value", "params": [], "template_language": "mustache", "template": { "function_score": { "query": { "match_all": {} }, "field_value_factor": { "field": "popularity", "missing": 0 } } } }, { "name": "product_of_relevance_and_popularity", "params": [], "template_language": "derived_expression", "template": "title_relevance * popularity_value" } ] } }
2. 機械学習モデルを構築してElasticsearchにアップロードする
elasticsearch-learning-to-rank.readthedocs.io
特徴量が定義できたら、機械学習でモデルを構築し、そのモデルをアップロードします。LTRプラグイン自体には、機械学習の機能は備わっていないため、モデルの学習には外部の機械学習ライブラリを利用する必要があります。なお、本記事の執筆時点でLTRプラグインが対応している機械学習ライブラリはRanklibとXGBoostの2つです。
POST _ltr/_featureset/my_featureset/_createmodel { "model": { "name": "my_model", "model": { "type": "model/xgboost+json", "definition": "(機械学習ライブラリを使って構築し保存したモデル)" } } }
なお、学習に用いるデータは、サービスで収集した実際のユーザの行動ログを利用するのが良いです。また、特徴量ベクトルの生成には、LTRライブラリの特徴量ログ出力機能が役に立つ場合があるので、確認しておくと良いでしょう。
3. アップロードしたモデルを検索クエリで指定して検索する
elasticsearch-learning-to-rank.readthedocs.io
前述までの手順で、機械学習モデルを用いた検索システムを動かす準備は整いました。しかし、1つ大きな問題点があります。機械学習モデルを用いたスコア計算は計算コストが高いという問題です。もし、検索クエリに一致した商品(ドキュメント)全てに対して機械学習モデルを適用しスコアを算出した場合、レスポンスタイムの悪化などを招きます。その結果、検索の総合的なパフォーマンスを劣化させる可能性が高まります。
そのため、機械学習モデルを検索に取り入れる際には、基本的にリランキングを行います。簡単なクエリで計算したスコアを用いて一次的なランキングを行い、それらの上位N件に対して機械学習モデルでスコアを再計算し並び替え、最終的な検索結果を出力します。リランキング対象とする件数は、Elasticsearchクラスタのマシンリソースなどを考慮し、検索の精度向上とスループットやレスポンスタイム悪化のトレードオフを見ながら許容できる範囲で調整します。
GET my_index/_search { "query": { // 簡単なクエリで絞り込みと一次ランキング "match": { "title": "ジャケット" } }, "rescore": { // 機械学習モデルでのリランキング "window_size": 1000, // 一次ランキングの上位1000件をリランキングする "query": { "rescore_query": { "sltr": { "params": { "keyword": "ジャケット" }, "model": "my_model" } } } } }
実運用から得られた知見と注意点
本章では、LTRプラグインをサービスに適用し、実運用から得られた知見や注意点を紹介します。LTRプラグインの導入に迷われている方、困っている方の参考になれば幸いです。なお、運用する中で独自に行った改修のいくつかは、LTRプラグインへコミットし、コミュニティに還元しています。
レスポンスタイムに悪影響を与える設定項目の調査
前述の通り、機械学習モデルによるリランキングを行うため、レスポンスタイムの悪化が想定されました。そのため、事前に負荷試験を実施し、レスポンスタイムに影響を与える設定項目の発見、及び一定のスループットを保てるラインを調査しました。
その調査の結果、想定していた通り、リランキングによりレスポンスタイムが悪化することが分かりました。また、モデルの特徴量数・複雑度やリランキング対象を操作することで、悪化の程度を調整できることも分かりました。
調査結果の概要は以下の通りです。
- モデルを構成する特徴量数の調整
- モデルを構成する特徴量の数を増やすと、レスポンスタイムが悪化することが確認された
- モデルの複雑度の調整
- モデルを構成する決定木の数やそれらの高さをコントロールしたところ、モデルが複雑になるとレスポンスタイムが悪化することが確認された
- 特徴量セットで定義する特徴量数の調整
- レスポンスタイムに変化はなかった
- 特徴量セットに定義された特徴量は毎回全て計算されるのではなく、モデルで参照される特徴量のみが都度計算されるように見受けられる
- リランキング対象数の調整
- 対象数を増やすと、レスポンスタイムが悪化することが確認された
特徴量が負の値を持つ場合の注意点
Elasticsearchは、クエリを実行して得られるスコアに負の値を許容しません。そのため、前述の特徴量セットの定義で紹介した以下のクエリでは、対応するフィールドの値を参照しているため、popularity
フィールドが負の値を持つ場合にエラーとなります。これは、Luceneの仕様によるものです。クエリ側で回避することが難しいため、特徴量セットを定義する際や、学習用のデータを作る際には負の値の扱いに注意が必要です。
{ "name": "popularity_value", "params": [], "template_language": "mustache", "template": { "function_score": { "query": { "match_all": {} }, "field_value_factor": { "field": "popularity", "missing": 0 } } } }
クライアントライブラリが存在しない
LTRプラグインが提供する拡張DSL("sltr"
など)に対応したクライアントライブラリが存在しません。LTRプラグインの機能を利用するには、クライアントを利用せずにクエリを構築しAPIにリクエストを投げるか、Elasticsearchのクライアントを独自に拡張DSLに対応させる必要があります。
弊社では、LTRプラグインのソースコードに含まれるQueryBuilder系のコードを参考にし、クライアントライブラリを拡張しています。
プラグインのバージョン制約
基本的に、Elasticsearchのプラグインは、使用しているElasticsearchのバージョンに合わせてビルドされたパッケージをインストールする必要があります。
しかし、LTRプラグインは、自身のバージョンアップがあっても、Elasticsearchの旧バージョン向けにはパッケージの配布が行われません。基本的に最新の機能や修正は、Elasticsearchの最新バージョンでしか利用できないという問題が発生しています。
例えば、LTRプラグインが更新された時点で、最新のElasticsearchのバージョンが7.16.2だったとしましょう。この場合、バージョン7.16.1以前のElasticsearchでは、LTRプラグインの最新バージョンで追加された機能を使ったり、バグの修正を適用することができません。
弊社では、LTRプラグインのリポジトリをクローンし、本家で新機能がリリースされた場合は、利用しているElasticsearchのバージョンに合わせて独自にプラグインをビルドし運用しています。
まとめ
本記事では、ElasticsearchのLTRプラグインの簡単な使い方と、運用を通して得られた知見や注意点を紹介しました。今後も継続的に改善していきますので、是非ZOZOTOWNの検索機能をお試しください。
さいごに、ZOZOでは検索エンジニア・MLエンジニア・サーバサイドエンジニアのメンバーを募集しています。検索機能の改善にご興味のある方は、以下のリンクからご応募ください。
また、本記事で紹介した施策にご協力いただいたヤフー株式会社の皆さんに改めて感謝いたします。