blob: 40fe24917c2a3d44e8214746bb334912458ef824 [file] [log] [blame]
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.BitSet;
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.document.Field.Store;
import org.apache.lucene.document.SortedDocValuesField;
import org.apache.lucene.document.StringField;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.LeafReader;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.PostingsEnum;
import org.apache.lucene.index.RandomIndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.search.FilteredQuery.FilterStrategy;
import org.apache.lucene.store.Directory;
import org.apache.lucene.util.BitDocIdSet;
import org.apache.lucene.util.Bits;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.FixedBitSet;
import org.apache.lucene.util.IOUtils;
import org.apache.lucene.util.LuceneTestCase;
import org.apache.lucene.util.TestUtil;
/**
* FilteredQuery JUnit tests.
*
* <p>Created: Apr 21, 2004 1:21:46 PM
*
*
* @since 1.4
*/
public class TestFilteredQuery extends LuceneTestCase {
private IndexSearcher searcher;
private IndexReader reader;
private Directory directory;
private Query query;
private Filter filter;
@Override
public void setUp() throws Exception {
super.setUp();
directory = newDirectory();
RandomIndexWriter writer = new RandomIndexWriter (random(), directory, newIndexWriterConfig(new MockAnalyzer(random())).setMergePolicy(newLogMergePolicy()));
Document doc = new Document();
doc.add (newTextField("field", "one two three four five", Field.Store.YES));
doc.add (newTextField("sorter", "b", Field.Store.YES));
doc.add (new SortedDocValuesField("sorter", new BytesRef("b")));
writer.addDocument (doc);
doc = new Document();
doc.add (newTextField("field", "one two three four", Field.Store.YES));
doc.add (newTextField("sorter", "d", Field.Store.YES));
doc.add (new SortedDocValuesField("sorter", new BytesRef("d")));
writer.addDocument (doc);
doc = new Document();
doc.add (newTextField("field", "one two three y", Field.Store.YES));
doc.add (newTextField("sorter", "a", Field.Store.YES));
doc.add (new SortedDocValuesField("sorter", new BytesRef("a")));
writer.addDocument (doc);
doc = new Document();
doc.add (newTextField("field", "one two x", Field.Store.YES));
doc.add (newTextField("sorter", "c", Field.Store.YES));
doc.add (new SortedDocValuesField("sorter", new BytesRef("c")));
writer.addDocument (doc);
// tests here require single segment (eg try seed
// 8239472272678419952L), because SingleDocTestFilter(x)
// blindly accepts that docID in any sub-segment
writer.forceMerge(1);
reader = writer.getReader();
writer.close();
searcher = newSearcher(reader);
query = new TermQuery (new Term ("field", "three"));
filter = newStaticFilterB();
}
// must be static for serialization tests
private static Filter newStaticFilterB() {
return new Filter() {
@Override
public DocIdSet getDocIdSet (LeafReaderContext context, Bits acceptDocs) {
if (acceptDocs == null) acceptDocs = new Bits.MatchAllBits(5);
FixedBitSet bitset = new FixedBitSet(context.reader().maxDoc());
if (acceptDocs.get(1)) bitset.set(1);
if (acceptDocs.get(3)) bitset.set(3);
return new BitDocIdSet(bitset);
}
@Override
public String toString(String field) {
return "staticFilterB";
}
};
}
@Override
public void tearDown() throws Exception {
reader.close();
directory.close();
super.tearDown();
}
public void testFilteredQuery() throws Exception {
// force the filter to be executed as bits
tFilteredQuery(true);
// force the filter to be executed as iterator
tFilteredQuery(false);
}
private void tFilteredQuery(final boolean useRandomAccess) throws Exception {
Query filteredquery = new FilteredQuery(query, filter, randomFilterStrategy(random(), useRandomAccess));
ScoreDoc[] hits = searcher.search (filteredquery, 1000).scoreDocs;
assertEquals (1, hits.length);
assertEquals (1, hits[0].doc);
QueryUtils.check(random(), filteredquery,searcher);
hits = searcher.search (filteredquery, 1000, new Sort(new SortField("sorter", SortField.Type.STRING))).scoreDocs;
assertEquals (1, hits.length);
assertEquals (1, hits[0].doc);
filteredquery = new FilteredQuery(new TermQuery (new Term ("field", "one")), filter, randomFilterStrategy(random(), useRandomAccess));
hits = searcher.search (filteredquery, 1000).scoreDocs;
assertEquals (2, hits.length);
QueryUtils.check(random(), filteredquery,searcher);
filteredquery = new FilteredQuery(new MatchAllDocsQuery(), filter, randomFilterStrategy(random(), useRandomAccess));
hits = searcher.search (filteredquery, 1000).scoreDocs;
assertEquals (2, hits.length);
QueryUtils.check(random(), filteredquery,searcher);
filteredquery = new FilteredQuery(new TermQuery (new Term ("field", "x")), filter, randomFilterStrategy(random(), useRandomAccess));
hits = searcher.search (filteredquery, 1000).scoreDocs;
assertEquals (1, hits.length);
assertEquals (3, hits[0].doc);
QueryUtils.check(random(), filteredquery,searcher);
filteredquery = new FilteredQuery(new TermQuery (new Term ("field", "y")), filter, randomFilterStrategy(random(), useRandomAccess));
hits = searcher.search (filteredquery, 1000).scoreDocs;
assertEquals (0, hits.length);
QueryUtils.check(random(), filteredquery,searcher);
// test boost
Filter f = newStaticFilterA();
float boost = 2.5f;
BooleanQuery.Builder bq1 = new BooleanQuery.Builder();
TermQuery tq = new TermQuery (new Term ("field", "one"));
tq.setBoost(boost);
bq1.add(tq, Occur.MUST);
bq1.add(new TermQuery (new Term ("field", "five")), Occur.MUST);
BooleanQuery.Builder bq2 = new BooleanQuery.Builder();
tq = new TermQuery (new Term ("field", "one"));
filteredquery = new FilteredQuery(tq, f, randomFilterStrategy(random(), useRandomAccess));
filteredquery.setBoost(boost);
bq2.add(filteredquery, Occur.MUST);
bq2.add(new TermQuery (new Term ("field", "five")), Occur.MUST);
assertScoreEquals(bq1.build(), bq2.build());
assertEquals(boost, filteredquery.getBoost(), 0);
assertEquals(1.0f, tq.getBoost(), 0); // the boost value of the underlying query shouldn't have changed
}
// must be static for serialization tests
private static Filter newStaticFilterA() {
return new Filter() {
@Override
public DocIdSet getDocIdSet (LeafReaderContext context, Bits acceptDocs) {
assertNull("acceptDocs should be null, as we have an index without deletions", acceptDocs);
FixedBitSet bitset = new FixedBitSet(context.reader().maxDoc());
bitset.set(0, Math.min(5, bitset.length()));
return new BitDocIdSet(bitset);
}
@Override
public String toString(String field) {
return "staticFilterA";
}
};
}
/**
* Tests whether the scores of the two queries are the same.
*/
public void assertScoreEquals(Query q1, Query q2) throws Exception {
ScoreDoc[] hits1 = searcher.search (q1, 1000).scoreDocs;
ScoreDoc[] hits2 = searcher.search (q2, 1000).scoreDocs;
assertEquals(hits1.length, hits2.length);
for (int i = 0; i < hits1.length; i++) {
assertEquals(hits1[i].doc, hits2[i].doc);
assertEquals(hits1[i].score, hits2[i].score, 0.000001f);
}
}
/**
* This tests FilteredQuery's rewrite correctness
*/
public void testRangeQuery() throws Exception {
// force the filter to be executed as bits
tRangeQuery(true);
tRangeQuery(false);
}
private void tRangeQuery(final boolean useRandomAccess) throws Exception {
TermRangeQuery rq = TermRangeQuery.newStringRange(
"sorter", "b", "d", true, true);
Query filteredquery = new FilteredQuery(rq, filter, randomFilterStrategy(random(), useRandomAccess));
ScoreDoc[] hits = searcher.search(filteredquery, 1000).scoreDocs;
assertEquals(2, hits.length);
QueryUtils.check(random(), filteredquery,searcher);
}
public void testBooleanMUST() throws Exception {
// force the filter to be executed as bits
tBooleanMUST(true);
// force the filter to be executed as iterator
tBooleanMUST(false);
}
private void tBooleanMUST(final boolean useRandomAccess) throws Exception {
BooleanQuery.Builder bq = new BooleanQuery.Builder();
Query query = new FilteredQuery(new TermQuery(new Term("field", "one")), new SingleDocTestFilter(0), randomFilterStrategy(random(), useRandomAccess));
bq.add(query, BooleanClause.Occur.MUST);
query = new FilteredQuery(new TermQuery(new Term("field", "one")), new SingleDocTestFilter(1), randomFilterStrategy(random(), useRandomAccess));
bq.add(query, BooleanClause.Occur.MUST);
ScoreDoc[] hits = searcher.search(bq.build(), 1000).scoreDocs;
assertEquals(0, hits.length);
QueryUtils.check(random(), query,searcher);
}
public void testBooleanSHOULD() throws Exception {
// force the filter to be executed as bits
tBooleanSHOULD(true);
// force the filter to be executed as iterator
tBooleanSHOULD(false);
}
private void tBooleanSHOULD(final boolean useRandomAccess) throws Exception {
BooleanQuery.Builder bq = new BooleanQuery.Builder();
Query query = new FilteredQuery(new TermQuery(new Term("field", "one")), new SingleDocTestFilter(0), randomFilterStrategy(random(), useRandomAccess));
bq.add(query, BooleanClause.Occur.SHOULD);
query = new FilteredQuery(new TermQuery(new Term("field", "one")), new SingleDocTestFilter(1), randomFilterStrategy(random(), useRandomAccess));
bq.add(query, BooleanClause.Occur.SHOULD);
ScoreDoc[] hits = searcher.search(bq.build(), 1000).scoreDocs;
assertEquals(2, hits.length);
QueryUtils.check(random(), query,searcher);
}
// Make sure BooleanQuery, which does out-of-order
// scoring, inside FilteredQuery, works
public void testBoolean2() throws Exception {
// force the filter to be executed as bits
tBoolean2(true);
// force the filter to be executed as iterator
tBoolean2(false);
}
private void tBoolean2(final boolean useRandomAccess) throws Exception {
BooleanQuery.Builder bq = new BooleanQuery.Builder();
bq.add(new TermQuery(new Term("field", "one")), BooleanClause.Occur.SHOULD);
bq.add(new TermQuery(new Term("field", "two")), BooleanClause.Occur.SHOULD);
Query query = new FilteredQuery(bq.build(), new SingleDocTestFilter(0), randomFilterStrategy(random(), useRandomAccess));
ScoreDoc[] hits = searcher.search(query, 1000).scoreDocs;
assertEquals(1, hits.length);
QueryUtils.check(random(), query, searcher);
}
public void testChainedFilters() throws Exception {
// force the filter to be executed as bits
tChainedFilters(true);
// force the filter to be executed as iterator
tChainedFilters(false);
}
// a filter for which other queries don't have special rewrite rules
private static class FilterWrapper extends Filter {
private final Filter in;
FilterWrapper(Filter in) {
this.in = in;
}
@Override
public DocIdSet getDocIdSet(LeafReaderContext context, Bits acceptDocs) throws IOException {
return in.getDocIdSet(context, acceptDocs);
}
@Override
public String toString(String field) {
return in.toString(field);
}
@Override
public boolean equals(Object obj) {
if (super.equals(obj) == false) {
return false;
}
return in.equals(((FilterWrapper) obj).in);
}
}
private void tChainedFilters(final boolean useRandomAccess) throws Exception {
Query query = new FilteredQuery(new FilteredQuery(
new MatchAllDocsQuery(), new FilterWrapper(new QueryWrapperFilter(new TermQuery(new Term("field", "three")))), randomFilterStrategy(random(), useRandomAccess)),
new FilterWrapper(new QueryWrapperFilter(new TermQuery(new Term("field", "four")))), randomFilterStrategy(random(), useRandomAccess));
ScoreDoc[] hits = searcher.search(query, 10).scoreDocs;
assertEquals(2, hits.length);
QueryUtils.check(random(), query, searcher);
// one more:
query = new FilteredQuery(query,
new FilterWrapper(new QueryWrapperFilter(new TermQuery(new Term("field", "five")))), randomFilterStrategy(random(), useRandomAccess));
hits = searcher.search(query, 10).scoreDocs;
assertEquals(1, hits.length);
QueryUtils.check(random(), query, searcher);
}
public void testEqualsHashcode() throws Exception {
// some tests before, if the used queries and filters work:
assertEquals(new PrefixQuery(new Term("field", "o")), new PrefixQuery(new Term("field", "o")));
assertFalse(new PrefixQuery(new Term("field", "a")).equals(new PrefixQuery(new Term("field", "o"))));
QueryUtils.checkHashEquals(new TermQuery(new Term("field", "one")));
QueryUtils.checkUnequal(
new TermQuery(new Term("field", "one")), new TermQuery(new Term("field", "two"))
);
// now test FilteredQuery equals/hashcode:
QueryUtils.checkHashEquals(new FilteredQuery(new TermQuery(new Term("field", "one")), new QueryWrapperFilter(new PrefixQuery(new Term("field", "o")))));
QueryUtils.checkUnequal(
new FilteredQuery(new TermQuery(new Term("field", "one")), new QueryWrapperFilter(new PrefixQuery(new Term("field", "o")))),
new FilteredQuery(new TermQuery(new Term("field", "two")), new QueryWrapperFilter(new PrefixQuery(new Term("field", "o"))))
);
QueryUtils.checkUnequal(
new FilteredQuery(new TermQuery(new Term("field", "one")), new QueryWrapperFilter(new PrefixQuery(new Term("field", "a")))),
new FilteredQuery(new TermQuery(new Term("field", "one")), new QueryWrapperFilter(new PrefixQuery(new Term("field", "o"))))
);
}
public void testInvalidArguments() throws Exception {
try {
new FilteredQuery(null, null);
fail("Should throw NullPointerException");
} catch (NullPointerException npe) {
// pass
}
try {
new FilteredQuery(new TermQuery(new Term("field", "one")), null);
fail("Should throw NullPointerException");
} catch (NullPointerException npe) {
// pass
}
try {
new FilteredQuery(null, new QueryWrapperFilter(new PrefixQuery(new Term("field", "o"))));
fail("Should throw NullPointerException");
} catch (NullPointerException npe) {
// pass
}
}
private FilterStrategy randomFilterStrategy() {
return randomFilterStrategy(random(), true);
}
public void testGetFilterStrategy() {
FilterStrategy randomFilterStrategy = randomFilterStrategy();
FilteredQuery filteredQuery = new FilteredQuery(new TermQuery(new Term("field", "one")), new QueryWrapperFilter(new PrefixQuery(new Term("field", "o"))), randomFilterStrategy);
assertSame(randomFilterStrategy, filteredQuery.getFilterStrategy());
}
private static FilteredQuery.FilterStrategy randomFilterStrategy(Random random, final boolean useRandomAccess) {
if (useRandomAccess) {
return new FilteredQuery.RandomAccessFilterStrategy() {
@Override
protected boolean useRandomAccess(Bits bits, long filterCost) {
return true;
}
};
}
return TestUtil.randomFilterStrategy(random);
}
/*
* Test if the QueryFirst strategy calls the bits only if the document has
* been matched by the query and not otherwise
*/
public void testQueryFirstFilterStrategy() throws IOException {
Directory directory = newDirectory();
RandomIndexWriter writer = new RandomIndexWriter(random(), directory,
newIndexWriterConfig(new MockAnalyzer(random())));
int numDocs = atLeast(50);
int totalDocsWithZero = 0;
for (int i = 0; i < numDocs; i++) {
Document doc = new Document();
int num = random().nextInt(5);
if (num == 0) {
totalDocsWithZero++;
}
doc.add(newTextField("field", "" + num, Field.Store.YES));
writer.addDocument(doc);
}
IndexReader reader = writer.getReader();
writer.close();
IndexSearcher searcher = newSearcher(reader);
searcher.setQueryCache(null); // needed otherwise the iterator may be used
Query query = new FilteredQuery(new TermQuery(new Term("field", "0")),
new Filter() {
@Override
public DocIdSet getDocIdSet(LeafReaderContext context,
Bits acceptDocs) throws IOException {
final boolean nullBitset = random().nextInt(10) == 5;
final LeafReader reader = context.reader();
PostingsEnum termPostingsEnum = reader.postings(new Term("field", "0"));
if (termPostingsEnum == null) {
return null; // no docs -- return null
}
final BitSet bitSet = new BitSet(reader.maxDoc());
int d;
while ((d = termPostingsEnum.nextDoc()) != PostingsEnum.NO_MORE_DOCS) {
bitSet.set(d, true);
}
return new DocIdSet() {
@Override
public long ramBytesUsed() {
return 0L;
}
@Override
public Bits bits() throws IOException {
if (nullBitset) {
return null;
}
return new Bits() {
@Override
public boolean get(int index) {
assertTrue("filter was called for a non-matching doc",
bitSet.get(index));
return bitSet.get(index);
}
@Override
public int length() {
return bitSet.length();
}
};
}
@Override
public DocIdSetIterator iterator() throws IOException {
assertTrue(
"iterator should not be called if bitset is present",
nullBitset);
return reader.postings(new Term("field", "0"));
}
};
}
@Override
public String toString(String field) {
return "filterField0";
}
}, FilteredQuery.QUERY_FIRST_FILTER_STRATEGY);
TopDocs search = searcher.search(query, 10);
assertEquals(totalDocsWithZero, search.totalHits);
IOUtils.close(reader, directory);
}
public void testPreservesScores() throws IOException {
Directory dir = newDirectory();
RandomIndexWriter writer = new RandomIndexWriter(random(), dir);
Document doc = new Document();
doc.add(new StringField("foo", "bar", Store.NO));
writer.addDocument(doc);
writer.commit();
final IndexReader reader = writer.getReader();
writer.close();
final IndexSearcher searcher = new IndexSearcher(reader);
final Query query = new TermQuery(new Term("foo", "bar"));
query.setBoost(random().nextFloat());
FilteredQuery fq = new FilteredQuery(query, new Filter() {
@Override
public DocIdSet getDocIdSet(final LeafReaderContext context, Bits acceptDocs)
throws IOException {
return new DocIdSet() {
@Override
public long ramBytesUsed() {
return 0;
}
@Override
public DocIdSetIterator iterator() throws IOException {
return DocIdSetIterator.all(context.reader().maxDoc());
}
};
}
@Override
public String toString(String field) {
return "dummy";
}
});
assertEquals(searcher.search(query, 1).scoreDocs[0].score, searcher.search(fq, 1).scoreDocs[0].score, 0f);
fq.setBoost(random().nextFloat());
// QueryWrapperFilter has special rewrite rules
FilteredQuery fq2 = new FilteredQuery(query, new QueryWrapperFilter(new MatchAllDocsQuery()));
fq2.setBoost(fq.getBoost());
assertEquals(searcher.search(fq, 1).scoreDocs[0].score, searcher.search(fq2, 1).scoreDocs[0].score, 10e-5);
reader.close();
dir.close();
}
}