Index: CHANGES.txt =================================================================== --- CHANGES.txt (revision 987778) +++ CHANGES.txt (working copy) @@ -201,6 +201,11 @@ * LUCENE-2559: Added SegmentReader.reopen methods (John Wang via Mike McCandless) +* LUCENE-2590: Added Scorer.visitSubScorers, and Scorer.freq. Along + with a custom Collector these experimental methods make it possible + to gather the hit-count per sub-clause and per document while a + search is running. (Simon Willnauer, Mike McCandless) + Optimizations * LUCENE-2410: ~20% speedup on exact (slop=0) PhraseQuery matching. Index: src/java/org/apache/lucene/search/BooleanQuery.java =================================================================== --- src/java/org/apache/lucene/search/BooleanQuery.java (revision 987778) +++ src/java/org/apache/lucene/search/BooleanQuery.java (working copy) @@ -319,7 +319,7 @@ // Check if we can return a BooleanScorer if (!scoreDocsInOrder && topScorer && required.size() == 0 && prohibited.size() < 32) { - return new BooleanScorer(similarity, minNrShouldMatch, optional, prohibited); + return new BooleanScorer(BooleanQuery.this, similarity, minNrShouldMatch, optional, prohibited); } if (required.size() == 0 && optional.size() == 0) { @@ -333,7 +333,7 @@ } // Return a BooleanScorer2 - return new BooleanScorer2(similarity, minNrShouldMatch, required, prohibited, optional); + return new BooleanScorer2(BooleanQuery.this, similarity, minNrShouldMatch, required, prohibited, optional); } @Override Index: src/java/org/apache/lucene/search/BooleanScorer.java =================================================================== --- src/java/org/apache/lucene/search/BooleanScorer.java (revision 987778) +++ src/java/org/apache/lucene/search/BooleanScorer.java (working copy) @@ -115,6 +115,7 @@ float score; int doc = NO_MORE_DOCS; + int freq; public BucketScorer() { super(null); } @@ -125,6 +126,9 @@ public int docID() { return doc; } @Override + public float freq() { return freq; } + + @Override public int nextDoc() throws IOException { return NO_MORE_DOCS; } @Override @@ -159,7 +163,8 @@ static final class SubScorer { public Scorer scorer; - public boolean required = false; + // TODO: re-enable this if BQ ever sends us required clauses + //public boolean required = false; public boolean prohibited = false; public Collector collector; public SubScorer next; @@ -167,8 +172,12 @@ public SubScorer(Scorer scorer, boolean required, boolean prohibited, Collector collector, SubScorer next) throws IOException { + if (required) { + throw new IllegalArgumentException("this scorer cannot handle required=true"); + } this.scorer = scorer; - this.required = required; + // TODO: re-enable this if BQ ever sends us required clauses + //this.required = required; this.prohibited = prohibited; this.collector = collector; this.next = next; @@ -179,17 +188,20 @@ private BucketTable bucketTable = new BucketTable(); private int maxCoord = 1; private final float[] coordFactors; - private int requiredMask = 0; + // TODO: re-enable this if BQ ever sends us required clauses + //private int requiredMask = 0; private int prohibitedMask = 0; private int nextMask = 1; private final int minNrShouldMatch; private int end; private Bucket current; private int doc = -1; + private final BooleanQuery q; - BooleanScorer(Similarity similarity, int minNrShouldMatch, + BooleanScorer(BooleanQuery q, Similarity similarity, int minNrShouldMatch, List optionalScorers, List prohibitedScorers) throws IOException { super(similarity); + this.q = q; this.minNrShouldMatch = minNrShouldMatch; if (optionalScorers != null && optionalScorers.size() > 0) { @@ -233,8 +245,11 @@ while (current != null) { // more queued // check prohibited & required - if ((current.bits & prohibitedMask) == 0 && - (current.bits & requiredMask) == requiredMask) { + if ((current.bits & prohibitedMask) == 0) { + + // TODO: re-enable this if BQ ever sends us required + // clauses + //&& (current.bits & requiredMask) == requiredMask) { if (current.doc >= max){ tmp = current; @@ -247,6 +262,7 @@ if (current.coord >= minNrShouldMatch) { bs.score = current.score * coordFactors[current.coord]; bs.doc = current.doc; + bs.freq = current.coord; collector.collect(current.doc); } } @@ -296,8 +312,9 @@ // check prohibited & required, and minNrShouldMatch if ((current.bits & prohibitedMask) == 0 && - (current.bits & requiredMask) == requiredMask && current.coord >= minNrShouldMatch) { + // TODO: re-enable this if BQ ever sends us required clauses + // (current.bits & requiredMask) == requiredMask && return doc = current.doc; } } @@ -341,5 +358,31 @@ buffer.append(")"); return buffer.toString(); } + + @Override + protected void visitSubScorers(ScorerVisitor visitor) { + final ScorerContext ctx = visitor.ctx; + ctx.child = q; + ctx.scorer = this; + visitor.current.call(ctx); + SubScorer sub = scorers; + ctx.parent = q; + while(sub != null) { + // TODO: re-enable this if BQ ever sends us required + //clauses + //if (sub.required) { + //sub.scorer.visitSubScorers(q, BooleanClause.Occur.MUST, visit); + if (!sub.prohibited) { + visitor.setOptional(); + } else { + // TODO: maybe it's pointless to do this, but, it is + // possible the doc may still be collected, eg foo + // OR (bar -fee) + visitor.setProhibited(); + } + sub.scorer.visitSubScorers(visitor); + sub = sub.next; + } + } } Index: src/java/org/apache/lucene/search/BooleanScorer2.java =================================================================== --- src/java/org/apache/lucene/search/BooleanScorer2.java (revision 987778) +++ src/java/org/apache/lucene/search/BooleanScorer2.java (working copy) @@ -21,6 +21,8 @@ import java.util.ArrayList; import java.util.List; +import org.apache.lucene.search.Scorer.ScorerVisitor; + /* See the description in BooleanScorer.java, comparing * BooleanScorer & BooleanScorer2 */ @@ -59,7 +61,8 @@ /** The number of optionalScorers that need to match (if there are any) */ private final int minNrShouldMatch; - + private final BooleanQuery q; + private int doc = -1; /** @@ -80,12 +83,13 @@ * @param optional * the list of optional scorers. */ - public BooleanScorer2(Similarity similarity, int minNrShouldMatch, + public BooleanScorer2(BooleanQuery q, Similarity similarity, int minNrShouldMatch, List required, List prohibited, List optional) throws IOException { super(similarity); if (minNrShouldMatch < 0) { throw new IllegalArgumentException("Minimum number of optional scorers should not be negative"); } + this.q = q; coordinator = new Coordinator(); this.minNrShouldMatch = minNrShouldMatch; @@ -305,9 +309,33 @@ } @Override + public float freq() { + return coordinator.nrMatchers; + } + + @Override public int advance(int target) throws IOException { return doc = countingSumScorer.advance(target); } -} - + @Override + protected void visitSubScorers(ScorerVisitor ctx) { + final ScorerContext scoreCtx = ctx.ctx; + scoreCtx.child = q; + scoreCtx.scorer = this; + ctx.current.call(scoreCtx); + scoreCtx.parent = q; + ctx.setOptional(); + for (Scorer s : optionalScorers) { + s.visitSubScorers(ctx); + } + ctx.setProhibited(); + for (Scorer s : prohibitedScorers) { + s.visitSubScorers(ctx); + } + ctx.setRequired(); + for (Scorer s : requiredScorers) { + s.visitSubScorers(ctx); + } + } +} Index: src/java/org/apache/lucene/search/ExactPhraseScorer.java =================================================================== --- src/java/org/apache/lucene/search/ExactPhraseScorer.java (revision 987778) +++ src/java/org/apache/lucene/search/ExactPhraseScorer.java (working copy) @@ -193,8 +193,8 @@ return "ExactPhraseScorer(" + weight + ")"; } - // used by MultiPhraseQuery - float currentFreq() { + @Override + public float freq() { return freq; } @@ -331,4 +331,12 @@ return freq; } + + @Override + protected void visitSubScorers(ScorerVisitor ctx) { + final ScorerContext scoreCtx = ctx.ctx; + scoreCtx.child = weight.getQuery(); + scoreCtx.scorer = this; + ctx.current.call(scoreCtx); + } } Index: src/java/org/apache/lucene/search/MultiPhraseQuery.java =================================================================== --- src/java/org/apache/lucene/search/MultiPhraseQuery.java (revision 987778) +++ src/java/org/apache/lucene/search/MultiPhraseQuery.java (working copy) @@ -271,11 +271,7 @@ int d = scorer.advance(doc); float phraseFreq; if (d == doc) { - if (slop == 0) { - phraseFreq = ((ExactPhraseScorer) scorer).currentFreq(); - } else { - phraseFreq = ((SloppyPhraseScorer) scorer).currentFreq(); - } + phraseFreq = scorer.freq(); } else { phraseFreq = 0.0f; } Index: src/java/org/apache/lucene/search/PhraseQuery.java =================================================================== --- src/java/org/apache/lucene/search/PhraseQuery.java (revision 987778) +++ src/java/org/apache/lucene/search/PhraseQuery.java (working copy) @@ -275,11 +275,7 @@ int d = scorer.advance(doc); float phraseFreq; if (d == doc) { - if (slop == 0) { - phraseFreq = ((ExactPhraseScorer) scorer).currentFreq(); - } else { - phraseFreq = ((SloppyPhraseScorer) scorer).currentFreq(); - } + phraseFreq = scorer.freq(); } else { phraseFreq = 0.0f; } Index: src/java/org/apache/lucene/search/PhraseScorer.java =================================================================== --- src/java/org/apache/lucene/search/PhraseScorer.java (revision 987778) +++ src/java/org/apache/lucene/search/PhraseScorer.java (working copy) @@ -129,8 +129,11 @@ /** * phrase frequency in current doc as computed by phraseFreq(). */ - public final float currentFreq() { return freq; } - + @Override + public final float freq() { + return freq; + } + /** * For a document containing all the phrase query terms, compute the * frequency of the phrase in that document. @@ -180,4 +183,11 @@ @Override public String toString() { return "scorer(" + weight + ")"; } + @Override + protected void visitSubScorers(ScorerVisitor visitor) { + final ScorerContext ctx = visitor.ctx; + ctx.child = weight.getQuery(); + ctx.scorer = this; + visitor.current.call(ctx); + } } Index: src/java/org/apache/lucene/search/Scorer.java =================================================================== --- src/java/org/apache/lucene/search/Scorer.java (revision 987778) +++ src/java/org/apache/lucene/search/Scorer.java (working copy) @@ -94,4 +94,247 @@ */ public abstract float score() throws IOException; + /** Returns number of matches for the current document. + * This returns a float (not int) because + * SloppyPhraseScorer discounts its freq according to how + * "sloppy" the match was. + * + * @lucene.experimental */ + public float freq() throws IOException { + throw new UnsupportedOperationException(this + " does not implement freq()"); + } + + /** Expert: A callback used to gather information from sub-scorers. + * @see ScorerVisitor + * + * @lucene.experimental */ + public static abstract class SubScorerCallback

