Index: lucene/CHANGES.txt =================================================================== --- lucene/CHANGES.txt (revision 1449182) +++ lucene/CHANGES.txt (working copy) @@ -76,6 +76,9 @@ should override FacetsAccumualtor and return the relevant aggregator, for aggregating the association values. (Shai Erera) +* LUCENE-4748: A FacetRequest on a non-existent field now returns an + empty FacetResult instead of skipping it. (Shai Erera, Mike McCandless) + Optimizations * LUCENE-4687: BloomFilterPostingsFormat now lazily initializes delegate @@ -179,6 +182,10 @@ * LUCENE-4780: Add MonotonicAppendingLongBuffer: an append-only buffer for monotonically increasing values. (Adrien Grand) + +* LUCENE-4748: Added DrillSideways utility class for computing both + drill-down and drill-sideways counts for a DrillDownQuery. (Mike + McCandless) API Changes Index: lucene/test-framework/src/java/org/apache/lucene/search/AssertingIndexSearcher.java =================================================================== --- lucene/test-framework/src/java/org/apache/lucene/search/AssertingIndexSearcher.java (revision 1449182) +++ lucene/test-framework/src/java/org/apache/lucene/search/AssertingIndexSearcher.java (working copy) @@ -98,6 +98,9 @@ @Override public boolean scoresDocsOutOfOrder() { + // TODO: if this returns false, we should wrap + // Scorer with AssertingScorer that confirms docIDs + // are in order? return w.scoresDocsOutOfOrder(); } }; Index: lucene/facet/src/java/org/apache/lucene/facet/search/DrillSidewaysScorer.java =================================================================== --- lucene/facet/src/java/org/apache/lucene/facet/search/DrillSidewaysScorer.java (revision 0) +++ lucene/facet/src/java/org/apache/lucene/facet/search/DrillSidewaysScorer.java (working copy) @@ -0,0 +1,634 @@ +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.Collection; +import java.util.Collections; + +import org.apache.lucene.index.AtomicReaderContext; +import org.apache.lucene.index.DocsEnum; +import org.apache.lucene.search.Collector; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Weight; +import org.apache.lucene.util.FixedBitSet; + +class DrillSidewaysScorer extends Scorer { + + //private static boolean DEBUG = false; + + private final Collector drillDownCollector; + + private final DocsEnumsAndFreq[] dims; + + // DrillDown DocsEnums: + private final Scorer baseScorer; + + private final AtomicReaderContext context; + + private static final int CHUNK = 2048; + private static final int MASK = CHUNK-1; + + private int collectDocID = -1; + private float collectScore; + + DrillSidewaysScorer(Weight w, AtomicReaderContext context, Scorer baseScorer, Collector drillDownCollector, + DocsEnumsAndFreq[] dims) { + super(w); + this.dims = dims; + this.context = context; + this.baseScorer = baseScorer; + this.drillDownCollector = drillDownCollector; + } + + @Override + public void score(Collector collector) throws IOException { + //if (DEBUG) { + // System.out.println("\nscore: reader=" + context.reader()); + //} + //System.out.println("score r=" + context.reader()); + collector.setScorer(this); + drillDownCollector.setScorer(this); + drillDownCollector.setNextReader(context); + for(DocsEnumsAndFreq dim : dims) { + dim.sidewaysCollector.setScorer(this); + dim.sidewaysCollector.setNextReader(context); + } + + // TODO: if we ever allow null baseScorer ... it will + // mean we DO score docs out of order ... hmm, or if we + // change up the order of the conjuntions below + assert baseScorer != null; + + // Position all scorers to their first matching doc: + int baseDocID = baseScorer.nextDoc(); + + for(DocsEnumsAndFreq dim : dims) { + for(DocsEnum docsEnum : dim.docsEnums) { + if (docsEnum != null) { + docsEnum.nextDoc(); + } + } + } + + final int numDims = dims.length; + + DocsEnum[][] docsEnums = new DocsEnum[numDims][]; + Collector[] sidewaysCollectors = new Collector[numDims]; + int maxFreq = 0; + for(int dim=0;dim 1 && (dims[1].freq < estBaseHitCount/10)) { + //System.out.println("drillDownAdvance"); + doDrillDownAdvanceScoring(collector, docsEnums, sidewaysCollectors); + } else { + //System.out.println("union"); + doUnionScoring(collector, docsEnums, sidewaysCollectors); + } + } + + /** Used when drill downs are highly constraining vs + * baseQuery. */ + private void doDrillDownAdvanceScoring(Collector collector, DocsEnum[][] docsEnums, Collector[] sidewaysCollectors) throws IOException { + final int maxDoc = context.reader().maxDoc(); + final int numDims = dims.length; + + //if (DEBUG) { + // System.out.println(" doDrillDownAdvanceScoring"); + //} + + // TODO: maybe a class like BS, instead of parallel arrays + int[] filledSlots = new int[CHUNK]; + int[] docIDs = new int[CHUNK]; + float[] scores = new float[CHUNK]; + int[] missingDims = new int[CHUNK]; + int[] counts = new int[CHUNK]; + + docIDs[0] = -1; + int nextChunkStart = CHUNK; + + final FixedBitSet seen = new FixedBitSet(CHUNK); + + while (true) { + //if (DEBUG) { + // System.out.println("\ncycle nextChunkStart=" + nextChunkStart + " docIds[0]=" + docIDs[0]); + //} + + // First dim: + //if (DEBUG) { + // System.out.println(" dim0"); + //} + for(DocsEnum docsEnum : docsEnums[0]) { + if (docsEnum == null) { + continue; + } + int docID = docsEnum.docID(); + while (docID < nextChunkStart) { + int slot = docID & MASK; + + if (docIDs[slot] != docID) { + seen.set(slot); + // Mark slot as valid: + //if (DEBUG) { + // System.out.println(" set docID=" + docID + " id=" + context.reader().document(docID).get("id")); + //} + docIDs[slot] = docID; + missingDims[slot] = 1; + counts[slot] = 1; + } + + docID = docsEnum.nextDoc(); + } + } + + // Second dim: + //if (DEBUG) { + // System.out.println(" dim1"); + //} + for(DocsEnum docsEnum : docsEnums[1]) { + if (docsEnum == null) { + continue; + } + int docID = docsEnum.docID(); + while (docID < nextChunkStart) { + int slot = docID & MASK; + + if (docIDs[slot] != docID) { + // Mark slot as valid: + seen.set(slot); + //if (DEBUG) { + // System.out.println(" set docID=" + docID + " missingDim=0 id=" + context.reader().document(docID).get("id")); + //} + docIDs[slot] = docID; + missingDims[slot] = 0; + counts[slot] = 1; + } else { + // TODO: single-valued dims will always be true + // below; we could somehow specialize + if (missingDims[slot] >= 1) { + missingDims[slot] = 2; + counts[slot] = 2; + //if (DEBUG) { + // System.out.println(" set docID=" + docID + " missingDim=2 id=" + context.reader().document(docID).get("id")); + //} + } else { + counts[slot] = 1; + //if (DEBUG) { + // System.out.println(" set docID=" + docID + " missingDim=" + missingDims[slot] + " id=" + context.reader().document(docID).get("id")); + //} + } + } + + docID = docsEnum.nextDoc(); + } + } + + // After this we can "upgrade" to conjunction, because + // any doc not seen by either dim 0 or dim 1 cannot be + // a hit or a near miss: + + //if (DEBUG) { + // System.out.println(" baseScorer"); + //} + + // Fold in baseScorer, using advance: + int filledCount = 0; + int slot0 = 0; + while (slot0 < CHUNK && (slot0 = seen.nextSetBit(slot0)) != -1) { + int ddDocID = docIDs[slot0]; + assert ddDocID != -1; + + int baseDocID = baseScorer.docID(); + if (baseDocID < ddDocID) { + baseDocID = baseScorer.advance(ddDocID); + } + if (baseDocID == ddDocID) { + //if (DEBUG) { + // System.out.println(" keep docID=" + ddDocID + " id=" + context.reader().document(ddDocID).get("id")); + //} + scores[slot0] = baseScorer.score(); + filledSlots[filledCount++] = slot0; + counts[slot0]++; + } else { + //if (DEBUG) { + // System.out.println(" no docID=" + ddDocID + " id=" + context.reader().document(ddDocID).get("id")); + //} + docIDs[slot0] = -1; + + // TODO: we could jump slot0 forward to the + // baseDocID ... but we'd need to set docIDs for + // intervening slots to -1 + } + slot0++; + } + seen.clear(0, CHUNK); + + if (filledCount == 0) { + if (nextChunkStart >= maxDoc) { + break; + } + nextChunkStart += CHUNK; + continue; + } + + // TODO: factor this out & share w/ union scorer, + // except we start from dim=2 instead: + for(int dim=2;dim= dim) { + // TODO: single-valued dims will always be true + // below; we could somehow specialize + if (missingDims[slot] >= dim) { + //if (DEBUG) { + // System.out.println(" set docID=" + docID + " count=" + (dim+2)); + //} + missingDims[slot] = dim+1; + counts[slot] = dim+2; + } else { + //if (DEBUG) { + // System.out.println(" set docID=" + docID + " missing count=" + (dim+1)); + //} + counts[slot] = dim+1; + } + } + // TODO: sometimes use advance? + docID = docsEnum.nextDoc(); + } + } + } + + // Collect: + //if (DEBUG) { + // System.out.println(" now collect: " + filledCount + " hits"); + //} + for(int i=0;i= maxDoc) { + break; + } + + nextChunkStart += CHUNK; + } + } + + @Override + public int docID() { + return collectDocID; + } + + @Override + public float score() { + return collectScore; + } + + @Override + public int freq() { + return 1+dims.length; + } + + @Override + public int nextDoc() { + throw new UnsupportedOperationException(); + } + + @Override + public int advance(int target) { + throw new UnsupportedOperationException(); + } + + @Override + public Collection getChildren() { + return Collections.singletonList(new ChildScorer(baseScorer, "MUST")); + } + + static class DocsEnumsAndFreq implements Comparable { + DocsEnum[] docsEnums; + // Max docFreq for all docsEnums for this dim: + int freq; + Collector sidewaysCollector; + String dim; + + @Override + public int compareTo(DocsEnumsAndFreq other) { + return freq - other.freq; + } + } +} Property changes on: lucene/facet/src/java/org/apache/lucene/facet/search/DrillSidewaysScorer.java ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: lucene/facet/src/java/org/apache/lucene/facet/search/DrillSidewaysQuery.java =================================================================== --- lucene/facet/src/java/org/apache/lucene/facet/search/DrillSidewaysQuery.java (revision 0) +++ lucene/facet/src/java/org/apache/lucene/facet/search/DrillSidewaysQuery.java (working copy) @@ -0,0 +1,169 @@ +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 org.apache.lucene.index.AtomicReader; +import org.apache.lucene.index.AtomicReaderContext; +import org.apache.lucene.index.DocsEnum; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.Terms; +import org.apache.lucene.index.TermsEnum; +import org.apache.lucene.search.Collector; +import org.apache.lucene.search.Explanation; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Weight; +import org.apache.lucene.util.Bits; + +class DrillSidewaysQuery extends Query { + final Query baseQuery; + final Collector drillDownCollector; + final Collector[] drillSidewaysCollectors; + final Term[][] drillDownTerms; + + DrillSidewaysQuery(Query baseQuery, Collector drillDownCollector, Collector[] drillSidewaysCollectors, Term[][] drillDownTerms) { + this.baseQuery = baseQuery; + this.drillDownCollector = drillDownCollector; + this.drillSidewaysCollectors = drillSidewaysCollectors; + this.drillDownTerms = drillDownTerms; + } + + @Override + public String toString(String field) { + return "DrillSidewaysQuery"; + } + + @Override + public Query rewrite(IndexReader reader) throws IOException { + Query newQuery = baseQuery; + while(true) { + Query rewrittenQuery = newQuery.rewrite(reader); + if (rewrittenQuery == newQuery) { + break; + } + newQuery = rewrittenQuery; + } + if (newQuery == baseQuery) { + return this; + } else { + return new DrillSidewaysQuery(newQuery, drillDownCollector, drillSidewaysCollectors, drillDownTerms); + } + } + + @Override + public Weight createWeight(IndexSearcher searcher) throws IOException { + final Weight baseWeight = baseQuery.createWeight(searcher); + + return new Weight() { + @Override + public Explanation explain(AtomicReaderContext context, int doc) throws IOException { + return baseWeight.explain(context, doc); + } + + @Override + public Query getQuery() { + return baseQuery; + } + + @Override + public float getValueForNormalization() throws IOException { + return baseWeight.getValueForNormalization(); + } + + @Override + public void normalize(float norm, float topLevelBoost) { + baseWeight.normalize(norm, topLevelBoost); + } + + @Override + public boolean scoresDocsOutOfOrder() { + // TODO: would be nice if AssertingIndexSearcher + // confirmed this for us + return false; + } + + @Override + public Scorer scorer(AtomicReaderContext context, boolean scoreDocsInOrder, + boolean topScorer, Bits acceptDocs) throws IOException { + + DrillSidewaysScorer.DocsEnumsAndFreq[] dims = new DrillSidewaysScorer.DocsEnumsAndFreq[drillDownTerms.length]; + TermsEnum termsEnum = null; + String lastField = null; + int nullCount = 0; + for(int dim=0;dim 1) { + return null; + } + + // Sort drill-downs by most restrictive first: + Arrays.sort(dims); + + // TODO: it could be better if we take acceptDocs + // into account instead of baseScorer? + Scorer baseScorer = baseWeight.scorer(context, scoreDocsInOrder, false, acceptDocs); + + if (baseScorer == null) { + return null; + } + + return new DrillSidewaysScorer(this, context, + baseScorer, + drillDownCollector, dims); + } + }; + } + + @Override + public int hashCode() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean equals(Object obj) { + throw new UnsupportedOperationException(); + } +} Property changes on: lucene/facet/src/java/org/apache/lucene/facet/search/DrillSidewaysQuery.java ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: lucene/facet/src/java/org/apache/lucene/facet/search/StandardFacetsAccumulator.java =================================================================== --- lucene/facet/src/java/org/apache/lucene/facet/search/StandardFacetsAccumulator.java (revision 1449182) +++ lucene/facet/src/java/org/apache/lucene/facet/search/StandardFacetsAccumulator.java (working copy) @@ -197,7 +197,13 @@ PartitionsFacetResultsHandler frHndlr = createFacetResultsHandler(fr); IntermediateFacetResult tmpResult = fr2tmpRes.get(fr); if (tmpResult == null) { - continue; // do not add a null to the list. + // Add empty FacetResult: + FacetResultNode root = new FacetResultNode(); + root.ordinal = TaxonomyReader.INVALID_ORDINAL; + root.label = fr.categoryPath; + root.value = 0; + res.add(new FacetResult(fr, root, 0)); + continue; } FacetResult facetRes = frHndlr.renderFacetResult(tmpResult); // final labeling if allowed (because labeling is a costly operation) Index: lucene/facet/src/java/org/apache/lucene/facet/search/FacetsCollector.java =================================================================== --- lucene/facet/src/java/org/apache/lucene/facet/search/FacetsCollector.java (revision 1449182) +++ lucene/facet/src/java/org/apache/lucene/facet/search/FacetsCollector.java (working copy) @@ -163,21 +163,11 @@ } /** - * Creates a {@link FacetsCollector} with the default - * {@link FacetsAccumulator}. + * Creates a {@link FacetsCollector} using the {@link + * FacetsAccumulator} from {@link FacetsAccumulator#create}. */ public static FacetsCollector create(FacetSearchParams fsp, IndexReader indexReader, TaxonomyReader taxoReader) { - if (fsp.indexingParams.getPartitionSize() != Integer.MAX_VALUE) { - return create(new StandardFacetsAccumulator(fsp, indexReader, taxoReader)); - } - - for (FacetRequest fr : fsp.facetRequests) { - if (!(fr instanceof CountFacetRequest)) { - return create(new StandardFacetsAccumulator(fsp, indexReader, taxoReader)); - } - } - - return create(new FacetsAccumulator(fsp, indexReader, taxoReader)); + return create(FacetsAccumulator.create(fsp, indexReader, taxoReader)); } /** Index: lucene/facet/src/java/org/apache/lucene/facet/search/DrillDownQuery.java =================================================================== --- lucene/facet/src/java/org/apache/lucene/facet/search/DrillDownQuery.java (revision 1449182) +++ lucene/facet/src/java/org/apache/lucene/facet/search/DrillDownQuery.java (working copy) @@ -18,8 +18,8 @@ */ import java.io.IOException; -import java.util.HashSet; -import java.util.Set; +import java.util.LinkedHashMap; +import java.util.Map; import org.apache.lucene.facet.params.CategoryListParams; import org.apache.lucene.facet.params.FacetIndexingParams; @@ -27,8 +27,11 @@ import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.Term; import org.apache.lucene.search.BooleanClause.Occur; +import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.ConstantScoreQuery; +import org.apache.lucene.search.Filter; +import org.apache.lucene.search.FilteredQuery; import org.apache.lucene.search.MatchAllDocsQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.TermQuery; @@ -49,7 +52,7 @@ public final class DrillDownQuery extends Query { /** Return a drill-down {@link Term} for a category. */ - public static final Term term(FacetIndexingParams iParams, CategoryPath path) { + public static Term term(FacetIndexingParams iParams, CategoryPath path) { CategoryListParams clp = iParams.getCategoryListParams(path); char[] buffer = new char[path.fullPathLength()]; iParams.drillDownTermText(path, buffer); @@ -57,21 +60,37 @@ } private final BooleanQuery query; - private final Set drillDownDims = new HashSet(); - + private final Map drillDownDims = new LinkedHashMap(); private final FacetIndexingParams fip; - /* Used by clone() */ - private DrillDownQuery(FacetIndexingParams fip, BooleanQuery query, Set drillDownDims) { + /** Used by clone() */ + DrillDownQuery(FacetIndexingParams fip, BooleanQuery query, Map drillDownDims) { this.fip = fip; this.query = query.clone(); - this.drillDownDims.addAll(drillDownDims); + this.drillDownDims.putAll(drillDownDims); } + /** Used by DrillSideways */ + DrillDownQuery(Filter filter, DrillDownQuery other) { + query = new BooleanQuery(true); // disable coord + + BooleanClause[] clauses = other.query.getClauses(); + if (clauses.length == other.drillDownDims.size()) { + throw new IllegalArgumentException("cannot apply filter unless baseQuery isn't null; pass ConstantScoreQuery instead"); + } + assert clauses.length == 1+other.drillDownDims.size(): clauses.length + " vs " + (1+other.drillDownDims.size()); + drillDownDims.putAll(other.drillDownDims); + query.add(new FilteredQuery(clauses[0].getQuery(), filter), Occur.MUST); + for(int i=1;i 0"); + } String dim = paths[0].components[0]; - if (drillDownDims.contains(dim)) { + if (drillDownDims.containsKey(dim)) { throw new IllegalArgumentException("dimension '" + dim + "' was already added"); } if (paths.length == 1) { - if (paths[0].length == 0) { - throw new IllegalArgumentException("all CategoryPaths must have length > 0"); - } q = new TermQuery(term(fip, paths[0])); } else { BooleanQuery bq = new BooleanQuery(true); // disable coord @@ -120,7 +139,7 @@ } q = bq; } - drillDownDims.add(dim); + drillDownDims.put(dim, drillDownDims.size()); final ConstantScoreQuery drillDownQuery = new ConstantScoreQuery(q); drillDownQuery.setBoost(0.0f); @@ -162,5 +181,12 @@ public String toString(String field) { return query.toString(field); } - + + BooleanQuery getBooleanQuery() { + return query; + } + + Map getDims() { + return drillDownDims; + } } Index: lucene/facet/src/java/org/apache/lucene/facet/search/FacetsAccumulator.java =================================================================== --- lucene/facet/src/java/org/apache/lucene/facet/search/FacetsAccumulator.java (revision 1449182) +++ lucene/facet/src/java/org/apache/lucene/facet/search/FacetsAccumulator.java (working copy) @@ -60,6 +60,26 @@ public FacetsAccumulator(FacetSearchParams searchParams, IndexReader indexReader, TaxonomyReader taxonomyReader) { this(searchParams, indexReader, taxonomyReader, null); } + + /** + * Creates an appropriate {@link FacetsAccumulator}, + * returning {@link FacetsAccumulator} when all requests + * are {@link CountFacetRequest} and only one partition is + * in use, otherwise {@link StandardFacetsAccumulator}. + */ + public static FacetsAccumulator create(FacetSearchParams fsp, IndexReader indexReader, TaxonomyReader taxoReader) { + if (fsp.indexingParams.getPartitionSize() != Integer.MAX_VALUE) { + return new StandardFacetsAccumulator(fsp, indexReader, taxoReader); + } + + for (FacetRequest fr : fsp.facetRequests) { + if (!(fr instanceof CountFacetRequest)) { + return new StandardFacetsAccumulator(fsp, indexReader, taxoReader); + } + } + + return new FacetsAccumulator(fsp, indexReader, taxoReader); + } /** * Initializes the accumulator with the given parameters as well as @@ -153,6 +173,12 @@ for (FacetRequest fr : searchParams.facetRequests) { int rootOrd = taxonomyReader.getOrdinal(fr.categoryPath); if (rootOrd == TaxonomyReader.INVALID_ORDINAL) { // category does not exist + // Add empty FacetResult: + FacetResultNode root = new FacetResultNode(); + root.ordinal = TaxonomyReader.INVALID_ORDINAL; + root.label = fr.categoryPath; + root.value = 0; + res.add(new FacetResult(fr, root, 0)); continue; } CategoryListParams clp = searchParams.indexingParams.getCategoryListParams(fr.categoryPath); Index: lucene/facet/src/java/org/apache/lucene/facet/search/DrillSideways.java =================================================================== --- lucene/facet/src/java/org/apache/lucene/facet/search/DrillSideways.java (revision 0) +++ lucene/facet/src/java/org/apache/lucene/facet/search/DrillSideways.java (working copy) @@ -0,0 +1,242 @@ +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.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.lucene.facet.params.FacetSearchParams; +import org.apache.lucene.facet.taxonomy.TaxonomyReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.Collector; +import org.apache.lucene.search.ConstantScoreQuery; +import org.apache.lucene.search.FieldDoc; +import org.apache.lucene.search.Filter; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.ScoreDoc; +import org.apache.lucene.search.Sort; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.search.TopFieldCollector; +import org.apache.lucene.search.TopScoreDocCollector; + +/** + * Computes drill down and sideways counts for the provided + * {@link DrillDownQuery}. Drill sideways counts include + * alternative values/aggregates for the drill-down + * dimensions so that a dimension does not disappear after + * the user drills down into it. + * + *

Use one of the static search + * methods to do the search, and then get the hits and facet + * results from the returned {@link DrillSidewaysResult}. + * + *

NOTE: this allocates one {@link + * FacetsCollector} for each drill-down, plus one. If your + * index has high number of facet labels then this will + * multiply your memory usage. + * + * @lucene.experimental + */ + +public class DrillSideways { + + protected final IndexSearcher searcher; + protected final TaxonomyReader taxoReader; + + /** Create a new {@code DrillSideways} instance. */ + public DrillSideways(IndexSearcher searcher, TaxonomyReader taxoReader) { + this.searcher = searcher; + this.taxoReader = taxoReader; + } + + /** + * Search, collecting hits with a {@link Collector}, and + * computing drill down and sideways counts. + */ + public DrillSidewaysResult search(DrillDownQuery query, + Collector hitCollector, FacetSearchParams fsp) throws IOException { + + Map drillDownDims = query.getDims(); + + if (drillDownDims.isEmpty()) { + throw new IllegalArgumentException("there must be at least one drill-down"); + } + + BooleanQuery ddq = query.getBooleanQuery(); + BooleanClause[] clauses = ddq.getClauses(); + + for(FacetRequest fr : fsp.facetRequests) { + if (fr.categoryPath.length == 0) { + throw new IllegalArgumentException("all FacetRequests must have CategoryPath with length > 0"); + } + } + + Query baseQuery; + int startClause; + if (clauses.length == drillDownDims.size()) { + // TODO: we could optimize this pure-browse case by + // making a custom scorer instead: + baseQuery = new MatchAllDocsQuery(); + startClause = 0; + } else { + assert clauses.length == 1+drillDownDims.size(); + baseQuery = clauses[0].getQuery(); + startClause = 1; + } + + Term[][] drillDownTerms = new Term[clauses.length-startClause][]; + for(int i=startClause;i 0; + if (fr.categoryPath.components[0].equals(dim)) { + if (drillSidewaysRequest != null) { + throw new IllegalArgumentException("multiple FacetRequests for drill-sideways dimension \"" + dim + "\""); + } + drillSidewaysRequest = fr; + } + } + if (drillSidewaysRequest == null) { + throw new IllegalArgumentException("could not find FacetRequest for drill-sideways dimension \"" + dim + "\""); + } + drillSidewaysCollectors[idx++] = FacetsCollector.create(getDrillSidewaysAccumulator(dim, new FacetSearchParams(fsp.indexingParams, drillSidewaysRequest))); + } + + DrillSidewaysQuery dsq = new DrillSidewaysQuery(baseQuery, drillDownCollector, drillSidewaysCollectors, drillDownTerms); + + searcher.search(dsq, hitCollector); + + List drillDownResults = drillDownCollector.getFacetResults(); + + List mergedResults = new ArrayList(); + for(int i=0;i 0; + Integer dimIndex = drillDownDims.get(fr.categoryPath.components[0]); + if (dimIndex == null) { + // Pure drill down dim (the current query didn't + // drill down on this dim): + mergedResults.add(drillDownResults.get(i)); + } else { + // Drill sideways dim: + List sidewaysResult = drillSidewaysCollectors[dimIndex.intValue()].getFacetResults(); + + assert sidewaysResult.size() == 1: "size=" + sidewaysResult.size(); + mergedResults.add(sidewaysResult.get(0)); + } + } + + return new DrillSidewaysResult(mergedResults, null); + } + + /** + * Search, sorting by {@link Sort}, and computing + * drill down and sideways counts. + */ + public DrillSidewaysResult search(DrillDownQuery query, + Filter filter, FieldDoc after, int topN, Sort sort, boolean doDocScores, + boolean doMaxScore, FacetSearchParams fsp) throws IOException { + if (filter != null) { + query = new DrillDownQuery(filter, query); + } + if (sort != null) { + final TopFieldCollector hitCollector = TopFieldCollector.create(sort, + Math.min(topN, searcher.getIndexReader().maxDoc()), + after, + true, + doDocScores, + doMaxScore, + true); + DrillSidewaysResult r = new DrillSideways(searcher, taxoReader).search(query, hitCollector, fsp); + r.hits = hitCollector.topDocs(); + return r; + } else { + return search(after, query, topN, fsp); + } + } + + /** + * Search, sorting by score, and computing + * drill down and sideways counts. + */ + public DrillSidewaysResult search(ScoreDoc after, + DrillDownQuery query, int topN, FacetSearchParams fsp) throws IOException { + TopScoreDocCollector hitCollector = TopScoreDocCollector.create(Math.min(topN, searcher.getIndexReader().maxDoc()), after, true); + DrillSidewaysResult r = new DrillSideways(searcher, taxoReader).search(query, hitCollector, fsp); + r.hits = hitCollector.topDocs(); + return r; + } + + /** Override this to use a custom drill-down {@link + * FacetsAccumulator}. */ + protected FacetsAccumulator getDrillDownAccumulator(FacetSearchParams fsp) { + return FacetsAccumulator.create(fsp, searcher.getIndexReader(), taxoReader); + } + + /** Override this to use a custom drill-sideways {@link + * FacetsAccumulator}. */ + protected FacetsAccumulator getDrillSidewaysAccumulator(String dim, FacetSearchParams fsp) { + return FacetsAccumulator.create(fsp, searcher.getIndexReader(), taxoReader); + } + + /** Represents the returned result from a drill sideways + * search. */ + public static class DrillSidewaysResult { + /** Combined drill down & sideways results. */ + public final List facetResults; + + /** Hits. */ + public TopDocs hits; + + DrillSidewaysResult(List facetResults, TopDocs hits) { + this.facetResults = facetResults; + this.hits = hits; + } + } +} + Property changes on: lucene/facet/src/java/org/apache/lucene/facet/search/DrillSideways.java ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: lucene/facet/src/test/org/apache/lucene/facet/search/TestTopKResultsHandler.java =================================================================== --- lucene/facet/src/test/org/apache/lucene/facet/search/TestTopKResultsHandler.java (revision 1449182) +++ lucene/facet/src/test/org/apache/lucene/facet/search/TestTopKResultsHandler.java (working copy) @@ -203,8 +203,8 @@ List facetResults = fc.getFacetResults(); assertEquals("Shouldn't have found anything for a FacetRequest " - + "of a facet that doesn't exist in the index.", 0, facetResults.size()); - + + "of a facet that doesn't exist in the index.", 1, facetResults.size()); + assertEquals("Miau Hattulla", facetResults.get(0).getFacetResultNode().label.components[0]); closeAll(); } } Index: lucene/facet/src/test/org/apache/lucene/facet/search/TestDrillSideways.java =================================================================== --- lucene/facet/src/test/org/apache/lucene/facet/search/TestDrillSideways.java (revision 0) +++ lucene/facet/src/test/org/apache/lucene/facet/search/TestDrillSideways.java (working copy) @@ -0,0 +1,829 @@ +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.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.lucene.analysis.MockAnalyzer; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.StringField; +import org.apache.lucene.facet.FacetTestCase; +import org.apache.lucene.facet.index.FacetFields; +import org.apache.lucene.facet.params.FacetSearchParams; +import org.apache.lucene.facet.search.DrillSideways.DrillSidewaysResult; +import org.apache.lucene.facet.taxonomy.CategoryPath; +import org.apache.lucene.facet.taxonomy.TaxonomyReader; +import org.apache.lucene.facet.taxonomy.directory.DirectoryTaxonomyReader; +import org.apache.lucene.facet.taxonomy.directory.DirectoryTaxonomyWriter; +import org.apache.lucene.index.AtomicReaderContext; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.index.RandomIndexWriter; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.Collector; +import org.apache.lucene.search.DocIdSet; +import org.apache.lucene.search.Filter; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.ScoreDoc; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Sort; +import org.apache.lucene.search.SortField; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.store.Directory; +import org.apache.lucene.util.Bits; +import org.apache.lucene.util.FixedBitSet; +import org.apache.lucene.util.InfoStream; +import org.apache.lucene.util._TestUtil; + +public class TestDrillSideways extends FacetTestCase { + + private DirectoryTaxonomyWriter taxoWriter; + private RandomIndexWriter writer; + private FacetFields facetFields; + + private void add(String ... categoryPaths) throws IOException { + Document doc = new Document(); + List paths = new ArrayList(); + for(String categoryPath : categoryPaths) { + paths.add(new CategoryPath(categoryPath, '/')); + } + facetFields.addFields(doc, paths); + writer.addDocument(doc); + } + + public void testBasic() throws Exception { + Directory dir = newDirectory(); + Directory taxoDir = newDirectory(); + writer = new RandomIndexWriter(random(), dir); + + // Writes facet ords to a separate directory from the + // main index: + taxoWriter = new DirectoryTaxonomyWriter(taxoDir, IndexWriterConfig.OpenMode.CREATE); + + // Reused across documents, to add the necessary facet + // fields: + facetFields = new FacetFields(taxoWriter); + + add("Author/Bob", "Publish Date/2010/10/15"); + add("Author/Lisa", "Publish Date/2010/10/20"); + add("Author/Lisa", "Publish Date/2012/1/1"); + add("Author/Susan", "Publish Date/2012/1/7"); + add("Author/Frank", "Publish Date/1999/5/5"); + + // NRT open + IndexSearcher searcher = newSearcher(writer.getReader()); + writer.close(); + + //System.out.println("searcher=" + searcher); + + // NRT open + TaxonomyReader taxoReader = new DirectoryTaxonomyReader(taxoWriter); + taxoWriter.close(); + + // Count both "Publish Date" and "Author" dimensions, in + // drill-down: + FacetSearchParams fsp = new FacetSearchParams( + new CountFacetRequest(new CategoryPath("Publish Date"), 10), + new CountFacetRequest(new CategoryPath("Author"), 10)); + + // 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); + + assertEquals(2, r.hits.totalHits); + assertEquals(2, r.facetResults.size()); + // Publish Date is only drill-down, and Lisa published + // one in 2012 and one in 2010: + assertEquals("Publish Date: 2012=1 2010=1", toString(r.facetResults.get(0))); + // Author is drill-sideways + drill-down: Lisa + // (drill-down) published twice, and Frank/Susan/Bob + // published once: + assertEquals("Author: Lisa=2 Frank=1 Susan=1 Bob=1", toString(r.facetResults.get(1))); + + // Same simple case, but no baseQuery (pure browse): + // drill-down on a single field; in this case the + // drill-sideways + drill-down counts == drill-down of + // just the query: + ddq = new DrillDownQuery(fsp.indexingParams); + ddq.add(new CategoryPath("Author", "Lisa")); + r = new DrillSideways(searcher, taxoReader).search(null, ddq, 10, fsp); + + assertEquals(2, r.hits.totalHits); + assertEquals(2, r.facetResults.size()); + // Publish Date is only drill-down, and Lisa published + // one in 2012 and one in 2010: + assertEquals("Publish Date: 2012=1 2010=1", toString(r.facetResults.get(0))); + // Author is drill-sideways + drill-down: Lisa + // (drill-down) published twice, and Frank/Susan/Bob + // published once: + assertEquals("Author: Lisa=2 Frank=1 Susan=1 Bob=1", toString(r.facetResults.get(1))); + + // Another simple case: drill-down on on single fields + // 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); + assertEquals(3, r.hits.totalHits); + assertEquals(2, r.facetResults.size()); + // Publish Date is only drill-down: Lisa and Bob + // (drill-down) published twice in 2010 and once in 2012: + assertEquals("Publish Date: 2010=2 2012=1", toString(r.facetResults.get(0))); + // Author is drill-sideways + drill-down: Lisa + // (drill-down) published twice, and Frank/Susan/Bob + // published once: + assertEquals("Author: Lisa=2 Frank=1 Susan=1 Bob=1", toString(r.facetResults.get(1))); + + // More interesting case: drill-down on two fields + 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); + assertEquals(1, r.hits.totalHits); + assertEquals(2, r.facetResults.size()); + // Publish Date is drill-sideways + drill-down: Lisa + // (drill-down) published once in 2010 and once in 2012: + assertEquals("Publish Date: 2012=1 2010=1", toString(r.facetResults.get(0))); + // Author is drill-sideways + drill-down: + // only Lisa & Bob published (once each) in 2010: + assertEquals("Author: Lisa=1 Bob=1", toString(r.facetResults.get(1))); + + // Even more interesting case: drill down on two fields, + // but one of them is OR + ddq = new DrillDownQuery(fsp.indexingParams, new MatchAllDocsQuery()); + + // Drill down on Lisa or Bob: + 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); + assertEquals(2, r.hits.totalHits); + assertEquals(2, r.facetResults.size()); + // Publish Date is both drill-sideways + drill-down: + // Lisa or Bob published twice in 2010 and once in 2012: + assertEquals("Publish Date: 2010=2 2012=1", toString(r.facetResults.get(0))); + // Author is drill-sideways + drill-down: + // only Lisa & Bob published (once each) in 2010: + assertEquals("Author: Lisa=1 Bob=1", toString(r.facetResults.get(1))); + + // Test drilling down on invalid field: + ddq = new DrillDownQuery(fsp.indexingParams, new MatchAllDocsQuery()); + ddq.add(new CategoryPath("Foobar", "Baz")); + 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); + assertEquals(0, r.hits.totalHits); + assertEquals(2, r.facetResults.size()); + assertEquals("Publish Date:", toString(r.facetResults.get(0))); + assertEquals("Foobar:", toString(r.facetResults.get(1))); + + // Test drilling down on valid term or'd with invalid term: + ddq = new DrillDownQuery(fsp.indexingParams, new MatchAllDocsQuery()); + ddq.add(new CategoryPath("Author", "Lisa"), + new CategoryPath("Author", "Tom")); + 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); + assertEquals(2, r.hits.totalHits); + assertEquals(2, r.facetResults.size()); + // Publish Date is only drill-down, and Lisa published + // one in 2012 and one in 2010: + assertEquals("Publish Date: 2012=1 2010=1", toString(r.facetResults.get(0))); + // Author is drill-sideways + drill-down: Lisa + // (drill-down) published twice, and Frank/Susan/Bob + // published once: + assertEquals("Author: Lisa=2 Frank=1 Susan=1 Bob=1", toString(r.facetResults.get(1))); + + // Test main query gets null scorer: + fsp = new FacetSearchParams( + new CountFacetRequest(new CategoryPath("Publish Date"), 10), + 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); + + assertEquals(0, r.hits.totalHits); + assertEquals(2, r.facetResults.size()); + assertEquals("Publish Date:", toString(r.facetResults.get(0))); + assertEquals("Author:", toString(r.facetResults.get(1))); + + searcher.getIndexReader().close(); + taxoReader.close(); + dir.close(); + taxoDir.close(); + } + + public void testSometimesInvalidDrillDown() throws Exception { + Directory dir = newDirectory(); + Directory taxoDir = newDirectory(); + writer = new RandomIndexWriter(random(), dir); + + // Writes facet ords to a separate directory from the + // main index: + taxoWriter = new DirectoryTaxonomyWriter(taxoDir, IndexWriterConfig.OpenMode.CREATE); + + // Reused across documents, to add the necessary facet + // fields: + facetFields = new FacetFields(taxoWriter); + + add("Author/Bob", "Publish Date/2010/10/15"); + add("Author/Lisa", "Publish Date/2010/10/20"); + writer.commit(); + // 2nd segment has no Author: + add("Foobar/Lisa", "Publish Date/2012/1/1"); + + // NRT open + IndexSearcher searcher = newSearcher(writer.getReader()); + writer.close(); + + //System.out.println("searcher=" + searcher); + + // NRT open + TaxonomyReader taxoReader = new DirectoryTaxonomyReader(taxoWriter); + taxoWriter.close(); + + // Count both "Publish Date" and "Author" dimensions, in + // drill-down: + FacetSearchParams fsp = new FacetSearchParams( + new CountFacetRequest(new CategoryPath("Publish Date"), 10), + new CountFacetRequest(new CategoryPath("Author"), 10)); + + 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); + + assertEquals(1, r.hits.totalHits); + assertEquals(2, r.facetResults.size()); + // Publish Date is only drill-down, and Lisa published + // one in 2012 and one in 2010: + assertEquals("Publish Date: 2010=1", toString(r.facetResults.get(0))); + // Author is drill-sideways + drill-down: Lisa + // (drill-down) published once, and Bob + // published once: + assertEquals("Author: Lisa=1 Bob=1", toString(r.facetResults.get(1))); + + searcher.getIndexReader().close(); + taxoReader.close(); + dir.close(); + taxoDir.close(); + } + + private static class Doc implements Comparable { + String id; + String contentToken; + + // -1 if the doc is missing this dim, else the index + // -into the values for this dim: + int[] dims; + + // 2nd value per dim for the doc (so we test + // multi-valued fields): + int[] dims2; + boolean deleted; + + @Override + public int compareTo(Doc other) { + return id.compareTo(other.id); + } + } + + private double aChance, bChance, cChance; + + private String randomContentToken(boolean isQuery) { + double d = random().nextDouble(); + if (isQuery) { + if (d < 0.33) { + return "a"; + } else if (d < 0.66) { + return "b"; + } else { + return "c"; + } + } else { + if (d <= aChance) { + return "a"; + } else if (d < aChance + bChance) { + return "b"; + } else { + return "c"; + } + } + } + + public void testRandom() throws Exception { + + while (aChance == 0.0) { + aChance = random().nextDouble(); + } + while (bChance == 0.0) { + bChance = random().nextDouble(); + } + while (cChance == 0.0) { + cChance = random().nextDouble(); + } + /* + aChance = .01; + bChance = 0.5; + cChance = 1.0; + */ + double sum = aChance + bChance + cChance; + aChance /= sum; + bChance /= sum; + cChance /= sum; + + int numDims = _TestUtil.nextInt(random(), 2, 5); + //int numDims = 3; + int numDocs = atLeast(3000); + //int numDocs = 20; + if (VERBOSE) { + System.out.println("numDims=" + numDims + " numDocs=" + numDocs + " aChance=" + aChance + " bChance=" + bChance + " cChance=" + cChance); + } + String[][] dimValues = new String[numDims][]; + int valueCount = 2; + for(int dim=0;dim values = new HashSet(); + while (values.size() < valueCount) { + String s = _TestUtil.randomRealisticUnicodeString(random()); + //String s = _TestUtil.randomSimpleString(random()); + if (s.length() > 0) { + values.add(s); + } + } + dimValues[dim] = values.toArray(new String[values.size()]); + valueCount *= 2; + } + + List docs = new ArrayList(); + for(int i=0;i paths = new ArrayList(); + + if (VERBOSE) { + System.out.println(" doc id=" + rawDoc.id + " token=" + rawDoc.contentToken); + } + for(int dim=0;dim lastDocID; + lastDocID = doc; + } + + @Override + public void setNextReader(AtomicReaderContext context) { + lastDocID = -1; + } + + @Override + public boolean acceptsDocsOutOfOrder() { + return false; + } + }, fsp); + + SimpleFacetResult expected = slowDrillSidewaysSearch(s, docs, contentToken, drillDowns, dimValues, filter); + + Sort sort = new Sort(new SortField("id", SortField.Type.STRING)); + DrillSidewaysResult actual = new DrillSideways(s, tr).search(ddq, filter, null, numDocs, sort, true, true, fsp); + + TopDocs hits = s.search(baseQuery, numDocs); + Map scores = new HashMap(); + for(ScoreDoc sd : hits.scoreDocs) { + scores.put(s.doc(sd.doc).get("id"), sd.score); + } + + verifyEquals(dimValues, s, expected, actual, scores); + } + + tr.close(); + r.close(); + td.close(); + d.close(); + } + + private static class Counters { + int[][] counts; + + public Counters(String[][] dimValues) { + counts = new int[dimValues.length][]; + for(int dim=0;dim hits; + int[][] counts; + } + + private SimpleFacetResult slowDrillSidewaysSearch(IndexSearcher s, List docs, String contentToken, String[][] drillDowns, + String[][] dimValues, Filter onlyEven) throws Exception { + int numDims = dimValues.length; + + List hits = new ArrayList(); + Counters drillDownCounts = new Counters(dimValues); + Counters[] drillSidewaysCounts = new Counters[dimValues.length]; + for(int dim=0;dim scores) throws Exception { + if (VERBOSE) { + System.out.println(" verify totHits=" + expected.hits.size()); + } + assertEquals(expected.hits.size(), actual.hits.totalHits); + assertEquals(expected.hits.size(), actual.hits.scoreDocs.length); + for(int i=0;i actualValues = new HashMap(); + for(FacetResultNode childNode : fr.getFacetResultNode().subResults) { + actualValues.put(childNode.label.components[1], (int) childNode.value); + if (VERBOSE) { + System.out.println(" " + childNode.label.components[1] + ": " + (int) childNode.value); + } + } + + if (VERBOSE) { + System.out.println(" expected"); + } + + int setCount = 0; + for(int i=0;i= 0 && index < numBits; + assert index >= 0 && index < numBits: "index=" + index + " numBits=" + numBits; int wordNum = index >> 6; // div 64 int bit = index & 0x3f; // mod 64 long bitmask = 1L << bit;