diff --git a/lucene/core/src/java/org/apache/lucene/search/BooleanArrayScorer.java b/lucene/core/src/java/org/apache/lucene/search/BooleanArrayScorer.java new file mode 100644 index 0000000..32eacf8 --- /dev/null +++ b/lucene/core/src/java/org/apache/lucene/search/BooleanArrayScorer.java @@ -0,0 +1,340 @@ +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 java.io.IOException; +import java.util.Collection; +import java.util.List; + +import org.apache.lucene.search.BooleanQuery.BooleanWeight; + +/** + * This is an improvement of {@link BooleanScorer}. + * It only supports cases where there is at least one MUST clause. + */ +final class BooleanArrayScorer extends Scorer { + + private static final class Bucket { + int doc; // doc id + // score is divided into RS and OS, so that its calculating order + // can be the same as the DAAT procedure. + double requiredScore; // incremental required score + double optionalScore; // incremental optional score + int coord; // count of terms in score + boolean valid; // valid bucket + } + + /** A simple hash table of document scores within a range. */ + private final class BucketTable { + static final int SIZE = 1 << 8; + + private final Bucket[] buckets = new Bucket[SIZE]; + // After collecting more documents, if there are more documents not collected. + boolean more = true; + private int numOfBuckets = 0; + private int numOfValidBuckets = 0; + + BucketTable() { + // Pre-fill to save the lazy init when collecting + // each sub: + for(int idx=0;idxmore will be true if more matching documents may remain. + */ + void collectMoreForce() throws IOException { + do { + // If there are more docs not collected, but no doc is collected in this iteration, + // collect more again. + collectMore(); + } while (more && numOfValidBuckets == 0); + } + + /** + * Collect more docs to bucket table. After calling this method, + * more will be true if more matching documents may remain. + */ + void collectMore() throws IOException { + numOfBuckets = 0; + + // Scan requiredDocs full fill the bucket table + while (numOfBuckets < SIZE) { + final int requiredDocID = requiredScorer.nextDoc(); + if (requiredDocID == DocIdSetIterator.NO_MORE_DOCS) { + more = false; + break; + } + final Bucket bucket = buckets[numOfBuckets ++]; + bucket.doc = requiredDocID; + bucket.coord = requiredNrMatch; + bucket.requiredScore = requiredScorer.score(); + bucket.optionalScore = 0; + bucket.valid = true; + } + numOfValidBuckets = numOfBuckets; + + // Scan prohibitedDocs to remove docs from bucket table. + for (Scorer prohibitedScorer : prohibitedScorers) { + int i = 0; + while (i < numOfBuckets) { + final Bucket bucket = buckets[i]; + int prohibitedDocID = prohibitedScorer.docID(); + if (prohibitedDocID < bucket.doc) { + // According to the definition of .advance(), + // prohibitedDocID should be less then bucket.doc, + // before calling .advance(bucket.doc); + + // Skip to bucket.doc, so that prohibitedDocID >= bucket.doc + prohibitedDocID = prohibitedScorer.advance(bucket.doc); + } + + if (prohibitedDocID == DocIdSetIterator.NO_MORE_DOCS) { + break; + + } else if (prohibitedDocID == bucket.doc) { + // remove the prohibited bucket. + if (bucket.valid) { + numOfValidBuckets --; + bucket.valid = false; + } + i ++; + + } else { // prohibitedDocID > bucket.doc + i = skipsTo(i, prohibitedDocID); + } + } + } + + // Scan optionalDocs to add coord and score. + // TODO: use countLeft to judge whether a bucket should be removed. +// int countLeft = optionalScorers.size(); + for (Scorer optionalScorer : optionalScorers) { +// countLeft --; + int i = 0; + while (i < numOfBuckets) { + final Bucket bucket = buckets[i]; + int optionalDocID = optionalScorer.docID(); + if (optionalDocID < bucket.doc) { + // According to the definition of .advance(), + // optionalDocID should be less then bucket.doc, + // before calling .advance(bucket.doc); + + // Skip to bucket.doc, so that optionalDocID >= bucket.doc + optionalDocID = optionalScorer.advance(bucket.doc); + } + + if (optionalDocID == DocIdSetIterator.NO_MORE_DOCS) { + break; + + } else if (optionalDocID == bucket.doc) { + bucket.coord ++; + bucket.optionalScore += optionalScorer.score(); + i ++; +// if (oldBucket.coord + countLeft < minNrShouldMatch) remove(oldBucket); + + } else { // optionalDocID > bucket.doc + // current bucket skips to optionalDocID. + i = skipsTo(i, optionalDocID); + } + } + } + } + + /** + * Skips to target from begin.
+ * NOTE: Undefined when buckets[i].doc >= target. + * @param begin the index begin to skip, MUST s.t. -1 ≤ begin. + * @param target the target doc to skip. + * @return the first index i s.t. buckets[i].doc >= target. + */ + int skipsTo(int begin, int target) { + final int DELTA = 16; + int i = begin + DELTA; + while (i < numOfBuckets && buckets[i].doc < target) { + i += DELTA; + } + + int end = i; + if (end >= numOfBuckets) { + end = numOfBuckets; + + } else if (buckets[end].doc == target) { + return end; + } + + i = i - DELTA + 1; + while (i < end && buckets[i].doc < target) { + i ++; + } + return i; + } + + void advance(int target) throws IOException { + // advance requiredConjunctionScorer to target doc + if (requiredScorer.docID() < target) { + requiredScorer.advance(target); + } + + // Scan prohibitedDocs to advance to target doc + for (Scorer prohibitedScorer : prohibitedScorers) { + if (prohibitedScorer.docID() < target) { + prohibitedScorer.advance(target); + } + } + + // Scan optionalDocs to advance to target doc + for (Scorer optionalScorer : optionalScorers) { + if (optionalScorer.docID() < target) { + optionalScorer.advance(target); + } + } + } + } + + private final BucketTable bucketTable = new BucketTable(); + private final float[] coordFactors; + private int currentIndex = -1; + private int currentDoc = -1; + + final private Scorer requiredScorer; + final int requiredNrMatch; +// final private List requiredScorers; + final private List optionalScorers; + final private List prohibitedScorers; + // minNrShouldMatch only applies to SHOULD clauses + final private int minNrShouldMatch; + + BooleanArrayScorer(BooleanWeight weight, boolean disableCoord, int minNrShouldMatch, + List requiredScorers, List optionalScorers, List prohibitedScorers, + int maxCoord) throws IOException { + this(weight, disableCoord, minNrShouldMatch, + requiredScorers.isEmpty() ? null : + new ConjunctionScorer(weight, requiredScorers.toArray(new Scorer[requiredScorers.size()])), + requiredScorers.size(), + optionalScorers, prohibitedScorers, maxCoord); + } + + BooleanArrayScorer(BooleanWeight weight, boolean disableCoord, int minNrShouldMatch, + Scorer requiredScorer, int requiredNrMatch, List optionalScorers, List prohibitedScorers, + int maxCoord) throws IOException { + super(weight); + if (requiredNrMatch <= 0) { + throw new IllegalArgumentException("requriedScorers.size() must be > 0"); + } + if (minNrShouldMatch < 0) { + throw new IllegalArgumentException("Minimum number of optional scorers should not be negative"); + } + this.minNrShouldMatch = minNrShouldMatch; + + this.requiredScorer = requiredScorer; + this.requiredNrMatch = requiredNrMatch; + this.optionalScorers = optionalScorers; + this.prohibitedScorers = prohibitedScorers; + + coordFactors = new float[requiredNrMatch + optionalScorers.size() + 1]; + for (int i = 0; i < coordFactors.length; i++) { + coordFactors[i] = disableCoord ? 1.0f : weight.coord(i, maxCoord); + } + } + + @Override + public String toString() { + StringBuffer buffer = new StringBuffer(); + buffer.append("boolean("); + buffer.append("+"); + buffer.append(requiredScorer.toString()); + buffer.append(" "); + for (Scorer optionalScorer : optionalScorers) { + buffer.append(optionalScorer.toString()); + buffer.append(" "); + } + for (Scorer prohibitedScorer : prohibitedScorers) { + buffer.append("-"); + buffer.append(prohibitedScorer.toString()); + buffer.append(" "); + } + // Delete the last whitespace + if (buffer.length() > "boolean(".length()) { + buffer.deleteCharAt(buffer.length() - 1); + } + buffer.append(")"); + return buffer.toString(); + } + + @Override + public float score() throws IOException { + final Bucket bucket = bucketTable.buckets[currentIndex]; + // Cast required score and optional score to float, + // in order to make the calculating procedure is the same as DAAT. + return (float) ((float) bucket.requiredScore + (float) bucket.optionalScore) * coordFactors[bucket.coord]; + } + + @Override + public int freq() throws IOException { + return bucketTable.buckets[currentIndex].coord; + } + + @Override + public int docID() { + return currentDoc; + } + + @Override + public int nextDoc() throws IOException { + currentIndex ++; + if (bucketTable.more && currentIndex >= bucketTable.numOfBuckets) { + bucketTable.collectMoreForce(); + currentIndex = 0; + } + + while (currentIndex < bucketTable.numOfBuckets) { + final Bucket bucket = bucketTable.buckets[currentIndex]; + + if (bucket.valid && bucket.coord - requiredNrMatch >= minNrShouldMatch) { + return currentDoc = bucket.doc; + } + + currentIndex ++; + if (bucketTable.more && currentIndex >= bucketTable.numOfBuckets) { + bucketTable.collectMoreForce(); + currentIndex = 0; + } + } + return currentDoc = DocIdSetIterator.NO_MORE_DOCS; + } + + @Override + public int advance(int target) throws IOException { + return slowAdvance(target); + } + + @Override + public long cost() { + return requiredScorer.cost(); + } + + @Override + public Collection getChildren() { + throw new UnsupportedOperationException(); + } +} diff --git a/lucene/core/src/java/org/apache/lucene/search/BooleanLinkedScorer.java b/lucene/core/src/java/org/apache/lucene/search/BooleanLinkedScorer.java new file mode 100644 index 0000000..d8185b5 --- /dev/null +++ b/lucene/core/src/java/org/apache/lucene/search/BooleanLinkedScorer.java @@ -0,0 +1,387 @@ +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 java.io.IOException; +import java.util.Collection; +import java.util.List; + +import org.apache.lucene.search.BooleanQuery.BooleanWeight; +import org.apache.lucene.util.FixedBitSet; + +/** + * This is an improvement of {@link BooleanScorer}. + * It only supports cases where there is at least one MUST clause. + */ +final class BooleanLinkedScorer extends Scorer { + + + private static final class Bucket { + int doc; // doc id + // score is divided into RS and OS, so that its calculating order + // can be the same as the DAAT procedure. + double requiredScore; // incremental required score + double optionalScore; // incremental optional score + int coord; // count of terms in score + Bucket next; // next valid bucket + Bucket prev; // previous valid bucket + } + + /** A simple hash table of document scores within a range. */ + private final class BucketTable { + static final int SIZE = 1 << 11; + static final int MASK = SIZE - 1; + + private FixedBitSet bitSet = new FixedBitSet(SIZE); + private boolean bitSetIsClean = true; + private final Bucket[] buckets = new Bucket[SIZE]; + private Bucket first = null; // head of valid list + private Bucket last = null; // tail of valid list + // After collecting more documents, if there are more documents not collected. + boolean more = true; + int end = 0; + + BucketTable() { + // Pre-fill to save the lazy init when collecting + // each sub: + for(int idx=0;idxmore will be true if more matching documents may remain. + */ + void collectMoreForce() throws IOException { + do { + // If there are more docs not collected, but no doc is collected in this iteration, + // collect more again. + collectMore(); + } while (more && first == null); + } + + /** + * Collect more docs to bucket table. After calling this method, + * more will be true if more matching documents may remain. + */ + private void collectMore() throws IOException { + if (!bitSetIsClean) { + // clean the bitset if it's not clean + bitSet.clear(0, SIZE); + bitSetIsClean = true; + } + first = last = null; + end += SIZE; + // Scan requiredDocs to build a required doc linked list on bucket table + int requiredDocID = requiredScorer.docID(); + if (requiredDocID < 0) { // should call nextDoc() to get the first document + requiredDocID = requiredScorer.nextDoc(); + } + while (requiredDocID < end) { + Bucket bucket = buckets[requiredDocID & MASK]; + bucket.doc = requiredDocID; + bucket.coord = requiredNrMatch; + bucket.requiredScore = requiredScorer.score(); + bucket.optionalScore = 0; + add(bucket); + requiredDocID = requiredScorer.nextDoc(); + } + if (requiredDocID == DocIdSetIterator.NO_MORE_DOCS) { + more = false; + } + // Scan prohibitedDocs to remove docs from bucket table. + for (Scorer prohibitedScorer : prohibitedScorers) { + Bucket bucket = first; + while (bucket != null) { + int prohibitedDocID = prohibitedScorer.docID(); + if (prohibitedDocID < bucket.doc) { + // According to the definition of .advance(), + // prohibitedDocID should be less then bucket.doc, + // before calling .advance(bucket.doc); + + // Skip to bucket.doc, so that prohibitedDocID >= bucket.doc + prohibitedDocID = prohibitedScorer.advance(bucket.doc); + } + + if (prohibitedDocID == DocIdSetIterator.NO_MORE_DOCS) { + break; + + } else if (prohibitedDocID == bucket.doc) { + // remove the prohibited bucket. + Bucket oldBucket = bucket; + bucket = bucket.next; + remove(oldBucket); + + } else { // prohibitedDocID > bucket.doc + if (prohibitedDocID >= end) { + bucket = null; + } else { + int bucketIndex = bitSet.nextSetBit(prohibitedDocID & MASK); + bucket = bucketIndex < 0 ? null : buckets[bucketIndex]; + } + } + } + } + + // Scan optionalDocs to add coord and score. + // TODO: use countLeft to judge whether a bucket should be removed. +// int countLeft = optionalScorers.size(); + for (Scorer optionalScorer : optionalScorers) { +// countLeft --; + Bucket bucket = first; + while (bucket != null) { + int optionalDocID = optionalScorer.docID(); + if (optionalDocID < bucket.doc) { + // According to the definition of .advance(), + // optionalDocID should be less then bucket.doc, + // before calling .advance(bucket.doc); + + // Skip to bucket.doc, so that optionalDocID >= bucket.doc + optionalDocID = optionalScorer.advance(bucket.doc); + } + + if (optionalDocID == DocIdSetIterator.NO_MORE_DOCS) { + break; + + } else if (optionalDocID == bucket.doc) { + Bucket oldBucket = bucket; + bucket = bucket.next; + oldBucket.coord ++; +// oldBucket.score += optionalScorer.score(); + oldBucket.optionalScore += optionalScorer.score(); +// if (oldBucket.coord + countLeft < minNrShouldMatch) remove(oldBucket); + + } else { // docID > bucket.doc + // current bucket advances to prohibtedDocID. + if (optionalDocID >= end) { + bucket = null; + } else { + int bucketIndex = bitSet.nextSetBit(optionalDocID & MASK); + bucket = bucketIndex < 0 ? null : buckets[bucketIndex]; + } + } + } + } + + } + + /** + * Remove bucket from the double-linked list [first, last]. + * @param bucket bucket to be removed from the double-linked list. + */ + private void remove(Bucket bucket) { + bitSet.clear(bucket.doc & MASK); // remove the bucket on bitset + if (first == bucket && last == bucket) { + first = last = null; + return; + } + if (first == bucket) { + first = first.next; + first.prev = null; + return; + } + if (last == bucket) { + last = last.prev; + last.next = null; + return; + } + Bucket prev = bucket.prev; + Bucket next = bucket.next; + prev.next = next; + next.prev = prev; + } + + /** + * Add bucket to the tail of double-linked list [first, last]. + * @param bucket bucket to be added to the double-linked list. + */ + private void add(Bucket bucket) { + bitSet.set(bucket.doc & MASK); // add the bucket to bitset + bitSetIsClean = false; // the bitset is not clean now + if (first == null) { + first = last = bucket; + bucket.prev = null; + bucket.next = null; + return; + } + last.next = bucket; + bucket.prev = last; + bucket.next = null; + last = bucket; + } + + void advance(int target) throws IOException { + end = target & ~MASK; + // advance requiredConjunctionScorer to target doc + if (requiredScorer.docID() < target) { + requiredScorer.advance(target); + } + + // Scan prohibitedDocs to advance to target doc. + for (Scorer prohibitedScorer : prohibitedScorers) { + if (prohibitedScorer.docID() < target) { + prohibitedScorer.advance(target); + } + } + + // Scan optionalDocs to advance to target doc. + for (Scorer optionalScorer : optionalScorers) { + if (optionalScorer.docID() < target) { + optionalScorer.advance(target); + } + } + current = null; + currentDoc = -1; + } + } + + private final BucketTable bucketTable = new BucketTable(); + private final float[] coordFactors; + // minNrShouldMatch only applies to SHOULD clauses + private Bucket current = null; + private int currentDoc = -1; + + final private Scorer requiredScorer; + final int requiredNrMatch; + final private List optionalScorers; + final private List prohibitedScorers; + final private int minNrShouldMatch; + + BooleanLinkedScorer(BooleanWeight weight, boolean disableCoord, int minNrShouldMatch, + List requiredScorers, List optionalScorers, List prohibitedScorers, + int maxCoord) throws IOException { + this(weight, disableCoord, minNrShouldMatch, + requiredScorers.isEmpty() ? null : + new ConjunctionScorer(weight, requiredScorers.toArray(new Scorer[requiredScorers.size()])), + requiredScorers.size(), + optionalScorers, prohibitedScorers, maxCoord); + } + + BooleanLinkedScorer(BooleanWeight weight, boolean disableCoord, int minNrShouldMatch, + Scorer requiredScorer, int requiredNrMatch, List optionalScorers, List prohibitedScorers, + int maxCoord) throws IOException { + super(weight); + if (requiredNrMatch <= 0) { + throw new IllegalArgumentException("requriedScorers.size() must be > 0"); + } + if (minNrShouldMatch < 0) { + throw new IllegalArgumentException("Minimum number of optional scorers should not be negative"); + } + this.minNrShouldMatch = minNrShouldMatch; + + this.requiredScorer = requiredScorer; + this.requiredNrMatch = requiredNrMatch; + this.optionalScorers = optionalScorers; + this.prohibitedScorers = prohibitedScorers; + + coordFactors = new float[requiredNrMatch + optionalScorers.size() + 1]; + for (int i = 0; i < coordFactors.length; i++) { + coordFactors[i] = disableCoord ? 1.0f : weight.coord(i, maxCoord); + } + } + + @Override + public String toString() { + StringBuffer buffer = new StringBuffer(); + buffer.append("boolean("); + buffer.append("+"); + buffer.append(requiredScorer.toString()); + buffer.append(" "); + for (Scorer optionalScorer : optionalScorers) { + buffer.append(optionalScorer.toString()); + buffer.append(" "); + } + for (Scorer prohibitedScorer : prohibitedScorers) { + buffer.append("-"); + buffer.append(prohibitedScorer.toString()); + buffer.append(" "); + } + // Delete the last whitespace + if (buffer.length() > "boolean(".length()) { + buffer.deleteCharAt(buffer.length() - 1); + } + buffer.append(")"); + return buffer.toString(); + } + + @Override + public float score() throws IOException { + return current != null ? + ((float) current.requiredScore + (float) current.optionalScore) * coordFactors[current.coord] : Float.NaN; + } + + @Override + public int freq() throws IOException { + return current != null ? current.coord : 0; + } + + @Override + public int docID() { + return currentDoc; + } + + @Override + public int nextDoc() throws IOException { + if (current != null) { + current = current.next; + } + if (bucketTable.more && current == null) { + bucketTable.collectMoreForce(); + current = bucketTable.first; + } + + while (current != null) { + if (current.coord - requiredNrMatch >= minNrShouldMatch) { + currentDoc = current.doc; + return currentDoc; + + } else { + current = current.next; + } + + if (bucketTable.more && current == null) { + bucketTable.collectMoreForce(); + current = bucketTable.first; + } + } + return currentDoc = DocIdSetIterator.NO_MORE_DOCS; + } + + @Override + public int advance(int target) throws IOException { + if (target < bucketTable.end) { + return slowAdvance(target); + + } else { + bucketTable.advance(target); + return slowAdvance(target); + } + } + + @Override + public long cost() { + return requiredScorer.cost(); + } + + @Override + public Collection getChildren() { + throw new UnsupportedOperationException(); + } +} diff --git a/lucene/core/src/java/org/apache/lucene/search/BooleanQuery.java b/lucene/core/src/java/org/apache/lucene/search/BooleanQuery.java index 4d7635d..538b78b 100644 --- a/lucene/core/src/java/org/apache/lucene/search/BooleanQuery.java +++ b/lucene/core/src/java/org/apache/lucene/search/BooleanQuery.java @@ -20,6 +20,7 @@ package org.apache.lucene.search; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Set; @@ -42,8 +43,8 @@ public class BooleanQuery extends Query implements Iterable { /** Thrown when an attempt is made to add more than {@link * #getMaxClauseCount()} clauses. This typically happens if - * a PrefixQuery, FuzzyQuery, WildcardQuery, or TermRangeQuery - * is expanded to many terms during search. + * a PrefixQuery, FuzzyQuery, WildcardQuery, or TermRangeQuery + * is expanded to many terms during search. */ public static class TooManyClauses extends RuntimeException { public TooManyClauses() { @@ -58,7 +59,7 @@ public class BooleanQuery extends Query implements Iterable { */ public static int getMaxClauseCount() { return maxClauseCount; } - /** + /** * Set the maximum number of clauses permitted per BooleanQuery. * Default value is 1024. */ @@ -286,10 +287,10 @@ public class BooleanQuery extends Query implements Iterable { "of optional clauses: " + minShouldMatch); return sumExpl; } - + sumExpl.setMatch(0 < coord ? Boolean.TRUE : Boolean.FALSE); sumExpl.setValue(sum); - + final float coordFactor = disableCoord ? 1.0f : coord(coord, maxCoord); if (coordFactor == 1.0f) { return sumExpl; // eliminate wrapper @@ -308,12 +309,13 @@ public class BooleanQuery extends Query implements Iterable { public BulkScorer bulkScorer(AtomicReaderContext context, boolean scoreDocsInOrder, Bits acceptDocs) throws IOException { - if (scoreDocsInOrder || minNrShouldMatch > 1) { + if (minNrShouldMatch > 1) { // TODO: (LUCENE-4872) in some cases BooleanScorer may be faster for minNrShouldMatch // but the same is even true of pure conjunctions... return super.bulkScorer(context, scoreDocsInOrder, acceptDocs); } + List required = new ArrayList(); List prohibited = new ArrayList(); List optional = new ArrayList(); Iterator cIter = clauses.iterator(); @@ -325,18 +327,123 @@ public class BooleanQuery extends Query implements Iterable { return null; } } else if (c.isRequired()) { - // TODO: there are some cases where BooleanScorer - // would handle conjunctions faster than - // BooleanScorer2... - return super.bulkScorer(context, scoreDocsInOrder, acceptDocs); + // Get required scorer + Scorer requiredSubScorer = w.scorer(context, acceptDocs); + // if no doc matches required, then return null to say + // no doc matches this Boolean Query. + if (requiredSubScorer == null) { + return null; + } + required.add(requiredSubScorer); + } else if (c.isProhibited()) { prohibited.add(subScorer); + } else { optional.add(subScorer); } } - return new BooleanScorer(this, disableCoord, minNrShouldMatch, optional, prohibited, maxCoord); + if (!scoreDocsInOrder && required.isEmpty()) { + return new BooleanScorer(this, disableCoord, minNrShouldMatch, required, optional, prohibited, maxCoord); + } + + int minShouldMatch = minNrShouldMatch; + + float requiredCost = Float.MAX_VALUE; + float optionalCost = 0F; + float prohibitedCost = 0F; + + List prohibitedScorers = new ArrayList<>(); + List optionalScorers = new ArrayList<>(); + cIter = clauses.iterator(); + for (Weight w : weights) { + BooleanClause c = cIter.next(); + Scorer subScorer = w.scorer(context, acceptDocs); + if (subScorer != null) { + if (c.isRequired()) { + requiredCost = Math.min(requiredCost, subScorer.cost()); + + } else if (c.isProhibited()) { + prohibitedScorers.add(subScorer); + prohibitedCost += subScorer.cost(); + + } else { + optionalScorers.add(subScorer); + optionalCost += subScorer.cost(); + } + } + } + requiredCost *= Math.pow(0.97, required.size()); + prohibitedCost /= prohibitedScorers.size(); + optionalCost /= optionalScorers.size(); + + // scorer simplifications: + if (optionalScorers.size() == minShouldMatch) { + // any optional clauses are in fact required + required.addAll(optionalScorers); + optionalScorers.clear(); + optional.clear(); + minShouldMatch = 0; + } + + if (required.isEmpty() && optionalScorers.isEmpty()) { + // no required and optional clauses. + return null; + } else if (optionalScorers.size() < minShouldMatch) { + // either >1 req scorer, or there are 0 req scorers and at least 1 + // optional scorer. Therefore if there are not enough optional scorers + // no documents will be matched by the query + return null; + } + + if (required.isEmpty()) { + // DAAT is the only choice + return new DefaultBulkScorer( + excl(opt(optionalScorers, minShouldMatch, disableCoord), prohibitedScorers)); + } + + Scorer req; + Class scorerNotClass = scorerClassForNot(requiredCost, prohibitedScorers.size(), prohibitedCost); +// if (!optionalScorers.isEmpty() || !prohibitedScorers.isEmpty()) +// scorerNotClass = BooleanArrayScorer.class; + if (scorerNotClass == BooleanArrayScorer.class) { + req = new BooleanArrayScorer(this, !optionalScorers.isEmpty() || disableCoord, 0, + required, Collections. emptyList(), prohibitedScorers, maxCoord); + } else if (scorerNotClass == BooleanLinkedScorer.class) { + req = new BooleanLinkedScorer(this, !optionalScorers.isEmpty() || disableCoord, 0, + required, Collections. emptyList(), prohibitedScorers, maxCoord); + } else { + req = excl(req(required, !optionalScorers.isEmpty() || disableCoord), prohibitedScorers); + } + + // required is not empty + // pure conjunction + if (optionalScorers.isEmpty()) { + return new DefaultBulkScorer(req); + } + + requiredCost *= Math.pow(0.97, prohibitedScorers.size()); + // conjunction-disjunction mix: + // we create the required and optional pieces with coord disabled, and then + // combine the two: if minNrShouldMatch > 0, then its a conjunction: because the + // optional side must match. otherwise its required + optional, factoring the + // number of optional terms into the coord calculation + Scorer opt; + Class scorerOrClass = scorerClassForOr(requiredCost, optionalScorers.size(), optionalCost); +// scorerOrClass = BooleanArrayScorer.class; + if (scorerOrClass == BooleanArrayScorer.class) { + return new DefaultBulkScorer(new BooleanArrayScorer(this, disableCoord, minShouldMatch, + req, required.size(), optionalScorers, Collections. emptyList(), maxCoord)); + } + if (scorerOrClass == BooleanLinkedScorer.class) { + return new DefaultBulkScorer(new BooleanLinkedScorer(this, disableCoord, minShouldMatch, + req, required.size(), optionalScorers, Collections. emptyList(), maxCoord)); + } + opt = opt(optionalScorers, minShouldMatch, true); + + return new DefaultBulkScorer( + getMixScorer(required.size(), optionalScorers.size(), req, opt, minShouldMatch)); } @Override @@ -366,16 +473,16 @@ public class BooleanQuery extends Query implements Iterable { optional.add(subScorer); } } - + // scorer simplifications: - + if (optional.size() == minShouldMatch) { // any optional clauses are in fact required required.addAll(optional); optional.clear(); minShouldMatch = 0; } - + if (required.isEmpty() && optional.isEmpty()) { // no required and optional clauses. return null; @@ -385,52 +492,116 @@ public class BooleanQuery extends Query implements Iterable { // no documents will be matched by the query return null; } - - // three cases: conjunction, disjunction, or mix - + + // three cases: disjunction, conjunction, or mix + // pure conjunction if (optional.isEmpty()) { return excl(req(required, disableCoord), prohibited); } - + // pure disjunction if (required.isEmpty()) { return excl(opt(optional, minShouldMatch, disableCoord), prohibited); } - + // conjunction-disjunction mix: // we create the required and optional pieces with coord disabled, and then // combine the two: if minNrShouldMatch > 0, then its a conjunction: because the // optional side must match. otherwise its required + optional, factoring the // number of optional terms into the coord calculation - + Scorer req = excl(req(required, true), prohibited); Scorer opt = opt(optional, minShouldMatch, true); + return getMixScorer(required.size(), optional.size(), req, opt, minShouldMatch); + } + + private Scorer getMixScorer(int requiredSize, int optionalSize, + Scorer req, Scorer opt, int minShouldMatch) { // TODO: clean this up: its horrible if (disableCoord) { if (minShouldMatch > 0) { return new ConjunctionScorer(this, new Scorer[] { req, opt }, 1F); } else { - return new ReqOptSumScorer(req, opt); + return new ReqOptSumScorer(req, opt); } - } else if (optional.size() == 1) { + } else if (optionalSize == 1) { if (minShouldMatch > 0) { - return new ConjunctionScorer(this, new Scorer[] { req, opt }, coord(required.size()+1, maxCoord)); + return new ConjunctionScorer(this, new Scorer[] { req, opt }, coord(requiredSize+1, maxCoord)); } else { - float coordReq = coord(required.size(), maxCoord); - float coordBoth = coord(required.size() + 1, maxCoord); + float coordReq = coord(requiredSize, maxCoord); + float coordBoth = coord(requiredSize + 1, maxCoord); return new BooleanTopLevelScorers.ReqSingleOptScorer(req, opt, coordReq, coordBoth); } } else { if (minShouldMatch > 0) { - return new BooleanTopLevelScorers.CoordinatingConjunctionScorer(this, coords(), req, required.size(), opt); + return new BooleanTopLevelScorers.CoordinatingConjunctionScorer(this, coords(), req, requiredSize, opt); } else { - return new BooleanTopLevelScorers.ReqMultiOptScorer(req, opt, required.size(), coords()); + return new BooleanTopLevelScorers.ReqMultiOptScorer(req, opt, requiredSize, coords()); } } } - + + private boolean isTons(int num) { + return num >= 10; + } + + private boolean isSome(int num) { + return num >= 3 && !isTons(num); + } + + private boolean isHigh(float cost) { + return cost >= 1e5f; + } + + private boolean isLow(float cost) { + return !isHigh(cost); + } + + private Class scorerClassForNot(float requiredCost, int prohibitedSize, float prohibitedCost) { +// if (isTons(prohibitedSize)) { +// return BooleanLinkedScorer.class; +// } +// if (isSome(prohibitedSize) && isHigh(prohibitedCost)) { +// return BooleanArrayScorer.class; +// } + final float a = (float) Math.log(requiredCost); + final float b = (float) Math.log(prohibitedCost); + final float c = a * b; + final float d = c / (a + b); + final float A = a * -12.3435f + b * -11.0941f + c * 0.0232f + d * 47.2820f; + final float B = a * 21.6271f + b * 26.6788f + c * -0.1548f + d * -97.0084f; + if (A * prohibitedSize + B > 5) { + return BooleanArrayScorer.class; + } + return null; + } + + private Class scorerClassForOr(float requiredCost, int optionalSize, float optionalCost) { +// if (isTons(optionalSize)) { // Tons +// if (isHigh(requiredCost)) { // HighAnd +// return BooleanLinkedScorer.class; +// +// } else { // LowAnd +// return BooleanArrayScorer.class; +// } +// } +// if (isSome(optionalSize) && isLow(requiredCost) && isHigh(optionalCost)) { // LowAnd5HighOr +// return BooleanArrayScorer.class; +// } + final float a = (float) Math.log(requiredCost); + final float b = (float) Math.log(optionalCost); + final float c = a * b; + final float d = c / (a + b); + final float A = a * -9.3337f + b * -8.6297f + c * 0.0146f + d * 36.6630f; + final float B = a * -7.8219f + b * 7.3330f + c * 0.1831f + d * -8.7804f; + if (A * optionalSize + B > 5) { + return BooleanArrayScorer.class; + } + return null; + } + @Override public boolean scoresDocsOutOfOrder() { if (minNrShouldMatch > 1) { @@ -446,15 +617,15 @@ public class BooleanQuery extends Query implements Iterable { optionalCount++; } } - + if (optionalCount == minNrShouldMatch) { return false; // BS2 (in-order) will be used, as this means conjunction } - + // scorer() will return an out-of-order scorer if requested. return true; } - + private Scorer req(List required, boolean disableCoord) { if (required.size() == 1) { Scorer req = required.get(0); @@ -464,12 +635,12 @@ public class BooleanQuery extends Query implements Iterable { return req; } } else { - return new ConjunctionScorer(this, + return new ConjunctionScorer(this, required.toArray(new Scorer[required.size()]), disableCoord ? 1.0F : coord(required.size(), maxCoord)); } } - + private Scorer excl(Scorer main, List prohibited) throws IOException { if (prohibited.isEmpty()) { return main; @@ -478,13 +649,13 @@ public class BooleanQuery extends Query implements Iterable { } else { float coords[] = new float[prohibited.size()+1]; Arrays.fill(coords, 1F); - return new ReqExclScorer(main, - new DisjunctionSumScorer(this, - prohibited.toArray(new Scorer[prohibited.size()]), + return new ReqExclScorer(main, + new DisjunctionSumScorer(this, + prohibited.toArray(new Scorer[prohibited.size()]), coords)); } } - + private Scorer opt(List optional, int minShouldMatch, boolean disableCoord) throws IOException { if (optional.size() == 1) { Scorer opt = optional.get(0); @@ -504,13 +675,13 @@ public class BooleanQuery extends Query implements Iterable { if (minShouldMatch > 1) { return new MinShouldMatchSumScorer(this, optional, minShouldMatch, coords); } else { - return new DisjunctionSumScorer(this, - optional.toArray(new Scorer[optional.size()]), + return new DisjunctionSumScorer(this, + optional.toArray(new Scorer[optional.size()]), coords); } } } - + private float[] coords() { float[] coords = new float[maxCoord+1]; coords[0] = 0F; @@ -656,5 +827,5 @@ public class BooleanQuery extends Query implements Iterable { return Float.floatToIntBits(getBoost()) ^ clauses.hashCode() + getMinimumNumberShouldMatch() + (disableCoord ? 17:0); } - + } diff --git a/lucene/core/src/java/org/apache/lucene/search/BooleanScorer.java b/lucene/core/src/java/org/apache/lucene/search/BooleanScorer.java index 173bb44..3356406 100644 --- a/lucene/core/src/java/org/apache/lucene/search/BooleanScorer.java +++ b/lucene/core/src/java/org/apache/lucene/search/BooleanScorer.java @@ -18,12 +18,8 @@ package org.apache.lucene.search; */ import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; import java.util.List; -import org.apache.lucene.index.AtomicReaderContext; -import org.apache.lucene.index.DocsEnum; import org.apache.lucene.search.BooleanQuery.BooleanWeight; /* Description from Doug Cutting (excerpted from @@ -60,69 +56,74 @@ import org.apache.lucene.search.BooleanQuery.BooleanWeight; * updates for the optional terms. */ final class BooleanScorer extends BulkScorer { - - private static final class BooleanScorerCollector extends SimpleCollector { + + private final class BooleanScorerCollector extends SimpleCollector { private BucketTable bucketTable; private int mask; private Scorer scorer; - + public BooleanScorerCollector(int mask, BucketTable bucketTable) { this.mask = mask; this.bucketTable = bucketTable; } - + @Override public void collect(final int doc) throws IOException { final BucketTable table = bucketTable; final int i = doc & BucketTable.MASK; final Bucket bucket = table.buckets[i]; - + + final int coord = (mask & REQUIRED_MASK) == REQUIRED_MASK ? requiredNrMatch : 1; if (bucket.doc != doc) { // invalid bucket bucket.doc = doc; // set doc - bucket.score = scorer.score(); // initialize score + bucket.requiredScore = 0; // initialize required score + bucket.optionalScore = 0; // initialize optional score bucket.bits = mask; // initialize mask - bucket.coord = 1; // initialize coord - + bucket.coord = coord; // initialize coord bucket.next = table.first; // push onto valid list table.first = bucket; } else { // valid bucket - bucket.score += scorer.score(); // increment score bucket.bits |= mask; // add bits in mask - bucket.coord++; // increment coord + bucket.coord += coord; // increment coord + } + + if ((mask & REQUIRED_MASK) == REQUIRED_MASK) { // Required doc + bucket.requiredScore += scorer.score(); + } else if (mask == 0) { // Optional doc + bucket.optionalScore += scorer.score(); } } - + @Override public void setScorer(Scorer scorer) { this.scorer = scorer; } - + @Override public boolean acceptsDocsOutOfOrder() { return true; } - } - + static final class Bucket { int doc = -1; // tells if bucket is valid - double score; // incremental score - // TODO: break out bool anyProhibited, int - // numRequiredMatched; then we can remove 32 limit on - // required clauses + // score is divided into RS and OS, so that its calculating order + // can be the same as the schedule of DAAT. + double requiredScore; // incremental required score + double optionalScore; // incremental optional score int bits; // used for bool constraints int coord; // count of terms in score Bucket next; // next valid bucket } - + /** A simple hash table of document scores within a range. */ - static final class BucketTable { + final class BucketTable { public static final int SIZE = 1 << 11; public static final int MASK = SIZE - 1; final Bucket[] buckets = new Bucket[SIZE]; Bucket first = null; // head of valid list - + public BucketTable() { // Pre-fill to save the lazy init when collecting // each sub: @@ -140,8 +141,7 @@ final class BooleanScorer extends BulkScorer { static final class SubScorer { public BulkScorer scorer; - // TODO: re-enable this if BQ ever sends us required clauses - //public boolean required = false; + public boolean required = false; public boolean prohibited; public LeafCollector collector; public SubScorer next; @@ -149,48 +149,65 @@ final class BooleanScorer extends BulkScorer { public SubScorer(BulkScorer scorer, boolean required, boolean prohibited, LeafCollector collector, SubScorer next) { - if (required) { - throw new IllegalArgumentException("this scorer cannot handle required=true"); - } this.scorer = scorer; this.more = true; - // TODO: re-enable this if BQ ever sends us required clauses - //this.required = required; + this.required = required; this.prohibited = prohibited; this.collector = collector; this.next = next; } } - + private SubScorer scorers = null; private BucketTable bucketTable = new BucketTable(); private final float[] coordFactors; - // TODO: re-enable this if BQ ever sends us required clauses - //private int requiredMask = 0; + // minNrShouldMatch only applies to SHOULD clauses private final int minNrShouldMatch; private int end; private Bucket current; // Any time a prohibited clause matches we set bit 0: private static final int PROHIBITED_MASK = 1; + // Any time a required clause matches we set bit 1: + private static final int REQUIRED_MASK = 2; + // requiredNrMatch applies to MUST clauses + private final int requiredNrMatch; + +// private final Weight weight; - private final Weight weight; + BooleanScorer(BooleanWeight weight, boolean disableCoord, int minNrShouldMatch, + List requiredScorers, List optionalScorers, List prohibitedScorers, + int maxCoord) throws IOException { + this(weight, disableCoord, minNrShouldMatch, + requiredScorers.isEmpty() ? null : + new ConjunctionScorer(weight, requiredScorers.toArray(new Scorer[requiredScorers.size()])), + requiredScorers.size(), + optionalScorers, prohibitedScorers, maxCoord); + } BooleanScorer(BooleanWeight weight, boolean disableCoord, int minNrShouldMatch, - List optionalScorers, List prohibitedScorers, int maxCoord) throws IOException { + Scorer requiredScorer, int requiredNrMatch, List optionalScorers, List prohibitedScorers, + int maxCoord) throws IOException { + this.minNrShouldMatch = minNrShouldMatch; - this.weight = weight; +// this.weight = weight; + + this.requiredNrMatch = requiredNrMatch; + if (requiredNrMatch > 0) { + BulkScorer required = new Weight.DefaultBulkScorer(requiredScorer); + scorers = new SubScorer(required, true, false, bucketTable.newCollector(REQUIRED_MASK), scorers); + } for (BulkScorer scorer : optionalScorers) { scorers = new SubScorer(scorer, false, false, bucketTable.newCollector(0), scorers); } - + for (BulkScorer scorer : prohibitedScorers) { scorers = new SubScorer(scorer, false, true, bucketTable.newCollector(PROHIBITED_MASK), scorers); } - coordFactors = new float[optionalScorers.size() + 1]; + coordFactors = new float[requiredNrMatch + optionalScorers.size() + 1]; for (int i = 0; i < coordFactors.length; i++) { - coordFactors[i] = disableCoord ? 1.0f : weight.coord(i, maxCoord); + coordFactors[i] = disableCoord ? 1.0f : weight.coord(i, maxCoord); } } @@ -205,16 +222,13 @@ final class BooleanScorer extends BulkScorer { collector.setScorer(fs); do { bucketTable.first = null; - - while (current != null) { // more queued + + while (current != null) { // more queued // check prohibited & required - if ((current.bits & PROHIBITED_MASK) == 0) { + if ((current.bits & PROHIBITED_MASK) == 0 && + (requiredNrMatch == 0 || (current.bits & REQUIRED_MASK) == REQUIRED_MASK)) { - // TODO: re-enable this if BQ ever sends us required - // clauses - //&& (current.bits & requiredMask) == requiredMask) { - // NOTE: Lucene always passes max = // Integer.MAX_VALUE today, because we never embed // a BooleanScorer inside another (even though @@ -228,18 +242,20 @@ final class BooleanScorer extends BulkScorer { bucketTable.first = tmp; continue; } - - if (current.coord >= minNrShouldMatch) { - fs.score = (float) (current.score * coordFactors[current.coord]); + + if (current.coord - requiredNrMatch >= minNrShouldMatch) { + // Cast required score and optional score to float, + // in order to make the calculating procedure is the same as DAAT. + fs.score = (float) (((float) current.requiredScore + (float) current.optionalScore) * coordFactors[current.coord]); fs.doc = current.doc; fs.freq = current.coord; collector.collect(current.doc); } } - + current = current.next; // pop the queue } - + if (bucketTable.first != null){ current = bucketTable.first; bucketTable.first = current.next; @@ -256,7 +272,7 @@ final class BooleanScorer extends BulkScorer { } } current = bucketTable.first; - + } while (current != null || more); return false; @@ -270,6 +286,10 @@ final class BooleanScorer extends BulkScorer { buffer.append(sub.scorer.toString()); buffer.append(" "); } + // Delete the last whitespace + if (buffer.length() > "boolean(".length()) { + buffer.deleteCharAt(buffer.length() - 1); + } buffer.append(")"); return buffer.toString(); } diff --git a/lucene/core/src/java/org/apache/lucene/search/ConjunctionScorer.java b/lucene/core/src/java/org/apache/lucene/search/ConjunctionScorer.java index 3e81187..e303ed8 100644 --- a/lucene/core/src/java/org/apache/lucene/search/ConjunctionScorer.java +++ b/lucene/core/src/java/org/apache/lucene/search/ConjunctionScorer.java @@ -102,12 +102,11 @@ class ConjunctionScorer extends Scorer { @Override public float score() throws IOException { - // TODO: sum into a double and cast to float if we ever send required clauses to BS1 - float sum = 0.0f; + double sum = 0.0f; for (DocsAndFreqs docs : docsAndFreqs) { sum += docs.scorer.score(); } - return sum * coord; + return (float) (sum * coord); } @Override diff --git a/lucene/core/src/test/org/apache/lucene/search/TestBooleanScorer.java b/lucene/core/src/test/org/apache/lucene/search/TestBooleanScorer.java index 358a513..69d71af 100644 --- a/lucene/core/src/test/org/apache/lucene/search/TestBooleanScorer.java +++ b/lucene/core/src/test/org/apache/lucene/search/TestBooleanScorer.java @@ -96,7 +96,7 @@ public class TestBooleanScorer extends LuceneTestCase { } }}; - BooleanScorer bs = new BooleanScorer(weight, false, 1, Arrays.asList(scorers), Collections.emptyList(), scorers.length); + BooleanScorer bs = new BooleanScorer(weight, false, 1, Collections.emptyList(), Arrays.asList(scorers), Collections.emptyList(), scorers.length); final List hits = new ArrayList<>(); bs.score(new SimpleCollector() { diff --git a/lucene/core/src/test/org/apache/lucene/search/TestBooleanUnevenly.java b/lucene/core/src/test/org/apache/lucene/search/TestBooleanUnevenly.java new file mode 100644 index 0000000..fce73f3 --- /dev/null +++ b/lucene/core/src/test/org/apache/lucene/search/TestBooleanUnevenly.java @@ -0,0 +1,132 @@ +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.analysis.MockAnalyzer; +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.apache.lucene.util.LuceneTestCase; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * BooleanQuery.scorer should be tested, when hit documents + * are very unevenly distributed. + */ +public class TestBooleanUnevenly extends LuceneTestCase { + private static IndexSearcher searcher; + private static IndexReader reader; + + public static final String field = "field"; + private static Directory directory; + + private static int count1 = 0; + + @BeforeClass + public static void beforeClass() throws Exception { + directory = newDirectory(); + RandomIndexWriter w = new RandomIndexWriter(random(), directory, new MockAnalyzer(random())); + Document doc; + for (int i=0;i<2;i++) { + for (int j=0;j<2048;j++) { + doc = new Document(); + doc.add(newTextField(field, "1", Field.Store.NO)); + count1 ++; + w.addDocument(doc); + } + for (int j=0;j<2048;j++) { + doc = new Document(); + doc.add(newTextField(field, "2", Field.Store.NO)); + w.addDocument(doc); + } + doc = new Document(); + doc.add(newTextField(field, "1", Field.Store.NO)); + count1 ++; + w.addDocument(doc); + for (int j=0;j<2048;j++) { + doc = new Document(); + doc.add(newTextField(field, "2", Field.Store.NO)); + w.addDocument(doc); + } + } + reader = w.getReader(); + searcher = newSearcher(reader); + w.shutdown(); + } + + @AfterClass + public static void afterClass() throws Exception { + reader.close(); + directory.close(); + searcher = null; + reader = null; + directory = null; + } + + @Test + public void testQueries01() throws Exception { + BooleanQuery query = new BooleanQuery(); + query.add(new TermQuery(new Term(field, "1")), BooleanClause.Occur.MUST); + query.add(new TermQuery(new Term(field, "1")), BooleanClause.Occur.SHOULD); + query.add(new TermQuery(new Term(field, "2")), BooleanClause.Occur.SHOULD); + + TopScoreDocCollector collector = TopScoreDocCollector.create(1000, false); + searcher.search(query, null, collector); + TopDocs tops1 = collector.topDocs(); + ScoreDoc[] hits1 = tops1.scoreDocs; + int hitsNum1 = tops1.totalHits; + + collector = TopScoreDocCollector.create(1000, true); + searcher.search(query, null, collector); + TopDocs tops2 = collector.topDocs(); + ScoreDoc[] hits2 = tops2.scoreDocs; + int hitsNum2 = tops2.totalHits; + + assertEquals(hitsNum1, count1); + assertEquals(hitsNum2, count1); + CheckHits.checkEqual(query, hits1, hits2); + } + + @Test + public void testQueries02() throws Exception { + BooleanQuery query = new BooleanQuery(); + query.add(new TermQuery(new Term(field, "1")), BooleanClause.Occur.SHOULD); + query.add(new TermQuery(new Term(field, "1")), BooleanClause.Occur.SHOULD); + + TopScoreDocCollector collector = TopScoreDocCollector.create(1000, false); + searcher.search(query, null, collector); + TopDocs tops1 = collector.topDocs(); + ScoreDoc[] hits1 = tops1.scoreDocs; + int hitsNum1 = tops1.totalHits; + + collector = TopScoreDocCollector.create(1000, true); + searcher.search(query, null, collector); + TopDocs tops2 = collector.topDocs(); + ScoreDoc[] hits2 = tops2.scoreDocs; + int hitsNum2 = tops2.totalHits; + + assertEquals(hitsNum1, count1); + assertEquals(hitsNum2, count1); + CheckHits.checkEqual(query, hits1, hits2); + } +}