LUCENE-4964: allow custom per-dimension drill-down queries

git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1477315 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Michael McCandless 2013-04-29 20:12:09 +00:00
parent b8fd868366
commit 9fc0678da1
7 changed files with 404 additions and 55 deletions

View File

@ -95,6 +95,10 @@ New Features
* SOLR-4761: Add SimpleMergedSegmentWarmer, which just initializes terms,
norms, docvalues, and so on. (Mark Miller, Mike McCandless, Robert Muir)
* LUCENE-4964: Allow arbitrary Query for per-dimension drill-down to
DrillDownQuery and DrillSideways, to support future dynamic faceting
methods (Mike McCandless)
======================= Lucene 4.3.0 =======================
Changes in backwards compatibility policy

View File

@ -167,11 +167,25 @@ public final class DrillDownQuery extends Query {
}
q = bq;
}
drillDownDims.put(dim, drillDownDims.size());
final ConstantScoreQuery drillDownQuery = new ConstantScoreQuery(q);
add(dim, q);
}
/** Expert: add a custom drill-down subQuery. Use this
* when you have a separate way to drill-down on the
* dimension than the indexed facet ordinals. */
public void add(String dim, Query subQuery) {
// TODO: we should use FilteredQuery?
// So scores of the drill-down query don't have an
// effect:
final ConstantScoreQuery drillDownQuery = new ConstantScoreQuery(subQuery);
drillDownQuery.setBoost(0.0f);
query.add(drillDownQuery, Occur.MUST);
drillDownDims.put(dim, drillDownDims.size());
}
@Override

View File

