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である。
SHOULD(最低一つ)とMUST(最低一つ)を持つTermQuery
リライトステップ1
同じくまずロジック2を実行する。
リライトステップ2「オプション」
同じくまずロジック7を実行する。下の図には、左辺はリライトする前のBooleanQuery、右辺はリライトしたBooleanQueryである。
リライトステップ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である。
多数の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である。
SHOULD(最低一つ)とMUST_NOT(最低一つ)を持つTermQuery
リライトステップ1
同じくまずロジック2を実行する。
リライトステップ2
同じくまずロジック4を実行する。
リライトステップ3「オプション」
ロジック7を実行する。
下の図には、左辺はリライトする前のBooleanQuery、右辺はリライトしたBooleanQueryである。
SHOULD(最低一つ)とMUST(最低一つ)MUST_NOT(最低一つ)を持つTermQuery
リライトステップ1
同じくまずロジック2を実行する。
リライトステップ2
同じくまずロジック4を実行する。
リライトステップ3
ロジック7を実行する。
リライトステップ4「オプション」
ロジック8を実行する。
下の図には、左辺はリライトする前のBooleanQuery、右辺はリライトしたBooleanQueryである。
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である。
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。