{ + public abstract void call(ScorerContext ctx); + } + + + /** + * A struct-like class that holds context information for a sub-scorer. An + * instance of this class is passed to + * {@link SubScorerCallback#call(ScorerContext)} for each sub-scorer during + * scorer traversal in {@link Scorer#accept()} + * + *

+ * Expert users aiming to extend the scorers visitor pattern can pass custom {@link ScorerContext} instance + * to {@link Scorer#accept(ScorerVisitor)} via {@link ScorerVisitor#ScorerVisitor(ScorerContext, SubScorerCallback)}. + *

+ * + * @lucene.experimental + */ + public static class ScorerContext

{ + /** + * The parent {@link Query} object of the {@link Query} that created the currently visited {@link Scorer}. + * Set to null if the visited {@link Scorer} is a top-level scorer. + */ + public P parent; + + /** + * The {@link Query} object which created the currently visited {@link Scorer} object + */ + public C child; + + /** + * The currently visited {@link Scorer} instance + */ + public S scorer; + + /** + * The relationship of {@link #child} within a + * Set to null if the visited {@link Scorer} is a top-level scorer or if the {@link #parent} is not a boolean query. + */ + public BooleanClause.Occur relationship; + + /** + * Resets the {@link ScorerContext} to its default values. Subclasses should + * override this method. + */ + public void clear(){ + parent = null; + child = null; + scorer = null; + relationship = null; + } + } + + /** + * Expert: This class is used to pass {@link SubScorerCallback} instances to + * all sub-scorers of this query. Depending on the sub-scorers boolean + * relationship correspondent {@link SubScorerCallback} instances are invoked + * on each sub-scorer to gather information from them. + * + *

+ * Expert users can use {@link ScorerVisitor} and {@link ScorerContext} to + * pass custom information across sub-scorers by passing customized subclasses + * to {@link Scorer#accept(ScorerVisitor)} + *

+ * + * @lucene.experimental + */ + public static class ScorerVisitor

