Lucene (Elasticsearch, Solr) のQuery#rewriteの役割と実装と、SolrのRankQueryにおける対応

検索ライブラリApache Luceneにおいて、ユーザからのクエリはメモリ上ではQueryクラスのオブジェクトで表現されます（Luceneに依存する検索エンジンElasticsearchやSolrにおいても同様）。そして、Queryクラスにはrewriteメソッドが実装されています。

# Query#rewriteの役割

このメソッドの役割は、主に以下の2つあるようです。

- そもそも直接は実行できないクエリを実行可能にする。
- クエリをパフォーマンス面で最適化する。

## 直接は実行できないクエリを実行可能にする例

直接は実行できないクエリの例として、プレフィックスクエリがあります。以下、具体的な例はSolr 8.6.3と、サンプルとして同梱されているtechproductsコレクションに基づきます。以下のリクエストは：

```
/solr/techproducts/select?q=cat:hard*
```

メモリ上では大まかに以下のように表現され、rewriteされます。

```
PrefixQuery "cat:hard*"

↓ rewrite

MultiTermQueryConstantScoreWrapper "cat:hard*"
  ┗ PrefixQuery "cat:hard*"

↓ rewrite

ConstantScoreQuery "ConstantScore(cat:hard cat:hardcover)"
  ┗ BooleanQuery "cat:hard cat:hardcover"
    ┣ TermQuery "cat:hard"
    ┗ TermQuery "cat:hardcover"
```

つまり、プレフィックスをマッチするタームに展開するのは、PrefixQuery#rewriteの役割のようです。

## クエリをパフォーマンス面で最適化する例

クエリをパフォーマンス面で最適化する例として、例えば以下のように明らかに最適化できそうなリクエストを投げた場合：

```
/solr/techproducts/select?q=cat:book AND cat:book
```

メモリ上では大まかに以下のように表現され、rewriteされます。

```
BooleanQuery "+cat:book +cat:book"
  ┣ TermQuery "cat:book"
  ┗ TermQuery "cat:book"

↓ rewrite

BooleanQuery "+(cat:book)^2.0"
  ┗ BoostQuery "(cat:book)^2.0"
    ┗ TermQuery "cat:book"

↓ rewrite

BoostQuery "(cat:book)^2.0"
  ┗ TermQuery "cat:book"
```

# Query#rewriteの実装

例の通り、1つのリクエストに対してQuery#rewriteは何度か実行されることがあります。これは、クエリが変更される限り、繰り返し実行されるようになっているからです。コードで言うと以下の箇所 (IndexSearcher#rewrite. Query#rewriteとは**異なります**) です。

```java
  public Query rewrite(Query original) throws IOException {
    Query query = original;
    for (Query rewrittenQuery = query.rewrite(reader); rewrittenQuery != query;
         rewrittenQuery = query.rewrite(reader)) {
      query = rewrittenQuery;
    }
    return query;
  }
```

引用元：https://github.com/apache/solr/blob/e001c2221812a0ba9e9378855040ce72f93eced4/lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java#L674-L681

このコードから、クエリが変更されたかどうかの判定は、インスタンスの同一性の判定 (`!=`) で行われていることが分かります。Query#rewriteで具体的に何をするかはQueryの目的によりますが、クエリを変更した場合は、必ず新しいインスタンスを作成して返す必要があるということになります。

# SolrのRankQueryにおける対応

Query#rewriteを意識する具体的な局面としては、Solrに独自のRankQueryを実装するときが挙げられます。RankQueryを実装してドキュメントを並べ替えること自体については、以下の記事などを参照してください。

https://qiita.com/tomanabe/items/a927e61232b1d3ad06cd

RankQuery#rewriteで具体的に何をするかも、RankQueryの目的によります。しかし、RankQueryは元のクエリを内包していますので、少なくとも元のクエリのrewriteメソッドも呼ぶ必要があり、かつ、内包するクエリがrewriteによって変更された場合には、RankQueryとしても新しいインスタンスを返す必要があります。

例えばSolrに同梱のRankQueryであるReRankQueryでは、以下の箇所 (AbstractReRankQuery#rewrite) でQuery#rewriteを実装しています。

```java
  public Query rewrite(IndexReader reader) throws IOException {
    Query q = mainQuery.rewrite(reader);
    if (q != mainQuery) {
      return rewrite(q);
    }
    return super.rewrite(reader);
  }
```

引用元：https://github.com/apache/solr/blob/e001c2221812a0ba9e9378855040ce72f93eced4/solr/core/src/java/org/apache/solr/search/AbstractReRankQuery.java#L72-L78

これは、上述した少なくとも必要な処理だけを行う例になっています。ここでは3種類のQuery#rewriteが呼ばれていて、ややこしいのですが、以下のような使い分けです。

- `mainQuery.rewrite`で、元のクエリのrewriteメソッドを呼んでいます。
- `rewrite(q)`は、`AbstarctReRankQuery#rewrite(Query)`を呼んでいます。ややこしいのですが、これは本記事の主題である`Query#rewrite(IndexSearcher)`とは**異なり**、単にAbstarctReRankQueryの新しいインスタンスを返すメソッドです。
- `super.rewrite`は、デフォルトのQuery#rewriteを呼んでいます。これは今のところ、単に`return this`します。