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のBooleanQueryについて

Last updated at Posted at 2022-07-20

BooleanQuery

BooleanQueryは多数のQueryサブクラスオブジェクトを組み合う。これらのQueryサブクラスオブジェクトはCluaseの実現を生成する。各Queryには4つの選択肢があって、BooleanClauseに定義されている:

public static enum Occur {
    /** Use this operator for clauses that <i>must</i> appear in the matching documents. */
    MUST     { @Override public String toString() { return "+"; } },
    /** Like {@link #MUST} except that these clauses do not participate in scoring. */
    FILTER   { @Override public String toString() { return "#"; } },
    /** Use this operator for clauses that <i>should</i> appear in the 
     * matching documents. For a BooleanQuery with no <code>MUST</code> 
     * clauses one or more <code>SHOULD</code> clauses must match a document 
     * for the BooleanQuery to match.
     * @see BooleanQuery.Builder#setMinimumNumberShouldMatch
     */
    SHOULD   { @Override public String toString() { return "";  } },
    /** Use this operator for clauses that <i>must not</i> appear in the matching documents.
     * Note that it is not possible to search for queries that only consist
     * of a <code>MUST_NOT</code> clause. These clauses do not contribute to the
     * score of documents. */
    MUST_NOT { @Override public String toString() { return "-"; } };
 }

MUST (+)

必ず全ての条件を満足するファイルを探す

SHOULD (" ")

最低一つの条件を満足するファイルを探す