{ + /* + * if additional information needs to be passed to sub-scorers we can either + * extend this class or add more members to it. - extending doesn't make + * sense for internal use but maybe for expert users with custom queries. + * Anyway this is way more flexible than a fixed method signature. + */ + /** + * A {@link ScorerContext} used to invoke each {@link SubScorerCallback} + */ + public final ScorerContext ctx; + /** + * The callback used for the currently visited sub-scorer - initially set to {@link #required}; + */ + public SubScorerCallback current; + /** + * {@link SubScorerCallback} invoked for all optional scorers + */ + public final SubScorerCallback optional; + /** + * {@link SubScorerCallback} invoked for all prohibited scorers + */ + public final SubScorerCallback prohibited; + /** + * {@link SubScorerCallback} invoked for all required scorers + */ + public final SubScorerCallback required; + + /* Placeholder instance to mock out null callback */ + private static final SubScorerCallback PLACEHOLDER = new SubScorerCallback() { + @Override + public void call(ScorerContext ctx) { + } + }; + + /** + * Creates a new {@link ScorerVisitor} + * + * @param callback {@link SubScorerCallback} invoked for all sub-scorers + */ + public ScorerVisitor(SubScorerCallback callback) { + this(new ScorerContext(), callback); + } + + /** + * Creates a new {@link ScorerVisitor} + * + * @param ctx the {@link ScorerContext} to invoke {@link SubScorerCallback} + * @param callback {@link SubScorerCallback} invoked for all sub-scorers + */ + public ScorerVisitor(ScorerContext ctx, + SubScorerCallback callback) { + this(ctx, callback, callback, callback); + } + + /** + * Creates a new {@link ScorerVisitor} + * + * @param ctx the {@link ScorerContext} to invoke {@link SubScorerCallback} + * @param optional {@link SubScorerCallback} invoked for optional scorers + * @param prohibited {@link SubScorerCallback} invoked for prohibited scorers + * @param required {@link SubScorerCallback} invoked for required scorers + */ + public ScorerVisitor(ScorerContext ctx, + SubScorerCallback optional, + SubScorerCallback prohibited, + SubScorerCallback required) { + this.ctx = ctx; + this.optional = ensureNotNull(optional); + this.prohibited = ensureNotNull(prohibited); + this.required = ensureNotNull(required); + this.current = required; // top-level - required is default + } + + @SuppressWarnings("unchecked") + private SubScorerCallback ensureNotNull( + SubScorerCallback visitor) { + return visitor == null ? (SubScorerCallback) PLACEHOLDER + : visitor; + } + + /** + * Sets the {@link #optional} callback to {@link #current}. + */ + public final void setOptional() { + current = optional; + ctx.relationship = BooleanClause.Occur.SHOULD; + } + + /** + * Sets the {@link #required} callback to {@link #current}. + */ + public void setRequired() { + current = required; + ctx.relationship = BooleanClause.Occur.MUST; + } + + /** + * Sets the {@link #prohibited} callback to {@link #current}. + */ + public void setProhibited() { + current = prohibited; + ctx.relationship = BooleanClause.Occur.MUST_NOT; + } + + public void clear() { + current = required; + ctx.clear(); + } + } + + /** + * Expert: call this to gather details for all sub-scorers for this query. + * This can be used, in conjunction with a custom {@link Collector} to gather + * details about how each sub-query matched the current hit. + * + * The returned {@link ScorerVisitor} instance holds the given callback and + * can be used in subsequent calls via {@link Scorer#accept(ScorerVisitor)} + * + * @param a callback invoked for each sub-scorer + * @return a {@link ScorerVisitor} instance holding the given callback. + * @lucene.experimental + */ + public ScorerVisitor accept(SubScorerCallback callback) { + final ScorerVisitor ctx = new ScorerVisitor(callback); + visitSubScorers(ctx); + return ctx; + } + + /** + * Expert: call this to gather details for all sub-scorers for this query. + * This can be used, in conjunction with a custom {@link Collector} to gather + * details about how each sub-query matched the current hit. + * + * @param visitor + * a visitor holding a {@link ScorerContext} and + * {@link SubScorerCallback} instances passed to each sub-scorers. + * @return the given {@link ScorerVisitor} instance + * @lucene.experimental + */ + public ScorerVisitor accept(ScorerVisitor visitor) { + // return the visitor for consistency + visitor.clear(); + visitSubScorers(visitor); + return visitor; + } + + /** + * {@link Scorer} subclass should implement this method to support gathering + * details for sub-scorers from {@link Collector} instances. + * + * The visitors {@link ScorerContext} is guaranteed to be initialized with the + * parent query ({@link ScorerContext#parent}) and the scorers boolean relationship + * ({@link ScorerContext#relationship}) or null if the scorer is a + * top-level scorer. Sub-scorers are responsible to pass other contextual + * information like {@link ScorerContext#scorer} to the context. + * + * @lucene.experimental + */ + protected void visitSubScorers(ScorerVisitor visitor) { + throw new UnsupportedOperationException(); + } } Index: src/java/org/apache/lucene/search/TermScorer.java =================================================================== --- src/java/org/apache/lucene/search/TermScorer.java (revision 987778) +++ src/java/org/apache/lucene/search/TermScorer.java (working copy) @@ -103,6 +103,11 @@ return doc; } + @Override + public float freq() { + return freq; + } + /** * Advances to the next document matching the query.
* The iterator over the matching documents is buffered using @@ -172,4 +177,12 @@ /** Returns a string representation of this TermScorer. */ @Override public String toString() { return "scorer(" + weight + ")"; } + + @Override + protected void visitSubScorers(ScorerVisitor visitor) { + final ScorerContext ctx = visitor.ctx; + ctx.child = weight.getQuery(); + ctx.scorer = this; + visitor.current.call(ctx); + } } Index: src/test/org/apache/lucene/search/TestBooleanScorer.java =================================================================== --- src/test/org/apache/lucene/search/TestBooleanScorer.java (revision 987778) +++ src/test/org/apache/lucene/search/TestBooleanScorer.java (working copy) @@ -90,7 +90,7 @@ } }}; - BooleanScorer bs = new BooleanScorer(sim, 1, Arrays.asList(scorers), null); + BooleanScorer bs = new BooleanScorer(null, sim, 1, Arrays.asList(scorers), null); assertEquals("should have received 3000", 3000, bs.nextDoc()); assertEquals("should have received NO_MORE_DOCS", DocIdSetIterator.NO_MORE_DOCS, bs.nextDoc()); Index: src/test/org/apache/lucene/search/TestSubScorerFreqs.java =================================================================== --- src/test/org/apache/lucene/search/TestSubScorerFreqs.java (revision 0) +++ src/test/org/apache/lucene/search/TestSubScorerFreqs.java (revision 0) @@ -0,0 +1,233 @@ +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.document.*; +import org.apache.lucene.index.*; +import org.apache.lucene.util.*; +import org.apache.lucene.search.BooleanClause.Occur; +import org.apache.lucene.search.Scorer.ScorerContext; +import org.apache.lucene.search.Scorer.ScorerVisitor; +import org.apache.lucene.search.Scorer.SubScorerCallback; +import org.apache.lucene.store.*; +import java.util.*; +import java.io.*; + +import org.junit.Test; +import org.junit.AfterClass; +import org.junit.BeforeClass; + +import static org.junit.Assert.*; + +public class TestSubScorerFreqs extends LuceneTestCaseJ4 { + + private static Directory dir; + private static IndexSearcher s; + + @BeforeClass + public static void makeIndex() throws Exception { + dir = new MockRAMDirectory(); + RandomIndexWriter w = new RandomIndexWriter( + newStaticRandom(TestSubScorerFreqs.class), dir); + // make sure we have more than one segment occationally + for (int i = 0; i < 31 * RANDOM_MULTIPLIER; i++) { + Document doc = new Document(); + doc.add(new Field("f", "a b c d b c d c d d", Field.Store.NO, + Field.Index.ANALYZED)); + w.addDocument(doc); + + doc = new Document(); + doc.add(new Field("f", "a b c d", Field.Store.NO, Field.Index.ANALYZED)); + w.addDocument(doc); + } + + s = new IndexSearcher(w.getReader()); + w.close(); + } + + @AfterClass + public static void finish() throws Exception { + s.getIndexReader().close(); + s.close(); + dir.close(); + } + + private static class CountingCollector extends Collector { + private final Collector other; + private int docBase; + + public final Map> docCounts = new HashMap>(); + + private final Map subScorers = new HashMap(); + private final SubScorerCallback callback = new Scorer.SubScorerCallback() { + @Override + public void call(ScorerContext ctx) { + subScorers.put(ctx.child, ctx.scorer); + } + }; + private final ScorerVisitor visitContext; + + public CountingCollector(Collector other) { + this(other, Occur.values()); + } + + @SuppressWarnings("unchecked") + public CountingCollector(Collector other, BooleanClause.Occur... occurs) { + this.other = other; + SubScorerCallback[] callbacks = new SubScorerCallback[3]; + for (Occur occur : occurs) { + switch (occur) { + case MUST: + callbacks[2] = callback; + + break; + case MUST_NOT: + callbacks[1] = callback; + + break; + case SHOULD: + callbacks[0] = callback; + break; + + } + } + + visitContext = new ScorerVisitor( + new ScorerContext(), callbacks[0], + callbacks[1], callbacks[2]); + } + + @Override + public void setScorer(Scorer scorer) throws IOException { + other.setScorer(scorer); + subScorers.clear(); + scorer.accept(visitContext); + } + + @Override + public void collect(int doc) throws IOException { + final Map freqs = new HashMap(); + for (Map.Entry ent : subScorers.entrySet()) { + Scorer value = ent.getValue(); + int matchId = value.docID(); + freqs.put(ent.getKey(), matchId == doc ? value.freq() : 0.0f); + } + docCounts.put(doc + docBase, freqs); + other.collect(doc); + } + + @Override + public void setNextReader(IndexReader reader, int docBase) + throws IOException { + this.docBase = docBase; + other.setNextReader(reader, docBase); + } + + @Override + public boolean acceptsDocsOutOfOrder() { + return other.acceptsDocsOutOfOrder(); + } + } + + private static final float FLOAT_TOLERANCE = 0.00001F; + + @Test + public void testTermQuery() throws Exception { + TermQuery q = new TermQuery(new Term("f", "d")); + CountingCollector c = new CountingCollector(TopScoreDocCollector.create(10, + true)); + s.search(q, null, c); + final int maxDocs = s.maxDoc(); + assertEquals(maxDocs, c.docCounts.size()); + for (int i = 0; i < maxDocs; i++) { + Map doc0 = c.docCounts.get(i); + assertEquals(1, doc0.size()); + assertEquals(4.0F, doc0.get(q), FLOAT_TOLERANCE); + + Map doc1 = c.docCounts.get(++i); + assertEquals(1, doc1.size()); + assertEquals(1.0F, doc1.get(q), FLOAT_TOLERANCE); + } + } + + @Test + public void testBooleanQuery() throws Exception { + TermQuery aQuery = new TermQuery(new Term("f", "a")); + TermQuery dQuery = new TermQuery(new Term("f", "d")); + TermQuery cQuery = new TermQuery(new Term("f", "c")); + TermQuery yQuery = new TermQuery(new Term("f", "y")); + + BooleanQuery query = new BooleanQuery(); + BooleanQuery inner = new BooleanQuery(); + + inner.add(cQuery, Occur.SHOULD); + inner.add(yQuery, Occur.MUST_NOT); + query.add(inner, Occur.MUST); + query.add(aQuery, Occur.MUST); + query.add(dQuery, Occur.MUST); + + for (int j = 0; j < 2; j++) { + Occur[] occur = j == 0 ? new Occur[] { Occur.MUST } : new Occur[] { + Occur.MUST, Occur.SHOULD }; + CountingCollector c = new CountingCollector(TopScoreDocCollector.create( + 10, true), occur); + s.search(query, null, c); + final int maxDocs = s.maxDoc(); + assertEquals(maxDocs, c.docCounts.size()); + boolean includeOptional = j==1; + for (int i = 0; i < maxDocs; i++) { + Map doc0 = c.docCounts.get(i); + assertEquals(includeOptional?5:4, doc0.size()); + assertEquals(1.0F, doc0.get(aQuery), FLOAT_TOLERANCE); + assertEquals(4.0F, doc0.get(dQuery), FLOAT_TOLERANCE); + if (includeOptional) + assertEquals(3.0F, doc0.get(cQuery), FLOAT_TOLERANCE); + + Map doc1 = c.docCounts.get(++i); + assertEquals(includeOptional?5:4, doc1.size()); + assertEquals(1.0F, doc1.get(aQuery), FLOAT_TOLERANCE); + assertEquals(1.0F, doc1.get(dQuery), FLOAT_TOLERANCE); + if (includeOptional) + assertEquals(1.0F, doc1.get(cQuery), FLOAT_TOLERANCE); + + } + } + } + + @Test + public void testPhraseQuery() throws Exception { + PhraseQuery q = new PhraseQuery(); + q.add(new Term("f", "b")); + q.add(new Term("f", "c")); + CountingCollector c = new CountingCollector(TopScoreDocCollector.create(10, + true)); + s.search(q, null, c); + final int maxDocs = s.maxDoc(); + assertEquals(maxDocs, c.docCounts.size()); + for (int i = 0; i < maxDocs; i++) { + Map doc0 = c.docCounts.get(i); + assertEquals(1, doc0.size()); + assertEquals(2.0F, doc0.get(q), FLOAT_TOLERANCE); + + Map doc1 = c.docCounts.get(++i); + assertEquals(1, doc1.size()); + assertEquals(1.0F, doc1.get(q), FLOAT_TOLERANCE); + } + + } +} Property changes on: src/test/org/apache/lucene/search/TestSubScorerFreqs.java ___________________________________________________________________ Added: svn:eol-style + native Added: svn:keywords + Date Author Id Revision HeadURL