Index: lucene/CHANGES.txt =================================================================== --- lucene/CHANGES.txt (revision 1416850) +++ lucene/CHANGES.txt (working copy) @@ -254,6 +254,9 @@ Users of this API can now simply obtain an instance via DocValues#getDirectSource per thread. (Simon Willnauer) +* LUCENE-4580: DrillDown.query variants return a ConstantScoreQuery with boost set to 0.0f + so that documents scores are not affected by running a drill-down query. (Shai Erera) + Documentation * LUCENE-4483: Refer to BytesRef.deepCopyOf in Term's constructor that takes BytesRef. Index: lucene/facet/src/examples/org/apache/lucene/facet/example/simple/SimpleSearcher.java =================================================================== --- lucene/facet/src/examples/org/apache/lucene/facet/example/simple/SimpleSearcher.java (revision 1416850) +++ lucene/facet/src/examples/org/apache/lucene/facet/example/simple/SimpleSearcher.java (working copy) @@ -138,6 +138,8 @@ public static List searchWithDrillDown(IndexReader indexReader, TaxonomyReader taxoReader) throws Exception { + final FacetIndexingParams indexingParams = new DefaultFacetIndexingParams(); + // base query the user is interested in Query baseQuery = new TermQuery(new Term(SimpleUtils.TEXT, "white")); @@ -145,7 +147,7 @@ CountFacetRequest facetRequest = new CountFacetRequest(new CategoryPath("root","a"), 10); // initial search - all docs matching the base query will contribute to the accumulation - List res1 = searchWithRequest(indexReader, taxoReader, null, facetRequest); + List res1 = searchWithRequest(indexReader, taxoReader, indexingParams, facetRequest); // a single result (because there was a single request) FacetResult fres = res1.get(0); @@ -157,12 +159,12 @@ CategoryPath categoryOfInterest = resIterator.next().getLabel(); // drill-down preparation: turn the base query into a drill-down query for the category of interest - Query q2 = DrillDown.query(baseQuery, categoryOfInterest); + Query q2 = DrillDown.query(indexingParams, baseQuery, categoryOfInterest); // that's it - search with the new query and we're done! // only documents both matching the base query AND containing the // category of interest will contribute to the new accumulation - return searchWithRequestAndQuery(q2, indexReader, taxoReader, null, facetRequest); + return searchWithRequestAndQuery(q2, indexReader, taxoReader, indexingParams, facetRequest); } } Index: lucene/facet/src/java/org/apache/lucene/facet/search/DrillDown.java =================================================================== --- lucene/facet/src/java/org/apache/lucene/facet/search/DrillDown.java (revision 1416850) +++ lucene/facet/src/java/org/apache/lucene/facet/search/DrillDown.java (working copy) @@ -2,6 +2,7 @@ import org.apache.lucene.index.Term; import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.ConstantScoreQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.BooleanClause.Occur; @@ -29,7 +30,14 @@ */ /** - * Creation of drill down term or query. + * Utility class for creating drill-down {@link Query queries} or {@link Term + * terms} over {@link CategoryPath}. This can be used to e.g. narrow down a + * user's search to selected categories. + *

