2015年10月17日土曜日

SolrCloud + 機械学習ランキングエンジン = R&R



【目的】
IBMがBluemix上で提供しているRetrieve and Rank(R&R)を利用し、使い方を覚えることを目的とする。
R&RとはオープンソースのApache SolrにIBMが独自に組み込んだ教師データを元にする機械学習エンジンを組み込んだ検索エンジン基盤である。
教師データを基に学習させることで検索結果のランキングをより有用性の高い順位付けにすることができる。
solrの基本はこちらを参考にしてほしい。

R&Rの名前の由来だが、通常のsolrとしての役割であるRetrieveと、機械学習を使ったRankを組み合わせたということだろう。

R&Rはまた別の見方もできるだろうか。
機械学習をさせなければ、そのままSolrとして使える。
Solrを分散化させるSolrCloudのインフラとプラットフォームを自前で用意しなくてもよいことを意味する。
IBMのクラウドをSolrのIaaS、PaaSとして使う用途にしてもいいかもしれない。



使い方を見る前に、少し脱線。
【Natural Language Classifier(NLC)との違い】
IBMのwatsonサービス群で同じようなサービスとしてNLCというものがあった。
※参考:NLCを利用したアプリ

どちらも同じ分類に対処するサービスだが、用途や学習の仕組みが違う。
(NCL)
文章のベクトル空間を作っていることで以下が実現できる。
 用途: 分類
 入力: 質問
 出力: 意図

(R&R)
質問と文章のベクトル空間を作っていることで以下が実現できる。
 用途: 検索のランク付け
 入力: 質問と回答
 出力: 関連度

NCLでFat Headな分類をした後にR&RでLong Taleな稀な種類の分類をする、という使い分けがいいだろうか。



【solrの予備知識】
R&Rの使い方を覚える前にsolrの一般的なことを確認しておく。
R&Rではclusterとcollectionまでを触れる自由度があるため、最低限ここの知識が必須となる。

R&Rのバックエンドにいるのはsolrではなく、SolrCloudである。
SolrCloudとは検索エンジンであるApache Solrのzookeeperを利用した拡張である。
勘違いがないよう補足するが、もともとSolrでも複数のノードで分散構成をとらせ、物理的、論理的に離れた位置にあるインデックス(シャード)から検索結果を得ることができる。
設定情報の一元化、スケーラビリティ、障害耐性などをzookeeperに任せることで管理の容易さをはかったものがSolrCloudである。


次に、用語の意味をまとめておく。
Solrでも一般的に使われるSolrCloudに限った話ではない。

cluster    : コレクションを管理するSolrCloudのインスタンス。

collection : 検索対象の論理的範囲を決める。
             schemaの設定ファイルはこの単位に適用される。

shard      : collectionを論理的に分割したセクション。

node       : 1つのJVMインスタンスで起動されるsolr。
           通常1サーバ内で1つだが、プロセスを分ければ複数起動可能。

core       : RDBのスキーマに相当。
             コアごとにスキーマ定義やクエリの設定を持つことが可能。


分散構成例を見たほうが理解が進むだろうか。
上位レイヤーから論理的分散させた図を書くとすると下図の関係になる。

この例では6台の物理サーバを使うことを想定する。
各論理nodeを物理境界としてのnodeとし、
若番号順3台で同じshardを持たせる(必然的にcoreも同一)。
それぞれを更新リーダとレプリカの関係にさせ、
結果2つのshard構成になっている。

cluster1
   |
collection1
   |
shard1(leader)     shard1(replica1)      shard1(replica2)
  on node1_core1     on node2_core1        on node3_core1

shard2(leader)     shard2(replica1)      shard2(replica2)
  on node4_core2     on node5_core2        on node6_core2


スケール時は次を頭に入れておくとよい。

上の図で言えば、
横軸:レプリカを増やすことで同時に処理できる検索リクエストの改善
   耐障害性の向上

縦軸:シャードを増やすことでインデクシングのパフォーマンス、データ量の増大対応
   検索における応答速度の改善

また、検索、更新ともに1つのnodeあたりのcore数は少なくした方がいい。
マルチコアさせるメリットは、ステージング環境を本番環境として入れ替えるために利用したり、または言語別にインデックスを分けて利用する場合などにあるだろう。
同一サーバ、同一アプリケーションでしか動作させられないため、可用性、バックアップ(スタンバイ)目的として増やすべきではない。

nodeを増やせば検索性能はあがるが更新性能はnodeが増えると遅くなる。
更新性能をあげるためにはshardを分けていくこととなる。

ただ、R&Rで操作できるのはcluster、collection部分だけであり、それは以下のレイヤは考慮不要である。



【操作手順】
特定の言語によらない汎用性を持たせるためcurlコマンドをシェルから実行する例を残しておく。