@ -26,6 +26,7 @@ import java.util.Set;
import org.apache.lucene.facet.params.FacetSearchParams;
import org.apache.lucene.facet.taxonomy.TaxonomyReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
@ -43,6 +44,7 @@ import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.search.TopFieldCollector;
import org.apache.lucene.search.TopScoreDocCollector;
import org.apache.lucene.search.Weight;
/**
* Computes drill down and sideways counts for the provided
@ -149,7 +151,7 @@ public class DrillSideways {
Map<String,Integer> drillDownDims = query.getDims();
if (drillDownDims.isEmpty()) {
// Just do ordinary search:
// Just do ordinary search when there are no drill-downs:
FacetsCollector c = FacetsCollector.create(getDrillDownAccumulator(fsp));
searcher.search(query, MultiCollector.wrap(hitCollector, c));
return new DrillSidewaysResult(c.getFacetResults(), null);
@ -171,25 +173,6 @@ public class DrillSideways {
startClause = 1;
}
Term[][] drillDownTerms = new Term[clauses.length-startClause][];
for(int i=startClause;i<clauses.length;i++) {
Query q = clauses[i].getQuery();
assert q instanceof ConstantScoreQuery;
q = ((ConstantScoreQuery) q).getQuery();
assert q instanceof TermQuery || q instanceof BooleanQuery;
if (q instanceof TermQuery) {
drillDownTerms[i-startClause] = new Term[] {((TermQuery) q).getTerm()};
} else {
BooleanQuery q2 = (BooleanQuery) q;
BooleanClause[] clauses2 = q2.getClauses();
drillDownTerms[i-startClause] = new Term[clauses2.length];
for(int j=0;j<clauses2.length;j++) {
assert clauses2[j].getQuery() instanceof TermQuery;
drillDownTerms[i-startClause][j] = ((TermQuery) clauses2[j].getQuery()).getTerm();
}
}
}
FacetsCollector drillDownCollector = FacetsCollector.create(getDrillDownAccumulator(fsp));
FacetsCollector[] drillSidewaysCollectors = new FacetsCollector[drillDownDims.size()];
@ -209,9 +192,53 @@ public class DrillSideways {
drillSidewaysCollectors[idx++] = FacetsCollector.create(getDrillSidewaysAccumulator(dim, new FacetSearchParams(fsp.indexingParams, requests)));
}
DrillSidewaysQuery dsq = new DrillSidewaysQuery(baseQuery, drillDownCollector, drillSidewaysCollectors, drillDownTerms, scoreSubDocsAtOnce());
boolean useCollectorMethod = scoreSubDocsAtOnce();
searcher.search(dsq, hitCollector);
Term[][] drillDownTerms = null;
if (!useCollectorMethod) {
// Optimistic: assume subQueries of the DDQ are either
// TermQuery or BQ OR of TermQuery; if this is wrong
// then we detect it and fallback to the mome general
// but slower DrillSidewaysCollector:
drillDownTerms = new Term[clauses.length-startClause][];
for(int i=startClause;i<clauses.length;i++) {
Query q = clauses[i].getQuery();
// DrillDownQuery always wraps each subQuery in
// ConstantScoreQuery:
assert q instanceof ConstantScoreQuery;
q = ((ConstantScoreQuery) q).getQuery();
if (q instanceof TermQuery) {
drillDownTerms[i-startClause] = new Term[] {((TermQuery) q).getTerm()};
} else if (q instanceof BooleanQuery) {
BooleanQuery q2 = (BooleanQuery) q;
BooleanClause[] clauses2 = q2.getClauses();
drillDownTerms[i-startClause] = new Term[clauses2.length];
for(int j=0;j<clauses2.length;j++) {
if (clauses2[j].getQuery() instanceof TermQuery) {
drillDownTerms[i-startClause][j] = ((TermQuery) clauses2[j].getQuery()).getTerm();
} else {
useCollectorMethod = true;
break;
}
}
}
}
}
if (useCollectorMethod) {
// TODO: maybe we could push the "collector method"
// down into the optimized scorer to have a tighter
// integration ... and so TermQuery clauses could
// continue to run "optimized"
collectorMethod(query, baseQuery, startClause, hitCollector, drillDownCollector, drillSidewaysCollectors);
} else {
DrillSidewaysQuery dsq = new DrillSidewaysQuery(baseQuery, drillDownCollector, drillSidewaysCollectors, drillDownTerms);
searcher.search(dsq, hitCollector);
}
int numDims = drillDownDims.size();
List<FacetResult>[] drillSidewaysResults = new List[numDims];
@ -230,7 +257,9 @@ public class DrillSideways {
// Lazy init, in case all requests were against
// drill-sideways dims:
drillDownResults = drillDownCollector.getFacetResults();
//System.out.println("get DD results");
}
//System.out.println("add dd results " + i);
mergedResults.add(drillDownResults.get(i));
} else {
// Drill sideways dim:
@ -250,6 +279,93 @@ public class DrillSideways {
return new DrillSidewaysResult(mergedResults, null);
}
/** Uses the more general but slower method of sideways
* counting. This method allows an arbitrary subQuery to
* implement the drill down for a given dimension. */
private void collectorMethod(DrillDownQuery ddq, Query baseQuery, int startClause, Collector hitCollector, Collector drillDownCollector, Collector[] drillSidewaysCollectors) throws IOException {
BooleanClause[] clauses = ddq.getBooleanQuery().getClauses();
Map<String,Integer> drillDownDims = ddq.getDims();
BooleanQuery topQuery = new BooleanQuery(true);
final DrillSidewaysCollector collector = new DrillSidewaysCollector(hitCollector, drillDownCollector, drillSidewaysCollectors,
drillDownDims);
// TODO: if query is already a BQ we could copy that and
// add clauses to it, instead of doing BQ inside BQ
// (should be more efficient)? Problem is this can
// affect scoring (coord) ... too bad we can't disable
// coord on a clause by clause basis:
topQuery.add(baseQuery, BooleanClause.Occur.MUST);
// NOTE: in theory we could just make a single BQ, with
// +query a b c minShouldMatch=2, but in this case,
// annoyingly, BS2 wraps a sub-scorer that always
// returns 2 as the .freq(), not how many of the
// SHOULD clauses matched:
BooleanQuery subQuery = new BooleanQuery(true);
Query wrappedSubQuery = new QueryWrapper(subQuery,
new SetWeight() {
@Override
public void set(Weight w) {
collector.setWeight(w, -1);
}
});
Query constantScoreSubQuery = new ConstantScoreQuery(wrappedSubQuery);
// Don't impact score of original query:
constantScoreSubQuery.setBoost(0.0f);
topQuery.add(constantScoreSubQuery, BooleanClause.Occur.MUST);
// Unfortunately this sub-BooleanQuery
// will never get BS1 because today BS1 only works
// if topScorer=true... and actually we cannot use BS1
// anyways because we need subDocsScoredAtOnce:
int dimIndex = 0;
for(int i=startClause;i<clauses.length;i++) {
Query q = clauses[i].getQuery();
// DrillDownQuery always wraps each subQuery in
// ConstantScoreQuery:
assert q instanceof ConstantScoreQuery;
q = ((ConstantScoreQuery) q).getQuery();
final int finalDimIndex = dimIndex;
subQuery.add(new QueryWrapper(q,
new SetWeight() {
@Override
public void set(Weight w) {
collector.setWeight(w, finalDimIndex);
}
}),
BooleanClause.Occur.SHOULD);
dimIndex++;
}
// TODO: we could better optimize the "just one drill
// down" case w/ a separate [specialized]
// collector...
int minShouldMatch = drillDownDims.size()-1;
if (minShouldMatch == 0) {
// Must add another "fake" clause so BQ doesn't erase
// itself by rewriting to the single clause:
Query end = new MatchAllDocsQuery();
end.setBoost(0.0f);
subQuery.add(end, BooleanClause.Occur.SHOULD);
minShouldMatch++;
}
subQuery.setMinimumNumberShouldMatch(minShouldMatch);
//System.out.println("EXE " + topQuery);
// Collects against the passed-in
// drillDown/SidewaysCollectors as a side effect:
searcher.search(topQuery, collector);
}
/**
* Search, sorting by {@link Sort}, and computing
* drill down and sideways counts.
@ -327,5 +443,55 @@ public class DrillSideways {
this.hits = hits;
}
}
private interface SetWeight {
public void set(Weight w);
}
/** Just records which Weight was given out for the
* (possibly rewritten) Query. */
private static class QueryWrapper extends Query {
private final Query originalQuery;
private final SetWeight setter;
public QueryWrapper(Query originalQuery, SetWeight setter) {
this.originalQuery = originalQuery;
this.setter = setter;
}
@Override
public Weight createWeight(final IndexSearcher searcher) throws IOException {
Weight w = originalQuery.createWeight(searcher);
setter.set(w);
return w;
}
@Override
public Query rewrite(IndexReader reader) throws IOException {
Query rewritten = originalQuery.rewrite(reader);
if (rewritten != originalQuery) {
return new QueryWrapper(rewritten, setter);
} else {
return this;
}
}
@Override
public String toString(String s) {
return originalQuery.toString(s);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof QueryWrapper)) return false;
final QueryWrapper other = (QueryWrapper) o;
return super.equals(o) && originalQuery.equals(other.originalQuery);
}
@Override
public int hashCode() {
return super.hashCode() * 31 + originalQuery.hashCode();
}
}
}

