lucene-java-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From yo...@apache.org
Subject 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
Date Wed, 16 Nov 2005 16:40:06 GMT
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"));
+        }
+    }
+}



Mime
View raw message