Solr の RankQuery プラグイン

昨年参加した[Solr勉強会](https://www.slideshare.net/techblogyahoo/solr-solrjp)で[yahooの矢野さん](https://qiita.com/yuyano)がお話されていた Solr の RankQuery を試してみました。Solr に実装されている ReRankQuery を試した後、フィールド値で上位N件を並び替えるプラグインを作成してみました。

## はじめに
RankQuery は、元のクエリの結果に対して追加処理を加えて動作を変更する機能になります。
具体的には、元のクエリのソート結果のトップN件数だけ追加指定した条件でソートし直すといった用途に使われているようです。詳細については矢野さんが[詳しい情報をQiitaで公開](https://qiita.com/yuyano/items/80a24b714f5a3efed384)されているので、そちらをご参照ください。
## 環境構築
[README.md](https://github.com/ft28/practice/tree/master/solr/suggest/README.md) の手順で環境構築を行います。

## ReRankQuery
ReRankQuery は、Solrに標準で実装されているクエリで、元のクエリの検索結果について、reRankDocs で指定した順位までを対象に、rqq で指定した query 実行時のスコアをreRankWeightの重みで元クエリのスコアに加算したものを新たなスコアとし、その値でソートした結果に置き換えるクエリです。rqq で指定したquery にヒットしない場合、元クエリのスコアがそのまま使われます。また、加算後スコアが同じ値の場合は、内部的に持つdocIDの小さい順にソートされます。

```
# 擬似コード
if(match_rqq_query) {
   new_score = original_score + reRankWeight * rqq_score;
} else {
   new_score = original_score;
}
```

### 元になるクエリ
「**山手**」を含むレコードを抽出し、**weighの大きい順**に取得するクエリです。

http://localhost:8983/solr/sample01/select?fl=id,payload,weight,score&q=payload:山手&wt=csv&rows=5&sort=weight desc

|id|payload|**weight**|score|
|---|---|---|---|
|24|東京 山手線|**915**|1.6172552|
|15|駒込 山手線|**881**|1.6172552|
|4|恵比寿 山手線|**868**|1.6172552|
|28|田町 山手線|**831**|1.6172552|
|17|西日暮里 山手線|**826**|1.6172552|

### リランククエリ1
reRankDocs=3 reRankWeight=1.0 rqq=(payload:恵比寿)　のReRankQueryを実行します。

http://localhost:8983/solr/sample01/select?fl=id,payload,weight,score&q=payload:山手&wt=csv&rows=5&sort=weight desc&&rq={!rerank reRankQuery=$rqq reRankDocs=3 reRankWeight=1.0}&rqq=(payload:恵比寿)

|id|payload|weight|score|順位|元順位|元score|
|---|---|---|---|---|---|---|
|4|恵比寿 山手線|868|**5.553081**|**1**|**3**|**1.6172552**
|**15**|駒込 山手線|881|1.6172552|**2**|**2**|1.6172552|
|**24**|東京 山手線|915|1.6172552|**3**|**1**|1.6172552|
|28|田町 山手線|831|1.6172552|4|4|1.6172552|
|17|西日暮里 山手線|826|1.6172552|5|5|1.6172552|

* 上位３件の順序が入れ替わっています。
  * 恵比寿を含むid=4のscoreだけ変化し、1位になっています。
  * id=15とid=24 の順序が変わっています。最終的なscoreが同じ場合、**docIDの小さいほうが先になる(※)**というルールが適用されています。
* id=28,id=17は並べ替え対象外なので、docIDの小さい順になっていません。

**　※docIDとフィールドidの大小が同じになるようデータ登録しています**

### リランククエリ2
reRankDocs=3 reRankWeight=1.0 rqq=(payload:田町)　のReRankQueryを実行します。

http://localhost:8983/solr/sample01/select?fl=id,payload,weight,score&q=payload:山手&wt=csv&rows=5&sort=weight desc&&rq={!rerank reRankQuery=$rqq reRankDocs=3 reRankWeight=1.0}&rqq=(payload:田町)

|id|payload|weight|score|順位|元順位|元score|
|---|---|---|---|---|---|---|
|4|恵比寿 山手線|868|**1.6172552**|1|3|**1.6172552**|
|15|駒込 山手線|881|1.6172552|2|2|1.6172552|
|24|東京 山手線|915|1.6172552|3|1|1.6172552|
|28|田町 山手線|831|1.6172552|4|4|1.6172552|
|17|西日暮里 山手線|826|1.6172552|5|5|1.6172552|

* リランククエリ1と同じ並びですが、id=4のscoreが元scoreと同じです。
* docIDの小さい順に並ぶというルールが適用されています。

## RankQuery plugin

ReRankQueryの実装である、[ReRanqQparserPlugin.java](https://github.com/ft28/lucene-solr/blob/ft28/solr/core/src/java/org/apache/solr/search/ReRankQParserPlugin.java) を参考にして、元クエリで検索後に、フィールド値を見てスコアを再計算するプラグインを作成しました。
[SampleReRankQParserPlugin.java](https://github.com/ft28/lucene-solr/blob/ft28/solr/contrib/sample/src/java/org/apache/solr/sample/SampleReRankQParserPlugin.java)

本来は、Weight/Scorer クラスも実装して実現するようですが、今回はそれらを実装せず直接フィールド値を見に行くという手抜き実装にっています。これにあわせて、ReRankQueryの中で使われている、[QueryRrescorer.java](https://github.com/ft28/lucene-solr/blob/ft28/lucene/core/src/java/org/apache/lucene/search/QueryRescorer.java)クラスの代わりに使う、クラスを実装しました。
[SampleRescorer.java]
(https://github.com/ft28/lucene-solr/blob/ft28-kuromoji-roman/solr/contrib/sample/src/java/org/apache/solr/sample/SampleRescorer.java)

### plugin 仕様
* rq={!sample ...} で呼び出します。
* reRankDocs で並び替えの対象とするドキュメント数を指定します。
* reRankModulo で指定した約数のid のスコアを最初のクエリの最大スコア + weight値で置き換えます。
* ReRanqQuery と違い、スコア再計算の際にクエリを使えません。（rqq指定なし)

```
#擬似コード
if((field_id % reRankModulo) == 0) {
   new_score = max(original_scores) + field_weight;
} else {
   new_score = original_score;
}
```

### plugin 仕様方法
パスの通った場所にコンパイル済のjarファイルを設置した後、solrconfgi.xml内に以下記述を追加してSolrを再起動します。

```xml:solrconfig.xml
<queryParser  name="sample" class="org.apache.solr.search.SampleReRankQParserPlugin" />
```

### リランククエリ3

http://localhost:8983/solr/sample01/select?fl=id,payload,weight,score&q=payload:山手&wt=csv&rows=5&sort=weight desc&rq={!sample reRankDocs=3 reRankModulo=2}

|id|payload|weight|score|順位|元順位|元score|
|---|---|---|---|---|---|---|
|24|東京 山手線|915|**916.61725**|1|1|**1.6172552**|
|4|恵比寿 山手線|868|**869.61725**|**3**|**2**|**1.6172552**|
|15|駒込 山手線|881|1.6172552|**2**|**3**|1.6172552|
|28|田町 山手線|831|1.6172552|4|4|1.6172552|
|17|西日暮里 山手線|826|1.6172552|5|5|1.6172552|

* 上位3件のうち、2の約数であるid=24,id=4のスコアが **元スコア+weight** になっています。

## まとめ
ReRankQuery を試した後、フィールド値で上位N件を並び替えるプラグインを作成してみました。
以前、複数フィールド値を加味した[独自score計算を実装](http://www.lifull.blog/entry/2014/03/28/181727) したときは、searchComponentから作成したのでとてもめんどくさかったのですが、今回の方法はずっと簡単に実装できました。また、reRankDocsで絞ってから計算を行えるので計算負荷を軽く出来そうです、しかなり使えそうです。

RankQuery にはReRankQueryの他にも、LTR(Learning To Rank)プラグインが既に本家に取り込まれています。LTRプラグインには事前に学習したモデルを使ったソートなど様々な機能が実装されており、そのうちに、こちらのプラグインも試してみたいと思います。

## 参考
本家の資料
    * [Query Re-Ranking | Apache Solr Reference Guide 7.2]
(https://lucene.apache.org/solr/guide/7_2/query-re-ranking.html)

矢野さんの資料s
    * [Solr勉強会の時のSlideShare](https://www.slideshare.net/techblogyahoo/solr-solrjp)
    * [Qiitaの記事](https://qiita.com/yuyano)