+ * NOTE: if you choose to create your own {@link Query} by calling + * {@link #term}, it is recommended to wrap it with {@link ConstantScoreQuery} + * and set the {@link ConstantScoreQuery#setBoost(float) boost} to {@code 0.0f}, + * so that it does not affect the scores of the documents. * * @lucene.experimental */ @@ -42,9 +50,7 @@ return term(sParams.getFacetIndexingParams(), path); } - /** - * Return a term for drilling down into a category. - */ + /** Return a drill-down {@link Term} for a category. */ public static final Term term(FacetIndexingParams iParams, CategoryPath path) { CategoryListParams clp = iParams.getCategoryListParams(path); char[] buffer = new char[path.charsNeededForFullPath()]; @@ -53,58 +59,51 @@ } /** - * Return a query for drilling down into all given categories (AND). - * @see #term(FacetSearchParams, CategoryPath) - * @see #query(FacetSearchParams, Query, CategoryPath...) + * Wraps a given {@link Query} as a drill-down query over the given + * categories, assuming all are required (e.g. {@code AND}). You can construct + * a query with different modes (such as {@code OR} or {@code AND} of + * {@code ORs}) by creating a {@link BooleanQuery} and call this method + * several times. Make sure to wrap the query in that case by + * {@link ConstantScoreQuery} and set the boost to 0.0f, so that it doesn't + * affect scoring. + *

+ * NOTE: {@code baseQuery} can be {@code null}, in which case only the + * {@link Query} over the categories will is returned. */ - public static final Query query(FacetIndexingParams iParams, CategoryPath... paths) { - if (paths==null || paths.length==0) { + public static final Query query(FacetIndexingParams iParams, Query baseQuery, CategoryPath... paths) { + if (paths == null || paths.length == 0) { throw new IllegalArgumentException("Empty category path not allowed for drill down query!"); } - if (paths.length==1) { - return new TermQuery(term(iParams, paths[0])); + + final Query q; + if (paths.length == 1) { + q = new TermQuery(term(iParams, paths[0])); + } else { + BooleanQuery bq = new BooleanQuery(true); // disable coord + for (CategoryPath cp : paths) { + bq.add(new TermQuery(term(iParams, cp)), Occur.MUST); + } + q = bq; } - BooleanQuery res = new BooleanQuery(); - for (CategoryPath cp : paths) { - res.add(new TermQuery(term(iParams, cp)), Occur.MUST); + + final ConstantScoreQuery drillDownQuery = new ConstantScoreQuery(q); + drillDownQuery.setBoost(0.0f); + + if (baseQuery == null) { + return drillDownQuery; + } else { + BooleanQuery res = new BooleanQuery(); + res.add(baseQuery, Occur.MUST); + res.add(drillDownQuery, Occur.MUST); + return res; } - return res; } - - /** - * Return a query for drilling down into all given categories (AND). - * @see #term(FacetSearchParams, CategoryPath) - * @see #query(FacetSearchParams, Query, CategoryPath...) - */ - public static final Query query(FacetSearchParams sParams, CategoryPath... paths) { - return query(sParams.getFacetIndexingParams(), paths); - } /** - * Turn a base query into a drilling-down query for all given category paths (AND). - * @see #query(FacetIndexingParams, CategoryPath...) + * @see #query(FacetIndexingParams, Query, CategoryPath...) */ - public static final Query query(FacetIndexingParams iParams, Query baseQuery, CategoryPath... paths) { - BooleanQuery res = new BooleanQuery(); - res.add(baseQuery, Occur.MUST); - res.add(query(iParams, paths), Occur.MUST); - return res; - } - - /** - * Turn a base query into a drilling-down query for all given category paths (AND). - * @see #query(FacetSearchParams, CategoryPath...) - */ public static final Query query(FacetSearchParams sParams, Query baseQuery, CategoryPath... paths) { return query(sParams.getFacetIndexingParams(), baseQuery, paths); } - /** - * Turn a base query into a drilling-down query using the default {@link FacetSearchParams} - * @see #query(FacetSearchParams, Query, CategoryPath...) - */ - public static final Query query(Query baseQuery, CategoryPath... paths) { - return query(new FacetSearchParams(), baseQuery, paths); - } - } Index: lucene/facet/src/test/org/apache/lucene/facet/search/DrillDownTest.java =================================================================== --- lucene/facet/src/test/org/apache/lucene/facet/search/DrillDownTest.java (revision 1416850) +++ lucene/facet/src/test/org/apache/lucene/facet/search/DrillDownTest.java (working copy) @@ -8,28 +8,28 @@ import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.TextField; +import org.apache.lucene.facet.index.CategoryDocumentBuilder; +import org.apache.lucene.facet.index.params.CategoryListParams; +import org.apache.lucene.facet.index.params.PerDimensionIndexingParams; +import org.apache.lucene.facet.search.params.FacetSearchParams; +import org.apache.lucene.facet.taxonomy.CategoryPath; +import org.apache.lucene.facet.taxonomy.TaxonomyWriter; +import org.apache.lucene.facet.taxonomy.directory.DirectoryTaxonomyReader; +import org.apache.lucene.facet.taxonomy.directory.DirectoryTaxonomyWriter; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.RandomIndexWriter; import org.apache.lucene.index.Term; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.Query; +import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.TopDocs; import org.apache.lucene.store.Directory; +import org.apache.lucene.util.LuceneTestCase; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; -import org.apache.lucene.util.LuceneTestCase; -import org.apache.lucene.facet.index.CategoryDocumentBuilder; -import org.apache.lucene.facet.index.params.CategoryListParams; -import org.apache.lucene.facet.index.params.PerDimensionIndexingParams; -import org.apache.lucene.facet.search.params.FacetSearchParams; -import org.apache.lucene.facet.taxonomy.CategoryPath; -import org.apache.lucene.facet.taxonomy.TaxonomyWriter; -import org.apache.lucene.facet.taxonomy.directory.DirectoryTaxonomyReader; -import org.apache.lucene.facet.taxonomy.directory.DirectoryTaxonomyWriter; - /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with @@ -128,7 +128,7 @@ IndexSearcher searcher = newSearcher(reader); // Making sure the query yields 25 documents with the facet "a" - Query q = DrillDown.query(defaultParams, new CategoryPath("a")); + Query q = DrillDown.query(defaultParams, null, new CategoryPath("a")); TopDocs docs = searcher.search(q, 100); assertEquals(25, docs.totalHits); @@ -139,7 +139,7 @@ assertEquals(5, docs.totalHits); // Making sure that a query of both facet "a" and facet "b" yields 5 results - Query q3 = DrillDown.query(defaultParams, new CategoryPath("a"), new CategoryPath("b")); + Query q3 = DrillDown.query(defaultParams, null, new CategoryPath("a"), new CategoryPath("b")); docs = searcher.search(q3, 100); assertEquals(5, docs.totalHits); @@ -156,18 +156,18 @@ IndexSearcher searcher = newSearcher(reader); // Create the base query to start with - Query q = DrillDown.query(defaultParams, new CategoryPath("a")); + Query q = DrillDown.query(defaultParams, null, new CategoryPath("a")); // Making sure the query yields 5 documents with the facet "b" and the // previous (facet "a") query as a base query - Query q2 = DrillDown.query(q, new CategoryPath("b")); + Query q2 = DrillDown.query(defaultParams, q, new CategoryPath("b")); TopDocs docs = searcher.search(q2, 100); assertEquals(5, docs.totalHits); // Check that content:foo (which yields 50% results) and facet/b (which yields 20%) // would gather together 10 results (10%..) Query fooQuery = new TermQuery(new Term("content", "foo")); - Query q4 = DrillDown.query(fooQuery, new CategoryPath("b")); + Query q4 = DrillDown.query(defaultParams, fooQuery, new CategoryPath("b")); docs = searcher.search(q4, 100); assertEquals(10, docs.totalHits); } @@ -187,5 +187,38 @@ dir.close(); taxoDir.close(); } + + @Test + public void testScoring() throws IOException { + // verify that drill-down queries do not modify scores + IndexSearcher searcher = newSearcher(reader); + + float[] scores = new float[reader.maxDoc()]; + Query q = new TermQuery(new Term("content", "foo")); + TopDocs docs = searcher.search(q, reader.maxDoc()); // fetch all available docs to this query + for (ScoreDoc sd : docs.scoreDocs) { + scores[sd.doc] = sd.score; + } + + // create a drill-down query with category "a", scores should not change + q = DrillDown.query(defaultParams, q, new CategoryPath("a")); + docs = searcher.search(q, reader.maxDoc()); // fetch all available docs to this query + for (ScoreDoc sd : docs.scoreDocs) { + assertEquals("score of doc=" + sd.doc + " modified", scores[sd.doc], sd.score, 0f); + } + } + + @Test + public void testScoringNoBaseQuery() throws IOException { + // verify that drill-down queries (with no base query) returns 0.0 score + IndexSearcher searcher = newSearcher(reader); + + Query q = DrillDown.query(defaultParams, null, new CategoryPath("a")); + TopDocs docs = searcher.search(q, reader.maxDoc()); // fetch all available docs to this query + for (ScoreDoc sd : docs.scoreDocs) { + assertEquals(0f, sd.score, 0f); + } + } + }