FILTER (#)

条件を満足するファイルを探して、ただしこのQueryはスコアリングしない。

MUST_NOT (-)

必ず全ての条件を満足しないファイルを探す
組合サーチ

例:「+a b -c d」

コードにすると以下のように:

BooleanQuery.Builder query = new BooleanQuery.Builder();
query.add(new TermQuery(new Term("content", "a")), BooleanClause.Occur.MUST);
query.add(new TermQuery(new Term("content", "b")), BooleanClause.Occur.SHOULD);
query.add(new TermQuery(new Term("content", "c")), BooleanClause.Occur.MUST_NOT);
query.add(new TermQuery(new Term("content", "d")), BooleanClause.Occur.SHOULD);

条件を満足するファイルは「a」を含んで、「c」を含まなくて、「b」と「d」に最低一つを含む。

BooleanQueryのメソッド

minimumNumberShouldMatchを設置

 public Builder setMinimumNumberShouldMatch(int min) {
     ......
    }

サーチに多数のSHOULDのQueryオブジェクトがある時、minimumNumberShouldMatchのQueryを満足する条件が必ずある。

CreateWeightオブジェクトを構築

public Weight createWeight(IndexSearcher searcher, boolean needsScores, float boost) throws IOException {
    ...
    return new BooleanWeight(query, searcher, needsScores, boost);
  }

Queryオブジェクトのサブクラスはこのメソッドをリライトする。BooleanQueryのcreateWeight(...)の実現に対して、全部のQueryサブクラスのcreateWeight(...)メソッドを読んでWeightを生成し、BooleanWeightにカプセルする。

Queryをリライト

public Query rewrite(IndexReader reader) throws IOException {
    ...
  }

BooleanQueryのrewrite(..)とcreateWeight(...)は同じなQuery子类的rewrite(...)を呼ぶ。だた、全てのQueryはリライトが必要ではない。例えばTermQueryなら、rewrite(...)はなくて、PrefixQueryならリライトをしなければならない。

例:PrefixQueryのキーワード「ca*」をリライトして、「car」や 「cat」のような形の新しいキーワードになる。各新しいキーワードを用いてTermQueryを生成する。TermQueryを用いてBooleanQueryを組み合わせてサーチを行う。

BooleanQueryのrewrite(...)には9種類がある。

一つのSHOULDやMUSTを持つTermQuery

リライトステップ1

if (clauses.size() == 1) {
      BooleanClause c = clauses.get(0);
      Query query = c.getQuery();
      if (minimumNumberShouldMatch == 1 && c.getOccur() == Occur.SHOULD) {
        return query;
      } else if (minimumNumberShouldMatch == 0) {
        switch (c.getOccur()) {
          case SHOULD:
          case MUST:
            return query;
          case FILTER:
            // no scoring clauses, so return a score of 0
            return new BoostQuery(new ConstantScoreQuery(query), 0);
          case MUST_NOT:
            // no positive clauses
            return new MatchNoDocsQuery("pure negative BooleanQuery");
          default:
            throw new AssertionError();
        }
      }
    }

多数のSHOULDを持つTermQuery

リライトステップ1

まずBooleanQueryにあるQueryオブジェクトをリライトする。だたしTermQueryに対してリライトはいらないので直接に自身を返す。

{ 
    BooleanQuery.Builder builder = new BooleanQuery.Builder();
    builder.setMinimumNumberShouldMatch(getMinimumNumberShouldMatch());
    boolean actuallyRewritten = false;
    for (BooleanClause clause : this) {
      Query query = clause.getQuery();
      Query rewritten = query.rewrite(reader);
      if (rewritten != query) {
        actuallyRewritten = true;
      }
      builder.add(rewritten, clause.getOccur());
    }
    if (actuallyRewritten) {
      return builder.build();
    }
}

リライトステップ2「オプション」

もしminimumNumberShouldMatchのバリューは <= 1であれば、ステップ2を実現する。もし多数の同じなTermQueryを持ち、SHOULDであれば、同じなTermQueryを一つのBoostQueryにカプセルし、boostのバリューをアップする。

if (clauseSets.get(Occur.SHOULD).size() > 0 && minimumNumberShouldMatch <= 1) {
      Map<Query, Double> shouldClauses = new HashMap<>();
      for (Query query : clauseSets.get(Occur.SHOULD)) {
        double boost = 1;
        while (query instanceof BoostQuery) {
          BoostQuery bq = (BoostQuery) query;
          boost *= bq.getBoost();
          query = bq.getQuery();
        }
        shouldClauses.put(query, shouldClauses.getOrDefault(query, 0d) + boost);
      }
      if (shouldClauses.size() != clauseSets.get(Occur.SHOULD).size()) {
        BooleanQuery.Builder builder = new BooleanQuery.Builder()
            .setMinimumNumberShouldMatch(minimumNumberShouldMatch);
        for (Map.Entry<Query,Double> entry : shouldClauses.entrySet()) {
          Query query = entry.getKey();
          float boost = entry.getValue().floatValue();
          if (boost != 1f) {
            query = new BoostQuery(query, boost);
          }
          builder.add(query, Occur.SHOULD);
        }
        for (BooleanClause clause : clauses) {
          if (clause.getOccur() != Occur.SHOULD) {
            builder.add(clause);
          }
        }
        return builder.build();
      }
    }

下の図には、左辺はリライトする前のBooleanQuery、右辺はリライトしたBooleanQueryである。
image.png

SHOULD(最低一つ)とMUST(最低一つ)を持つTermQuery

リライトステップ1

同じくまずロジック2を実行する。

リライトステップ2「オプション」

同じくまずロジック7を実行する。下の図には、左辺はリライトする前のBooleanQuery、右辺はリライトしたBooleanQueryである。
image.png

リライトステップ3

もし多数の同じなTermQueryを持ち、MUSTであれば、同じなTermQueryを一つのBoostQueryにカプセルし、boostのバリューをアップする。

if (clauseSets.get(Occur.MUST).size() > 0) {
      Map<Query, Double> mustClauses = new HashMap<>();
      for (Query query : clauseSets.get(Occur.MUST)) {
        double boost = 1;
        while (query instanceof BoostQuery) {
          BoostQuery bq = (BoostQuery) query;
          boost *= bq.getBoost();
          query = bq.getQuery();
        }
        mustClauses.put(query, mustClauses.getOrDefault(query, 0d) + boost);
      }
      if (mustClauses.size() != clauseSets.get(Occur.MUST).size()) {
        BooleanQuery.Builder builder = new BooleanQuery.Builder()
                .setMinimumNumberShouldMatch(minimumNumberShouldMatch);
        for (Map.Entry<Query,Double> entry : mustClauses.entrySet()) {
          Query query = entry.getKey();
          float boost = entry.getValue().floatValue(); 
          if (boost != 1f) {
            query = new BoostQuery(query, boost);
          }
          builder.add(query, Occur.MUST);
        }
        for (BooleanClause clause : clauses) {
          if (clause.getOccur() != Occur.MUST) {
            builder.add(clause);
          }
        }
        return builder.build();
      }
    }

下の図には、左辺はリライトする前のBooleanQuery、右辺はリライトしたBooleanQueryである。
image.png

多数のMUSTを持つTermQuery

リライトステップ1

同じくまずロジック2を実行する。

リライトステップ2

もし多数の同じなTermQueryを持ち、MUSTであれば、同じなTermQueryを一つのBoostQueryにカプセルし、boostのバリューをアップする。そしてロジック8を実行する。

MUST(最低一つ)とMUST_NOT(最低一つ)を持つTermQuery

リライトステップ1

同じくまずロジック2を実行する。

リライトステップ2

もしここで結果をだしたら、MatchNoDocsQueryオブジェクトを返す。

 final Collection<Query> mustNotClauses = clauseSets.get(Occur.MUST_NOT);
    if (!mustNotClauses.isEmpty()) {
      final Predicate<Query> p = clauseSets.get(Occur.MUST)::contains;
      if (mustNotClauses.stream().anyMatch(p.or(clauseSets.get(Occur.FILTER)::contains)))     {
        return new MatchNoDocsQuery("FILTER or MUST clause also in MUST_NOT");
      }
      if (mustNotClauses.contains(new MatchAllDocsQuery())) {
        return new MatchNoDocsQuery("MUST_NOT clause is MatchAllDocsQuery");
      }
    }

リライトステップ3

もし多数の同じなTermQueryを持ち、MUSTであれば、同じなTermQueryを一つのBoostQueryにカプセルし、boostのバリューをアップする。そしてロジック8を実行する。
下の図には、左辺はリライトする前のBooleanQuery、右辺はリライトしたBooleanQueryである。
image.png

SHOULD(最低一つ)とMUST_NOT(最低一つ)を持つTermQuery

リライトステップ1

同じくまずロジック2を実行する。

リライトステップ2

同じくまずロジック4を実行する。

リライトステップ3「オプション」

ロジック7を実行する。
下の図には、左辺はリライトする前のBooleanQuery、右辺はリライトしたBooleanQueryである。
image.png

SHOULD(最低一つ)とMUST(最低一つ)MUST_NOT(最低一つ)を持つTermQuery

リライトステップ1

同じくまずロジック2を実行する。

リライトステップ2

同じくまずロジック4を実行する。

リライトステップ3

ロジック7を実行する。

リライトステップ4「オプション」

ロジック8を実行する。
下の図には、左辺はリライトする前のBooleanQuery、右辺はリライトしたBooleanQueryである。
image.png

SHOULD(最低一つ)とFILTER(最低一つ)を持つTermQuery

リライトステップ1

同じくまずロジック2を実行する。

リライトステップ2

FILTERのQueryのtermの対して、スコアリングしなくて、だたサーチにはこのtermを含まなければならない。もしSHOULDのQueryにも同じなtermがあれば、このQueryのSHOULDをMUSTに変更し、minShouldMatchのバリューを1でマイナスする。

if (clauseSets.get(Occur.SHOULD).size() > 0 && clauseSets.get(Occur.FILTER).size() > 0) {
      final Collection<Query> filters = clauseSets.get(Occur.FILTER);
      final Collection<Query> shoulds = clauseSets.get(Occur.SHOULD);
      Set<Query> intersection = new HashSet<>(filters);
      intersection.retainAll(shoulds);
      if (intersection.isEmpty() == false) {
        BooleanQuery.Builder builder = new BooleanQuery.Builder();
        int minShouldMatch = getMinimumNumberShouldMatch();
        for (BooleanClause clause : clauses) {
          if (intersection.contains(clause.getQuery())) {
            if (clause.getOccur() == Occur.SHOULD) {
              builder.add(new BooleanClause(clause.getQuery(), Occur.MUST));
              minShouldMatch--;
            }
          } else {
            builder.add(clause);
          }
        }
        builder.setMinimumNumberShouldMatch(Math.max(0, minShouldMatch));
        return builder.build();
      }
    }

リライトステップ3「オプション」

ロジック7を実行する。

リライトステップ4

ロジック8を実行する。
下の図には、左辺はリライトする前のBooleanQuery、右辺はリライトしたBooleanQueryである。
image.png

MUST(最低一つ)とFILTER(最低一つ)を持つTermQuery

リライトステップ1

同じくまずロジック2を実行する。

リライトステップ2

一つのtermに対応するQueryはMUSTであり、FILTERであれば、FILTERのQueryを削除する。

if (clauseSets.get(Occur.MUST).size() > 0 && clauseSets.get(Occur.FILTER).size() > 0) {
      final Set<Query> filters = new HashSet<Query>(clauseSets.get(Occur.FILTER));
      boolean modified = filters.remove(new MatchAllDocsQuery());
      modified |= filters.removeAll(clauseSets.get(Occur.MUST));
      if (modified) {
        BooleanQuery.Builder builder = new BooleanQuery.Builder();
        builder.setMinimumNumberShouldMatch(getMinimumNumberShouldMatch());
        for (BooleanClause clause : clauses) {
          if (clause.getOccur() != Occur.FILTER) {
            builder.add(clause);
          }
        }
        for (Query filter : filters) {
          builder.add(filter, Occur.FILTER);
        }
        return builder.build();
      }
    }

リライトステップ4

ロジック8を実行する。
下の図には、左辺はリライトする前のBooleanQuery、右辺はリライトしたBooleanQueryである。
BooleanQuery。
image.png

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?