| /* |
| * 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.List; |
| import java.util.Random; |
| |
| import junit.framework.Assert; |
| import org.apache.lucene.index.BinaryDocValues; |
| import org.apache.lucene.index.FieldInfos; |
| import org.apache.lucene.index.Fields; |
| import org.apache.lucene.index.IndexReader; |
| import org.apache.lucene.index.LeafMetaData; |
| import org.apache.lucene.index.LeafReader; |
| import org.apache.lucene.index.LeafReaderContext; |
| import org.apache.lucene.index.MultiReader; |
| import org.apache.lucene.index.NumericDocValues; |
| import org.apache.lucene.index.PointValues; |
| import org.apache.lucene.index.SortedDocValues; |
| import org.apache.lucene.index.SortedNumericDocValues; |
| import org.apache.lucene.index.SortedSetDocValues; |
| import org.apache.lucene.index.StoredFieldVisitor; |
| import org.apache.lucene.index.Terms; |
| import org.apache.lucene.util.Bits; |
| import org.apache.lucene.util.LuceneTestCase; |
| import org.apache.lucene.util.Version; |
| |
| import static junit.framework.Assert.assertEquals; |
| import static junit.framework.Assert.assertFalse; |
| import static junit.framework.Assert.assertTrue; |
| |
| /** |
| * Utility class for sanity-checking queries. |
| */ |
| public class QueryUtils { |
| |
| /** Check the types of things query objects should be able to do. */ |
| public static void check(Query q) { |
| checkHashEquals(q); |
| } |
| |
| /** check very basic hashCode and equals */ |
| public static void checkHashEquals(Query q) { |
| checkEqual(q,q); |
| |
| // test that a class check is done so that no exception is thrown |
| // in the implementation of equals() |
| Query whacky = new Query() { |
| @Override |
| public String toString(String field) { |
| return "My Whacky Query"; |
| } |
| |
| @Override |
| public void visit(QueryVisitor visitor) { } |
| |
| @Override |
| public boolean equals(Object o) { |
| return o == this; |
| } |
| |
| @Override |
| public int hashCode() { |
| return System.identityHashCode(this); |
| } |
| |
| }; |
| checkUnequal(q, whacky); |
| |
| // null test |
| assertFalse(q.equals(null)); |
| } |
| |
| public static void checkEqual(Query q1, Query q2) { |
| assertEquals(q1, q2); |
| assertEquals(q1.hashCode(), q2.hashCode()); |
| } |
| |
| public static void checkUnequal(Query q1, Query q2) { |
| assertFalse(q1 + " equal to " + q2, q1.equals(q2)); |
| assertFalse(q2 + " equal to " + q1, q2.equals(q1)); |
| } |
| |
| /** deep check that explanations of a query 'score' correctly */ |
| public static void checkExplanations (final Query q, final IndexSearcher s) throws IOException { |
| CheckHits.checkExplanations(q, null, s, true); |
| } |
| |
| /** |
| * Various query sanity checks on a searcher, some checks are only done for |
| * instanceof IndexSearcher. |
| * |
| * @see #check(Query) |
| * @see #checkFirstSkipTo |
| * @see #checkSkipTo |
| * @see #checkExplanations |
| * @see #checkEqual |
| * @see CheckHits#checkMatches(Query, IndexSearcher) |
| */ |
| public static void check(Random random, Query q1, IndexSearcher s) { |
| check(random, q1, s, true); |
| } |
| public static void check(Random random, Query q1, IndexSearcher s, boolean wrap) { |
| try { |
| check(q1); |
| if (s!=null) { |
| checkFirstSkipTo(q1,s); |
| checkSkipTo(q1,s); |
| checkBulkScorerSkipTo(random, q1, s); |
| if (wrap) { |
| check(random, q1, wrapUnderlyingReader(random, s, -1), false); |
| check(random, q1, wrapUnderlyingReader(random, s, 0), false); |
| check(random, q1, wrapUnderlyingReader(random, s, +1), false); |
| } |
| checkExplanations(q1,s); |
| CheckHits.checkMatches(q1, s); |
| } |
| } catch (IOException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| /** |
| * Given an IndexSearcher, returns a new IndexSearcher whose IndexReader |
| * is a MultiReader containing the Reader of the original IndexSearcher, |
| * as well as several "empty" IndexReaders -- some of which will have |
| * deleted documents in them. This new IndexSearcher should |
| * behave exactly the same as the original IndexSearcher. |
| * @param s the searcher to wrap |
| * @param edge if negative, s will be the first sub; if 0, s will be in the middle, if positive s will be the last sub |
| */ |
| public static IndexSearcher wrapUnderlyingReader(Random random, final IndexSearcher s, final int edge) |
| throws IOException { |
| |
| IndexReader r = s.getIndexReader(); |
| |
| // we can't put deleted docs before the nested reader, because |
| // it will throw off the docIds |
| IndexReader[] readers = new IndexReader[] { |
| edge < 0 ? r : new MultiReader(), |
| new MultiReader(), |
| new MultiReader(edge < 0 ? emptyReader(4) : new MultiReader(), |
| new MultiReader(), |
| 0 == edge ? r : new MultiReader()), |
| 0 < edge ? new MultiReader() : emptyReader(7), |
| new MultiReader(), |
| new MultiReader(0 < edge ? new MultiReader() : emptyReader(5), |
| new MultiReader(), |
| 0 < edge ? r : new MultiReader()) |
| }; |
| |
| IndexSearcher out = LuceneTestCase.newSearcher(new MultiReader(readers)); |
| out.setSimilarity(s.getSimilarity()); |
| return out; |
| } |
| |
| private static IndexReader emptyReader(final int maxDoc) { |
| return new LeafReader() { |
| |
| @Override |
| public Terms terms(String field) throws IOException { |
| return null; |
| } |
| |
| @Override |
| public NumericDocValues getNumericDocValues(String field) throws IOException { |
| return null; |
| } |
| |
| @Override |
| public BinaryDocValues getBinaryDocValues(String field) throws IOException { |
| return null; |
| } |
| |
| @Override |
| public SortedDocValues getSortedDocValues(String field) throws IOException { |
| return null; |
| } |
| |
| @Override |
| public SortedNumericDocValues getSortedNumericDocValues(String field) throws IOException { |
| return null; |
| } |
| |
| @Override |
| public SortedSetDocValues getSortedSetDocValues(String field) throws IOException { |
| return null; |
| } |
| |
| @Override |
| public NumericDocValues getNormValues(String field) throws IOException { |
| return null; |
| } |
| |
| @Override |
| public FieldInfos getFieldInfos() { |
| return FieldInfos.EMPTY; |
| } |
| |
| final Bits liveDocs = new Bits.MatchNoBits(maxDoc); |
| @Override |
| public Bits getLiveDocs() { |
| return liveDocs; |
| } |
| |
| @Override |
| public PointValues getPointValues(String fieldName) { |
| return null; |
| } |
| |
| @Override |
| public void checkIntegrity() throws IOException {} |
| |
| @Override |
| public Fields getTermVectors(int docID) throws IOException { |
| return null; |
| } |
| |
| @Override |
| public int numDocs() { |
| return 0; |
| } |
| |
| @Override |
| public int maxDoc() { |
| return maxDoc; |
| } |
| |
| @Override |
| public void document(int docID, StoredFieldVisitor visitor) throws IOException {} |
| |
| @Override |
| protected void doClose() throws IOException {} |
| |
| @Override |
| public LeafMetaData getMetaData() { |
| return new LeafMetaData(Version.LATEST.major, Version.LATEST, null); |
| } |
| |
| @Override |
| public CacheHelper getCoreCacheHelper() { |
| return null; |
| } |
| |
| @Override |
| public CacheHelper getReaderCacheHelper() { |
| return null; |
| } |
| }; |
| } |
| |
| /** alternate scorer advance(),advance(),next(),next(),advance(),advance(), etc |
| * and ensure a hitcollector receives same docs and scores |
| */ |
| public static void checkSkipTo(final Query q, final IndexSearcher s) throws IOException { |
| //System.out.println("Checking "+q); |
| final List<LeafReaderContext> readerContextArray = s.getTopReaderContext().leaves(); |
| |
| final int skip_op = 0; |
| final int next_op = 1; |
| final int orders [][] = { |
| {next_op}, |
| {skip_op}, |
| {skip_op, next_op}, |
| {next_op, skip_op}, |
| {skip_op, skip_op, next_op, next_op}, |
| {next_op, next_op, skip_op, skip_op}, |
| {skip_op, skip_op, skip_op, next_op, next_op}, |
| }; |
| for (int k = 0; k < orders.length; k++) { |
| |
| final int order[] = orders[k]; |
| // System.out.print("Order:");for (int i = 0; i < order.length; i++) |
| // System.out.print(order[i]==skip_op ? " skip()":" next()"); |
| // System.out.println(); |
| final int opidx[] = { 0 }; |
| final int lastDoc[] = {-1}; |
| |
| // FUTURE: ensure scorer.doc()==-1 |
| |
| final float maxDiff = 1e-5f; |
| final LeafReader lastReader[] = {null}; |
| |
| s.search(q, new SimpleCollector() { |
| private Scorable sc; |
| private Scorer scorer; |
| private DocIdSetIterator iterator; |
| private int leafPtr; |
| |
| @Override |
| public void setScorer(Scorable scorer) { |
| this.sc = scorer; |
| } |
| |
| @Override |
| public void collect(int doc) throws IOException { |
| float score = sc.score(); |
| lastDoc[0] = doc; |
| try { |
| if (scorer == null) { |
| Query rewritten = s.rewrite(q); |
| Weight w = s.createWeight(rewritten, ScoreMode.COMPLETE, 1); |
| LeafReaderContext context = readerContextArray.get(leafPtr); |
| scorer = w.scorer(context); |
| iterator = scorer.iterator(); |
| } |
| |
| int op = order[(opidx[0]++) % order.length]; |
| // System.out.println(op==skip_op ? |
| // "skip("+(sdoc[0]+1)+")":"next()"); |
| boolean more = op == skip_op ? iterator.advance(scorer.docID() + 1) != DocIdSetIterator.NO_MORE_DOCS |
| : iterator.nextDoc() != DocIdSetIterator.NO_MORE_DOCS; |
| int scorerDoc = scorer.docID(); |
| float scorerScore = scorer.score(); |
| float scorerScore2 = scorer.score(); |
| float scoreDiff = Math.abs(score - scorerScore); |
| float scorerDiff = Math.abs(scorerScore2 - scorerScore); |
| |
| boolean success = false; |
| try { |
| assertTrue(more); |
| assertEquals("scorerDoc=" + scorerDoc + ",doc=" + doc, scorerDoc, doc); |
| assertTrue("score=" + score + ", scorerScore=" + scorerScore, scoreDiff <= maxDiff); |
| assertTrue("scorerScorer=" + scorerScore + ", scorerScore2=" + scorerScore2, scorerDiff <= maxDiff); |
| success = true; |
| } finally { |
| if (!success) { |
| if (LuceneTestCase.VERBOSE) { |
| StringBuilder sbord = new StringBuilder(); |
| for (int i = 0; i < order.length; i++) { |
| sbord.append(order[i] == skip_op ? " skip()" : " next()"); |
| } |
| System.out.println("ERROR matching docs:" + "\n\t" |
| + (doc != scorerDoc ? "--> " : "") + "doc=" + doc + ", scorerDoc=" + scorerDoc |
| + "\n\t" + (!more ? "--> " : "") + "tscorer.more=" + more |
| + "\n\t" + (scoreDiff > maxDiff ? "--> " : "") |
| + "scorerScore=" + scorerScore + " scoreDiff=" + scoreDiff |
| + " maxDiff=" + maxDiff + "\n\t" |
| + (scorerDiff > maxDiff ? "--> " : "") + "scorerScore2=" |
| + scorerScore2 + " scorerDiff=" + scorerDiff |
| + "\n\thitCollector.doc=" + doc + " score=" + score |
| + "\n\t Scorer=" + scorer + "\n\t Query=" + q + " " |
| + q.getClass().getName() + "\n\t Searcher=" + s |
| + "\n\t Order=" + sbord + "\n\t Op=" |
| + (op == skip_op ? " skip()" : " next()")); |
| } |
| } |
| } |
| } catch (IOException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| @Override |
| public ScoreMode scoreMode() { |
| return ScoreMode.COMPLETE; |
| } |
| |
| @Override |
| protected void doSetNextReader(LeafReaderContext context) throws IOException { |
| // confirm that skipping beyond the last doc, on the |
| // previous reader, hits NO_MORE_DOCS |
| if (lastReader[0] != null) { |
| final LeafReader previousReader = lastReader[0]; |
| IndexSearcher indexSearcher = LuceneTestCase.newSearcher(previousReader, false); |
| indexSearcher.setSimilarity(s.getSimilarity()); |
| Query rewritten = indexSearcher.rewrite(q); |
| Weight w = indexSearcher.createWeight(rewritten, ScoreMode.COMPLETE, 1); |
| LeafReaderContext ctx = (LeafReaderContext)indexSearcher.getTopReaderContext(); |
| Scorer scorer = w.scorer(ctx); |
| if (scorer != null) { |
| DocIdSetIterator iterator = scorer.iterator(); |
| boolean more = false; |
| final Bits liveDocs = context.reader().getLiveDocs(); |
| for (int d = iterator.advance(lastDoc[0] + 1); d != DocIdSetIterator.NO_MORE_DOCS; d = iterator.nextDoc()) { |
| if (liveDocs == null || liveDocs.get(d)) { |
| more = true; |
| break; |
| } |
| } |
| Assert.assertFalse("query's last doc was "+ lastDoc[0] +" but advance("+(lastDoc[0]+1)+") got to "+scorer.docID(),more); |
| } |
| leafPtr++; |
| } |
| lastReader[0] = context.reader(); |
| assert readerContextArray.get(leafPtr).reader() == context.reader(); |
| this.scorer = null; |
| lastDoc[0] = -1; |
| } |
| }); |
| |
| if (lastReader[0] != null) { |
| // confirm that skipping beyond the last doc, on the |
| // previous reader, hits NO_MORE_DOCS |
| final LeafReader previousReader = lastReader[0]; |
| IndexSearcher indexSearcher = LuceneTestCase.newSearcher(previousReader, false); |
| indexSearcher.setSimilarity(s.getSimilarity()); |
| Query rewritten = indexSearcher.rewrite(q); |
| Weight w = indexSearcher.createWeight(rewritten, ScoreMode.COMPLETE, 1); |
| LeafReaderContext ctx = previousReader.getContext(); |
| Scorer scorer = w.scorer(ctx); |
| if (scorer != null) { |
| DocIdSetIterator iterator = scorer.iterator(); |
| boolean more = false; |
| final Bits liveDocs = lastReader[0].getLiveDocs(); |
| for (int d = iterator.advance(lastDoc[0] + 1); d != DocIdSetIterator.NO_MORE_DOCS; d = iterator.nextDoc()) { |
| if (liveDocs == null || liveDocs.get(d)) { |
| more = true; |
| break; |
| } |
| } |
| Assert.assertFalse("query's last doc was "+ lastDoc[0] +" but advance("+(lastDoc[0]+1)+") got to "+scorer.docID(),more); |
| } |
| } |
| } |
| } |
| |
| /** check that first skip on just created scorers always goes to the right doc */ |
| public static void checkFirstSkipTo(final Query q, final IndexSearcher s) throws IOException { |
| //System.out.println("checkFirstSkipTo: "+q); |
| final float maxDiff = 1e-3f; |
| final int lastDoc[] = {-1}; |
| final LeafReader lastReader[] = {null}; |
| final List<LeafReaderContext> context = s.getTopReaderContext().leaves(); |
| Query rewritten = s.rewrite(q); |
| s.search(q,new SimpleCollector() { |
| private Scorable scorer; |
| private int leafPtr; |
| @Override |
| public void setScorer(Scorable scorer) { |
| this.scorer = scorer; |
| } |
| @Override |
| public void collect(int doc) throws IOException { |
| float score = scorer.score(); |
| try { |
| long startMS = System.currentTimeMillis(); |
| for (int i=lastDoc[0]+1; i<=doc; i++) { |
| Weight w = s.createWeight(rewritten, ScoreMode.COMPLETE, 1); |
| Scorer scorer = w.scorer(context.get(leafPtr)); |
| Assert.assertTrue("query collected "+doc+" but advance("+i+") says no more docs!",scorer.iterator().advance(i) != DocIdSetIterator.NO_MORE_DOCS); |
| Assert.assertEquals("query collected "+doc+" but advance("+i+") got to "+scorer.docID(),doc,scorer.docID()); |
| float advanceScore = scorer.score(); |
| Assert.assertEquals("unstable advance("+i+") score!",advanceScore,scorer.score(),maxDiff); |
| Assert.assertEquals("query assigned doc "+doc+" a score of <"+score+"> but advance("+i+") has <"+advanceScore+">!",score,advanceScore,maxDiff); |
| |
| // Hurry things along if they are going slow (eg |
| // if you got SimpleText codec this will kick in): |
| if (i < doc && System.currentTimeMillis() - startMS > 5) { |
| i = doc-1; |
| } |
| } |
| lastDoc[0] = doc; |
| } catch (IOException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| @Override |
| public ScoreMode scoreMode() { |
| return ScoreMode.COMPLETE; |
| } |
| |
| @Override |
| protected void doSetNextReader(LeafReaderContext context) throws IOException { |
| // confirm that skipping beyond the last doc, on the |
| // previous reader, hits NO_MORE_DOCS |
| if (lastReader[0] != null) { |
| final LeafReader previousReader = lastReader[0]; |
| IndexSearcher indexSearcher = LuceneTestCase.newSearcher(previousReader, false); |
| indexSearcher.setSimilarity(s.getSimilarity()); |
| Weight w = indexSearcher.createWeight(rewritten, ScoreMode.COMPLETE, 1); |
| Scorer scorer = w.scorer((LeafReaderContext)indexSearcher.getTopReaderContext()); |
| if (scorer != null) { |
| DocIdSetIterator iterator = scorer.iterator(); |
| boolean more = false; |
| final Bits liveDocs = context.reader().getLiveDocs(); |
| for (int d = iterator.advance(lastDoc[0] + 1); d != DocIdSetIterator.NO_MORE_DOCS; d = iterator.nextDoc()) { |
| if (liveDocs == null || liveDocs.get(d)) { |
| more = true; |
| break; |
| } |
| } |
| Assert.assertFalse("query's last doc was "+ lastDoc[0] +" but advance("+(lastDoc[0]+1)+") got to "+scorer.docID(),more); |
| } |
| leafPtr++; |
| } |
| |
| lastReader[0] = context.reader(); |
| lastDoc[0] = -1; |
| } |
| }); |
| |
| if (lastReader[0] != null) { |
| // confirm that skipping beyond the last doc, on the |
| // previous reader, hits NO_MORE_DOCS |
| final LeafReader previousReader = lastReader[0]; |
| IndexSearcher indexSearcher = LuceneTestCase.newSearcher(previousReader, false); |
| indexSearcher.setSimilarity(s.getSimilarity()); |
| Weight w = indexSearcher.createWeight(rewritten, ScoreMode.COMPLETE, 1); |
| Scorer scorer = w.scorer((LeafReaderContext)indexSearcher.getTopReaderContext()); |
| if (scorer != null) { |
| DocIdSetIterator iterator = scorer.iterator(); |
| boolean more = false; |
| final Bits liveDocs = lastReader[0].getLiveDocs(); |
| for (int d = iterator.advance(lastDoc[0] + 1); d != DocIdSetIterator.NO_MORE_DOCS; d = iterator.nextDoc()) { |
| if (liveDocs == null || liveDocs.get(d)) { |
| more = true; |
| break; |
| } |
| } |
| Assert.assertFalse("query's last doc was "+ lastDoc[0] +" but advance("+(lastDoc[0]+1)+") got to "+scorer.docID(),more); |
| } |
| } |
| } |
| |
| /** Check that the scorer and bulk scorer advance consistently. */ |
| public static void checkBulkScorerSkipTo(Random r, Query query, IndexSearcher searcher) throws IOException { |
| query = searcher.rewrite(query); |
| Weight weight = searcher.createWeight(query, ScoreMode.COMPLETE, 1); |
| for (LeafReaderContext context : searcher.getIndexReader().leaves()) { |
| final Scorer scorer = weight.scorer(context); |
| final BulkScorer bulkScorer = weight.bulkScorer(context); |
| if (scorer == null && bulkScorer == null) { |
| continue; |
| } else if (bulkScorer == null) { |
| // ensure scorer is exhausted (it just didnt return null) |
| assert scorer.iterator().nextDoc() == DocIdSetIterator.NO_MORE_DOCS; |
| continue; |
| } |
| DocIdSetIterator iterator = scorer.iterator(); |
| int upTo = 0; |
| while (true) { |
| final int min = upTo + r.nextInt(5); |
| final int max = min + 1 + r.nextInt(r.nextBoolean() ? 10 : 5000); |
| if (scorer.docID() < min) { |
| iterator.advance(min); |
| } |
| final int next = bulkScorer.score(new LeafCollector() { |
| Scorable scorer2; |
| @Override |
| public void setScorer(Scorable scorer) throws IOException { |
| this.scorer2 = scorer; |
| } |
| @Override |
| public void collect(int doc) throws IOException { |
| assert doc >= min; |
| assert doc < max; |
| Assert.assertEquals(scorer.docID(), doc); |
| Assert.assertEquals(scorer.score(), scorer2.score(), 0.01f); |
| iterator.nextDoc(); |
| } |
| }, null, min, max); |
| assert max <= next; |
| assert next <= scorer.docID(); |
| upTo = max; |
| |
| if (scorer.docID() == DocIdSetIterator.NO_MORE_DOCS) { |
| bulkScorer.score(new LeafCollector() { |
| @Override |
| public void setScorer(Scorable scorer) throws IOException {} |
| |
| @Override |
| public void collect(int doc) throws IOException { |
| // no more matches |
| assert false; |
| } |
| }, null, upTo, DocIdSetIterator.NO_MORE_DOCS); |
| break; |
| } |
| } |
| } |
| } |
| } |