You are viewing a plain text version of this content. The canonical link for it is here.
Posted to java-commits@lucene.apache.org by yo...@apache.org on 2005/11/16 17:40:06 UTC
svn commit: r345056 - in /lucene/java/trunk: CHANGES.txt
src/java/org/apache/lucene/search/BooleanQuery.java
src/java/org/apache/lucene/search/BooleanScorer2.java
src/test/org/apache/lucene/search/TestBooleanMinShouldMatch.java
Author: yonik
Date: Wed Nov 16 08:39:59 2005
New Revision: 345056
URL: http://svn.apache.org/viewcvs?rev=345056&view=rev
Log:
BooleanQuery/BooleanScorer minNrShouldMatch implementation: LUCENE-395
Added:
lucene/java/trunk/src/test/org/apache/lucene/search/TestBooleanMinShouldMatch.java
Modified:
lucene/java/trunk/CHANGES.txt
lucene/java/trunk/src/java/org/apache/lucene/search/BooleanQuery.java
lucene/java/trunk/src/java/org/apache/lucene/search/BooleanScorer2.java
Modified: lucene/java/trunk/CHANGES.txt
URL: http://svn.apache.org/viewcvs/lucene/java/trunk/CHANGES.txt?rev=345056&r1=345055&r2=345056&view=diff
==============================================================================
--- lucene/java/trunk/CHANGES.txt (original)
+++ lucene/java/trunk/CHANGES.txt Wed Nov 16 08:39:59 2005
@@ -179,6 +179,10 @@
number of terms the range can cover. Both endpoints may also be open.
(Yonik Seeley, LUCENE-383)
+26. Added ability to specify a minimum number of optional clauses that
+ must match in a BooleanQuery. See BooleanQuery.setMinimumNumberShouldMatch().
+ (Paul Elschot, Chris Hostetter via Yonik Seeley, LUCENE-395)
+
API Changes
1. Several methods and fields have been deprecated. The API documentation
Modified: lucene/java/trunk/src/java/org/apache/lucene/search/BooleanQuery.java
URL: http://svn.apache.org/viewcvs/lucene/java/trunk/src/java/org/apache/lucene/search/BooleanQuery.java?rev=345056&r1=345055&r2=345056&view=diff
==============================================================================
--- lucene/java/trunk/src/java/org/apache/lucene/search/BooleanQuery.java (original)
+++ lucene/java/trunk/src/java/org/apache/lucene/search/BooleanQuery.java Wed Nov 16 08:39:59 2005
@@ -107,6 +107,41 @@
return result;
}
+ /**
+ * Specifies a minimum number of the optional BooleanClauses
+ * which must be satisifed.
+ *
+ * <p>
+ * By default no optional clauses are neccessary for a match
+ * (unless there are no required clauses). If this method is used,
+ * then the specified numebr of clauses is required.
+ * </p>
+ * <p>
+ * Use of this method is totally independant of specifying that
+ * any specific clauses are required (or prohibited). This number will
+ * only be compared against the number of matching optional clauses.
+ * </p>
+ * <p>
+ * EXPERT NOTE: Using this method will force the use of BooleanWeight2,
+ * regardless of wether setUseScorer14(true) has been called.
+ * </p>
+ *
+ * @param min the number of optional clauses that must match
+ * @see #setUseScorer14
+ */
+ public void setMinimumNumberShouldMatch(int min) {
+ this.minNrShouldMatch = min;
+ }
+ protected int minNrShouldMatch = 0;
+
+ /**
+ * Gets the minimum number of the optional BooleanClauses
+ * which must be satisifed.
+ */
+ public int getMinimumNumberShouldMatch() {
+ return minNrShouldMatch;
+ }
+
/** Adds a clause to a boolean query. Clauses may be:
* <ul>
* <li><code>required</code> which means that documents which <i>do not</i>
@@ -299,7 +334,8 @@
* and scores documents in document number order.
*/
public Scorer scorer(IndexReader reader) throws IOException {
- BooleanScorer2 result = new BooleanScorer2(similarity);
+ BooleanScorer2 result = new BooleanScorer2(similarity,
+ minNrShouldMatch);
for (int i = 0 ; i < weights.size(); i++) {
BooleanClause c = (BooleanClause)clauses.elementAt(i);
@@ -327,6 +363,12 @@
}
protected Weight createWeight(Searcher searcher) throws IOException {
+
+ if (0 < minNrShouldMatch) {
+ // :TODO: should we throw an exception if getUseScorer14 ?
+ return new BooleanWeight2(searcher);
+ }
+
return getUseScorer14() ? (Weight) new BooleanWeight(searcher)
: (Weight) new BooleanWeight2(searcher);
}
@@ -382,7 +424,8 @@
/** Prints a user-readable version of this query. */
public String toString(String field) {
StringBuffer buffer = new StringBuffer();
- if (getBoost() != 1.0) {
+ boolean needParens=(getBoost() != 1.0) || (getMinimumNumberShouldMatch()>0) ;
+ if (needParens) {
buffer.append("(");
}
@@ -405,8 +448,17 @@
buffer.append(" ");
}
- if (getBoost() != 1.0) {
+ if (needParens) {
buffer.append(")");
+ }
+
+ if (getMinimumNumberShouldMatch()>0) {
+ buffer.append('~');
+ buffer.append(getMinimumNumberShouldMatch());
+ }
+
+ if (getBoost() != 1.0f)
+ {
buffer.append(ToStringUtils.boost(getBoost()));
}
@@ -419,12 +471,14 @@
return false;
BooleanQuery other = (BooleanQuery)o;
return (this.getBoost() == other.getBoost())
- && this.clauses.equals(other.clauses);
+ && this.clauses.equals(other.clauses)
+ && this.getMinimumNumberShouldMatch() == other.getMinimumNumberShouldMatch();
}
/** Returns a hash code value for this object.*/
public int hashCode() {
- return Float.floatToIntBits(getBoost()) ^ clauses.hashCode();
+ return Float.floatToIntBits(getBoost()) ^ clauses.hashCode()
+ + getMinimumNumberShouldMatch();
}
}
Modified: lucene/java/trunk/src/java/org/apache/lucene/search/BooleanScorer2.java
URL: http://svn.apache.org/viewcvs/lucene/java/trunk/src/java/org/apache/lucene/search/BooleanScorer2.java?rev=345056&r1=345055&r2=345056&view=diff
==============================================================================
--- lucene/java/trunk/src/java/org/apache/lucene/search/BooleanScorer2.java (original)
+++ lucene/java/trunk/src/java/org/apache/lucene/search/BooleanScorer2.java Wed Nov 16 08:39:59 2005
@@ -62,9 +62,33 @@
*/
private Scorer countingSumScorer = null;
- public BooleanScorer2(Similarity similarity) {
+ /** The number of optionalScorers that need to match (if there are any) */
+ private final int minNrShouldMatch;
+
+ /** Create a BooleanScorer2.
+ * @param similarity The similarity to be used.
+ * @param minNrShouldMatch The minimum number of optional added scorers
+ * that should match during the search.
+ * In case no required scorers are added,
+ * at least one of the optional scorers will have to
+ * match during the search.
+ */
+ public BooleanScorer2(Similarity similarity, int minNrShouldMatch) {
super(similarity);
+ if (minNrShouldMatch < 0) {
+ throw new IllegalArgumentException("Minimum number of optional scorers should not be negative");
+ }
coordinator = new Coordinator();
+ this.minNrShouldMatch = minNrShouldMatch;
+ }
+
+ /** Create a BooleanScorer2.
+ * In no required scorers are added,
+ * at least one of the optional scorers will have to match during the search.
+ * @param similarity The similarity to be used.
+ */
+ public BooleanScorer2(Similarity similarity) {
+ this(similarity, 0);
}
public void add(final Scorer scorer, boolean required, boolean prohibited) {
@@ -126,10 +150,11 @@
}
}
- private Scorer countingDisjunctionSumScorer(List scorers)
+ private Scorer countingDisjunctionSumScorer(List scorers,
+ int minMrShouldMatch)
// each scorer from the list counted as a single matcher
{
- return new DisjunctionSumScorer(scorers) {
+ return new DisjunctionSumScorer(scorers, minMrShouldMatch) {
private int lastScoredDoc = -1;
public float score() throws IOException {
if (doc() > lastScoredDoc) {
@@ -143,9 +168,8 @@
private static Similarity defaultSimilarity = new DefaultSimilarity();
- private Scorer countingConjunctionSumScorer(List requiredScorers)
- // each scorer from the list counted as a single matcher
- {
+ private Scorer countingConjunctionSumScorer(List requiredScorers) {
+ // each scorer from the list counted as a single matcher
final int requiredNrMatchers = requiredScorers.size();
ConjunctionScorer cs = new ConjunctionScorer(defaultSimilarity) {
private int lastScoredDoc = -1;
@@ -169,91 +193,89 @@
return cs;
}
+ private Scorer dualConjunctionSumScorer(Scorer req1, Scorer req2) { // non counting.
+ final int requiredNrMatchers = requiredScorers.size();
+ ConjunctionScorer cs = new ConjunctionScorer(defaultSimilarity);
+ // All scorers match, so defaultSimilarity super.score() always has 1 as
+ // the coordination factor.
+ // Therefore the sum of the scores of two scorers
+ // is used as score.
+ cs.add(req1);
+ cs.add(req2);
+ return cs;
+ }
+
/** Returns the scorer to be used for match counting and score summing.
* Uses requiredScorers, optionalScorers and prohibitedScorers.
*/
- private Scorer makeCountingSumScorer()
- // each scorer counted as a single matcher
- {
- if (requiredScorers.size() == 0) {
- if (optionalScorers.size() == 0) {
- return new NonMatchingScorer(); // only prohibited scorers
- } else if (optionalScorers.size() == 1) {
- return makeCountingSumScorer2( // the only optional scorer is required
- new SingleMatchScorer((Scorer) optionalScorers.get(0)),
- new ArrayList()); // no optional scorers left
- } else { // more than 1 optionalScorers, no required scorers
- return makeCountingSumScorer2( // at least one optional scorer is required
- countingDisjunctionSumScorer(optionalScorers),
- new ArrayList()); // no optional scorers left
- }
- } else if (requiredScorers.size() == 1) { // 1 required
- return makeCountingSumScorer2(
- new SingleMatchScorer((Scorer) requiredScorers.get(0)),
- optionalScorers);
- } else { // more required scorers
- return makeCountingSumScorer2(
- countingConjunctionSumScorer(requiredScorers),
- optionalScorers);
+ private Scorer makeCountingSumScorer() { // each scorer counted as a single matcher
+ return (requiredScorers.size() == 0)
+ ? makeCountingSumScorerNoReq()
+ : makeCountingSumScorerSomeReq();
+ }
+
+ private Scorer makeCountingSumScorerNoReq() { // No required scorers
+ if (optionalScorers.size() == 0) {
+ return new NonMatchingScorer(); // no clauses or only prohibited clauses
+ } else { // No required scorers. At least one optional scorer.
+ // minNrShouldMatch optional scorers are required, but at least 1
+ int nrOptRequired = (minNrShouldMatch < 1) ? 1 : minNrShouldMatch;
+ if (optionalScorers.size() < nrOptRequired) {
+ return new NonMatchingScorer(); // fewer optional clauses than minimum (at least 1) that should match
+ } else { // optionalScorers.size() >= nrOptRequired, no required scorers
+ Scorer requiredCountingSumScorer =
+ (optionalScorers.size() > nrOptRequired)
+ ? countingDisjunctionSumScorer(optionalScorers, nrOptRequired)
+ : // optionalScorers.size() == nrOptRequired (all optional scorers are required), no required scorers
+ (optionalScorers.size() == 1)
+ ? new SingleMatchScorer((Scorer) optionalScorers.get(0))
+ : countingConjunctionSumScorer(optionalScorers);
+ return addProhibitedScorers( requiredCountingSumScorer);
+ }
}
}
- /** Returns the scorer to be used for match counting and score summing.
- * Uses the arguments and prohibitedScorers.
- * @param requiredCountingSumScorer A required scorer already built.
- * @param optionalScorers A list of optional scorers, possibly empty.
- */
- private Scorer makeCountingSumScorer2(
- Scorer requiredCountingSumScorer,
- List optionalScorers) // not match counting
- {
- if (optionalScorers.size() == 0) { // no optional
- if (prohibitedScorers.size() == 0) { // no prohibited
- return requiredCountingSumScorer;
- } else if (prohibitedScorers.size() == 1) { // no optional, 1 prohibited
- return new ReqExclScorer(
- requiredCountingSumScorer,
- (Scorer) prohibitedScorers.get(0)); // not match counting
- } else { // no optional, more prohibited
- return new ReqExclScorer(
- requiredCountingSumScorer,
- new DisjunctionSumScorer(prohibitedScorers)); // score unused. not match counting
- }
- } else if (optionalScorers.size() == 1) { // 1 optional
- return makeCountingSumScorer3(
- requiredCountingSumScorer,
- new SingleMatchScorer((Scorer) optionalScorers.get(0)));
- } else { // more optional
- return makeCountingSumScorer3(
- requiredCountingSumScorer,
- countingDisjunctionSumScorer(optionalScorers));
+ private Scorer makeCountingSumScorerSomeReq() { // At least one required scorer.
+ if (optionalScorers.size() < minNrShouldMatch) {
+ return new NonMatchingScorer(); // fewer optional clauses than minimum that should match
+ } else if (optionalScorers.size() == minNrShouldMatch) { // all optional scorers also required.
+ ArrayList allReq = new ArrayList(requiredScorers);
+ allReq.addAll(optionalScorers);
+ return addProhibitedScorers( countingConjunctionSumScorer(allReq));
+ } else { // optionalScorers.size() > minNrShouldMatch, and at least one required scorer
+ Scorer requiredCountingSumScorer =
+ (requiredScorers.size() == 1)
+ ? new SingleMatchScorer((Scorer) requiredScorers.get(0))
+ : countingConjunctionSumScorer(requiredScorers);
+ if (minNrShouldMatch > 0) { // use a required disjunction scorer over the optional scorers
+ return addProhibitedScorers(
+ dualConjunctionSumScorer( // non counting
+ requiredCountingSumScorer,
+ countingDisjunctionSumScorer(
+ optionalScorers,
+ minNrShouldMatch)));
+ } else { // minNrShouldMatch == 0
+ return new ReqOptSumScorer(
+ addProhibitedScorers(requiredCountingSumScorer),
+ ((optionalScorers.size() == 1)
+ ? new SingleMatchScorer((Scorer) optionalScorers.get(0))
+ : countingDisjunctionSumScorer(optionalScorers, 1))); // require 1 in combined, optional scorer.
+ }
}
}
-
+
/** Returns the scorer to be used for match counting and score summing.
- * Uses the arguments and prohibitedScorers.
+ * Uses the given required scorer and the prohibitedScorers.
* @param requiredCountingSumScorer A required scorer already built.
- * @param optionalCountingSumScorer An optional scorer already built.
*/
- private Scorer makeCountingSumScorer3(
- Scorer requiredCountingSumScorer,
- Scorer optionalCountingSumScorer)
+ private Scorer addProhibitedScorers(Scorer requiredCountingSumScorer)
{
- if (prohibitedScorers.size() == 0) { // no prohibited
- return new ReqOptSumScorer(requiredCountingSumScorer,
- optionalCountingSumScorer);
- } else if (prohibitedScorers.size() == 1) { // 1 prohibited
- return new ReqOptSumScorer(
- new ReqExclScorer(requiredCountingSumScorer,
- (Scorer) prohibitedScorers.get(0)), // not match counting
- optionalCountingSumScorer);
- } else { // more prohibited
- return new ReqOptSumScorer(
- new ReqExclScorer(
- requiredCountingSumScorer,
- new DisjunctionSumScorer(prohibitedScorers)), // score unused. not match counting
- optionalCountingSumScorer);
- }
+ return (prohibitedScorers.size() == 0)
+ ? requiredCountingSumScorer // no prohibited
+ : new ReqExclScorer(requiredCountingSumScorer,
+ ((prohibitedScorers.size() == 1)
+ ? (Scorer) prohibitedScorers.get(0)
+ : new DisjunctionSumScorer(prohibitedScorers)));
}
/** Scores and collects all matching documents.
Added: lucene/java/trunk/src/test/org/apache/lucene/search/TestBooleanMinShouldMatch.java
URL: http://svn.apache.org/viewcvs/lucene/java/trunk/src/test/org/apache/lucene/search/TestBooleanMinShouldMatch.java?rev=345056&view=auto
==============================================================================
--- lucene/java/trunk/src/test/org/apache/lucene/search/TestBooleanMinShouldMatch.java (added)
+++ lucene/java/trunk/src/test/org/apache/lucene/search/TestBooleanMinShouldMatch.java Wed Nov 16 08:39:59 2005
@@ -0,0 +1,380 @@
+package org.apache.lucene.search;
+
+/**
+ * Copyright 2005 Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+import org.apache.lucene.store.RAMDirectory;
+import org.apache.lucene.store.Directory;
+
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.Term;
+
+import org.apache.lucene.analysis.WhitespaceAnalyzer;
+
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+
+import org.apache.lucene.queryParser.QueryParser;
+import org.apache.lucene.queryParser.ParseException;
+
+import junit.framework.TestCase;
+
+import java.text.DecimalFormat;
+import java.util.Random;
+
+/** Test that BooleanQuery.setMinimumNumberShouldMatch works.
+ */
+public class TestBooleanMinShouldMatch extends TestCase {
+
+
+ public Directory index;
+ public IndexReader r;
+ public IndexSearcher s;
+
+ public void setUp() throws Exception {
+
+
+ String[] data = new String [] {
+ "A 1 2 3 4 5 6",
+ "Z 4 5 6",
+ null,
+ "B 2 4 5 6",
+ "Y 3 5 6",
+ null,
+ "C 3 6",
+ "X 4 5 6"
+ };
+
+ index = new RAMDirectory();
+ IndexWriter writer = new IndexWriter(index,
+ new WhitespaceAnalyzer(),
+ true);
+
+ for (int i = 0; i < data.length; i++) {
+ Document doc = new Document();
+ doc.add(Field.Keyword("id",String.valueOf(i)));
+ doc.add(Field.Keyword("all","all"));
+ if (null != data[i]) {
+ doc.add(Field.Text("data",data[i]));
+ }
+ writer.addDocument(doc);
+ }
+
+ writer.optimize();
+ writer.close();
+
+ r = IndexReader.open(index);
+ s = new IndexSearcher(r);
+
+//System.out.println("Set up " + getName());
+ }
+
+ public void verifyNrHits(Query q, int expected) throws Exception {
+ Hits h = s.search(q);
+ if (expected != h.length()) {
+ printHits(getName(), h);
+ }
+ assertEquals("result count", expected, h.length());
+ }
+
+ public void testAllOptional() throws Exception {
+
+ BooleanQuery q = new BooleanQuery();
+ for (int i = 1; i <=4; i++) {
+ q.add(new TermQuery(new Term("data",""+i)), false, false);
+ }
+ q.setMinimumNumberShouldMatch(2); // match at least two of 4
+ verifyNrHits(q, 2);
+ }
+
+ public void testOneReqAndSomeOptional() throws Exception {
+
+ /* one required, some optional */
+ BooleanQuery q = new BooleanQuery();
+ q.add(new TermQuery(new Term("all", "all" )), true, false);
+ q.add(new TermQuery(new Term("data", "5" )), false, false);
+ q.add(new TermQuery(new Term("data", "4" )), false, false);
+ q.add(new TermQuery(new Term("data", "3" )), false, false);
+
+ q.setMinimumNumberShouldMatch(2); // 2 of 3 optional
+
+ verifyNrHits(q, 5);
+ }
+
+ public void testSomeReqAndSomeOptional() throws Exception {
+
+ /* two required, some optional */
+ BooleanQuery q = new BooleanQuery();
+ q.add(new TermQuery(new Term("all", "all" )), true, false);
+ q.add(new TermQuery(new Term("data", "6" )), true, false);
+ q.add(new TermQuery(new Term("data", "5" )), false, false);
+ q.add(new TermQuery(new Term("data", "4" )), false, false);
+ q.add(new TermQuery(new Term("data", "3" )), false, false);
+
+ q.setMinimumNumberShouldMatch(2); // 2 of 3 optional
+
+ verifyNrHits(q, 5);
+ }
+
+ public void testOneProhibAndSomeOptional() throws Exception {
+
+ /* one prohibited, some optional */
+ BooleanQuery q = new BooleanQuery();
+ q.add(new TermQuery(new Term("data", "1" )), false, false);
+ q.add(new TermQuery(new Term("data", "2" )), false, false);
+ q.add(new TermQuery(new Term("data", "3" )), false, true );
+ q.add(new TermQuery(new Term("data", "4" )), false, false);
+
+ q.setMinimumNumberShouldMatch(2); // 2 of 3 optional
+
+ verifyNrHits(q, 1);
+ }
+
+ public void testSomeProhibAndSomeOptional() throws Exception {
+
+ /* two prohibited, some optional */
+ BooleanQuery q = new BooleanQuery();
+ q.add(new TermQuery(new Term("data", "1" )), false, false);
+ q.add(new TermQuery(new Term("data", "2" )), false, false);
+ q.add(new TermQuery(new Term("data", "3" )), false, true );
+ q.add(new TermQuery(new Term("data", "4" )), false, false);
+ q.add(new TermQuery(new Term("data", "C" )), false, true );
+
+ q.setMinimumNumberShouldMatch(2); // 2 of 3 optional
+
+ verifyNrHits(q, 1);
+ }
+
+ public void testOneReqOneProhibAndSomeOptional() throws Exception {
+
+ /* one required, one prohibited, some optional */
+ BooleanQuery q = new BooleanQuery();
+ q.add(new TermQuery(new Term("data", "6" )), true, false);
+ q.add(new TermQuery(new Term("data", "5" )), false, false);
+ q.add(new TermQuery(new Term("data", "4" )), false, false);
+ q.add(new TermQuery(new Term("data", "3" )), false, true );
+ q.add(new TermQuery(new Term("data", "2" )), false, false);
+ q.add(new TermQuery(new Term("data", "1" )), false, false);
+
+ q.setMinimumNumberShouldMatch(3); // 3 of 4 optional
+
+ verifyNrHits(q, 1);
+ }
+
+ public void testSomeReqOneProhibAndSomeOptional() throws Exception {
+
+ /* two required, one prohibited, some optional */
+ BooleanQuery q = new BooleanQuery();
+ q.add(new TermQuery(new Term("all", "all")), true, false);
+ q.add(new TermQuery(new Term("data", "6" )), true, false);
+ q.add(new TermQuery(new Term("data", "5" )), false, false);
+ q.add(new TermQuery(new Term("data", "4" )), false, false);
+ q.add(new TermQuery(new Term("data", "3" )), false, true );
+ q.add(new TermQuery(new Term("data", "2" )), false, false);
+ q.add(new TermQuery(new Term("data", "1" )), false, false);
+
+ q.setMinimumNumberShouldMatch(3); // 3 of 4 optional
+
+ verifyNrHits(q, 1);
+ }
+
+ public void testOneReqSomeProhibAndSomeOptional() throws Exception {
+
+ /* one required, two prohibited, some optional */
+ BooleanQuery q = new BooleanQuery();
+ q.add(new TermQuery(new Term("data", "6" )), true, false);
+ q.add(new TermQuery(new Term("data", "5" )), false, false);
+ q.add(new TermQuery(new Term("data", "4" )), false, false);
+ q.add(new TermQuery(new Term("data", "3" )), false, true );
+ q.add(new TermQuery(new Term("data", "2" )), false, false);
+ q.add(new TermQuery(new Term("data", "1" )), false, false);
+ q.add(new TermQuery(new Term("data", "C" )), false, true );
+
+ q.setMinimumNumberShouldMatch(3); // 3 of 4 optional
+
+ verifyNrHits(q, 1);
+ }
+
+ public void testSomeReqSomeProhibAndSomeOptional() throws Exception {
+
+ /* two required, two prohibited, some optional */
+ BooleanQuery q = new BooleanQuery();
+ q.add(new TermQuery(new Term("all", "all")), true, false);
+ q.add(new TermQuery(new Term("data", "6" )), true, false);
+ q.add(new TermQuery(new Term("data", "5" )), false, false);
+ q.add(new TermQuery(new Term("data", "4" )), false, false);
+ q.add(new TermQuery(new Term("data", "3" )), false, true );
+ q.add(new TermQuery(new Term("data", "2" )), false, false);
+ q.add(new TermQuery(new Term("data", "1" )), false, false);
+ q.add(new TermQuery(new Term("data", "C" )), false, true );
+
+ q.setMinimumNumberShouldMatch(3); // 3 of 4 optional
+
+ verifyNrHits(q, 1);
+ }
+
+ public void testMinHigherThenNumOptional() throws Exception {
+
+ /* two required, two prohibited, some optional */
+ BooleanQuery q = new BooleanQuery();
+ q.add(new TermQuery(new Term("all", "all")), true, false);
+ q.add(new TermQuery(new Term("data", "6" )), true, false);
+ q.add(new TermQuery(new Term("data", "5" )), false, false);
+ q.add(new TermQuery(new Term("data", "4" )), false, false);
+ q.add(new TermQuery(new Term("data", "3" )), false, true );
+ q.add(new TermQuery(new Term("data", "2" )), false, false);
+ q.add(new TermQuery(new Term("data", "1" )), false, false);
+ q.add(new TermQuery(new Term("data", "C" )), false, true );
+
+ q.setMinimumNumberShouldMatch(90); // 90 of 4 optional ?!?!?!
+
+ verifyNrHits(q, 0);
+ }
+
+ public void testMinEqualToNumOptional() throws Exception {
+
+ /* two required, two optional */
+ BooleanQuery q = new BooleanQuery();
+ q.add(new TermQuery(new Term("all", "all" )), false, false);
+ q.add(new TermQuery(new Term("data", "6" )), true, false);
+ q.add(new TermQuery(new Term("data", "3" )), true, false);
+ q.add(new TermQuery(new Term("data", "2" )), false, false);
+
+ q.setMinimumNumberShouldMatch(2); // 2 of 2 optional
+
+ verifyNrHits(q, 1);
+ }
+
+ public void testOneOptionalEqualToMin() throws Exception {
+
+ /* two required, one optional */
+ BooleanQuery q = new BooleanQuery();
+ q.add(new TermQuery(new Term("all", "all" )), true, false);
+ q.add(new TermQuery(new Term("data", "3" )), false, false);
+ q.add(new TermQuery(new Term("data", "2" )), true, false);
+
+ q.setMinimumNumberShouldMatch(1); // 1 of 1 optional
+
+ verifyNrHits(q, 1);
+ }
+
+ public void testNoOptionalButMin() throws Exception {
+
+ /* two required, no optional */
+ BooleanQuery q = new BooleanQuery();
+ q.add(new TermQuery(new Term("all", "all" )), true, false);
+ q.add(new TermQuery(new Term("data", "2" )), true, false);
+
+ q.setMinimumNumberShouldMatch(1); // 1 of 0 optional
+
+ verifyNrHits(q, 0);
+ }
+
+
+ public void testRandomQueries() throws Exception {
+ final Random rnd = new Random(0);
+
+ String field="data";
+ String[] vals = {"1","2","3","4","5","6","A","Z","B","Y","Z","X","foo"};
+ int maxLev=4;
+
+ // callback object to set a random setMinimumNumberShouldMatch
+ TestBoolean2.Callback minNrCB = new TestBoolean2.Callback() {
+ public void postCreate(BooleanQuery q) {
+ BooleanClause[] c =q.getClauses();
+ int opt=0;
+ for (int i=0; i<c.length;i++) {
+ if (c[i].getOccur() == BooleanClause.Occur.SHOULD) opt++;
+ }
+ q.setMinimumNumberShouldMatch(rnd.nextInt(opt+2));
+ }
+ };
+
+
+ int tot=0;
+ // increase number of iterations for more complete testing
+ for (int i=0; i<1000; i++) {
+ int lev = rnd.nextInt(maxLev);
+ BooleanQuery q1 = TestBoolean2.randBoolQuery(new Random(i), lev, field, vals, null);
+ // BooleanQuery q2 = TestBoolean2.randBoolQuery(new Random(i), lev, field, vals, minNrCB);
+ BooleanQuery q2 = TestBoolean2.randBoolQuery(new Random(i), lev, field, vals, null);
+ // only set minimumNumberShouldMatch on the top level query since setting
+ // at a lower level can change the score.
+ minNrCB.postCreate(q2);
+
+ // Can't use Hits because normalized scores will mess things
+ // up. The non-sorting version of search() that returns TopDocs
+ // will not normalize scores.
+ TopDocs top1 = s.search(q1,null,100);
+ TopDocs top2 = s.search(q2,null,100);
+ tot+=top2.totalHits;
+
+ // The constrained query
+ // should be a superset to the unconstrained query.
+ if (top2.totalHits > top1.totalHits) {
+ TestCase.fail("Constrained results not a subset:\n"
+ + CheckHits.topdocsString(top1,0,0)
+ + CheckHits.topdocsString(top2,0,0)
+ + "for query:" + q2.toString());
+ }
+
+ for (int hit=0; hit<top2.totalHits; hit++) {
+ int id = top2.scoreDocs[hit].doc;
+ float score = top2.scoreDocs[hit].score;
+ boolean found=false;
+ // find this doc in other hits
+ for (int other=0; other<top1.totalHits; other++) {
+ if (top1.scoreDocs[other].doc == id) {
+ found=true;
+ float otherScore = top1.scoreDocs[other].score;
+ // check if scores match
+ if (Math.abs(otherScore-score)>1.0e-6f) {
+ TestCase.fail("Doc " + id + " scores don't match\n"
+ + CheckHits.topdocsString(top1,0,0)
+ + CheckHits.topdocsString(top2,0,0)
+ + "for query:" + q2.toString());
+ }
+ }
+ }
+
+ // check if subset
+ if (!found) TestCase.fail("Doc " + id + " not found\n"
+ + CheckHits.topdocsString(top1,0,0)
+ + CheckHits.topdocsString(top2,0,0)
+ + "for query:" + q2.toString());
+ }
+ }
+ // System.out.println("Total hits:"+tot);
+ }
+
+
+
+ protected void printHits(String test, Hits h) throws Exception {
+
+ System.err.println("------- " + test + " -------");
+
+ DecimalFormat f = new DecimalFormat("0.000000");
+
+ for (int i = 0; i < h.length(); i++) {
+ Document d = h.doc(i);
+ float score = h.score(i);
+ System.err.println("#" + i + ": " + f.format(score) + " - " +
+ d.get("id") + " - " + d.get("data"));
+ }
+ }
+}