View File

@ -0,0 +1,174 @@
package org.apache.lucene.facet.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.Arrays;
import java.util.IdentityHashMap;
import java.util.Map;
import org.apache.lucene.index.AtomicReaderContext;
import org.apache.lucene.search.Collector;
import org.apache.lucene.search.Scorer.ChildScorer;
import org.apache.lucene.search.Scorer;
import org.apache.lucene.search.Weight;
/** Collector that scrutinizes each hit to determine if it
* passed all constraints (a true hit) or if it missed
* exactly one dimension (a near-miss, to count for
* drill-sideways counts on that dimension). */
class DrillSidewaysCollector extends Collector {
private final Collector hitCollector;
private final Collector drillDownCollector;
private final Collector[] drillSidewaysCollectors;
private final Scorer[] subScorers;
private final int exactCount;
// Maps Weight to either -1 (mainQuery) or to integer
// index of the dims drillDown. We needs this when
// visiting the child scorers to correlate back to the
// right scorers:
private final Map<Weight,Integer> weightToIndex = new IdentityHashMap<Weight,Integer>();
private Scorer mainScorer;
public DrillSidewaysCollector(Collector hitCollector, Collector drillDownCollector, Collector[] drillSidewaysCollectors,
Map<String,Integer> dims) {
this.hitCollector = hitCollector;
this.drillDownCollector = drillDownCollector;
this.drillSidewaysCollectors = drillSidewaysCollectors;
subScorers = new Scorer[dims.size()];
if (dims.size() == 1) {
// When we have only one dim, we insert the
// MatchAllDocsQuery, bringing the clause count to
// 2:
exactCount = 2;
} else {
exactCount = dims.size();
}
}
@Override
public void collect(int doc) throws IOException {
//System.out.println("collect doc=" + doc + " main.freq=" + mainScorer.freq() + " main.doc=" + mainScorer.docID() + " exactCount=" + exactCount);
if (mainScorer == null) {
// This segment did not have any docs with any
// drill-down field & value:
return;
}
if (mainScorer.freq() == exactCount) {
// All sub-clauses from the drill-down filters
// matched, so this is a "real" hit, so we first
// collect in both the hitCollector and the
// drillDown collector:
//System.out.println(" hit " + drillDownCollector);
hitCollector.collect(doc);
drillDownCollector.collect(doc);
// Also collect across all drill-sideways counts so
// we "merge in" drill-down counts for this
// dimension.
for(int i=0;i<subScorers.length;i++) {
// This cannot be null, because it was a hit,
// meaning all drill-down dims matched, so all
// dims must have non-null scorers:
assert subScorers[i] != null;
int subDoc = subScorers[i].docID();
assert subDoc == doc;
drillSidewaysCollectors[i].collect(doc);
}
} else {
for(int i=0;i<subScorers.length;i++) {
if (subScorers[i] == null) {
// This segment did not have any docs with this
// drill-down field & value:
continue;
}
int subDoc = subScorers[i].docID();
//System.out.println(" sub: " + subDoc);
if (subDoc != doc) {
assert subDoc > doc: "subDoc=" + subDoc + " doc=" + doc;
drillSidewaysCollectors[i].collect(doc);
assert allMatchesFrom(i+1, doc);
break;
}
}
}
}
// Only used by assert:
private boolean allMatchesFrom(int startFrom, int doc) {
for(int i=startFrom;i<subScorers.length;i++) {
assert subScorers[i].docID() == doc;
}
return true;
}
@Override
public boolean acceptsDocsOutOfOrder() {
// We actually could accept docs out of order, but, we
// need to force BooleanScorer2 so that the
// sub-scorers are "on" each docID we are collecting:
return false;
}
@Override
public void setNextReader(AtomicReaderContext leaf) throws IOException {
hitCollector.setNextReader(leaf);
drillDownCollector.setNextReader(leaf);
for(Collector dsc : drillSidewaysCollectors) {
dsc.setNextReader(leaf);
}
}
void setWeight(Weight weight, int index) {
assert !weightToIndex.containsKey(weight);
weightToIndex.put(weight, index);
}
private void findScorers(Scorer scorer) {
Integer index = weightToIndex.get(scorer.getWeight());
if (index != null) {
if (index.intValue() == -1) {
mainScorer = scorer;
} else {
subScorers[index] = scorer;
}
}
for(ChildScorer child : scorer.getChildren()) {
findScorers(child.child);
}
}
@Override
public void setScorer(Scorer scorer) throws IOException {
mainScorer = null;
Arrays.fill(subScorers, null);
findScorers(scorer);
hitCollector.setScorer(scorer);
drillDownCollector.setScorer(scorer);
for(Collector dsc : drillSidewaysCollectors) {
dsc.setScorer(scorer);
}
}
}

