検索ライブラリ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とは異なります) です。
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;
}
このコードから、クエリが変更されたかどうかの判定は、インスタンスの同一性の判定 (!=
) で行われていることが分かります。Query#rewriteで具体的に何をするかはQueryの目的によりますが、クエリを変更した場合は、必ず新しいインスタンスを作成して返す必要があるということになります。
SolrのRankQueryにおける対応
Query#rewriteを意識する具体的な局面としては、Solrに独自のRankQueryを実装するときが挙げられます。RankQueryを実装してドキュメントを並べ替えること自体については、以下の記事などを参照してください。
RankQuery#rewriteで具体的に何をするかも、RankQueryの目的によります。しかし、RankQueryは元のクエリを内包していますので、少なくとも元のクエリのrewriteメソッドも呼ぶ必要があり、かつ、内包するクエリがrewriteによって変更された場合には、RankQueryとしても新しいインスタンスを返す必要があります。
例えばSolrに同梱のRankQueryであるReRankQueryでは、以下の箇所 (AbstractReRankQuery#rewrite) でQuery#rewriteを実装しています。
public Query rewrite(IndexReader reader) throws IOException {
Query q = mainQuery.rewrite(reader);
if (q != mainQuery) {
return rewrite(q);
}
return super.rewrite(reader);
}
これは、上述した少なくとも必要な処理だけを行う例になっています。ここでは3種類のQuery#rewriteが呼ばれていて、ややこしいのですが、以下のような使い分けです。
-
mainQuery.rewrite
で、元のクエリのrewriteメソッドを呼んでいます。 -
rewrite(q)
は、AbstarctReRankQuery#rewrite(Query)
を呼んでいます。ややこしいのですが、これは本記事の主題であるQuery#rewrite(IndexSearcher)
とは異なり、単にAbstarctReRankQueryの新しいインスタンスを返すメソッドです。 -
super.rewrite
は、デフォルトのQuery#rewriteを呼んでいます。これは今のところ、単にreturn this
します。