LUCENE-7132: BooleanQuery sometimes assigned the wrong score when ranges of documents had only one clause matching while other ranges had more than one clause matchng

This commit is contained in:
Mike McCandless 2016-06-06 10:35:16 -04:00
parent b64c558e3e
commit c8570ed821
8 changed files with 422 additions and 42 deletions

View File

@ -148,6 +148,11 @@ Bug Fixes
* LUCENE-7312: Fix geo3d's x/y/z double to int encoding to ensure it always
rounds down (Karl Wright, Mike McCandless)
* LUCENE-7132: BooleanQuery sometimes assigned too-low scores in cases
where ranges of documents had only a single clause matching while
other ranges had more than one clause matching (Ahmet Arslan,
hossman, Mike McCandless)
Documentation
* LUCENE-7223: Improve XXXPoint javadocs to make it clear that you

View File

@ -275,13 +275,13 @@ final class BooleanScorer extends BulkScorer {
}
}
private void scoreWindowSingleScorer(BulkScorerAndDoc bulkScorer, LeafCollector collector,
private void scoreWindowSingleScorer(BulkScorerAndDoc bulkScorer, LeafCollector collector, LeafCollector singleClauseCollector,
Bits acceptDocs, int windowMin, int windowMax, int max) throws IOException {
assert tail.size() == 0;
final int nextWindowBase = head.top().next & ~MASK;
final int end = Math.max(windowMax, Math.min(max, nextWindowBase));
bulkScorer.score(collector, acceptDocs, windowMin, end);
bulkScorer.score(singleClauseCollector, acceptDocs, windowMin, end);
// reset the scorer that should be used for the general case
collector.setScorer(fakeScorer);
@ -304,7 +304,7 @@ final class BooleanScorer extends BulkScorer {
// special case: only one scorer can match in the current window,
// we can collect directly
final BulkScorerAndDoc bulkScorer = leads[0];
scoreWindowSingleScorer(bulkScorer, singleClauseCollector, acceptDocs, windowMin, windowMax, max);
scoreWindowSingleScorer(bulkScorer, collector, singleClauseCollector, acceptDocs, windowMin, windowMax, max);
return head.add(bulkScorer);
} else {
// general case, collect through a bit set first and then replay

View File

@ -45,7 +45,12 @@ public abstract class FilterLeafCollector implements LeafCollector {
@Override
public String toString() {
return getClass().getSimpleName() + "(" + in + ")";
String name = getClass().getSimpleName();
if (name.length() == 0) {
// an anonoymous subclass will have empty name?
name = "FilterLeafCollector";
}
return name + "(" + in + ")";
}
}

View File

@ -18,15 +18,18 @@ package org.apache.lucene.search;
import java.util.Arrays;
import java.util.Random;
import org.apache.lucene.analysis.MockAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.RandomIndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.search.similarities.ClassicSimilarity;
import org.apache.lucene.search.similarities.Similarity;
import org.apache.lucene.store.Directory;
@ -42,24 +45,45 @@ import org.junit.Test;
*/
public class TestBoolean2 extends LuceneTestCase {
private static IndexSearcher searcher;
private static IndexSearcher singleSegmentSearcher;
private static IndexSearcher bigSearcher;
private static IndexReader reader;
private static IndexReader littleReader;
private static int NUM_EXTRA_DOCS = 6000;
private static IndexReader singleSegmentReader;
/** num of empty docs injected between every doc in the (main) index */
private static int NUM_FILLER_DOCS;
/** num of empty docs injected prior to the first doc in the (main) index */
private static int PRE_FILLER_DOCS;
/** num "extra" docs containing value in "field2" added to the "big" clone of the index */
private static final int NUM_EXTRA_DOCS = 6000;
public static final String field = "field";
private static Directory directory;
private static Directory singleSegmentDirectory;
private static Directory dir2;
private static int mulFactor;
@BeforeClass
public static void beforeClass() throws Exception {
// in some runs, test immediate adjacency of matches - in others, force a full bucket gap betwen docs
NUM_FILLER_DOCS = random().nextBoolean() ? 0 : BooleanScorer.SIZE;
PRE_FILLER_DOCS = TestUtil.nextInt(random(), 0, (NUM_FILLER_DOCS / 2));
directory = newDirectory();
RandomIndexWriter writer= new RandomIndexWriter(random(), directory, newIndexWriterConfig(new MockAnalyzer(random())).setMergePolicy(newLogMergePolicy()));
for (int i = 0; i < docFields.length; i++) {
Document doc = new Document();
for (int filler = 0; filler < PRE_FILLER_DOCS; filler++) {
writer.addDocument(doc);
}
for (int i = 0; i < docFields.length; i++) {
doc.add(newTextField(field, docFields[i], Field.Store.NO));
writer.addDocument(doc);
doc = new Document();
for (int filler = 0; filler < NUM_FILLER_DOCS; filler++) {
writer.addDocument(doc);
}
}
writer.close();
littleReader = DirectoryReader.open(directory);
@ -67,6 +91,18 @@ public class TestBoolean2 extends LuceneTestCase {
// this is intentionally using the baseline sim, because it compares against bigSearcher (which uses a random one)
searcher.setSimilarity(new ClassicSimilarity());
// make a copy of our index using a single segment
singleSegmentDirectory = new MockDirectoryWrapper(random(), TestUtil.ramCopyOf(directory));
IndexWriterConfig iwc = newIndexWriterConfig(new MockAnalyzer(random()));
// we need docID order to be preserved:
iwc.setMergePolicy(newLogMergePolicy());
try (IndexWriter w = new IndexWriter(singleSegmentDirectory, iwc)) {
w.forceMerge(1, true);
}
singleSegmentReader = DirectoryReader.open(singleSegmentDirectory);
singleSegmentSearcher = newSearcher(singleSegmentReader);
singleSegmentSearcher.setSimilarity(searcher.getSimilarity(true));
// Make big index
dir2 = new MockDirectoryWrapper(random(), TestUtil.ramCopyOf(directory));
@ -86,12 +122,12 @@ public class TestBoolean2 extends LuceneTestCase {
docCount = w.maxDoc();
w.close();
mulFactor *= 2;
} while(docCount < 3000);
} while(docCount < 3000 * NUM_FILLER_DOCS);
RandomIndexWriter w = new RandomIndexWriter(random(), dir2,
newIndexWriterConfig(new MockAnalyzer(random()))
.setMaxBufferedDocs(TestUtil.nextInt(random(), 50, 1000)));
Document doc = new Document();
doc = new Document();
doc.add(newTextField("field2", "xxx", Field.Store.NO));
for(int i=0;i<NUM_EXTRA_DOCS/2;i++) {
w.addDocument(doc);
@ -110,8 +146,13 @@ public class TestBoolean2 extends LuceneTestCase {
public static void afterClass() throws Exception {
reader.close();
littleReader.close();
singleSegmentReader.close();
dir2.close();
directory.close();
singleSegmentDirectory.close();
singleSegmentSearcher = null;
singleSegmentReader = null;
singleSegmentDirectory = null;
searcher = null;
reader = null;
littleReader = null;
@ -128,26 +169,57 @@ public class TestBoolean2 extends LuceneTestCase {
};
public void queriesTest(Query query, int[] expDocNrs) throws Exception {
// adjust the expected doc numbers according to our filler docs
if (0 < NUM_FILLER_DOCS) {
expDocNrs = Arrays.copyOf(expDocNrs, expDocNrs.length);
for (int i=0; i < expDocNrs.length; i++) {
expDocNrs[i] = PRE_FILLER_DOCS + ((NUM_FILLER_DOCS + 1) * expDocNrs[i]);
}
}
final int topDocsToCheck = atLeast(1000);
// The asserting searcher will sometimes return the bulk scorer and
// sometimes return a default impl around the scorer so that we can
// compare BS1 and BS2
TopScoreDocCollector collector = TopScoreDocCollector.create(1000);
TopScoreDocCollector collector = TopScoreDocCollector.create(topDocsToCheck);
searcher.search(query, collector);
ScoreDoc[] hits1 = collector.topDocs().scoreDocs;
collector = TopScoreDocCollector.create(1000);
collector = TopScoreDocCollector.create(topDocsToCheck);
searcher.search(query, collector);
ScoreDoc[] hits2 = collector.topDocs().scoreDocs;
CheckHits.checkHitsQuery(query, hits1, hits2, expDocNrs);
// Since we have no deleted docs, we should also be able to verify identical matches &
// scores against an single segment copy of our index
collector = TopScoreDocCollector.create(topDocsToCheck);
singleSegmentSearcher.search(query, collector);
hits2 = collector.topDocs().scoreDocs;
CheckHits.checkHitsQuery(query, hits1, hits2, expDocNrs);
// sanity check expected num matches in bigSearcher
assertEquals(mulFactor * collector.totalHits,
bigSearcher.search(query, 1).totalHits);
CheckHits.checkHitsQuery(query, hits1, hits2, expDocNrs);
// now check 2 diff scorers from the bigSearcher as well
collector = TopScoreDocCollector.create(topDocsToCheck);
bigSearcher.search(query, collector);
hits1 = collector.topDocs().scoreDocs;
collector = TopScoreDocCollector.create(topDocsToCheck);
bigSearcher.search(query, collector);
hits2 = collector.topDocs().scoreDocs;
// NOTE: just comparing results, not vetting against expDocNrs
// since we have dups in bigSearcher
CheckHits.checkEqual(query, hits1, hits2);
}
@Test
public void testQueries01() throws Exception {
BooleanQuery.Builder query = new BooleanQuery.Builder();
query.setDisableCoord(random().nextBoolean());
query.add(new TermQuery(new Term(field, "w3")), BooleanClause.Occur.MUST);
query.add(new TermQuery(new Term(field, "xx")), BooleanClause.Occur.MUST);
int[] expDocNrs = {2,3};
@ -157,6 +229,7 @@ public class TestBoolean2 extends LuceneTestCase {
@Test
public void testQueries02() throws Exception {
BooleanQuery.Builder query = new BooleanQuery.Builder();
query.setDisableCoord(random().nextBoolean());
query.add(new TermQuery(new Term(field, "w3")), BooleanClause.Occur.MUST);
query.add(new TermQuery(new Term(field, "xx")), BooleanClause.Occur.SHOULD);
int[] expDocNrs = {2,3,1,0};
@ -166,6 +239,7 @@ public class TestBoolean2 extends LuceneTestCase {
@Test
public void testQueries03() throws Exception {
BooleanQuery.Builder query = new BooleanQuery.Builder();
query.setDisableCoord(random().nextBoolean());
query.add(new TermQuery(new Term(field, "w3")), BooleanClause.Occur.SHOULD);
query.add(new TermQuery(new Term(field, "xx")), BooleanClause.Occur.SHOULD);
int[] expDocNrs = {2,3,1,0};
@ -175,6 +249,7 @@ public class TestBoolean2 extends LuceneTestCase {
@Test
public void testQueries04() throws Exception {
BooleanQuery.Builder query = new BooleanQuery.Builder();
query.setDisableCoord(random().nextBoolean());
query.add(new TermQuery(new Term(field, "w3")), BooleanClause.Occur.SHOULD);
query.add(new TermQuery(new Term(field, "xx")), BooleanClause.Occur.MUST_NOT);
int[] expDocNrs = {1,0};
@ -184,6 +259,7 @@ public class TestBoolean2 extends LuceneTestCase {
@Test
public void testQueries05() throws Exception {
BooleanQuery.Builder query = new BooleanQuery.Builder();
query.setDisableCoord(random().nextBoolean());
query.add(new TermQuery(new Term(field, "w3")), BooleanClause.Occur.MUST);
query.add(new TermQuery(new Term(field, "xx")), BooleanClause.Occur.MUST_NOT);
int[] expDocNrs = {1,0};
@ -193,6 +269,7 @@ public class TestBoolean2 extends LuceneTestCase {
@Test
public void testQueries06() throws Exception {
BooleanQuery.Builder query = new BooleanQuery.Builder();
query.setDisableCoord(random().nextBoolean());
query.add(new TermQuery(new Term(field, "w3")), BooleanClause.Occur.MUST);
query.add(new TermQuery(new Term(field, "xx")), BooleanClause.Occur.MUST_NOT);
query.add(new TermQuery(new Term(field, "w5")), BooleanClause.Occur.MUST_NOT);
@ -203,6 +280,7 @@ public class TestBoolean2 extends LuceneTestCase {
@Test
public void testQueries07() throws Exception {
BooleanQuery.Builder query = new BooleanQuery.Builder();
query.setDisableCoord(random().nextBoolean());
query.add(new TermQuery(new Term(field, "w3")), BooleanClause.Occur.MUST_NOT);
query.add(new TermQuery(new Term(field, "xx")), BooleanClause.Occur.MUST_NOT);
query.add(new TermQuery(new Term(field, "w5")), BooleanClause.Occur.MUST_NOT);
@ -213,6 +291,7 @@ public class TestBoolean2 extends LuceneTestCase {
@Test
public void testQueries08() throws Exception {
BooleanQuery.Builder query = new BooleanQuery.Builder();
query.setDisableCoord(random().nextBoolean());
query.add(new TermQuery(new Term(field, "w3")), BooleanClause.Occur.MUST);
query.add(new TermQuery(new Term(field, "xx")), BooleanClause.Occur.SHOULD);
query.add(new TermQuery(new Term(field, "w5")), BooleanClause.Occur.MUST_NOT);
@ -223,6 +302,7 @@ public class TestBoolean2 extends LuceneTestCase {
@Test
public void testQueries09() throws Exception {
BooleanQuery.Builder query = new BooleanQuery.Builder();
query.setDisableCoord(random().nextBoolean());
query.add(new TermQuery(new Term(field, "w3")), BooleanClause.Occur.MUST);
query.add(new TermQuery(new Term(field, "xx")), BooleanClause.Occur.MUST);
query.add(new TermQuery(new Term(field, "w2")), BooleanClause.Occur.MUST);
@ -234,6 +314,7 @@ public class TestBoolean2 extends LuceneTestCase {
@Test
public void testQueries10() throws Exception {
BooleanQuery.Builder query = new BooleanQuery.Builder();
query.setDisableCoord(random().nextBoolean());
query.add(new TermQuery(new Term(field, "w3")), BooleanClause.Occur.MUST);
query.add(new TermQuery(new Term(field, "xx")), BooleanClause.Occur.MUST);
query.add(new TermQuery(new Term(field, "w2")), BooleanClause.Occur.MUST);
@ -241,16 +322,19 @@ public class TestBoolean2 extends LuceneTestCase {
int[] expDocNrs = {2, 3};
Similarity oldSimilarity = searcher.getSimilarity(true);
try {
searcher.setSimilarity(new ClassicSimilarity(){
Similarity newSimilarity = new ClassicSimilarity() {
@Override
public float coord(int overlap, int maxOverlap) {
return overlap / ((float)maxOverlap - 1);
}
});
};
try {
searcher.setSimilarity(newSimilarity);
singleSegmentSearcher.setSimilarity(newSimilarity);
queriesTest(query.build(), expDocNrs);
} finally {
searcher.setSimilarity(oldSimilarity);
singleSegmentSearcher.setSimilarity(oldSimilarity);
}
}
@ -282,15 +366,11 @@ public class TestBoolean2 extends LuceneTestCase {
searcher.setSimilarity(new ClassicSimilarity()); // restore
}
TopFieldCollector collector = TopFieldCollector.create(sort, 1000,
false, true, true);
// check diff (randomized) scorers (from AssertingSearcher) produce the same results
TopFieldCollector collector = TopFieldCollector.create(sort, 1000, false, true, true);
searcher.search(q1, collector);
ScoreDoc[] hits1 = collector.topDocs().scoreDocs;
collector = TopFieldCollector.create(sort, 1000,
false, true, true);
collector = TopFieldCollector.create(sort, 1000, false, true, true);
searcher.search(q1, collector);
ScoreDoc[] hits2 = collector.topDocs().scoreDocs;
tot+=hits2.length;
@ -301,6 +381,16 @@ public class TestBoolean2 extends LuceneTestCase {
q3.add(new PrefixQuery(new Term("field2", "b")), BooleanClause.Occur.SHOULD);
TopDocs hits4 = bigSearcher.search(q3.build(), 1);
assertEquals(mulFactor*collector.totalHits + NUM_EXTRA_DOCS/2, hits4.totalHits);
// test diff (randomized) scorers produce the same results on bigSearcher as well
collector = TopFieldCollector.create(sort, 1000 * mulFactor, false, true, true);
bigSearcher.search(q1, collector);
hits1 = collector.topDocs().scoreDocs;
collector = TopFieldCollector.create(sort, 1000 * mulFactor, false, true, true);
bigSearcher.search(q1, collector);
hits2 = collector.topDocs().scoreDocs;
CheckHits.checkEqual(q1, hits1, hits2);
}
} catch (Exception e) {

View File

@ -296,6 +296,7 @@ public class TestSimpleExplanations extends BaseExplanationTestCase {
outerQuery.add(new TermQuery(new Term(FIELD, "w1")), BooleanClause.Occur.MUST);
BooleanQuery.Builder innerQuery = new BooleanQuery.Builder();
innerQuery.setDisableCoord(random().nextBoolean());
innerQuery.add(new TermQuery(new Term(FIELD, "qq")), BooleanClause.Occur.SHOULD);
BooleanQuery.Builder childLeft = new BooleanQuery.Builder();
@ -317,6 +318,7 @@ public class TestSimpleExplanations extends BaseExplanationTestCase {
outerQuery.add(new TermQuery(new Term(FIELD, "w1")), BooleanClause.Occur.MUST);
BooleanQuery.Builder innerQuery = new BooleanQuery.Builder();
innerQuery.setDisableCoord(random().nextBoolean());
innerQuery.add(new TermQuery(new Term(FIELD, "qq")), BooleanClause.Occur.SHOULD);
BooleanQuery.Builder childLeft = new BooleanQuery.Builder();
@ -338,6 +340,7 @@ public class TestSimpleExplanations extends BaseExplanationTestCase {
outerQuery.add(new TermQuery(new Term(FIELD, "w1")), BooleanClause.Occur.MUST);
BooleanQuery.Builder innerQuery = new BooleanQuery.Builder();
innerQuery.setDisableCoord(random().nextBoolean());
innerQuery.add(new TermQuery(new Term(FIELD, "qq")), BooleanClause.Occur.SHOULD);
BooleanQuery.Builder childLeft = new BooleanQuery.Builder();
@ -359,6 +362,7 @@ public class TestSimpleExplanations extends BaseExplanationTestCase {
outerQuery.add(new TermQuery(new Term(FIELD, "w1")), BooleanClause.Occur.MUST);
BooleanQuery.Builder innerQuery = new BooleanQuery.Builder();
innerQuery.setDisableCoord(random().nextBoolean());
innerQuery.add(new TermQuery(new Term(FIELD, "qq")), BooleanClause.Occur.SHOULD);
BooleanQuery.Builder childLeft = new BooleanQuery.Builder();
@ -377,6 +381,7 @@ public class TestSimpleExplanations extends BaseExplanationTestCase {
}
public void testBQ11() throws Exception {
BooleanQuery.Builder query = new BooleanQuery.Builder();
query.setDisableCoord(random().nextBoolean());
query.add(new TermQuery(new Term(FIELD, "w1")), BooleanClause.Occur.SHOULD);
TermQuery boostedQuery = new TermQuery(new Term(FIELD, "w1"));
query.add(new BoostQuery(boostedQuery, 1000), BooleanClause.Occur.SHOULD);
@ -385,21 +390,21 @@ public class TestSimpleExplanations extends BaseExplanationTestCase {
}
public void testBQ14() throws Exception {
BooleanQuery.Builder q = new BooleanQuery.Builder();
q.setDisableCoord(true);
q.setDisableCoord(random().nextBoolean());
q.add(new TermQuery(new Term(FIELD, "QQQQQ")), BooleanClause.Occur.SHOULD);
q.add(new TermQuery(new Term(FIELD, "w1")), BooleanClause.Occur.SHOULD);
qtest(q.build(), new int[] { 0,1,2,3 });
}
public void testBQ15() throws Exception {
BooleanQuery.Builder q = new BooleanQuery.Builder();
q.setDisableCoord(true);
q.setDisableCoord(random().nextBoolean());
q.add(new TermQuery(new Term(FIELD, "QQQQQ")), BooleanClause.Occur.MUST_NOT);
q.add(new TermQuery(new Term(FIELD, "w1")), BooleanClause.Occur.SHOULD);
qtest(q.build(), new int[] { 0,1,2,3 });
}
public void testBQ16() throws Exception {
BooleanQuery.Builder q = new BooleanQuery.Builder();
q.setDisableCoord(true);
q.setDisableCoord(random().nextBoolean());
q.add(new TermQuery(new Term(FIELD, "QQQQQ")), BooleanClause.Occur.SHOULD);
BooleanQuery.Builder booleanQuery = new BooleanQuery.Builder();
@ -411,7 +416,7 @@ public class TestSimpleExplanations extends BaseExplanationTestCase {
}
public void testBQ17() throws Exception {
BooleanQuery.Builder q = new BooleanQuery.Builder();
q.setDisableCoord(true);
q.setDisableCoord(random().nextBoolean());
q.add(new TermQuery(new Term(FIELD, "w2")), BooleanClause.Occur.SHOULD);
BooleanQuery.Builder booleanQuery = new BooleanQuery.Builder();
@ -431,6 +436,7 @@ public class TestSimpleExplanations extends BaseExplanationTestCase {
public void testBQ20() throws Exception {
BooleanQuery.Builder q = new BooleanQuery.Builder();
q.setDisableCoord(random().nextBoolean());
q.setMinimumNumberShouldMatch(2);
q.add(new TermQuery(new Term(FIELD, "QQQQQ")), BooleanClause.Occur.SHOULD);
q.add(new TermQuery(new Term(FIELD, "yy")), BooleanClause.Occur.SHOULD);
@ -442,6 +448,16 @@ public class TestSimpleExplanations extends BaseExplanationTestCase {
}
public void testBQ21() throws Exception {
BooleanQuery.Builder q = new BooleanQuery.Builder();
q.setDisableCoord(random().nextBoolean());
q.add(new TermQuery(new Term(FIELD, "yy")), BooleanClause.Occur.SHOULD);
q.add(new TermQuery(new Term(FIELD, "zz")), BooleanClause.Occur.SHOULD);
qtest(q.build(), new int[] { 1,2,3 });
}
public void testBQ23() throws Exception {
BooleanQuery.Builder query = new BooleanQuery.Builder();
query.add(new TermQuery(new Term(FIELD, "w1")), BooleanClause.Occur.FILTER);
@ -488,6 +504,7 @@ public class TestSimpleExplanations extends BaseExplanationTestCase {
}
public void testMultiFieldBQ3() throws Exception {
BooleanQuery.Builder query = new BooleanQuery.Builder();
query.setDisableCoord(random().nextBoolean());
query.add(new TermQuery(new Term(FIELD, "yy")), BooleanClause.Occur.SHOULD);
query.add(new TermQuery(new Term(ALTFIELD, "w3")), BooleanClause.Occur.MUST);
@ -495,6 +512,7 @@ public class TestSimpleExplanations extends BaseExplanationTestCase {
}
public void testMultiFieldBQ4() throws Exception {
BooleanQuery.Builder outerQuery = new BooleanQuery.Builder();
outerQuery.setDisableCoord(random().nextBoolean());
outerQuery.add(new TermQuery(new Term(FIELD, "w1")), BooleanClause.Occur.SHOULD);
BooleanQuery.Builder innerQuery = new BooleanQuery.Builder();
@ -506,6 +524,7 @@ public class TestSimpleExplanations extends BaseExplanationTestCase {
}
public void testMultiFieldBQ5() throws Exception {
BooleanQuery.Builder outerQuery = new BooleanQuery.Builder();
outerQuery.setDisableCoord(random().nextBoolean());
outerQuery.add(new TermQuery(new Term(FIELD, "w1")), BooleanClause.Occur.SHOULD);
BooleanQuery.Builder innerQuery = new BooleanQuery.Builder();

View File

@ -0,0 +1,126 @@
/*
* 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.
*/
package org.apache.lucene.search;
import java.util.Arrays;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.RandomIndexWriter;
import org.apache.lucene.util.TestUtil;
import org.junit.BeforeClass;
import org.junit.Assume;
/**
* subclass of TestSimpleExplanations that adds a lot of filler docs which will be ignored at query time.
* These filler docs will either all be empty in which case the queries will be unmodified, or they will
* all use terms from same set of source data as our regular docs (to emphasis the DocFreq factor in scoring),
* in which case the queries will be wrapped so they can be excluded.
*/
public class TestSimpleExplanationsWithFillerDocs extends TestSimpleExplanations {
/** num of empty docs injected between every doc in the index */
private static final int NUM_FILLER_DOCS = BooleanScorer.SIZE;
/** num of empty docs injected prior to the first doc in the (main) index */
private static int PRE_FILLER_DOCS;
/**
* If non-null then the filler docs are not empty, and need to be filtered out from queries
* using this as both field name &amp; field value
*/
public static String EXTRA = null;
private static final Document EMPTY_DOC = new Document();
/**
* Replaces the index created by our superclass with a new one that includes a lot of docs filler docs.
* {@link #qtest} will account for these extra filler docs.
* @see #qtest
*/
@BeforeClass
public static void replaceIndex() throws Exception {
EXTRA = random().nextBoolean() ? null : "extra";
PRE_FILLER_DOCS = TestUtil.nextInt(random(), 0, (NUM_FILLER_DOCS / 2));
// free up what our super class created that we won't be using
reader.close();
directory.close();
directory = newDirectory();
try (RandomIndexWriter writer = new RandomIndexWriter(random(), directory, newIndexWriterConfig(analyzer).setMergePolicy(newLogMergePolicy()))) {
for (int filler = 0; filler < PRE_FILLER_DOCS; filler++) {
writer.addDocument(makeFillerDoc());
}
for (int i = 0; i < docFields.length; i++) {
writer.addDocument(createDoc(i));
for (int filler = 0; filler < NUM_FILLER_DOCS; filler++) {
writer.addDocument(makeFillerDoc());
}
}
reader = writer.getReader();
searcher = newSearcher(reader);
}
}
private static Document makeFillerDoc() {
if (null == EXTRA) {
return EMPTY_DOC;
}
Document doc = createDoc(TestUtil.nextInt(random(), 0, docFields.length-1));
doc.add(newStringField(EXTRA, EXTRA, Field.Store.NO));
return doc;
}
/**
* Adjusts <code>expDocNrs</code> based on the filler docs injected in the index,
* and if neccessary wraps the <code>q</code> in a BooleanQuery that will filter out all
* filler docs using the {@link #EXTRA} field.
*
* @see #replaceIndex
*/
@Override
public void qtest(Query q, int[] expDocNrs) throws Exception {
expDocNrs = Arrays.copyOf(expDocNrs, expDocNrs.length);
for (int i=0; i < expDocNrs.length; i++) {
expDocNrs[i] = PRE_FILLER_DOCS + ((NUM_FILLER_DOCS + 1) * expDocNrs[i]);
}
if (null != EXTRA) {
BooleanQuery.Builder builder = new BooleanQuery.Builder();
builder.add(new BooleanClause(q, BooleanClause.Occur.MUST));
builder.add(new BooleanClause(new TermQuery(new Term(EXTRA, EXTRA)), BooleanClause.Occur.MUST_NOT));
q = builder.build();
}
super.qtest(q, expDocNrs);
}
public void testMA1() throws Exception {
Assume.assumeNotNull("test is not viable with empty filler docs", EXTRA);
super.testMA1();
}
public void testMA2() throws Exception {
Assume.assumeNotNull("test is not viable with empty filler docs", EXTRA);
super.testMA2();
}
}

View File

@ -71,21 +71,25 @@ public abstract class BaseExplanationTestCase extends LuceneTestCase {
public static void beforeClassTestExplanations() throws Exception {
directory = newDirectory();
analyzer = new MockAnalyzer(random());
RandomIndexWriter writer= new RandomIndexWriter(random(), directory, newIndexWriterConfig(analyzer).setMergePolicy(newLogMergePolicy()));
try (RandomIndexWriter writer = new RandomIndexWriter(random(), directory, newIndexWriterConfig(analyzer).setMergePolicy(newLogMergePolicy()))) {
for (int i = 0; i < docFields.length; i++) {
Document doc = new Document();
doc.add(newStringField(KEY, ""+i, Field.Store.NO));
doc.add(new SortedDocValuesField(KEY, new BytesRef(""+i)));
Field f = newTextField(FIELD, docFields[i], Field.Store.NO);
f.setBoost(i);
doc.add(f);
doc.add(newTextField(ALTFIELD, docFields[i], Field.Store.NO));
writer.addDocument(doc);
writer.addDocument(createDoc(i));
}
reader = writer.getReader();
writer.close();
searcher = newSearcher(reader);
}
}
public static Document createDoc(int index) {
Document doc = new Document();
doc.add(newStringField(KEY, ""+index, Field.Store.NO));
doc.add(new SortedDocValuesField(KEY, new BytesRef(""+index)));
Field f = newTextField(FIELD, docFields[index], Field.Store.NO);
f.setBoost(index);
doc.add(f);
doc.add(newTextField(ALTFIELD, docFields[index], Field.Store.NO));
return doc;
}
protected static final String[] docFields = {
"w1 w2 w3 w4 w5",
@ -94,8 +98,19 @@ public abstract class BaseExplanationTestCase extends LuceneTestCase {
"w1 w3 xx w2 yy w3 zz"
};
/** check the expDocNrs first, then check the query (and the explanations) */
/**
* check the expDocNrs match and have scores that match the explanations.
* Query may be randomly wrapped in a BooleanQuery with a term that matches no documents in
* order to trigger coord logic.
*/
public void qtest(Query q, int[] expDocNrs) throws Exception {
if (random().nextBoolean()) {
BooleanQuery.Builder bq = new BooleanQuery.Builder();
bq.setDisableCoord(random().nextBoolean());
bq.add(q, BooleanClause.Occur.SHOULD);
bq.add(new TermQuery(new Term("NEVER","MATCH")), BooleanClause.Occur.SHOULD);
q = bq.build();
}
CheckHits.checkHitCollector(random(), q, FIELD, searcher, expDocNrs);
}

View File

@ -0,0 +1,120 @@
/*
* 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.
*/
package org.apache.lucene.search;
import java.io.IOException;
import java.util.Set;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.Term;
import junit.framework.AssertionFailedError;
/**
* Tests that the {@link BaseExplanationTestCase} helper code, as well as
* {@link CheckHits#checkNoMatchExplanations} are checking what they are suppose to.
*/
public class TestBaseExplanationTestCase extends BaseExplanationTestCase {
public void testQueryNoMatchWhenExpected() throws Exception {
expectThrows(AssertionFailedError.class, () -> {
qtest(new TermQuery(new Term(FIELD, "BOGUS")), new int[] { 3 /* none */ });
});
}
public void testQueryMatchWhenNotExpected() throws Exception {
expectThrows(AssertionFailedError.class, () -> {
qtest(new TermQuery(new Term(FIELD, "w1")), new int[] { 0, 1 /*, 2, 3 */ });
});
}
public void testIncorrectExplainScores() throws Exception {
// sanity check what a real TermQuery matches
qtest(new TermQuery(new Term(FIELD, "zz")), new int[] { 1, 3 });
// ensure when the Explanations are broken, we get an error about those matches
expectThrows(AssertionFailedError.class, () -> {
qtest(new BrokenExplainTermQuery(new Term(FIELD, "zz"), false, true), new int[] { 1, 3 });
});
}
public void testIncorrectExplainMatches() throws Exception {
// sanity check what a real TermQuery matches
qtest(new TermQuery(new Term(FIELD, "zz")), new int[] { 1, 3 });
// ensure when the Explanations are broken, we get an error about the non matches
expectThrows(AssertionFailedError.class, () -> {
CheckHits.checkNoMatchExplanations(new BrokenExplainTermQuery(new Term(FIELD, "zz"), true, false),
FIELD, searcher, new int[] { 1, 3 });
});
}
public static final class BrokenExplainTermQuery extends TermQuery {
public final boolean toggleExplainMatch;
public final boolean breakExplainScores;
public BrokenExplainTermQuery(Term t, boolean toggleExplainMatch, boolean breakExplainScores) {
super(t);
this.toggleExplainMatch = toggleExplainMatch;
this.breakExplainScores = breakExplainScores;
}
public Weight createWeight(IndexSearcher searcher, boolean needsScores) throws IOException {
return new BrokenExplainWeight(this, super.createWeight(searcher,needsScores));
}
}
public static final class BrokenExplainWeight extends Weight {
final Weight in;
public BrokenExplainWeight(BrokenExplainTermQuery q, Weight in) {
super(q);
this.in = in;
}
public BulkScorer bulkScorer(LeafReaderContext context) throws IOException {
return in.bulkScorer(context);
}
public Explanation explain(LeafReaderContext context, int doc) throws IOException {
BrokenExplainTermQuery q = (BrokenExplainTermQuery) this.getQuery();
Explanation result = in.explain(context, doc);
if (result.isMatch()) {
if (q.breakExplainScores) {
result = Explanation.match(-1F * result.getValue(), "Broken Explanation Score", result);
}
if (q.toggleExplainMatch) {
result = Explanation.noMatch("Broken Explanation Matching", result);
}
} else {
if (q.toggleExplainMatch) {
result = Explanation.match(-42.0F, "Broken Explanation Matching", result);
}
}
return result;
}
public void extractTerms(Set<Term> terms) {
in.extractTerms(terms);
}
public float getValueForNormalization() throws IOException {
return in.getValueForNormalization();
}
public void normalize(float norm, float boost) {
in.normalize(norm, boost);
}
public Scorer scorer(LeafReaderContext context) throws IOException {
return in.scorer(context);
}
}
}