View File

@ -39,14 +39,12 @@ class DrillSidewaysQuery extends Query {
final Collector drillDownCollector;
final Collector[] drillSidewaysCollectors;
final Term[][] drillDownTerms;
final boolean scoreSubDocsAtOnce;
DrillSidewaysQuery(Query baseQuery, Collector drillDownCollector, Collector[] drillSidewaysCollectors, Term[][] drillDownTerms, boolean scoreSubDocsAtOnce) {
DrillSidewaysQuery(Query baseQuery, Collector drillDownCollector, Collector[] drillSidewaysCollectors, Term[][] drillDownTerms) {
this.baseQuery = baseQuery;
this.drillDownCollector = drillDownCollector;
this.drillSidewaysCollectors = drillSidewaysCollectors;
this.drillDownTerms = drillDownTerms;
this.scoreSubDocsAtOnce = scoreSubDocsAtOnce;
}
@Override
@ -67,7 +65,7 @@ class DrillSidewaysQuery extends Query {
if (newQuery == baseQuery) {
return this;
} else {
return new DrillSidewaysQuery(newQuery, drillDownCollector, drillSidewaysCollectors, drillDownTerms, scoreSubDocsAtOnce);
return new DrillSidewaysQuery(newQuery, drillDownCollector, drillSidewaysCollectors, drillDownTerms);
}
}
@ -157,7 +155,7 @@ class DrillSidewaysQuery extends Query {
return new DrillSidewaysScorer(this, context,
baseScorer,
drillDownCollector, dims, scoreSubDocsAtOnce);
drillDownCollector, dims);
}
};
}

