/* * 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. */ package org.apache.lucene.spatial.prefix; import java.io.IOException; import org.locationtech.spatial4j.shape.Shape; import org.apache.lucene.index.IndexReaderContext; import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.index.PostingsEnum; import org.apache.lucene.search.DocIdSet; import org.apache.lucene.search.DocIdSetIterator; import org.apache.lucene.spatial.prefix.tree.Cell; import org.apache.lucene.spatial.prefix.tree.SpatialPrefixTree; import org.apache.lucene.util.Bits; /** * Computes facets on cells for {@link org.apache.lucene.spatial.prefix.PrefixTreeStrategy}. * <p> * <em>NOTE:</em> If for a given document and a given field using * {@link org.apache.lucene.spatial.prefix.RecursivePrefixTreeStrategy} * multiple values are indexed (i.e. multi-valued) and at least one of them is a non-point, then there is a possibility * of double-counting the document in the facet results. Since each shape is independently turned into grid cells at * a resolution chosen by the shape's size, it's possible they will be indexed at different resolutions. This means * the document could be present in BOTH the postings for a cell in both its prefix and leaf variants. To avoid this, * use a single valued field with a {@link org.locationtech.spatial4j.shape.ShapeCollection} (or WKT equivalent). Or * calculate a suitable level/distErr to index both and call * {@link org.apache.lucene.spatial.prefix.PrefixTreeStrategy#createIndexableFields(org.locationtech.spatial4j.shape.Shape, int)} * with the same value for all shapes for a given document/field. * * @lucene.experimental */ public class PrefixTreeFacetCounter { /** A callback/visitor of facet counts. */ public static abstract class FacetVisitor { /** Called at the start of the segment, if there is indexed data. */ public void startOfSegment() {} /** Called for cells with a leaf, or cells at the target facet level. {@code count} is greater than zero. * When an ancestor cell is given with non-zero count, the count can be considered to be added to all cells * below. You won't necessarily get a cell at level {@code facetLevel} if the indexed data is courser (bigger). */ public abstract void visit(Cell cell, int count); } private PrefixTreeFacetCounter() { } /** * Computes facets using a callback/visitor style design, allowing flexibility for the caller to determine what to do * with each underlying count. * @param strategy the prefix tree strategy (contains the field reference, grid, max levels) * @param context the IndexReader's context * @param topAcceptDocs a Bits to limit counted docs. If null, live docs are counted. * @param queryShape the shape to limit the range of facet counts to * @param facetLevel the maximum depth (detail) of faceted cells * @param facetVisitor the visitor/callback to receive the counts */ public static void compute(PrefixTreeStrategy strategy, IndexReaderContext context, Bits topAcceptDocs, Shape queryShape, int facetLevel, FacetVisitor facetVisitor) throws IOException { //We collect per-leaf for (final LeafReaderContext leafCtx : context.leaves()) { //determine leaf acceptDocs Bits Bits leafAcceptDocs; if (topAcceptDocs == null) { leafAcceptDocs = leafCtx.reader().getLiveDocs();//filter deleted } else { leafAcceptDocs = new Bits() { @Override public boolean get(int index) { return topAcceptDocs.get(leafCtx.docBase + index); } @Override public int length() { return leafCtx.reader().maxDoc(); } }; } compute(strategy, leafCtx, leafAcceptDocs, queryShape, facetLevel, facetVisitor); } } /** Lower-level per-leaf segment method. */ public static void compute(final PrefixTreeStrategy strategy, final LeafReaderContext context, final Bits acceptDocs, final Shape queryShape, final int facetLevel, final FacetVisitor facetVisitor) throws IOException { if (acceptDocs != null && acceptDocs.length() != context.reader().maxDoc()) { throw new IllegalArgumentException( "acceptDocs bits length " + acceptDocs.length() +" != leaf maxdoc " + context.reader().maxDoc()); } final SpatialPrefixTree tree = strategy.getGrid(); //scanLevel is an optimization knob of AbstractVisitingPrefixTreeFilter. It's unlikely // another scanLevel would be much faster and it tends to be a risky knob (can help a little, can hurt a ton). // TODO use RPT's configured scan level? Do we know better here? Hard to say. final int scanLevel = tree.getMaxLevels(); //AbstractVisitingPrefixTreeFilter is a Lucene Filter. We don't need a filter; we use it for its great prefix-tree // traversal code. TODO consider refactoring if/when it makes sense (more use cases than this) new AbstractVisitingPrefixTreeQuery(queryShape, strategy.getFieldName(), tree, facetLevel, scanLevel) { @Override public String toString(String field) { return "anonPrefixTreeQuery";//un-used } @Override public DocIdSet getDocIdSet(LeafReaderContext contexts) throws IOException { assert facetLevel == super.detailLevel;//same thing, FYI. (constant) return new VisitorTemplate(context) { @Override protected void start() throws IOException { facetVisitor.startOfSegment(); } @Override protected DocIdSet finish() throws IOException { return null;//unused; } @Override protected boolean visitPrefix(Cell cell) throws IOException { // At facetLevel... if (cell.getLevel() == facetLevel) { // Count docs visitLeaf(cell);//we're not a leaf but we treat it as such at facet level return false;//don't descend further; this is enough detail } // We optimize for discriminating filters (reflected in acceptDocs) and short-circuit if no // matching docs. We could do this at all levels or never but the closer we get to the facet level, the // higher the probability this is worthwhile. We do when docFreq == 1 because it's a cheap check, especially // due to "pulsing" in the codec. //TODO this opt should move to VisitorTemplate (which contains an optimization TODO to this effect) if (cell.getLevel() == facetLevel - 1 || termsEnum.docFreq() == 1) { if (!hasDocsAtThisTerm()) { return false; } } return true; } @Override protected void visitLeaf(Cell cell) throws IOException { final int count = countDocsAtThisTerm(); if (count > 0) { facetVisitor.visit(cell, count); } } private int countDocsAtThisTerm() throws IOException { if (acceptDocs == null) { return termsEnum.docFreq(); } int count = 0; postingsEnum = termsEnum.postings(postingsEnum, PostingsEnum.NONE); while (postingsEnum.nextDoc() != DocIdSetIterator.NO_MORE_DOCS) { if (acceptDocs.get(postingsEnum.docID()) == false) { continue; } count++; } return count; } private boolean hasDocsAtThisTerm() throws IOException { if (acceptDocs == null) { return true; } postingsEnum = termsEnum.postings(postingsEnum, PostingsEnum.NONE); int nextDoc = postingsEnum.nextDoc(); while (nextDoc != DocIdSetIterator.NO_MORE_DOCS && acceptDocs.get(nextDoc) == false) { nextDoc = postingsEnum.nextDoc(); } return nextDoc != DocIdSetIterator.NO_MORE_DOCS; } }.getDocIdSet(); } }.getDocIdSet(context); } }