0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Lucene の QueryParser を読んでみる

Posted at

Lucene の QueryParser とは?

Lucene で検索をする時にクエリを入力する必要があるのですが、そのクエリにはたくさんの機能があります。
それが 公式ドキュメントを見れば分かりますが、ワイルドカード・ブーストやAND, OR検索などの機能になります。
ここでは、その QueryParser のソースコードを読んで、内容を理解したいと思います。

QueryParser のソースコードの場所

QueryParser は、queryparserフォルダの下の classic の配下の QueryParser.jj というファイルにあります。
この jjファイルというのが、JavaCC というコンパイラを作るコンパイラを使う時のファイルになりまして、ここの記事 などを見れば内容を理解できると思います。

今回はこの QueryParser.jj を見てみたいと思います。

でも、この ファイルのどこがスタートなのかすぐには分かりづらいですよね。ですが、そのヒントが最初の方のコメントにあります。

QueryParser.jj 44行目
/**
 * This class is generated by JavaCC.  The most important method is
 * {@link #parse(String)}.

parse は QueryParser.jj に見当たらないので、この QueryParser の extends している QueryParserBase.java の中を探すと parse が見つかります。

QueryParserBase.java 112行目
  public Query parse(String query) throws ParseException {
    ReInit(new FastCharStream(new StringReader(query)));
    try {
      // TopLevelQuery is a Query followed by the end-of-input (EOF)
      Query res = TopLevelQuery(field);
      return res != null ? res : newBooleanQuery().build();

TopLevelQuery(field) が QueryParser.jj の最初のスタート地点になります。また ReInit も JavaCC の組み込みの関数で、コンパイルする文字列の初期化を行っています。

QueryParser.jj を読んでみる

では、TopLevelQuery(field) から読んでみましょう。

QueryParser.jj
// This makes sure that there is no garbage after the query string
Query TopLevelQuery(String field) : {
  Query q;
}
{
  q=Query(field) <EOF>
  { return q; }
}

ここの q=Query(field) <EOF> の部分は、QueryParser.jj の Query を判定してあったら までの部分を q とするの意味になります。
Query q の Query は、import org.apache.lucene.search.Query; になるので、勘違いしないようにしましょう。
では、Query を読んでみましょう。

QueryParser.jj
Query Query(String field) :
{
  List<BooleanClause> clauses = new ArrayList<BooleanClause>();
  Query q, firstQuery=null;
  int conj, mods;
}
{
  (
    LOOKAHEAD(2)
    firstQuery=MultiTerm(field, clauses)
    | mods=Modifiers() q=Clause(field)
      {
        addClause(clauses, CONJ_NONE, mods, q);
        if (mods == MOD_NONE) {
          firstQuery = q;
        }
      }
  )
  (
    LOOKAHEAD(2)
    MultiTerm(field, clauses)
    | conj=Conjunction() mods=Modifiers() q=Clause(field)
      { addClause(clauses, conj, mods, q); }
  )*
  {
    if (clauses.size() == 1 && firstQuery != null) {
      return firstQuery;
    } else {
      return getBooleanQuery(clauses);
    }
  }
}

なんだか長ったらしいものが出てきましたね。
まず、このプログラムは、2つの () (パースする部分) と 最後の処理の {} でできています。
まずは、最初の () を見てみます。

QueryParser.jj
  (
    LOOKAHEAD(2)
    firstQuery=MultiTerm(field, clauses)
    | mods=Modifiers() q=Clause(field)
      {
        addClause(clauses, CONJ_NONE, mods, q);
        if (mods == MOD_NONE) {
          firstQuery = q;
        }
      }
  )

まずは、空白で分かれた2単語以上をパースするのが MultiTerm(field, clauses) になり、それがない場合に Modifiers()Clause(field) の順番でパースする形になっています。
Modifiers() では + - をパースしているのは見れば分かりやすいと思いますが、Clause(field) は何をパースしているのでしょうか?

QueryParser.jj
Query Clause(String field) : {
  Query q;
  Token fieldToken=null, boost=null;
}
{
  [
    LOOKAHEAD(2)
    (
      fieldToken=<TERM> <COLON> {field=discardEscapeChar(fieldToken.image);}
      | <STAR> <COLON> {field="*";}
    )
  ]
  (
    q=Term(field)
    | <LPAREN> q=Query(field) <RPAREN> [ <CARAT> boost=<NUMBER> ]
  )
  { return handleBoost(q, boost); }
}

これも大きく見ると、[] のかっこと () のかっこ と 最後に {}の処理が続きます。
[] はなくてもいいパース部分を意味しますが、ここでは検索するフィールド: もしくは *:(全てのフィールドを検索対象にする) を指定しています。
次に () では、Term で単語 (ワイルドカードか正規表現か*検索 or 日時範囲 or "で囲われた文字) を取得するか 再帰的に () の間で Query をパースしています。
つまり、Clause では オプションのフィールド検索 + 「単語 or Query」をパースしています。

ここで Query にの最初の () に戻ると、Modifiers() の後で単純に考えれば 「オプションのフィールド検索 + 単語」を取得していることになります。
そして、それに対して、

QueryParser.jj
      {
        addClause(clauses, CONJ_NONE, mods, q);
        if (mods == MOD_NONE) {
          firstQuery = q;
        }
      }

を実行しています。addClause の cluase は 1つ1つの検索ワードを示していて、addClause でそれぞれ AND か OR のどれかでどの単語が追加されたかを記憶しています。
Query の 次の () では、

QueryParser.jj
  (
    LOOKAHEAD(2)
    MultiTerm(field, clauses)
    | conj=Conjunction() mods=Modifiers() q=Clause(field)
      { addClause(clauses, conj, mods, q); }
  )*

のような内容になっていますが、Conjunction() 以外は上と同じですね。Conjunction() で 前の条件との関係性 つまり AND か OR かを書いて、これも addClause で clauses に保存しています。
最後にこれらに処理を加えるのが、

QueryParser.jj
  {
    if (clauses.size() == 1 && firstQuery != null) {
      return firstQuery;
    } else {
      return getBooleanQuery(clauses);
    }
  }

の部分ですが、clauses にはどんどん内容が加わっているので、検索単語が1つの場合とかに1つ目の if が通りそうで、そうでない時は getBooleanQuery(clauses) になります。
TopLevelQuery からの流れで見ると、ここの return が res に入って、パースの結果として IndexSearcher に入れ込まれるようになります。

getBooleanQuery を見る(QueryParserBase.javaにあります)と BooleanQuery.Builder に add して BooleanQuery を作る build につなげていることが分かります。この BooleanQuery を使い AND OR 検索を行います。

では、cluases.size == 1 の場合の if はどうでしょうか?firstQuery には q=Clause(field) が入りますが、Clause の最後の return には handleBoost というのがついています。これも QueryParserBase.java にあって、new BoostQuery(q, f)q を 返していることが分かります。boost は <CARAT> boost=<NUMBER> で定義していますが、公式ドキュメント を見ればわかる通り 検索結果で優先されるようになります。

ここまでの流れを整理すると、

1:QueryParser.jj のスタートは TopLevelQuery
2:TopLevelQuery の Query で、「<複数単語> or -> <複数単語> or 」の連続をパース。
3:Query の Clause で、「<検索フィールド:> + < Term or (Query) [boost] > 」をパース
4:結果、BooleanQuery や 場合によっては TermQuery が返ってくる

していることが分かりました。
ちなみに wildcard の分岐は Term の中の最初の () にある handleBareTokenQuery で行っています。

以上が、QueryParser の簡単な概要でした。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?