Index: lucene/CHANGES.txt =================================================================== --- lucene/CHANGES.txt (revision 1213452) +++ lucene/CHANGES.txt (working copy) @@ -385,6 +385,10 @@ (Mike McCandless, Uwe Schindler, Robert Muir, Chris Male, Yonik Seeley, Jason Rutherglen, Paul Elschot) +* LUCENE-3643: FilteredQuery and IndexSearcher.search(Query, Filter,...) + now allow null as Query, so it's possible to search an index with only + the Filter. Previously, a MatchAllDocsQuery was required. (Uwe Schindler) + New features * LUCENE-2604: Added RegexpQuery support to QueryParser. Regular expressions @@ -638,6 +642,10 @@ boolean clauses are required and instances of TermQuery. (Simon Willnauer, Robert Muir) +* LUCENE-3643: FilteredQuery and IndexSearcher.search(Query, Filter,...) + now optimize the special cases query == MatchAllDocsQuery / null to + execute as ConstantScoreQuery. (Uwe Schindler) + Bug fixes * LUCENE-2803: The FieldCache can miss values if an entry for a reader Index: lucene/src/java/org/apache/lucene/search/FilteredQuery.java =================================================================== --- lucene/src/java/org/apache/lucene/search/FilteredQuery.java (revision 1213452) +++ lucene/src/java/org/apache/lucene/search/FilteredQuery.java (working copy) @@ -33,25 +33,23 @@ *

Note: the bits are retrieved from the filter each time this * query is used in a search - use a CachingWrapperFilter to avoid * regenerating the bits every time. - * - *

