人間だったら考えて

なんでよ?考えて考えてっ 人間だったら考えて

RankSVMで得られたランクモデルをSolrにデプロイしてみる

この記事はランク学習(Learning to Rank) Advent Calendar 2018 - Adventarの14本目の記事です

この記事は何?

検索エンジンの有名どころとしてSolrが挙げられますが、Solrにはランク学習によるランキングモデルで文書をランキングする機能があります。

Learning To Rank | Apache Solr Reference Guide 7.6

この記事では、RankSVMを使ってランキングモデルを構築し、得られたランキングモデルをSolrにデプロイするまでを紹介します。


同じくSolr+ランク学習の導入として以下記事が参考になるので、合わせてご確認ください。
qiita.com




Solrの準備

この記事ではSolr7.6を使います。Apache Solr - Downloadsからダウンロードし、solrコマンドが打てるようにしておきます。
この記事では/opt以下にSolrディレクトリを配置したとして進めていきます。


この記事ではSolrのexamplesとして入ってくる"techproducts"を使います。
以下のコマンドでSolrを立ち上げます。

$ /opt/solr-7.6.0/bin/solr start -e techproducts -Dsolr.ltr.enabled=true

立ち上げたあとは、以下のcurlコマンドでSolrに入っている文書を確認できます。

$ curl "http://localhost:8983/solr/techproducts/select?q=*:*"
{
  "responseHeader":{
    "status":0,
    "QTime":0,
    "params":{
      "q":"*:*"}},
  "response":{"numFound":32,"start":0,"docs":[
      {
        "id":"GB18030TEST",
        "name":"Test with some GB18030 encoded characters",
...

特徴量の追加

Solrに入っている各文書に対して特徴量を追加します。

まず以下のような特徴量を表すjsonファイル(myFeatures.json)を作成します。
(公式のドキュメントと変えています。無意味?な特徴量が含まれていたので。。。)

[
  {
    "name" : "isElectronics",
    "class" : "org.apache.solr.ltr.feature.SolrFeature",
    "params" : {
      "fq": ["{!terms f=cat}electronics"]
    }
  },
  {
    "name" : "originalScore",
    "class" : "org.apache.solr.ltr.feature.OriginalScoreFeature",
    "params" : {}
  }
]

このjsonファイルには以下の2つの特徴量が記述されています。

  • isElectronics:catというフィールドに"electronics"が含まれているかどうかを返しています。
  • originalScore:Solrで文書をソートする際にデフォルトで用いられるスコアで、現在のバージョンではBM25(のはず)が使われます。


上で作成したjsonファイルを用いて、特徴量を登録します。

$ curl -XPUT 'http://localhost:8983/solr/techproducts/schema/feature-store' --data-binary "@myFeatures.json" -H 'Content-type:application/json'


flパラメータに"[features]"を付けて検索を投げることで、特徴量を取得することができます。

$ curl "http://localhost:8983/solr/techproducts/query?q=canon&fl=id,score,\[features\]"
{
  "responseHeader":{
    "status":0,
    "QTime":0,
    "params":{
      "q":"canon",
      "fl":"id,score,[features]"}},
  "response":{"numFound":2,"start":0,"maxScore":2.8250465,"docs":[
      {
        "id":"9885A004",
        "score":2.8250465,
        "[features]":"isElectronics=1.0,originalScore=2.8250465"},
      {
        "id":"0579B002",
        "score":2.3429825,
        "[features]":"isElectronics=1.0,originalScore=2.3429825"}]
  }}

というわけで、特徴量を追加できました。



学習データの作成

次に、ランキングモデルを構築するための学習データを作成します。
今回は以下の記事で紹介したsvm-rankを使って、RankSVMによるランキングモデルを作ってみます。

szdr.hatenablog.com

…といっても学習データのラベルはどうしますかね…今回は適当にクエリを投げて、それぞれ0/1を振ります。
これが実サービスであれば、ユーザーからのクリックログなどを使ってラベルを振ることができます。


Solrに"canon"・"test"・"hard"・"one"というクエリを投げて、返ってきた結果をsvm-rankが読み込める形式に変換します。
以下のスクリプトでチャチャッと学習データを作成します。

import random
import json
import requests


def docs2labels(docs):
    # ラベルはランダムに振る
    return [random.choice([0, 1]) for _ in docs]


def docs2feature_mat(docs, feature_names):
    feature_mat = []
    for doc in docs:
        f_dict = {}
        for f_name_val in doc["[features]"].split(","):
            f_name, f_val = f_name_val.split("=")
            f_dict[f_name] = f_val
        features = [f_dict[f_name] for f_name in feature_names]
        feature_mat.append(features)
    return feature_mat


if __name__ == '__main__':
    random.seed(4)

    queries = ["canon", "test", "hard", "one"]
    feature_names = ["isElectronics", "originalScore"]

    for qid, query in enumerate(queries):
        url = f"http://localhost:8983/solr/techproducts/query?q={query}&fl=[features]"
        r = requests.get(url)
        docs = r.json()["response"]["docs"]
        labels = docs2labels(docs)
        feature_mat = docs2feature_mat(docs, feature_names)
        for label, features in zip(labels, feature_mat):
            feature_line = " ".join([f"{i}:{val}" for i, val in enumerate(features, 1)])
            print(f"{label} qid:{qid} {feature_line}")

これを実行すると、以下のような学習データを出力します。

0 qid:0 1:1.0 2:2.8250465
1 qid:0 1:1.0 2:2.3429825
0 qid:1 1:0.0 2:2.1024432
1 qid:1 1:0.0 2:1.7054993
1 qid:2 1:1.0 2:3.2845886
0 qid:2 1:1.0 2:2.9977448
0 qid:3 1:0.0 2:2.0025148
0 qid:3 1:0.0 2:2.0025148
0 qid:3 1:0.0 2:1.9665208
1 qid:3 1:0.0 2:1.9665208
1 qid:3 1:1.0 2:0.9889032

特徴量1が上で定義した"isElectronics"に対応し、特徴量2が"originalScore"に対応します。



svm-rankによる学習

では、svm-rankで学習してみましょう。

$ /path/to/svm-rank/svm_rank_learn -c 3 train.dat

実行すると、"svm_struct_model"というモデルファイルが出力されます。
このファイル中に、以下のように重みパラメータを表す行があるはずです。

1 1:0.3393442 2:-0.83800596 #

"isElectronics"の重みが0.3393442・"originalScore"の重みが-0.83800596という結果が得られました。



モデルのデプロイ

以下のようにモデルを表すjsonファイル(myModel.json)を作成します。

{
  "class" : "org.apache.solr.ltr.model.LinearModel",
  "name" : "myModel",
  "features" : [
    { "name" : "isElectronics" },
    { "name" : "originalScore" }
  ],
  "params" : {
    "weights" : {
      "isElectronics" : 0.3393442,
      "originalScore" : -0.83800596
    }
  }
}

今回は線形のRankSVMを使ったので、classの部分にはLinearModelを設定します。
features/weightsは上で求めたものを指定します。


では、いよいよモデルをデプロイしてみます。

$ curl -XPUT 'http://localhost:8983/solr/techproducts/schema/model-store' --data-binary "@myModel.json" -H 'Content-type:application/json'

これで、RankSVMで得られたランクモデルでランキングできるようになりました!

http://localhost:8983/solr/techproducts/query?q=*:*&rq={!ltr%20model=myModel}のように、RerankQueryで"myModel"を指定すると、今回デプロイしたランクモデルでランキングしてくれます。

クエリ"canon"で、ランクモデル使わなかった場合

{
  "responseHeader":{
    "status":0,
    "QTime":0,
    "params":{
      "q":"canon",
      "fl":"id,score,[features]"}},
  "response":{"numFound":2,"start":0,"maxScore":2.8250465,"docs":[
      {
        "id":"9885A004",
        "score":2.8250465,
        "[features]":"isElectronics=1.0,originalScore=2.8250465"},
      {
        "id":"0579B002",
        "score":2.3429825,
        "[features]":"isElectronics=1.0,originalScore=2.3429825"}]
  }}

同じくクエリ"canon"で、ランクモデル使った場合

{
  "responseHeader":{
    "status":0,
    "QTime":1,
    "params":{
      "q":"canon",
      "fl":"id,score,[features]",
      "rq":"{!ltr model=myModel}"}},
  "response":{"numFound":2,"start":0,"maxScore":-1.624089,"docs":[
      {
        "id":"0579B002",
        "score":-1.624089,
        "[features]":"isElectronics=1.0,originalScore=2.3429825"},
      {
        "id":"9885A004",
        "score":-2.0280616,
        "[features]":"isElectronics=1.0,originalScore=2.8250465"}]
  }}

ちゃんとランクモデルが反映されてることが分かりますね!



まとめ

この記事では、RankSVMで得られたランクモデルをSolrにデプロイするまでの流れを紹介しました。

基本的にはSolrのランク学習ドキュメントに沿った内容でしたが、実際に手を動かしてみると、自分はSolrのことを全然分かってない。。。という気持ちになりました。

実運用してみると、ハマりどころとかボコボコ出てきそうですね。。。