package org.apache.lucene.search.join;
/*
* 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 java.util.Set;
import org.apache.lucene.index.AtomicReaderContext;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.DocIdSet;
import org.apache.lucene.search.Explanation;
import org.apache.lucene.search.Filter;
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;
import org.apache.lucene.util.FixedBitSet;
/**
* 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 mis-use,
* when the parent query incorrectly returns child docs. */
static final String INVALID_QUERY_MESSAGE = "Parent query yields document which is not matched by parents filter, docID=";
private final Filter parentsFilter;
private final Query parentQuery;
// If we are rewritten, this is the original parentQuery we
// were passed; we use this for .equals() and
// .hashCode(). This makes rewritten query equal the
// original, so that user does not have to .rewrite() their
// query before searching:
private final Query origParentQuery;
private final boolean doScores;
/**
* Create a ToChildBlockJoinQuery.
*
* @param parentQuery Query that matches parent documents
* @param parentsFilter Filter (must produce FixedBitSet
* per-segment, like {@link FixedBitSetCachingWrapperFilter})
* identifying the parent documents.
* @param doScores true if parent scores should be calculated
*/
public ToChildBlockJoinQuery(Query parentQuery, Filter parentsFilter, boolean doScores) {
super();
this.origParentQuery = parentQuery;
this.parentQuery = parentQuery;
this.parentsFilter = parentsFilter;
this.doScores = doScores;
}
private ToChildBlockJoinQuery(Query origParentQuery, Query parentQuery, Filter parentsFilter, boolean doScores) {
super();
this.origParentQuery = origParentQuery;
this.parentQuery = parentQuery;
this.parentsFilter = parentsFilter;
this.doScores = doScores;
}
@Override
public Weight createWeight(IndexSearcher searcher) throws IOException {
return new ToChildBlockJoinWeight(this, parentQuery.createWeight(searcher), parentsFilter, doScores);
}
private static class ToChildBlockJoinWeight extends Weight {
private final Query joinQuery;
private final Weight parentWeight;
private final Filter parentsFilter;
private final boolean doScores;
public ToChildBlockJoinWeight(Query joinQuery, Weight parentWeight, Filter parentsFilter, boolean doScores) {
super();
this.joinQuery = joinQuery;
this.parentWeight = parentWeight;
this.parentsFilter = parentsFilter;
this.doScores = doScores;
}
@Override
public Query getQuery() {
return joinQuery;
}
@Override
public float getValueForNormalization() throws IOException {
return parentWeight.getValueForNormalization() * joinQuery.getBoost() * joinQuery.getBoost();
}
@Override
public void normalize(float norm, float topLevelBoost) {
parentWeight.normalize(norm, topLevelBoost * joinQuery.getBoost());
}
// NOTE: acceptDocs applies (and is checked) only in the
// child document space
@Override
public Scorer scorer(AtomicReaderContext readerContext, Bits acceptDocs) throws IOException {
final Scorer parentScorer = parentWeight.scorer(readerContext, null);
if (parentScorer == null) {
// No matches
return null;
}
// NOTE: we cannot pass acceptDocs here because this
// will (most likely, justifiably) cause the filter to
// not return a FixedBitSet but rather a
// BitsFilteredDocIdSet. Instead, we filter by
// acceptDocs when we score:
final DocIdSet parents = parentsFilter.getDocIdSet(readerContext, null);
if (parents == null) {
// No matches
return null;
}
if (!(parents instanceof FixedBitSet)) {
throw new IllegalStateException("parentFilter must return FixedBitSet; got " + parents);
}
return new ToChildBlockJoinScorer(this, parentScorer, (FixedBitSet) parents, doScores, acceptDocs);
}
@Override
public Explanation explain(AtomicReaderContext reader, int doc) throws IOException {
// TODO
throw new UnsupportedOperationException(getClass().getName() +
" cannot explain match on parent document");
}
@Override
public boolean scoresDocsOutOfOrder() {
return false;
}
}
static class ToChildBlockJoinScorer extends Scorer {
private final Scorer parentScorer;
private final FixedBitSet parentBits;
private final boolean doScores;
private final Bits acceptDocs;
private float parentScore;
private int parentFreq = 1;
private int childDoc = -1;
private int parentDoc;
public ToChildBlockJoinScorer(Weight weight, Scorer parentScorer, FixedBitSet parentBits, boolean doScores, Bits acceptDocs) {
super(weight);
this.doScores = doScores;
this.parentBits = parentBits;
this.parentScorer = parentScorer;
this.acceptDocs = acceptDocs;
}
@Override
public Collection<ChildScorer> getChildren() {
return Collections.singleton(new ChildScorer(parentScorer, "BLOCK_JOIN"));
}
@Override
public int nextDoc() throws IOException {
//System.out.println("Q.nextDoc() parentDoc=" + parentDoc + " childDoc=" + childDoc);
// Loop until we hit a childDoc that's accepted
nextChildDoc:
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 = parentScorer.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 = parentScorer.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 (acceptDocs != null && !acceptDocs.get(childDoc)) {
continue nextChildDoc;
}
if (childDoc < parentDoc) {
if (doScores) {
parentScore = parentScorer.score();
parentFreq = parentScorer.freq();
}
//System.out.println(" " + childDoc);
return childDoc;
} else {
// Degenerate but allowed: parent has no children
}
}
} else {
assert childDoc < parentDoc: "childDoc=" + childDoc + " parentDoc=" + parentDoc;
childDoc++;
if (acceptDocs != null && !acceptDocs.get(childDoc)) {
continue;
}
//System.out.println(" " + childDoc);
return childDoc;
}
}
}
/** Detect mis-use, where provided parent query in fact
* sometimes returns child documents. */
private void validateParentDoc() {
if (parentDoc != 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 int freq() throws IOException {
return parentFreq;
}
@Override
public int advance(int childTarget) throws IOException {
assert childTarget >= parentBits.length() || !parentBits.get(childTarget);
//System.out.println("Q.advance childTarget=" + childTarget);
if (childTarget == NO_MORE_DOCS) {
//System.out.println(" END");
return childDoc = parentDoc = NO_MORE_DOCS;
}
assert childDoc == -1 || childTarget != parentDoc: "childTarget=" + childTarget;
if (childDoc == -1 || childTarget > parentDoc) {
// Advance to new parent:
parentDoc = parentScorer.advance(childTarget);
validateParentDoc();
//System.out.println(" advance to parentDoc=" + parentDoc);
assert parentDoc > childTarget;
if (parentDoc == NO_MORE_DOCS) {
//System.out.println(" END");
return childDoc = NO_MORE_DOCS;
}
if (doScores) {
parentScore = parentScorer.score();
parentFreq = parentScorer.freq();
}
final int firstChild = parentBits.prevSetBit(parentDoc-1);
//System.out.println(" firstChild=" + firstChild);
childTarget = Math.max(childTarget, firstChild);
}
assert childTarget < parentDoc;
// Advance within children of current parent:
childDoc = childTarget;
//System.out.println(" " + childDoc);
if (acceptDocs != null && !acceptDocs.get(childDoc)) {
nextDoc();
}
return childDoc;
}
@Override
public long cost() {
return parentScorer.cost();
}
}
@Override
public void extractTerms(Set<Term> terms) {
parentQuery.extractTerms(terms);
}
@Override
public Query rewrite(IndexReader reader) throws IOException {
final Query parentRewrite = parentQuery.rewrite(reader);
if (parentRewrite != parentQuery) {
Query rewritten = new ToChildBlockJoinQuery(parentQuery,
parentRewrite,
parentsFilter,
doScores);
rewritten.setBoost(getBoost());
return rewritten;
} else {
return this;
}
}
@Override
public String toString(String field) {
return "ToChildBlockJoinQuery ("+parentQuery.toString()+")";
}
@Override
public boolean equals(Object _other) {
if (_other instanceof ToChildBlockJoinQuery) {
final ToChildBlockJoinQuery other = (ToChildBlockJoinQuery) _other;
return origParentQuery.equals(other.origParentQuery) &&
parentsFilter.equals(other.parentsFilter) &&
doScores == other.doScores &&
super.equals(other);
} else {
return false;
}
}
@Override
public int hashCode() {
final int prime = 31;
int hash = super.hashCode();
hash = prime * hash + origParentQuery.hashCode();
hash = prime * hash + new Boolean(doScores).hashCode();
hash = prime * hash + parentsFilter.hashCode();
return hash;
}
@Override
public ToChildBlockJoinQuery clone() {
return new ToChildBlockJoinQuery(origParentQuery.clone(),
parentsFilter,
doScores);
}
}