--- /dev/null
+package org.apache.lucene.search;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.util.LuceneTestCase;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.RandomIndexWriter;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.store.Directory;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+import java.text.DecimalFormat;
+import java.util.Random;
+
+/** Test that BooleanQuery.setMinimumNumberShouldMatch works.
+ */
+public class TestBooleanMinShouldMatch extends LuceneTestCase {
+
+ private static Directory index;
+ private static IndexReader r;
+ private static IndexSearcher s;
+
+ @BeforeClass
+ public static void beforeClass() 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 = newDirectory();
+ RandomIndexWriter w = new RandomIndexWriter(random, index);
+
+ for (int i = 0; i < data.length; i++) {
+ Document doc = new Document();
+ doc.add(newField("id", String.valueOf(i), Field.Store.YES, Field.Index.NOT_ANALYZED));//Field.Keyword("id",String.valueOf(i)));
+ doc.add(newField("all", "all", Field.Store.YES, Field.Index.NOT_ANALYZED));//Field.Keyword("all","all"));
+ if (null != data[i]) {
+ doc.add(newField("data", data[i], Field.Store.YES, Field.Index.ANALYZED));//Field.Text("data",data[i]));
+ }
+ w.addDocument(doc);
+ }
+
+ r = w.getReader();
+ s = newSearcher(r);
+ w.close();
+//System.out.println("Set up " + getName());
+ }
+
+ @AfterClass
+ public static void afterClass() throws Exception {
+ s.close();
+ s = null;
+ r.close();
+ r = null;
+ index.close();
+ index = null;
+ }
+
+
+ public void verifyNrHits(Query q, int expected) throws Exception {
+ ScoreDoc[] h = s.search(q, null, 1000).scoreDocs;
+ if (expected != h.length) {
+ printHits(getName(), h, s);
+ }
+ assertEquals("result count", expected, h.length);
+ QueryUtils.check(random, q,s);
+ }
+
+ public void testAllOptional() throws Exception {
+
+ BooleanQuery q = new BooleanQuery();
+ for (int i = 1; i <=4; i++) {
+ q.add(new TermQuery(new Term("data",""+i)), BooleanClause.Occur.SHOULD);//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" )), BooleanClause.Occur.MUST);//true, false);
+ q.add(new TermQuery(new Term("data", "5" )), BooleanClause.Occur.SHOULD);//false, false);
+ q.add(new TermQuery(new Term("data", "4" )), BooleanClause.Occur.SHOULD);//false, false);
+ q.add(new TermQuery(new Term("data", "3" )), BooleanClause.Occur.SHOULD);//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" )), BooleanClause.Occur.MUST);//true, false);
+ q.add(new TermQuery(new Term("data", "6" )), BooleanClause.Occur.MUST);//true, false);
+ q.add(new TermQuery(new Term("data", "5" )), BooleanClause.Occur.SHOULD);//false, false);
+ q.add(new TermQuery(new Term("data", "4" )), BooleanClause.Occur.SHOULD);//false, false);
+ q.add(new TermQuery(new Term("data", "3" )), BooleanClause.Occur.SHOULD);//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" )), BooleanClause.Occur.SHOULD);//false, false);
+ q.add(new TermQuery(new Term("data", "2" )), BooleanClause.Occur.SHOULD);//false, false);
+ q.add(new TermQuery(new Term("data", "3" )), BooleanClause.Occur.MUST_NOT);//false, true );
+ q.add(new TermQuery(new Term("data", "4" )), BooleanClause.Occur.SHOULD);//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" )), BooleanClause.Occur.SHOULD);//false, false);
+ q.add(new TermQuery(new Term("data", "2" )), BooleanClause.Occur.SHOULD);//false, false);
+ q.add(new TermQuery(new Term("data", "3" )), BooleanClause.Occur.MUST_NOT);//false, true );
+ q.add(new TermQuery(new Term("data", "4" )), BooleanClause.Occur.SHOULD);//false, false);
+ q.add(new TermQuery(new Term("data", "C" )), BooleanClause.Occur.MUST_NOT);//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" )), BooleanClause.Occur.MUST);// true, false);
+ q.add(new TermQuery(new Term("data", "5" )), BooleanClause.Occur.SHOULD);//false, false);
+ q.add(new TermQuery(new Term("data", "4" )), BooleanClause.Occur.SHOULD);//false, false);
+ q.add(new TermQuery(new Term("data", "3" )), BooleanClause.Occur.MUST_NOT);//false, true );
+ q.add(new TermQuery(new Term("data", "2" )), BooleanClause.Occur.SHOULD);//false, false);
+ q.add(new TermQuery(new Term("data", "1" )), BooleanClause.Occur.SHOULD);//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")), BooleanClause.Occur.MUST);//true, false);
+ q.add(new TermQuery(new Term("data", "6" )), BooleanClause.Occur.MUST);//true, false);
+ q.add(new TermQuery(new Term("data", "5" )), BooleanClause.Occur.SHOULD);//false, false);
+ q.add(new TermQuery(new Term("data", "4" )), BooleanClause.Occur.SHOULD);//false, false);
+ q.add(new TermQuery(new Term("data", "3" )), BooleanClause.Occur.MUST_NOT);//false, true );
+ q.add(new TermQuery(new Term("data", "2" )), BooleanClause.Occur.SHOULD);//false, false);
+ q.add(new TermQuery(new Term("data", "1" )), BooleanClause.Occur.SHOULD);//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" )), BooleanClause.Occur.MUST);//true, false);
+ q.add(new TermQuery(new Term("data", "5" )), BooleanClause.Occur.SHOULD);//false, false);
+ q.add(new TermQuery(new Term("data", "4" )), BooleanClause.Occur.SHOULD);//false, false);
+ q.add(new TermQuery(new Term("data", "3" )), BooleanClause.Occur.MUST_NOT);//false, true );
+ q.add(new TermQuery(new Term("data", "2" )), BooleanClause.Occur.SHOULD);//false, false);
+ q.add(new TermQuery(new Term("data", "1" )), BooleanClause.Occur.SHOULD);//false, false);
+ q.add(new TermQuery(new Term("data", "C" )), BooleanClause.Occur.MUST_NOT);//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")), BooleanClause.Occur.MUST);//true, false);
+ q.add(new TermQuery(new Term("data", "6" )), BooleanClause.Occur.MUST);//true, false);
+ q.add(new TermQuery(new Term("data", "5" )), BooleanClause.Occur.SHOULD);//false, false);
+ q.add(new TermQuery(new Term("data", "4" )), BooleanClause.Occur.SHOULD);//false, false);
+ q.add(new TermQuery(new Term("data", "3" )), BooleanClause.Occur.MUST_NOT);//false, true );
+ q.add(new TermQuery(new Term("data", "2" )), BooleanClause.Occur.SHOULD);//false, false);
+ q.add(new TermQuery(new Term("data", "1" )), BooleanClause.Occur.SHOULD);//false, false);
+ q.add(new TermQuery(new Term("data", "C" )), BooleanClause.Occur.MUST_NOT);//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")), BooleanClause.Occur.MUST);//true, false);
+ q.add(new TermQuery(new Term("data", "6" )), BooleanClause.Occur.MUST);//true, false);
+ q.add(new TermQuery(new Term("data", "5" )), BooleanClause.Occur.SHOULD);//false, false);
+ q.add(new TermQuery(new Term("data", "4" )), BooleanClause.Occur.SHOULD);//false, false);
+ q.add(new TermQuery(new Term("data", "3" )), BooleanClause.Occur.MUST_NOT);//false, true );
+ q.add(new TermQuery(new Term("data", "2" )), BooleanClause.Occur.SHOULD);//false, false);
+ q.add(new TermQuery(new Term("data", "1" )), BooleanClause.Occur.SHOULD);//false, false);
+ q.add(new TermQuery(new Term("data", "C" )), BooleanClause.Occur.MUST_NOT);//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" )), BooleanClause.Occur.SHOULD);//false, false);
+ q.add(new TermQuery(new Term("data", "6" )), BooleanClause.Occur.MUST);//true, false);
+ q.add(new TermQuery(new Term("data", "3" )), BooleanClause.Occur.MUST);//true, false);
+ q.add(new TermQuery(new Term("data", "2" )), BooleanClause.Occur.SHOULD);//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" )), BooleanClause.Occur.MUST);//true, false);
+ q.add(new TermQuery(new Term("data", "3" )), BooleanClause.Occur.SHOULD);//false, false);
+ q.add(new TermQuery(new Term("data", "2" )), BooleanClause.Occur.MUST);//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" )), BooleanClause.Occur.MUST);//true, false);
+ q.add(new TermQuery(new Term("data", "2" )), BooleanClause.Occur.MUST);//true, false);
+
+ q.setMinimumNumberShouldMatch(1); // 1 of 0 optional
+
+ verifyNrHits(q, 0);
+ }
+
+ public void testNoOptionalButMin2() throws Exception {
+
+ /* one required, no optional */
+ BooleanQuery q = new BooleanQuery();
+ q.add(new TermQuery(new Term("all", "all" )), BooleanClause.Occur.MUST);//true, false);
+
+ q.setMinimumNumberShouldMatch(1); // 1 of 0 optional
+
+ verifyNrHits(q, 0);
+ }
+
+ public void testRandomQueries() throws Exception {
+ 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(random.nextInt(opt+2));
+ }
+ };
+
+
+
+ // increase number of iterations for more complete testing
+ int num = atLeast(10);
+ for (int i = 0; i < num; i++) {
+ int lev = random.nextInt(maxLev);
+ final long seed = random.nextLong();
+ BooleanQuery q1 = TestBoolean2.randBoolQuery(new Random(seed), true, lev, field, vals, null);
+ // BooleanQuery q2 = TestBoolean2.randBoolQuery(new Random(seed), lev, field, vals, minNrCB);
+ BooleanQuery q2 = TestBoolean2.randBoolQuery(new Random(seed), true, 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);
+ if (i < 100) {
+ QueryUtils.check(random, q1,s);
+ QueryUtils.check(random, q2,s);
+ }
+ // The constrained query
+ // should be a superset to the unconstrained query.
+ if (top2.totalHits > top1.totalHits) {
+ 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) {
+ 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) 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, ScoreDoc[] h, Searcher searcher) throws Exception {
+
+ System.err.println("------- " + test + " -------");
+
+ DecimalFormat f = new DecimalFormat("0.000000");
+
+ for (int i = 0; i < h.length; i++) {
+ Document d = searcher.doc(h[i].doc);
+ float score = h[i].score;
+ System.err.println("#" + i + ": " + f.format(score) + " - " +
+ d.get("id") + " - " + d.get("data"));
+ }
+ }
+}