Created: Apr 20, 2004 8:58:29 AM - * * @since 1.4 * @see CachingWrapperFilter */ -public class FilteredQuery -extends Query { +public class FilteredQuery extends Query { - Query query; - Filter filter; + private final Query query; + private final Filter filter; /** * Constructs a new query which applies a filter to the results of the original query. - * Filter.getDocIdSet() will be called every time this query is used in a search. - * @param query Query to be filtered, cannot be null. - * @param filter Filter to apply to query results, cannot be null. + * {@link Filter#getDocIdSet} will be called every time this query is used in a search. + * @param query Query to be filtered, can {@code null} if filter is not {@code null}. + * @param filter Filter to apply to query results, can {@code null} if query is not {@code null}. */ public FilteredQuery (Query query, Filter filter) { + if (query == null && filter == null) + throw new IllegalArgumentException("Both query and filter cannot be null."); this.query = query; this.filter = filter; } @@ -78,7 +76,9 @@ */ @Override public Weight createWeight(final IndexSearcher searcher) throws IOException { - final Weight weight = query.createWeight (searcher); + if (query == null || filter == null) + throw new UnsupportedOperationException("Please rewrite this query first."); + final Weight weight = query.createWeight(searcher); return new Weight() { @Override @@ -229,58 +229,94 @@ }; } - /** Rewrites the wrapped query. */ + /** Rewrites the query. If the wrapped {@code query == null} or an instance of + * {@link MatchAllDocsQuery} it returns a {@link ConstantScoreQuery}, if + * {@code filter == null} it returns the wrapped query itsself. Otherwise it + * returns a new {@code FilteredQuery} wrapping the rewritten query. */ @Override public Query rewrite(IndexReader reader) throws IOException { - Query rewritten = query.rewrite(reader); - if (rewritten != query) { - FilteredQuery clone = (FilteredQuery)this.clone(); - clone.query = rewritten; - return clone; + if ((filter != null) && (query == null || query instanceof MatchAllDocsQuery)) { + // Special case #1: If we have no query at all or it is + // a MatchAllDocsQuery, we only return a CSQ(filter). + final Query rewritten = new ConstantScoreQuery(filter); + float boost = this.getBoost(); + // If the query is not null (it's the MatchAllDocs), we will + // multiply in the query's boost (otherwise boost would get lost): + if (query != null) { + assert query instanceof MatchAllDocsQuery; + boost *= query.getBoost(); + } + rewritten.setBoost(boost); + return rewritten; + } + assert query != null : "at this place we must have query != null, as filter and "+ + "query cannot be null at same time (checked by ctor)"; + if (filter == null) { + // Special case #2: We have no filter at all, so we can rewrite to the wrapped + // query. In that case we must clone, as we need to modify the original query + // by including the boost of this FilteredQuery (otherwise boost would get lost): + final Query rewritten = (Query) query.clone(); + rewritten.setBoost(this.getBoost() * query.getBoost()); + return rewritten; + } + final Query queryRewritten = query.rewrite(reader); + if (queryRewritten != query) { + final Query rewritten = new FilteredQuery(queryRewritten, filter); + rewritten.setBoost(this.getBoost()); + return rewritten; } else { + // nothing to rewrite, we are done! return this; } } - public Query getQuery() { + public final Query getQuery() { return query; } - public Filter getFilter() { + public final Filter getFilter() { return filter; } // inherit javadoc @Override public void extractTerms(Set terms) { - getQuery().extractTerms(terms); + if (query != null) + query.extractTerms(terms); } /** Prints a user-readable version of this query. */ @Override public String toString (String s) { - StringBuilder buffer = new StringBuilder(); - buffer.append("filtered("); - buffer.append(query.toString(s)); - buffer.append(")->"); - buffer.append(filter); - buffer.append(ToStringUtils.boost(getBoost())); + StringBuilder buffer = new StringBuilder("filtered("); + buffer.append((query == null) ? ((String) null) : query.toString(s)); + buffer.append(")->").append(filter).append(ToStringUtils.boost(getBoost())); return buffer.toString(); } /** Returns true iff o is equal to this. */ @Override public boolean equals(Object o) { - if (o instanceof FilteredQuery) { - FilteredQuery fq = (FilteredQuery) o; - return (query.equals(fq.query) && filter.equals(fq.filter) && getBoost()==fq.getBoost()); - } - return false; + if (o == this) + return true; + if (!super.equals(o)) + return false; + assert o instanceof FilteredQuery; + final FilteredQuery fq = (FilteredQuery) o; + return ( + (fq.query == null ? this.query == null : fq.query.equals(this.query)) && + (fq.filter == null ? this.filter == null : fq.filter.equals(this.filter)) + ); } /** Returns a hash code value for this object. */ @Override public int hashCode() { - return query.hashCode() ^ filter.hashCode() + Float.floatToRawIntBits(getBoost()); + int hash = super.hashCode(); + if (query != null) + hash = hash * 31 + query.hashCode(); + if (filter != null) + hash = hash * 31 + filter.hashCode(); + return hash; } } Index: lucene/src/java/org/apache/lucene/search/IndexSearcher.java =================================================================== --- lucene/src/java/org/apache/lucene/search/IndexSearcher.java (revision 1213452) +++ lucene/src/java/org/apache/lucene/search/IndexSearcher.java (working copy) @@ -219,6 +219,7 @@ /** Finds the top n * hits for query, applying filter if non-null, * where all results are after a previous result (after). + * It is also possible to pass a null query and a non-null filter. *

* By passing the bottom result from a previous page as after, * this method can be used for efficient 'deep-paging' across potentially @@ -243,6 +244,7 @@ /** Finds the top n * hits for query, applying filter if non-null. + * It is also possible to pass a null query and a non-null filter. * * @throws BooleanQuery.TooManyClauses */ @@ -262,7 +264,7 @@ * IndexSearcher#search(Query, Filter, int)}) is usually more efficient, as it skips * non-high-scoring hits. * - * @param query to match documents + * @param query to match documents, must be non-null if filter is null. * @param filter if non-null, used to permit documents to be collected. * @param results to receive hits * @throws BooleanQuery.TooManyClauses @@ -294,6 +296,7 @@ * the top n hits for query, applying * filter if non-null, and sorting the hits by the criteria in * sort. + * It is also possible to pass a null query and a non-null filter. * *

NOTE: this does not compute scores by default; use * {@link IndexSearcher#setDefaultFieldSortScoring} to Index: lucene/src/test-framework/java/org/apache/lucene/search/QueryUtils.java =================================================================== --- lucene/src/test-framework/java/org/apache/lucene/search/QueryUtils.java (revision 1213452) +++ lucene/src/test-framework/java/org/apache/lucene/search/QueryUtils.java (working copy) @@ -79,8 +79,8 @@ } public static void checkUnequal(Query q1, Query q2) { - Assert.assertTrue(!q1.equals(q2)); - Assert.assertTrue(!q2.equals(q1)); + Assert.assertFalse(q1 + " equal to " + q2, q1.equals(q2)); + Assert.assertFalse(q2 + " equal to " + q1, q2.equals(q1)); // possible this test can fail on a hash collision... if that // happens, please change test to use a different example. Index: lucene/src/test/org/apache/lucene/search/TestFilteredQuery.java =================================================================== --- lucene/src/test/org/apache/lucene/search/TestFilteredQuery.java (revision 1213452) +++ lucene/src/test/org/apache/lucene/search/TestFilteredQuery.java (working copy) @@ -132,6 +132,26 @@ assertEquals (2, hits.length); QueryUtils.check(random, filteredquery,searcher); + filteredquery = new FilteredQueryRA(null, filter, useRandomAccess); + hits = searcher.search (filteredquery, null, 1000).scoreDocs; + assertEquals (2, hits.length); + QueryUtils.check(random, filteredquery,searcher); + + filteredquery = new FilteredQueryRA(new MatchAllDocsQuery(), filter, useRandomAccess); + hits = searcher.search (filteredquery, null, 1000).scoreDocs; + assertEquals (2, hits.length); + QueryUtils.check(random, filteredquery,searcher); + + filteredquery = new FilteredQueryRA(new TermQuery (new Term ("field", "one")), null, useRandomAccess); + hits = searcher.search (filteredquery, null, 1000).scoreDocs; + assertEquals (4, hits.length); + QueryUtils.check(random, filteredquery,searcher); + + filteredquery = new FilteredQueryRA(new MatchAllDocsQuery(), null, useRandomAccess); + hits = searcher.search (filteredquery, null, 1000).scoreDocs; + assertEquals (4, hits.length); + QueryUtils.check(random, filteredquery,searcher); + filteredquery = new FilteredQueryRA(new TermQuery (new Term ("field", "x")), filter, useRandomAccess); hits = searcher.search (filteredquery, null, 1000).scoreDocs; assertEquals (1, hits.length); @@ -220,9 +240,9 @@ private void tBooleanMUST(final boolean useRandomAccess) throws Exception { BooleanQuery bq = new BooleanQuery(); - Query query = new FilteredQueryRA(new MatchAllDocsQuery(), new SingleDocTestFilter(0), useRandomAccess); + Query query = new FilteredQueryRA(new TermQuery(new Term("field", "one")), new SingleDocTestFilter(0), useRandomAccess); bq.add(query, BooleanClause.Occur.MUST); - query = new FilteredQueryRA(new MatchAllDocsQuery(), new SingleDocTestFilter(1), useRandomAccess); + query = new FilteredQueryRA(new TermQuery(new Term("field", "one")), new SingleDocTestFilter(1), useRandomAccess); bq.add(query, BooleanClause.Occur.MUST); ScoreDoc[] hits = searcher.search(bq, null, 1000).scoreDocs; assertEquals(0, hits.length); @@ -238,9 +258,9 @@ private void tBooleanSHOULD(final boolean useRandomAccess) throws Exception { BooleanQuery bq = new BooleanQuery(); - Query query = new FilteredQueryRA(new MatchAllDocsQuery(), new SingleDocTestFilter(0), useRandomAccess); + Query query = new FilteredQueryRA(new TermQuery(new Term("field", "one")), new SingleDocTestFilter(0), useRandomAccess); bq.add(query, BooleanClause.Occur.SHOULD); - query = new FilteredQueryRA(new MatchAllDocsQuery(), new SingleDocTestFilter(1), useRandomAccess); + query = new FilteredQueryRA(new TermQuery(new Term("field", "one")), new SingleDocTestFilter(1), useRandomAccess); bq.add(query, BooleanClause.Occur.SHOULD); ScoreDoc[] hits = searcher.search(bq, null, 1000).scoreDocs; assertEquals(2, hits.length); @@ -288,7 +308,85 @@ 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 PrefixFilter(new Term("field", "o")), new PrefixFilter(new Term("field", "o"))); + assertFalse(new PrefixFilter(new Term("field", "a")).equals(new PrefixFilter(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 PrefixFilter(new Term("field", "o")))); + QueryUtils.checkHashEquals(new FilteredQuery(null, new PrefixFilter(new Term("field", "o")))); + QueryUtils.checkHashEquals(new FilteredQuery(new TermQuery(new Term("field", "one")), null)); + QueryUtils.checkUnequal( + new FilteredQuery(new TermQuery(new Term("field", "one")), new PrefixFilter(new Term("field", "o"))), + new FilteredQuery(new TermQuery(new Term("field", "two")), new PrefixFilter(new Term("field", "o"))) + ); + QueryUtils.checkUnequal( + new FilteredQuery(new TermQuery(new Term("field", "one")), new PrefixFilter(new Term("field", "a"))), + new FilteredQuery(new TermQuery(new Term("field", "one")), new PrefixFilter(new Term("field", "o"))) + ); + QueryUtils.checkUnequal( + new FilteredQuery(new TermQuery(new Term("field", "one")), new PrefixFilter(new Term("field", "o"))), + new FilteredQuery(null, new PrefixFilter(new Term("field", "o"))) + ); + QueryUtils.checkUnequal( + new FilteredQuery(new TermQuery(new Term("field", "one")), new PrefixFilter(new Term("field", "o"))), + new FilteredQuery(new TermQuery(new Term("field", "one")), null) + ); + } + + public void testInvalidArguments() throws Exception { + try { + new FilteredQuery(null, null); + fail("Should throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // pass + } + } + + private void assertRewrite(FilteredQuery fq, Class clazz) throws Exception { + // assign crazy boost to FQ + final float boost = random.nextFloat() * 100.f; + fq.setBoost(boost); + + // assign crazy boost to inner + final float innerBoost; + if (fq.getQuery() != null) { + innerBoost = random.nextFloat() * 100.f; + fq.getQuery().setBoost(innerBoost); + } else { + innerBoost = 1.f; + } + + // check the class and boosts of rewritten query + final Query rewritten = searcher.rewrite(fq); + assertTrue("is not instance of " + clazz.getName(), clazz.isInstance(rewritten)); + if (rewritten instanceof FilteredQuery) { + assertEquals(boost, rewritten.getBoost(), 1.E-5f); + assertEquals(innerBoost, ((FilteredQuery) rewritten).getQuery().getBoost(), 1.E-5f); + } else { + assertEquals(boost * innerBoost, rewritten.getBoost(), 1.E-5f); + } + + // check that the original query was not modified + assertEquals(boost, fq.getBoost(), 1.E-5f); + if (fq.getQuery() != null) { + assertEquals(innerBoost, fq.getQuery().getBoost(), 1.E-5f); + } + } + public void testRewrite() throws Exception { + assertRewrite(new FilteredQuery(new TermQuery(new Term("field", "one")), new PrefixFilter(new Term("field", "o"))), FilteredQuery.class); + assertRewrite(new FilteredQuery(new MatchAllDocsQuery(), new PrefixFilter(new Term("field", "o"))), ConstantScoreQuery.class); + assertRewrite(new FilteredQuery(null, new PrefixFilter(new Term("field", "o"))), ConstantScoreQuery.class); + assertRewrite(new FilteredQuery(new MatchAllDocsQuery(), null), MatchAllDocsQuery.class); + assertRewrite(new FilteredQuery(new TermQuery(new Term("field", "one")), null), TermQuery.class); + } + public static final class FilteredQueryRA extends FilteredQuery { private final boolean useRandomAccess;