BooleanQuery/BooleanScorer minNrShouldMatch implementation: LUCENE-395

git-svn-id: https://svn.apache.org/repos/asf/lucene/java/trunk@345056 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Yonik Seeley 2005-11-16 16:39:59 +00:00
parent 31c271c84b
commit c40c7cd114
4 changed files with 542 additions and 82 deletions

View File

@ -179,6 +179,10 @@ New features
number of terms the range can cover. Both endpoints may also be open. number of terms the range can cover. Both endpoints may also be open.
(Yonik Seeley, LUCENE-383) (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 API Changes
1. Several methods and fields have been deprecated. The API documentation 1. Several methods and fields have been deprecated. The API documentation

View File

@ -107,6 +107,41 @@ public class BooleanQuery extends Query {
return result; 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: /** Adds a clause to a boolean query. Clauses may be:
* <ul> * <ul>
* <li><code>required</code> which means that documents which <i>do not</i> * <li><code>required</code> which means that documents which <i>do not</i>
@ -299,7 +334,8 @@ public class BooleanQuery extends Query {
* and scores documents in document number order. * and scores documents in document number order.
*/ */
public Scorer scorer(IndexReader reader) throws IOException { 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++) { for (int i = 0 ; i < weights.size(); i++) {
BooleanClause c = (BooleanClause)clauses.elementAt(i); BooleanClause c = (BooleanClause)clauses.elementAt(i);
@ -327,6 +363,12 @@ public class BooleanQuery extends Query {
} }
protected Weight createWeight(Searcher searcher) throws IOException { 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) return getUseScorer14() ? (Weight) new BooleanWeight(searcher)
: (Weight) new BooleanWeight2(searcher); : (Weight) new BooleanWeight2(searcher);
} }
@ -382,7 +424,8 @@ public class BooleanQuery extends Query {
/** Prints a user-readable version of this query. */ /** Prints a user-readable version of this query. */
public String toString(String field) { public String toString(String field) {
StringBuffer buffer = new StringBuffer(); StringBuffer buffer = new StringBuffer();
if (getBoost() != 1.0) { boolean needParens=(getBoost() != 1.0) || (getMinimumNumberShouldMatch()>0) ;
if (needParens) {
buffer.append("("); buffer.append("(");
} }
@ -405,8 +448,17 @@ public class BooleanQuery extends Query {
buffer.append(" "); buffer.append(" ");
} }
if (getBoost() != 1.0) { if (needParens) {
buffer.append(")"); buffer.append(")");
}
if (getMinimumNumberShouldMatch()>0) {
buffer.append('~');
buffer.append(getMinimumNumberShouldMatch());
}
if (getBoost() != 1.0f)
{
buffer.append(ToStringUtils.boost(getBoost())); buffer.append(ToStringUtils.boost(getBoost()));
} }
@ -419,12 +471,14 @@ public class BooleanQuery extends Query {
return false; return false;
BooleanQuery other = (BooleanQuery)o; BooleanQuery other = (BooleanQuery)o;
return (this.getBoost() == other.getBoost()) 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.*/ /** Returns a hash code value for this object.*/
public int hashCode() { public int hashCode() {
return Float.floatToIntBits(getBoost()) ^ clauses.hashCode(); return Float.floatToIntBits(getBoost()) ^ clauses.hashCode()
+ getMinimumNumberShouldMatch();
} }
} }

View File

@ -62,9 +62,33 @@ class BooleanScorer2 extends Scorer {
*/ */
private Scorer countingSumScorer = null; 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); super(similarity);
if (minNrShouldMatch < 0) {
throw new IllegalArgumentException("Minimum number of optional scorers should not be negative");
}
coordinator = new Coordinator(); 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) { public void add(final Scorer scorer, boolean required, boolean prohibited) {
@ -126,10 +150,11 @@ class BooleanScorer2 extends Scorer {
} }
} }
private Scorer countingDisjunctionSumScorer(List scorers) private Scorer countingDisjunctionSumScorer(List scorers,
int minMrShouldMatch)
// each scorer from the list counted as a single matcher // each scorer from the list counted as a single matcher
{ {
return new DisjunctionSumScorer(scorers) { return new DisjunctionSumScorer(scorers, minMrShouldMatch) {
private int lastScoredDoc = -1; private int lastScoredDoc = -1;
public float score() throws IOException { public float score() throws IOException {
if (doc() > lastScoredDoc) { if (doc() > lastScoredDoc) {
@ -143,9 +168,8 @@ class BooleanScorer2 extends Scorer {
private static Similarity defaultSimilarity = new DefaultSimilarity(); private static Similarity defaultSimilarity = new DefaultSimilarity();
private Scorer countingConjunctionSumScorer(List requiredScorers) private Scorer countingConjunctionSumScorer(List requiredScorers) {
// each scorer from the list counted as a single matcher // each scorer from the list counted as a single matcher
{
final int requiredNrMatchers = requiredScorers.size(); final int requiredNrMatchers = requiredScorers.size();
ConjunctionScorer cs = new ConjunctionScorer(defaultSimilarity) { ConjunctionScorer cs = new ConjunctionScorer(defaultSimilarity) {
private int lastScoredDoc = -1; private int lastScoredDoc = -1;
@ -169,91 +193,89 @@ class BooleanScorer2 extends Scorer {
return cs; 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. /** Returns the scorer to be used for match counting and score summing.
* Uses requiredScorers, optionalScorers and prohibitedScorers. * Uses requiredScorers, optionalScorers and prohibitedScorers.
*/ */
private Scorer makeCountingSumScorer() private Scorer makeCountingSumScorer() { // each scorer counted as a single matcher
// each scorer counted as a single matcher return (requiredScorers.size() == 0)
{ ? makeCountingSumScorerNoReq()
if (requiredScorers.size() == 0) { : makeCountingSumScorerSomeReq();
if (optionalScorers.size() == 0) { }
return new NonMatchingScorer(); // only prohibited scorers
} else if (optionalScorers.size() == 1) { private Scorer makeCountingSumScorerNoReq() { // No required scorers
return makeCountingSumScorer2( // the only optional scorer is required if (optionalScorers.size() == 0) {
new SingleMatchScorer((Scorer) optionalScorers.get(0)), return new NonMatchingScorer(); // no clauses or only prohibited clauses
new ArrayList()); // no optional scorers left } else { // No required scorers. At least one optional scorer.
} else { // more than 1 optionalScorers, no required scorers // minNrShouldMatch optional scorers are required, but at least 1
return makeCountingSumScorer2( // at least one optional scorer is required int nrOptRequired = (minNrShouldMatch < 1) ? 1 : minNrShouldMatch;
countingDisjunctionSumScorer(optionalScorers), if (optionalScorers.size() < nrOptRequired) {
new ArrayList()); // no optional scorers left 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);
}
}
}
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.
} }
} 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);
} }
} }
/** Returns the scorer to be used for match counting and score summing. /** 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 requiredCountingSumScorer A required scorer already built.
* @param optionalScorers A list of optional scorers, possibly empty.
*/ */
private Scorer makeCountingSumScorer2( private Scorer addProhibitedScorers(Scorer requiredCountingSumScorer)
Scorer requiredCountingSumScorer,
List optionalScorers) // not match counting
{ {
if (optionalScorers.size() == 0) { // no optional return (prohibitedScorers.size() == 0)
if (prohibitedScorers.size() == 0) { // no prohibited ? requiredCountingSumScorer // no prohibited
return requiredCountingSumScorer; : new ReqExclScorer(requiredCountingSumScorer,
} else if (prohibitedScorers.size() == 1) { // no optional, 1 prohibited ((prohibitedScorers.size() == 1)
return new ReqExclScorer( ? (Scorer) prohibitedScorers.get(0)
requiredCountingSumScorer, : new DisjunctionSumScorer(prohibitedScorers)));
(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));
}
}
/** 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 optionalCountingSumScorer An optional scorer already built.
*/
private Scorer makeCountingSumScorer3(
Scorer requiredCountingSumScorer,
Scorer optionalCountingSumScorer)
{
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);
}
} }
/** Scores and collects all matching documents. /** Scores and collects all matching documents.

View File

@ -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"));
}
}
}