| /* |
| * 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.search.join; |
| |
| import java.io.IOException; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Locale; |
| |
| import org.apache.lucene.index.IndexReader; |
| import org.apache.lucene.index.LeafReaderContext; |
| import org.apache.lucene.search.DocIdSetIterator; |
| import org.apache.lucene.search.Explanation; |
| import org.apache.lucene.search.FilterWeight; |
| import org.apache.lucene.search.IndexSearcher; |
| import org.apache.lucene.search.Query; |
| import org.apache.lucene.search.QueryVisitor; |
| import org.apache.lucene.search.Scorer; |
| import org.apache.lucene.search.Weight; |
| import org.apache.lucene.util.BitSet; |
| |
| /** |
| * Just like {@link ToParentBlockJoinQuery}, except this |
| * query joins in reverse: you provide a Query matching |
| * parent documents and it joins down to child |
| * documents. |
| * |
| * @lucene.experimental |
| */ |
| |
| public class ToChildBlockJoinQuery extends Query { |
| |
| /** Message thrown from {@link |
| * ToChildBlockJoinScorer#validateParentDoc} on misuse, |
| * when the parent query incorrectly returns child docs. */ |
| static final String INVALID_QUERY_MESSAGE = "Parent query must not match any docs besides parent filter. " |
| + "Combine them as must (+) and must-not (-) clauses to find a problem doc. docID="; |
| static final String ILLEGAL_ADVANCE_ON_PARENT = "Expect to be advanced on child docs only. got docID="; |
| |
| private final BitSetProducer parentsFilter; |
| private final Query parentQuery; |
| |
| /** |
| * Create a ToChildBlockJoinQuery. |
| * |
| * @param parentQuery Query that matches parent documents |
| * @param parentsFilter Filter identifying the parent documents. |
| */ |
| public ToChildBlockJoinQuery(Query parentQuery, BitSetProducer parentsFilter) { |
| super(); |
| this.parentQuery = parentQuery; |
| this.parentsFilter = parentsFilter; |
| } |
| |
| @Override |
| public void visit(QueryVisitor visitor) { |
| visitor.visitLeaf(this); |
| } |
| |
| @Override |
| public Weight createWeight(IndexSearcher searcher, org.apache.lucene.search.ScoreMode scoreMode, float boost) throws IOException { |
| return new ToChildBlockJoinWeight(this, parentQuery.createWeight(searcher, scoreMode, boost), parentsFilter, scoreMode.needsScores()); |
| } |
| |
| /** Return our parent query. */ |
| public Query getParentQuery() { |
| return parentQuery; |
| } |
| |
| private static class ToChildBlockJoinWeight extends FilterWeight { |
| private final BitSetProducer parentsFilter; |
| private final boolean doScores; |
| |
| public ToChildBlockJoinWeight(Query joinQuery, Weight parentWeight, BitSetProducer parentsFilter, boolean doScores) { |
| super(joinQuery, parentWeight); |
| this.parentsFilter = parentsFilter; |
| this.doScores = doScores; |
| } |
| |
| // NOTE: acceptDocs applies (and is checked) only in the |
| // child document space |
| @Override |
| public Scorer scorer(LeafReaderContext readerContext) throws IOException { |
| |
| final Scorer parentScorer = in.scorer(readerContext); |
| |
| if (parentScorer == null) { |
| // No matches |
| return null; |
| } |
| |
| // NOTE: this doesn't take acceptDocs into account, the responsibility |
| // to not match deleted docs is on the scorer |
| final BitSet parents = parentsFilter.getBitSet(readerContext); |
| if (parents == null) { |
| // No parents |
| return null; |
| } |
| |
| return new ToChildBlockJoinScorer(this, parentScorer, parents, doScores); |
| } |
| |
| @Override |
| public Explanation explain(LeafReaderContext context, int doc) throws IOException { |
| ToChildBlockJoinScorer scorer = (ToChildBlockJoinScorer) scorer(context); |
| if (scorer != null && scorer.iterator().advance(doc) == doc) { |
| int parentDoc = scorer.getParentDoc(); |
| return Explanation.match( |
| scorer.score(), |
| String.format(Locale.ROOT, "Score based on parent document %d", parentDoc + context.docBase), |
| in.explain(context, parentDoc) |
| ); |
| } |
| return Explanation.noMatch("Not a match"); |
| } |
| } |
| |
| static class ToChildBlockJoinScorer extends Scorer { |
| private final Scorer parentScorer; |
| private final DocIdSetIterator parentIt; |
| private final BitSet parentBits; |
| private final boolean doScores; |
| |
| private float parentScore; |
| |
| private int childDoc = -1; |
| private int parentDoc = 0; |
| |
| public ToChildBlockJoinScorer(Weight weight, Scorer parentScorer, BitSet parentBits, boolean doScores) { |
| super(weight); |
| this.doScores = doScores; |
| this.parentBits = parentBits; |
| this.parentScorer = parentScorer; |
| this.parentIt = parentScorer.iterator(); |
| } |
| |
| @Override |
| public Collection<ChildScorable> getChildren() { |
| return Collections.singleton(new ChildScorable(parentScorer, "BLOCK_JOIN")); |
| } |
| |
| @Override |
| public DocIdSetIterator iterator() { |
| return new DocIdSetIterator() { |
| |
| @Override |
| public int docID() { |
| return childDoc; |
| } |
| |
| @Override |
| public int nextDoc() throws IOException { |
| //System.out.println("Q.nextDoc() parentDoc=" + parentDoc + " childDoc=" + childDoc); |
| |
| while (true) { |
| if (childDoc+1 == parentDoc) { |
| // OK, we are done iterating through all children |
| // matching this one parent doc, so we now nextDoc() |
| // the parent. Use a while loop because we may have |
| // to skip over some number of parents w/ no |
| // children: |
| while (true) { |
| parentDoc = parentIt.nextDoc(); |
| validateParentDoc(); |
| |
| if (parentDoc == 0) { |
| // Degenerate but allowed: first parent doc has no children |
| // TODO: would be nice to pull initial parent |
| // into ctor so we can skip this if... but it's |
| // tricky because scorer must return -1 for |
| // .doc() on init... |
| parentDoc = parentIt.nextDoc(); |
| validateParentDoc(); |
| } |
| |
| if (parentDoc == NO_MORE_DOCS) { |
| childDoc = NO_MORE_DOCS; |
| //System.out.println(" END"); |
| return childDoc; |
| } |
| |
| // Go to first child for this next parentDoc: |
| childDoc = 1 + parentBits.prevSetBit(parentDoc-1); |
| |
| if (childDoc == parentDoc) { |
| // This parent has no children; continue |
| // parent loop so we move to next parent |
| continue; |
| } |
| |
| if (childDoc < parentDoc) { |
| if (doScores) { |
| parentScore = parentScorer.score(); |
| } |
| //System.out.println(" " + childDoc); |
| return childDoc; |
| } else { |
| // Degenerate but allowed: parent has no children |
| } |
| } |
| } else { |
| assert childDoc < parentDoc: "childDoc=" + childDoc + " parentDoc=" + parentDoc; |
| childDoc++; |
| //System.out.println(" " + childDoc); |
| return childDoc; |
| } |
| } |
| } |
| |
| @Override |
| public int advance(int childTarget) throws IOException { |
| if (childTarget >= parentDoc) { |
| if (childTarget == NO_MORE_DOCS) { |
| return childDoc = parentDoc = NO_MORE_DOCS; |
| } |
| parentDoc = parentIt.advance(childTarget + 1); |
| validateParentDoc(); |
| |
| if (parentDoc == NO_MORE_DOCS) { |
| return childDoc = NO_MORE_DOCS; |
| } |
| |
| // scan to the first parent that has children |
| while (true) { |
| final int firstChild = parentBits.prevSetBit(parentDoc-1) + 1; |
| if (firstChild != parentDoc) { |
| // this parent has children |
| childTarget = Math.max(childTarget, firstChild); |
| break; |
| } |
| // parent with no children, move to the next one |
| parentDoc = parentIt.nextDoc(); |
| validateParentDoc(); |
| if (parentDoc == NO_MORE_DOCS) { |
| return childDoc = NO_MORE_DOCS; |
| } |
| } |
| |
| if (doScores) { |
| parentScore = parentScorer.score(); |
| } |
| } |
| |
| assert childTarget < parentDoc; |
| assert !parentBits.get(childTarget); |
| childDoc = childTarget; |
| //System.out.println(" " + childDoc); |
| return childDoc; |
| } |
| |
| @Override |
| public long cost() { |
| return parentIt.cost(); |
| } |
| }; |
| } |
| |
| /** Detect mis-use, where provided parent query in fact |
| * sometimes returns child documents. */ |
| private void validateParentDoc() { |
| if (parentDoc != DocIdSetIterator.NO_MORE_DOCS && !parentBits.get(parentDoc)) { |
| throw new IllegalStateException(INVALID_QUERY_MESSAGE + parentDoc); |
| } |
| } |
| |
| @Override |
| public int docID() { |
| return childDoc; |
| } |
| |
| @Override |
| public float score() throws IOException { |
| return parentScore; |
| } |
| |
| @Override |
| public float getMaxScore(int upTo) throws IOException { |
| return Float.POSITIVE_INFINITY; |
| } |
| |
| int getParentDoc() { |
| return parentDoc; |
| } |
| } |
| |
| @Override |
| public Query rewrite(IndexReader reader) throws IOException { |
| final Query parentRewrite = parentQuery.rewrite(reader); |
| if (parentRewrite != parentQuery) { |
| return new ToChildBlockJoinQuery(parentRewrite, parentsFilter); |
| } else { |
| return super.rewrite(reader); |
| } |
| } |
| |
| @Override |
| public String toString(String field) { |
| return "ToChildBlockJoinQuery ("+parentQuery.toString()+")"; |
| } |
| |
| @Override |
| public boolean equals(Object other) { |
| return sameClassAs(other) && |
| equalsTo(getClass().cast(other)); |
| } |
| |
| private boolean equalsTo(ToChildBlockJoinQuery other) { |
| return parentQuery.equals(other.parentQuery) && |
| parentsFilter.equals(other.parentsFilter); |
| } |
| |
| @Override |
| public int hashCode() { |
| final int prime = 31; |
| int hash = classHash(); |
| hash = prime * hash + parentQuery.hashCode(); |
| hash = prime * hash + parentsFilter.hashCode(); |
| return hash; |
| } |
| } |