View File

@ -43,19 +43,17 @@ class DrillSidewaysScorer extends Scorer {
private static final int CHUNK = 2048;
private static final int MASK = CHUNK-1;
private final boolean scoreSubDocsAtOnce;
private int collectDocID = -1;
private float collectScore;
DrillSidewaysScorer(Weight w, AtomicReaderContext context, Scorer baseScorer, Collector drillDownCollector,
DocsEnumsAndFreq[] dims, boolean scoreSubDocsAtOnce) {
DocsEnumsAndFreq[] dims) {
super(w);
this.dims = dims;
this.context = context;
this.baseScorer = baseScorer;
this.drillDownCollector = drillDownCollector;
this.scoreSubDocsAtOnce = scoreSubDocsAtOnce;
}
@Override
@ -114,22 +112,15 @@ class DrillSidewaysScorer extends Scorer {
}
*/
//System.out.println("DS score " + scoreSubDocsAtOnce);
if (!scoreSubDocsAtOnce) {
if (baseQueryCost < drillDownCost/10) {
//System.out.println("baseAdvance");
doBaseAdvanceScoring(collector, docsEnums, sidewaysCollectors);
} else if (numDims > 1 && (dims[1].maxCost < baseQueryCost/10)) {
//System.out.println("drillDownAdvance");
doDrillDownAdvanceScoring(collector, docsEnums, sidewaysCollectors);
} else {
//System.out.println("union");
doUnionScoring(collector, docsEnums, sidewaysCollectors);
}
} else {
// TODO: we should fallback to BS2 ReqOptSum scorer here
if (baseQueryCost < drillDownCost/10) {
//System.out.println("baseAdvance");
doBaseAdvanceScoring(collector, docsEnums, sidewaysCollectors);
} else if (numDims > 1 && (dims[1].maxCost < baseQueryCost/10)) {
//System.out.println("drillDownAdvance");
doDrillDownAdvanceScoring(collector, docsEnums, sidewaysCollectors);
} else {
//System.out.println("union");
doUnionScoring(collector, docsEnums, sidewaysCollectors);
}
}

View File

@ -120,12 +120,14 @@ public class TestDrillSideways extends FacetTestCase {
new CountFacetRequest(new CategoryPath("Publish Date"), 10),
new CountFacetRequest(new CategoryPath("Author"), 10));
DrillSideways ds = new DrillSideways(searcher, taxoReader);
// Simple case: drill-down on a single field; in this
// case the drill-sideways + drill-down counts ==
// drill-down of just the query:
DrillDownQuery ddq = new DrillDownQuery(fsp.indexingParams, new MatchAllDocsQuery());
ddq.add(new CategoryPath("Author", "Lisa"));
DrillSidewaysResult r = new DrillSideways(searcher, taxoReader).search(null, ddq, 10, fsp);
DrillSidewaysResult r = ds.search(null, ddq, 10, fsp);
assertEquals(2, r.hits.totalHits);
assertEquals(2, r.facetResults.size());
@ -143,7 +145,7 @@ public class TestDrillSideways extends FacetTestCase {
// just the query:
ddq = new DrillDownQuery(fsp.indexingParams);
ddq.add(new CategoryPath("Author", "Lisa"));
r = new DrillSideways(searcher, taxoReader).search(null, ddq, 10, fsp);
r = ds.search(null, ddq, 10, fsp);
assertEquals(2, r.hits.totalHits);
assertEquals(2, r.facetResults.size());
@ -162,7 +164,7 @@ public class TestDrillSideways extends FacetTestCase {
// but OR of two values
ddq = new DrillDownQuery(fsp.indexingParams, new MatchAllDocsQuery());
ddq.add(new CategoryPath("Author", "Lisa"), new CategoryPath("Author", "Bob"));
r = new DrillSideways(searcher, taxoReader).search(null, ddq, 10, fsp);
r = ds.search(null, ddq, 10, fsp);
assertEquals(3, r.hits.totalHits);
assertEquals(2, r.facetResults.size());
// Publish Date is only drill-down: Lisa and Bob
@ -177,7 +179,7 @@ public class TestDrillSideways extends FacetTestCase {
ddq = new DrillDownQuery(fsp.indexingParams, new MatchAllDocsQuery());
ddq.add(new CategoryPath("Author", "Lisa"));
ddq.add(new CategoryPath("Publish Date", "2010"));
r = new DrillSideways(searcher, taxoReader).search(null, ddq, 10, fsp);
r = ds.search(null, ddq, 10, fsp);
assertEquals(1, r.hits.totalHits);
assertEquals(2, r.facetResults.size());
// Publish Date is drill-sideways + drill-down: Lisa
@ -195,7 +197,7 @@ public class TestDrillSideways extends FacetTestCase {
ddq.add(new CategoryPath("Author", "Lisa"),
new CategoryPath("Author", "Bob"));
ddq.add(new CategoryPath("Publish Date", "2010"));
r = new DrillSideways(searcher, taxoReader).search(null, ddq, 10, fsp);
r = ds.search(null, ddq, 10, fsp);
assertEquals(2, r.hits.totalHits);
assertEquals(2, r.facetResults.size());
// Publish Date is both drill-sideways + drill-down:
@ -211,7 +213,7 @@ public class TestDrillSideways extends FacetTestCase {
fsp = new FacetSearchParams(
new CountFacetRequest(new CategoryPath("Publish Date"), 10),
new CountFacetRequest(new CategoryPath("Foobar"), 10));
r = new DrillSideways(searcher, taxoReader).search(null, ddq, 10, fsp);
r = ds.search(null, ddq, 10, fsp);
assertEquals(0, r.hits.totalHits);
assertEquals(2, r.facetResults.size());
assertEquals("Publish Date:", toString(r.facetResults.get(0)));
@ -224,7 +226,7 @@ public class TestDrillSideways extends FacetTestCase {
fsp = new FacetSearchParams(
new CountFacetRequest(new CategoryPath("Publish Date"), 10),
new CountFacetRequest(new CategoryPath("Author"), 10));
r = new DrillSideways(searcher, taxoReader).search(null, ddq, 10, fsp);
r = ds.search(null, ddq, 10, fsp);
assertEquals(2, r.hits.totalHits);
assertEquals(2, r.facetResults.size());
// Publish Date is only drill-down, and Lisa published
@ -242,7 +244,7 @@ public class TestDrillSideways extends FacetTestCase {
new CategoryPath("Author", "Tom"));
fsp = new FacetSearchParams(
new CountFacetRequest(new CategoryPath("Publish Date"), 10));
r = new DrillSideways(searcher, taxoReader).search(null, ddq, 10, fsp);
r = ds.search(null, ddq, 10, fsp);
assertEquals(2, r.hits.totalHits);
assertEquals(1, r.facetResults.size());
// Publish Date is only drill-down, and Lisa published
@ -255,7 +257,7 @@ public class TestDrillSideways extends FacetTestCase {
new CountFacetRequest(new CategoryPath("Author"), 10));
ddq = new DrillDownQuery(fsp.indexingParams, new TermQuery(new Term("foobar", "baz")));
ddq.add(new CategoryPath("Author", "Lisa"));
r = new DrillSideways(searcher, taxoReader).search(null, ddq, 10, fsp);
r = ds.search(null, ddq, 10, fsp);
assertEquals(0, r.hits.totalHits);
assertEquals(2, r.facetResults.size());