1 package org.apache.lucene.search;
4 * Licensed to the Apache Software Foundation (ASF) under one or more
5 * contributor license agreements. See the NOTICE file distributed with
6 * this work for additional information regarding copyright ownership.
7 * The ASF licenses this file to You under the Apache License, Version 2.0
8 * (the "License"); you may not use this file except in compliance with
9 * the License. You may obtain a copy of the License at
11 * http://www.apache.org/licenses/LICENSE-2.0
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
20 import org.apache.lucene.index.IndexReader;
21 import org.apache.lucene.index.Term;
22 import org.apache.lucene.util.ToStringUtils;
23 import org.apache.lucene.search.BooleanClause.Occur;
25 import java.io.IOException;
28 /** A Query that matches documents matching boolean combinations of other
29 * queries, e.g. {@link TermQuery}s, {@link PhraseQuery}s or other
32 public class BooleanQuery extends Query implements Iterable<BooleanClause> {
34 private static int maxClauseCount = 1024;
36 /** Thrown when an attempt is made to add more than {@link
37 * #getMaxClauseCount()} clauses. This typically happens if
38 * a PrefixQuery, FuzzyQuery, WildcardQuery, or TermRangeQuery
39 * is expanded to many terms during search.
41 public static class TooManyClauses extends RuntimeException {
42 public TooManyClauses() {
43 super("maxClauseCount is set to " + maxClauseCount);
47 /** Return the maximum number of clauses permitted, 1024 by default.
48 * Attempts to add more than the permitted number of clauses cause {@link
49 * TooManyClauses} to be thrown.
50 * @see #setMaxClauseCount(int)
52 public static int getMaxClauseCount() { return maxClauseCount; }
55 * Set the maximum number of clauses permitted per BooleanQuery.
56 * Default value is 1024.
58 public static void setMaxClauseCount(int maxClauseCount) {
59 if (maxClauseCount < 1)
60 throw new IllegalArgumentException("maxClauseCount must be >= 1");
61 BooleanQuery.maxClauseCount = maxClauseCount;
64 private ArrayList<BooleanClause> clauses = new ArrayList<BooleanClause>();
65 private final boolean disableCoord;
67 /** Constructs an empty boolean query. */
68 public BooleanQuery() {
72 /** Constructs an empty boolean query.
74 * {@link Similarity#coord(int,int)} may be disabled in scoring, as
75 * appropriate. For example, this score factor does not make sense for most
76 * automatically generated queries, like {@link WildcardQuery} and {@link
79 * @param disableCoord disables {@link Similarity#coord(int,int)} in scoring.
81 public BooleanQuery(boolean disableCoord) {
82 this.disableCoord = disableCoord;
85 /** Returns true iff {@link Similarity#coord(int,int)} is disabled in
86 * scoring for this query instance.
87 * @see #BooleanQuery(boolean)
89 public boolean isCoordDisabled() { return disableCoord; }
92 * Specifies a minimum number of the optional BooleanClauses
93 * which must be satisfied.
96 * By default no optional clauses are necessary for a match
97 * (unless there are no required clauses). If this method is used,
98 * then the specified number of clauses is required.
101 * Use of this method is totally independent of specifying that
102 * any specific clauses are required (or prohibited). This number will
103 * only be compared against the number of matching optional clauses.
106 * @param min the number of optional clauses that must match
108 public void setMinimumNumberShouldMatch(int min) {
109 this.minNrShouldMatch = min;
111 protected int minNrShouldMatch = 0;
114 * Gets the minimum number of the optional BooleanClauses
115 * which must be satisfied.
117 public int getMinimumNumberShouldMatch() {
118 return minNrShouldMatch;
121 /** Adds a clause to a boolean query.
123 * @throws TooManyClauses if the new number of clauses exceeds the maximum clause number
124 * @see #getMaxClauseCount()
126 public void add(Query query, BooleanClause.Occur occur) {
127 add(new BooleanClause(query, occur));
130 /** Adds a clause to a boolean query.
131 * @throws TooManyClauses if the new number of clauses exceeds the maximum clause number
132 * @see #getMaxClauseCount()
134 public void add(BooleanClause clause) {
135 if (clauses.size() >= maxClauseCount)
136 throw new TooManyClauses();
141 /** Returns the set of clauses in this query. */
142 public BooleanClause[] getClauses() {
143 return clauses.toArray(new BooleanClause[clauses.size()]);
146 /** Returns the list of clauses in this query. */
147 public List<BooleanClause> clauses() { return clauses; }
149 /** Returns an iterator on the clauses in this query. It implements the {@link Iterable} interface to
150 * make it possible to do:
151 * <pre>for (BooleanClause clause : booleanQuery) {}</pre>
153 public final Iterator<BooleanClause> iterator() { return clauses().iterator(); }
156 * Expert: the Weight for BooleanQuery, used to
157 * normalize, score and explain these queries.
159 * <p>NOTE: this API and implementation is subject to
160 * change suddenly in the next release.</p>
162 protected class BooleanWeight extends Weight {
163 /** The Similarity implementation. */
164 protected Similarity similarity;
165 protected ArrayList<Weight> weights;
166 protected int maxCoord; // num optional + num required
167 private final boolean disableCoord;
169 public BooleanWeight(Searcher searcher, boolean disableCoord)
171 this.similarity = getSimilarity(searcher);
172 this.disableCoord = disableCoord;
173 weights = new ArrayList<Weight>(clauses.size());
174 for (int i = 0 ; i < clauses.size(); i++) {
175 BooleanClause c = clauses.get(i);
176 weights.add(c.getQuery().createWeight(searcher));
177 if (!c.isProhibited()) maxCoord++;
182 public Query getQuery() { return BooleanQuery.this; }
185 public float getValue() { return getBoost(); }
188 public float sumOfSquaredWeights() throws IOException {
190 for (int i = 0 ; i < weights.size(); i++) {
191 // call sumOfSquaredWeights for all clauses in case of side effects
192 float s = weights.get(i).sumOfSquaredWeights(); // sum sub weights
193 if (!clauses.get(i).isProhibited())
194 // only add to sum for non-prohibited clauses
198 sum *= getBoost() * getBoost(); // boost each sub-weight
205 public void normalize(float norm) {
206 norm *= getBoost(); // incorporate boost
207 for (Weight w : weights) {
208 // normalize all clauses, (even if prohibited in case of side affects)
214 public Explanation explain(IndexReader reader, int doc)
216 final int minShouldMatch =
217 BooleanQuery.this.getMinimumNumberShouldMatch();
218 ComplexExplanation sumExpl = new ComplexExplanation();
219 sumExpl.setDescription("sum of:");
222 boolean fail = false;
223 int shouldMatchCount = 0;
224 Iterator<BooleanClause> cIter = clauses.iterator();
225 for (Iterator<Weight> wIter = weights.iterator(); wIter.hasNext();) {
226 Weight w = wIter.next();
227 BooleanClause c = cIter.next();
228 if (w.scorer(reader, true, true) == null) {
229 if (c.isRequired()) {
231 Explanation r = new Explanation(0.0f, "no match on required clause (" + c.getQuery().toString() + ")");
232 sumExpl.addDetail(r);
236 Explanation e = w.explain(reader, doc);
238 if (!c.isProhibited()) {
239 sumExpl.addDetail(e);
244 new Explanation(0.0f, "match on prohibited clause (" + c.getQuery().toString() + ")");
246 sumExpl.addDetail(r);
249 if (c.getOccur() == Occur.SHOULD)
251 } else if (c.isRequired()) {
252 Explanation r = new Explanation(0.0f, "no match on required clause (" + c.getQuery().toString() + ")");
254 sumExpl.addDetail(r);
259 sumExpl.setMatch(Boolean.FALSE);
260 sumExpl.setValue(0.0f);
261 sumExpl.setDescription
262 ("Failure to meet condition(s) of required/prohibited clause(s)");
264 } else if (shouldMatchCount < minShouldMatch) {
265 sumExpl.setMatch(Boolean.FALSE);
266 sumExpl.setValue(0.0f);
267 sumExpl.setDescription("Failure to match minimum number "+
268 "of optional clauses: " + minShouldMatch);
272 sumExpl.setMatch(0 < coord ? Boolean.TRUE : Boolean.FALSE);
273 sumExpl.setValue(sum);
275 final float coordFactor = disableCoord ? 1.0f : similarity.coord(coord, maxCoord);
276 if (coordFactor == 1.0f) {
277 return sumExpl; // eliminate wrapper
279 ComplexExplanation result = new ComplexExplanation(sumExpl.isMatch(),
282 result.addDetail(sumExpl);
283 result.addDetail(new Explanation(coordFactor,
284 "coord("+coord+"/"+maxCoord+")"));
290 public Scorer scorer(IndexReader reader, boolean scoreDocsInOrder, boolean topScorer)
292 List<Scorer> required = new ArrayList<Scorer>();
293 List<Scorer> prohibited = new ArrayList<Scorer>();
294 List<Scorer> optional = new ArrayList<Scorer>();
295 Iterator<BooleanClause> cIter = clauses.iterator();
296 for (Weight w : weights) {
297 BooleanClause c = cIter.next();
298 Scorer subScorer = w.scorer(reader, true, false);
299 if (subScorer == null) {
300 if (c.isRequired()) {
303 } else if (c.isRequired()) {
304 required.add(subScorer);
305 } else if (c.isProhibited()) {
306 prohibited.add(subScorer);
308 optional.add(subScorer);
312 // Check if we can return a BooleanScorer
313 if (!scoreDocsInOrder && topScorer && required.size() == 0) {
314 return new BooleanScorer(this, disableCoord, similarity, minNrShouldMatch, optional, prohibited, maxCoord);
317 if (required.size() == 0 && optional.size() == 0) {
318 // no required and optional clauses.
320 } else if (optional.size() < minNrShouldMatch) {
321 // either >1 req scorer, or there are 0 req scorers and at least 1
322 // optional scorer. Therefore if there are not enough optional scorers
323 // no documents will be matched by the query
327 // Return a BooleanScorer2
328 return new BooleanScorer2(this, disableCoord, similarity, minNrShouldMatch, required, prohibited, optional, maxCoord);
332 public boolean scoresDocsOutOfOrder() {
333 for (BooleanClause c : clauses) {
334 if (c.isRequired()) {
335 return false; // BS2 (in-order) will be used by scorer()
339 // scorer() will return an out-of-order scorer if requested.
346 public Weight createWeight(Searcher searcher) throws IOException {
347 return new BooleanWeight(searcher, disableCoord);
351 public Query rewrite(IndexReader reader) throws IOException {
352 if (minNrShouldMatch == 0 && clauses.size() == 1) { // optimize 1-clause queries
353 BooleanClause c = clauses.get(0);
354 if (!c.isProhibited()) { // just return clause
356 Query query = c.getQuery().rewrite(reader); // rewrite first
358 if (getBoost() != 1.0f) { // incorporate boost
359 if (query == c.getQuery()) // if rewrite was no-op
360 query = (Query)query.clone(); // then clone before boost
361 query.setBoost(getBoost() * query.getBoost());
368 BooleanQuery clone = null; // recursively rewrite
369 for (int i = 0 ; i < clauses.size(); i++) {
370 BooleanClause c = clauses.get(i);
371 Query query = c.getQuery().rewrite(reader);
372 if (query != c.getQuery()) { // clause rewrote: must clone
374 clone = (BooleanQuery)this.clone();
375 clone.clauses.set(i, new BooleanClause(query, c.getOccur()));
379 return clone; // some clauses rewrote
381 return this; // no clauses rewrote
386 public void extractTerms(Set<Term> terms) {
387 for (BooleanClause clause : clauses) {
388 clause.getQuery().extractTerms(terms);
392 @Override @SuppressWarnings("unchecked")
393 public Object clone() {
394 BooleanQuery clone = (BooleanQuery)super.clone();
395 clone.clauses = (ArrayList<BooleanClause>) this.clauses.clone();
399 /** Prints a user-readable version of this query. */
401 public String toString(String field) {
402 StringBuilder buffer = new StringBuilder();
403 boolean needParens=(getBoost() != 1.0) || (getMinimumNumberShouldMatch()>0) ;
408 for (int i = 0 ; i < clauses.size(); i++) {
409 BooleanClause c = clauses.get(i);
410 if (c.isProhibited())
412 else if (c.isRequired())
415 Query subQuery = c.getQuery();
416 if (subQuery != null) {
417 if (subQuery instanceof BooleanQuery) { // wrap sub-bools in parens
419 buffer.append(subQuery.toString(field));
422 buffer.append(subQuery.toString(field));
425 buffer.append("null");
428 if (i != clauses.size()-1)
436 if (getMinimumNumberShouldMatch()>0) {
438 buffer.append(getMinimumNumberShouldMatch());
441 if (getBoost() != 1.0f)
443 buffer.append(ToStringUtils.boost(getBoost()));
446 return buffer.toString();
449 /** Returns true iff <code>o</code> is equal to this. */
451 public boolean equals(Object o) {
452 if (!(o instanceof BooleanQuery))
454 BooleanQuery other = (BooleanQuery)o;
455 return (this.getBoost() == other.getBoost())
456 && this.clauses.equals(other.clauses)
457 && this.getMinimumNumberShouldMatch() == other.getMinimumNumberShouldMatch()
458 && this.disableCoord == other.disableCoord;
461 /** Returns a hash code value for this object.*/
463 public int hashCode() {
464 return Float.floatToIntBits(getBoost()) ^ clauses.hashCode()
465 + getMinimumNumberShouldMatch() + (disableCoord ? 17:0);