LuceneのBooleanQueryについて

# BooleanQuery
BooleanQueryは多数のQueryサブクラスオブジェクトを組み合う。これらのQueryサブクラスオブジェクトはCluaseの実現を生成する。各Queryには４つの選択肢があって、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(...)には９種類がある。
### 一つのSHOULDやMUSTを持つTermQuery
#### リライトステップ１
```
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
#### リライトステップ１
まず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();
    }
}
```
#### リライトステップ２「オプション」
もしminimumNumberShouldMatchのバリューは <= 1であれば、ステップ２を実現する。もし多数の同じな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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648622/0640f926-1d6e-2205-ab34-2e2614fb0367.png)

### SHOULD(最低一つ)とMUST(最低一つ)を持つTermQuery
#### リライトステップ１
同じくまずロジック２を実行する。
#### リライトステップ２「オプション」
同じくまずロジック７を実行する。下の図には、左辺はリライトする前のBooleanQuery、右辺はリライトしたBooleanQueryである。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648622/1ac1669b-ee8e-30f6-f1f1-605a99830a81.png)
#### リライトステップ３
もし多数の同じな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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648622/78486bcd-6aba-c840-5fc3-8b5d82d31461.png)
### 多数のMUSTを持つTermQuery
#### リライトステップ１
同じくまずロジック２を実行する。
#### リライトステップ２
もし多数の同じなTermQueryを持ち、MUSTであれば、同じなTermQueryを一つのBoostQueryにカプセルし、boostのバリューをアップする。そしてロジック８を実行する。

### MUST(最低一つ)とMUST_NOT(最低一つ)を持つTermQuery
#### リライトステップ１
同じくまずロジック２を実行する。
#### リライトステップ２
もしここで結果をだしたら、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");
      }
    }
```
#### リライトステップ３
もし多数の同じなTermQueryを持ち、MUSTであれば、同じなTermQueryを一つのBoostQueryにカプセルし、boostのバリューをアップする。そしてロジック８を実行する。
下の図には、左辺はリライトする前のBooleanQuery、右辺はリライトしたBooleanQueryである。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648622/bb55fe71-e2ad-f5ac-9414-fc926091ae2a.png)

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

#### リライトステップ１
同じくまずロジック２を実行する。
#### リライトステップ２
同じくまずロジック４を実行する。
#### リライトステップ３「オプション」
ロジック７を実行する。
下の図には、左辺はリライトする前のBooleanQuery、右辺はリライトしたBooleanQueryである。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648622/afaf5c56-46db-fc55-be8e-df3f14a66ab1.png)

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

#### リライトステップ１
同じくまずロジック２を実行する。
#### リライトステップ２
同じくまずロジック４を実行する。
#### リライトステップ３
ロジック７を実行する。
#### リライトステップ４「オプション」
ロジック８を実行する。
下の図には、左辺はリライトする前のBooleanQuery、右辺はリライトしたBooleanQueryである。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648622/04093a08-d516-821c-680b-f6438ea9cc1a.png)

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

#### リライトステップ１
同じくまずロジック２を実行する。
#### リライトステップ２
FILTERのQueryのtermの対して、スコアリングしなくて、だたサーチにはこのtermを含まなければならない。もしSHOULDのQueryにも同じなtermがあれば、このQueryのSHOULDをMUSTに変更し、minShouldMatchのバリューを１でマイナスする。
```
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();
      }
    }
```
#### リライトステップ３「オプション」
ロジック７を実行する。
#### リライトステップ４
ロジック８を実行する。
下の図には、左辺はリライトする前のBooleanQuery、右辺はリライトしたBooleanQueryである。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648622/b39b797b-dc2a-569f-3b29-48ddeeb37790.png)

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

#### リライトステップ１
同じくまずロジック２を実行する。
#### リライトステップ２
一つの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();
      }
    }
```
#### リライトステップ４
ロジック８を実行する。
下の図には、左辺はリライトする前のBooleanQuery、右辺はリライトしたBooleanQueryである。
BooleanQuery。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648622/0055425b-d36f-a813-8979-abe8e21969c9.png)