username、passwordはbluemixで独自に取得したものを利用すること。
url名は長いため、ベース部分を変数としておく。
# username="***"
# password="***"
# url="https://gateway.watsonplatform.net/retrieve-and-rank/api"

◆ cluster操作
(cluster情報の取得)
# curl -X GET -u "${username}":"${password}" ${url}/v1/solr_clusters | jq .


(clusterの作成)
# curl -X POST -u "${username}":"${password}" \
-H "Content-Type: application/json" \
-d "{\"cluster_size\":\"1\",\"cluster_name\":\"My cluster\"}" \
"${url}/v1/solr_clusters" | jq .

ステータスがREADYになるまで利用はできない。数分待つ必要がある。
  "solr_cluster_status": "NOT_AVAILABLE" ⇒ "READY"


(特定のclusterの詳細情報の取得)
# solr_cluster_id="***"

# curl -X GET -u "${username}":"${password}" "${url}/v1/solr_clusters/${solr_cluster_id}" | jq .

(clusterの削除)
# solr_cluster_id="***"

# curl -X DELETE -u "${username}":"${password}" ${url}/v1/solr_clusters/${solr_cluster_id}



◆ config操作
configはcollectionを作成する前に用意しておく必要がある。
collection作成時に適用するconfigを指定するためである。
※configの適用範囲はcollection単位である。

今回はサンプルにあるconfig群を活用する。
solrconfig.xml, schema.xmlなどが含まれている。

・コンフィグ サンプル(今回はこれを使う)
http://www.ibm.com/smarterplanet/us/en/ibmwatson/developercloud/doc/retrieve-rank/resources/cranfield_solr_config.zip

自前で一から作る場合は無地のサンプルもある。
・コンフィグ プレイン(無地)サンプル
http://www.ibm.com/smarterplanet/us/en/ibmwatson/developercloud/doc/retrieve-rank/resources/blank_example_solr_config.zip

schema.xmlではサンプルとして以下のスキーマを使う。
一般のsolrとの相違点として、機械学習の対象とするの型にはwatson_text_XXを指定する必要がある。

# head configs/schema.xml
-snip-
<field name="id" type="string" indexed="true" stored="true" required="true" multiValued="false" />
<field name="title" type="watson_text_en" indexed="true" stored="true" required="false" multiValued="true" />
<field name="author" type="watson_text_en" indexed="true" stored="true" required="false" multiValued="true" />
<field name="bibliography" type="watson_text_en" indexed="true" stored="true" required="false" multiValued="true" />
<field name="body" type="watson_text_en" indexed="true" stored="true" required="false" multiValued="true" />

<copyField source="title" dest="text"/>
<copyField source="body" dest="text"/>
<copyField source="author" dest="text"/>
<copyField source="bibliography" dest="text"/>

<copyField source="title" dest="watson_text"/>
<copyField source="body" dest="watson_text"/>
<copyField source="author" dest="watson_text"/>
<copyField source="bibliography" dest="watson_text"/>
-snip-

(config群のアップロード)
# config_name="example-config"

ファイルは圧縮しておくこと。
# file=configs/cranfield_solr_config.zip

# curl -v -i -X POST \
-H 'Content-Type: application/zip' \
-u "${username}":"${password}" \
--data-binary @${file} \
"${url}/v1/solr_clusters/${solr_cluster_id}/config/${config_name}"


(アップロードされているconfig群の取得)
# curl  -X GET -u "${username}":"${password}" \
"${url}/v1/solr_clusters/${solr_cluster_id}/config" | jq .


(config群の削除)
# curl  -X DELETE -u "${username}":"${password}" \
"${url}/v1/solr_clusters/${solr_cluster_id}/config/${config_name}" | jq .



◆ collectionの操作
(collectionの作成)
# collection_name="example-collection"

# curl -X POST -u "${username}":"${password}" \
-d "action=CREATE&name=${collection_name}&collection.configName=${config_name}" \
"${url}/v1/solr_clusters/${solr_cluster_id}/solr/admin/collections"


(collectionの取得)
# curl -s -X GET -u ${username}:${password} \
"${url}/v1/solr_clusters/${solr_cluster_id}/solr/admin/collections?action=LIST"


(collectionの削除)
# curl -s -X GET -u ${username}:${password} \
"${url}/v1/solr_clusters/${solr_cluster_id}/solr/admin/collections?action=DELETE&name=${collection}"



◆ document操作
documentとはつまり検索対象となるものである。
schema.xmlで指定した型に従ったデータをjsonで用意する。

schema.xmlの型定義にも合致しているサンプルデータを利用する。
http://www.ibm.com/smarterplanet/us/en/ibmwatson/developercloud/doc/retrieve-rank/resources/cranfield_data.json

