Adventarの4日目の記事です。検索システムを開発・運用していると、検索ランキングの精度を改善したくなります。(なりますよね?)ElasticsearchやSolrでは、検索ランキングで使われるデフォルトのスコアとして、検索クエリとドキュメントのフィールドとのマッチ度(BM25)が使われます。マッチ度だけでなく、ドキュメントの新しさや人気度を考慮することで、よりユーザーが求めているドキュメントを検索結果の上位に掲出できるでしょう。マッチ度・新しさ・人気度・...を考慮したスコアリングロジックを人手でコネコネ作ることもできますが、訓練データを用意することで、各要素を良い感じに考慮した機械学習によるランキングも実現できます。「複雑怪奇な検索クエリになっちゃった🥺」という沼に陥っていきます、つらいですね。...話は変わりますが、OSSの検索エンジンであるVespaを紹介します。AOL等50以上のテクノロジーとメディアを運営する子会社)からOSSとして公開された...という経緯があります。Vespaの特徴として「検索パフォーマンスがとても良い」という点があげられます。検索ランキング改善の視点からすると、「検索ランキングロジックを非常に柔軟に記述できる」というメリットがあります。この記事では、Vespaを使うとちょっとテクい検索ランキングを簡単に実現できますよ、ということを紹介したいと思います。検索ランキング改善に携わっている方に楽しんでいただけると嬉しいです。Startを利用して解説します。Startでは、以下の5つのアルバムに関するドキュメントをフィードします。jazz)に属するスコアが紐付いています。Vespaでは以下のような形式でスキーマを定義します。}本筋と離れてしまうため詳しくは説明しませんが、スキーマの詳細な説明についてはSchemasをご覧ください。こちらのチュートリアルに従うと、以下のように検索を行うことができます。}}Vespaにおけるランキングロジックの実装(rank-profile)Vespaにドキュメントをフィードできたので、次はランキングロジックを実装してみましょう。Vespaでは、rank-profileという形式でランキングロジックを実装します。year順にドキュメントをランキングするには、以下のように記述します。Vespaでは2段階のランキングを行える。今回は1段階のランキングで実装。Featuresをご覧ください。上記で定義したrank_yearでランキングするには、以下のようにリクエストします。}}relevanceの値がyearの値と等しくなっており、yearの大きい順にドキュメントがランキングされていることが分かります。Vespaにおける、検索クエリとフィールド間のマッチ度計算には、nativeRankという計算方法がデフォルトで利用されます。前節で「albumフィールドに'head'を含むドキュメントを取得」の例を紹介しましたが、検索クエリ'head'とalbumフィールドの間で計算されたnativeRankの値がrelevance=0.16343879032006287として出力されています。Referenceをご覧ください。sqrtをはじめとした数学関数、さらにif文なども利用できます。yearが2016以上ならyear+10を、2016未満ならsqrt(year)をスコアとして利用する、という無茶苦茶なランキングロジックは、以下のように記述できます。}}ちょっとテクい検索ランキング集ここからは、Vespaを使ってちょっとテクい検索ランキングをいくつか紹介していきます。検索結果をランダムに並び替える検索クエリにマッチしたドキュメントから、ランダムにドキュメントを掲出したい...というケースがあります。ないです、レコメンドならあるかな...Vespaではrandomという記述で、ドキュメントごとにランダム値を得ることができます。}もちろん、ランダム値をif文の条件などに使うことも可能です。検索時の時刻を利用して並び替えるドキュメントの新しさを考慮してランキングするために、検索時の時刻を利用したい...というケースがあります。フィールドに投稿時刻を格納しておき、検索時の時刻との差分を取ることで、ドキュメントの新しさを考慮できます。Vespaではnowという記述で、検索時の時刻をunixtimeで取得できます。以下の例では全てのドキュメントのスコアが検索時の時刻(unixtime)となる。}検索リクエスト時にランキングで利用する値を渡すドキュメントに付与されている情報だけでなく、検索リクエストに紐づく情報もランキングで利用したい...というケースがあります。ユーザーの年齢をランキングで考慮したい新規ユーザー・既存ユーザーでランキングを変更したいといったケースが考えられます。Vespaでは検索リクエスト時のパラメータをrank-profile内で利用する機能が実装されています。yearからユーザーの年齢を引いた値をスコアとして利用したい際には、以下のようにrank-profileを定義します。}検索時は、ranking.features.query(key)=valueを付与してリクエストします。}}もちろんif文の条件にも使えるので、ユーザーの属性によるランキングを出し分けなどにも応用できます。Ranking時代です。クエリベクトルとドキュメントベクトルの間で計算されるスコアをランキングで使いたくなります。Vespaではベクトルにまつわる計算を行うために、tensor型をサポートしています。}以下のドキュメントは、popに属するスコア=1、rockに属するスコア=0.2、jazzに属するスコア=0を表します。}}さて、ユーザーの各カテゴリへの興味度合いをランキングに反映してみましょう。ユーザーのpopへの興味が0.5、rockへの興味が0.3、jazzへの興味が0.2だとします。ドキュメントのスコアを、ユーザーの各カテゴリへの興味度合いと、アルバムが各カテゴリに属するスコアとの内積で定義します。rank-profileは以下のように定義します。}上で、検索リクエストにranking.features.query(key)=valueを付与することで、ランキングで利用する値を渡せることを紹介しました。同じように、検索リクエストにユーザーの各カテゴリへの興味度合いを渡してリクエストしてみます。}}tensor型を使いこなすと、様々なランキングロジックを実装できます。Factorizationなどでユーザーベクトル・ドキュメントベクトルを作成し、内積を計算なんかも実現できます。内積値をそのまま使うだけでなく、検索クエリとのマッチ度との和など、他スコアと組み合わせて使うこともできます。ランキングアイデアが膨らみますね!機械学習モデルでランキングするVespaは四則演算やif文をサポートしているので、その気になれば線形モデル・NeuralNetモデル・GBDTモデルを記述していくことができます...が、かなり面倒です。ここでは、LightGBMで構築したモデルをVespaのランキングで使う方法を紹介します。rank-profileは以下のように記述します。}LightGBMモデルは、以下のコードで適当に作ってみました。}}というわけで、LightGBMモデルを使ってスコア計算できました。LightGBMモデルのスコアをそのままランキングに使っても良いですが、ビジネスロジックなどを混ぜ込むことも可能です!まとめこの記事ではVespaの機能および、ちょっとテクい検索ランキングの実現方法を紹介しました。普段ElasticsearchやSolrを利用している方は、Vespaではとても柔軟にランキングロジックを記述できることに驚かれたのではないでしょうか。驚いてくださいVespaの機能はとても豊富なので、この記事では扱いきれていない機能もあります。この記事の例ではcatをキーとしたmap型のように定義していますが、配列型のように定義することも可能です。*2:これらのフレームワークに対応していれば、実用上はほぼ不便無いと思います。6はてなブログをはじめよう!sz_drさんは、はてなブログを使っています。ブログを報告する引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。