# head cranfield_data.json
-snip-
{
  "add" : {
    "doc" : {
      "id" : 1,
      "author" : "brenckman,m.",
      "bibliography" : "j. ae. scs. 25, 1958, 324.",
      "body" : "experimental investigation of the aerodynamics...",
      "title" : "experimental investigation of the aerodynamics of a wing in a slipstream ."
    }
  },
  "add" : {
    "doc" : {
      "id" : 2,
      "author" : "ting-yili",
      "bibliography" : "department of aeronautical engineering, rensselaer polytechnic institute troy, n.y.",
      "body" : "simple shear flow past a flat plate...",
      "title" : "simple shear flow past a flat plate in an incompressible fluid of small viscosity ."
    }
  },
-snip-

(アップロード)
# data="cranfield_data.json"
# collection="example-collection"

# curl -X POST -u "${username}":"${password}" \
-H "Content-Type: application/json" \
--data-binary @${data} \
"${url}/v1/solr_clusters/${solr_cluster_id}/solr/${collection_name}/update"


(問合わせ)
学習データを使っていない、solrだけの機能からの応答が返る。
つまりretrieve and rankのretrieve部分だけの処理結果が見える。

# qst="what is the basic mechanism of the transonic aileron buzz"
# qst_encoded=`echo $qst | nkf -wMQ | sed 's/=$//g' | tr = # | tr -d "\n"`
# wt="json"
# fl="id,title"

# curl -X GET -u "${username}":"${password}" \
"${url}/v1/solr_clusters/${solr_cluster_id}/solr/${collection_name}/select?\
q=${qst_encoded}&wt=${wt}&fl=${fl}" | jq .



◆ rankerの操作
(学習)
データ間の関連を記載した学習データを用意する。
http://www.ibm.com/smarterplanet/us/en/ibmwatson/developercloud/doc/retrieve-rank/resources/cranfield_gt.csv

学習データは以下の形式で指定する必要がある。
"{question}","{answer_id1}","{relevance_label1}","{answer_id2}","{relevance_label2}","{...}"


# head cranfield_gt.csv
-snip-
"what similarity laws must be obeyed when constructing aeroelastic models of heated high speed aircraft.","184","3","29","3","31","3","12","2","51","2","102","2","13","1","14","1","15","1","57","3","378","3","859","3","185","2","30","2","37","2","52","1","142","1","195","1","875","3","56","2","66","2","95","2","462","1","497","2","858","2","876","2","879","2","880","2","486","0"
"what are the structural and aeroelastic problems associated with flight of high speed aircraft.","12","4","15","3","184","3","858","3","51","2","102","2","202","2","14","1","52","1","380","1","746","4","859","3","948","3","285","2","390","2","391","2","442","1","497","2","643","2","856","2","857","2","877","2","864","2","658","2","486","0"
-snip-

# train_csv="cranfield_gt.csv"
# collection="example-collection"
# ranker="example-ranker"

以下ツールでアップロードする。
http://www.ibm.com/smarterplanet/us/en/ibmwatson/developercloud/doc/retrieve-rank/resources/train.py

学習データとtrain.pyツールは同じフォルダに置くこと(パス指定だとエラーになる)。

# python train.py -u "${username}:${password}" \
-i "${train_csv}" \
-c "${solr_cluster_id}" \
-x "${collection_name}" \
-n "${ranker}"

ここだけRESTではない理由だが、トレーニング用のcsvが一回変換かけてからPOSTされているためである。
0、1で埋まったtrainingdata.txt という名前のファイルができている。
負荷がかかる処理なのだろうか。この処理はローカルで任されている。

アップロード後に学習が完了するまで数分、時間がかかる。
個別のranker情報を参照し、statusが変わるまで待つ必要がある。
"Training" ⇒ "Available"
問い合わせてもエラーが返る。


(rankerの確認)
# curl -u "${username}":"${password}" "${url}/v1/rankers/" | jq .


(個別のranker情報の取得)
# ranker_id="***"

# curl -u "${username}":"${password}" "${url}/v1/rankers/${ranker_id}" | jq .


(問合わせ)
solr単独の機能による応答結果とどう変わったのかを見てほしい。

# qst="what is the basic mechanism of the transonic aileron buzz"
# qst_encoded=`echo $qst | nkf -wMQ | sed 's/=$//g' | tr = # | tr -d "\n"`
# wt="json"
# fl="id,title"

# curl -X GET -u "${username}":"${password}" \
"${url}/v1/solr_clusters/${solr_cluster_id}/solr/${collection_name}/fcselect?\
ranker_id=${ranker_id}&q=${qst_encoded}&wt=${wt}&fl=${fl}" | jq .


(rankerの削除)
# ranker_id="***"

# curl -X DELETE -u "${username}":"${password}" \
"${url}/v1/rankers/${ranker_id}"



参考
http://www.ibm.com/smarterplanet/us/en/ibmwatson/developercloud/retrieve-rank.html