blob: 31f3538b5674707ece7a13d50f3b13009db1575d [file] [log] [blame]
// Lucene version compatibility level 4.8.1
using Lucene.Net.Diagnostics;
using Lucene.Net.Index;
using Lucene.Net.Search;
using Lucene.Net.Util;
using System;
using System.Collections.Generic;
namespace Lucene.Net.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.
*/
/// <summary>
/// Just like <see cref="ToParentBlockJoinQuery"/>, except this
/// query joins in reverse: you provide a <see cref="Query"/> matching
/// parent documents and it joins down to child
/// documents.
///
/// @lucene.experimental
/// </summary>
public class ToChildBlockJoinQuery : Query
{
/// <summary>
/// Message thrown from <see cref="ToChildBlockJoinScorer.ValidateParentDoc"/>
/// on mis-use, when the parent query incorrectly returns child docs.
/// </summary>
internal const string INVALID_QUERY_MESSAGE = "Parent query yields document which is not matched by parents filter, docID=";
private readonly Filter _parentsFilter;
private readonly 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 readonly Query _origParentQuery;
private readonly bool _doScores;
/// <summary>
/// Create a <see cref="ToChildBlockJoinQuery"/>.
/// </summary>
/// <param name="parentQuery"><see cref="Query"/> that matches parent documents</param>
/// <param name="parentsFilter"><see cref="Filter"/> (must produce FixedBitSet per-segment, like <see cref="FixedBitSetCachingWrapperFilter"/>)
/// identifying the parent documents.</param>
/// <param name="doScores">True if parent scores should be calculated.</param>
public ToChildBlockJoinQuery(Query parentQuery, Filter parentsFilter, bool doScores)
: base()
{
_origParentQuery = parentQuery;
_parentQuery = parentQuery;
_parentsFilter = parentsFilter;
_doScores = doScores;
}
private ToChildBlockJoinQuery(Query origParentQuery, Query parentQuery, Filter parentsFilter, bool doScores)
: base()
{
_origParentQuery = origParentQuery;
_parentQuery = parentQuery;
_parentsFilter = parentsFilter;
_doScores = doScores;
}
public override Weight CreateWeight(IndexSearcher searcher)
{
return new ToChildBlockJoinWeight(this, _parentQuery.CreateWeight(searcher), _parentsFilter, _doScores);
}
private class ToChildBlockJoinWeight : Weight
{
private readonly Query _joinQuery;
private readonly Weight _parentWeight;
private readonly Filter _parentsFilter;
private readonly bool _doScores;
public ToChildBlockJoinWeight(Query joinQuery, Weight parentWeight, Filter parentsFilter, bool doScores)
: base()
{
_joinQuery = joinQuery;
_parentWeight = parentWeight;
_parentsFilter = parentsFilter;
_doScores = doScores;
}
public override Query Query => _joinQuery;
public override float GetValueForNormalization()
{
return _parentWeight.GetValueForNormalization() * _joinQuery.Boost*_joinQuery.Boost;
}
public override void Normalize(float norm, float topLevelBoost)
{
_parentWeight.Normalize(norm, topLevelBoost * _joinQuery.Boost);
}
// NOTE: acceptDocs applies (and is checked) only in the child document space
public override Scorer GetScorer(AtomicReaderContext readerContext, IBits acceptDocs)
{
Scorer parentScorer = _parentWeight.GetScorer(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:
DocIdSet parents = _parentsFilter.GetDocIdSet(readerContext, null);
if (parents == null)
{
// No matches
return null;
}
if (!(parents is FixedBitSet))
{
throw new InvalidOperationException("parentFilter must return FixedBitSet; got " + parents);
}
return new ToChildBlockJoinScorer(this, parentScorer, (FixedBitSet)parents, _doScores, acceptDocs);
}
public override Explanation Explain(AtomicReaderContext reader, int doc)
{
// TODO
throw new NotSupportedException(GetType().Name + " cannot explain match on parent document");
}
public override bool ScoresDocsOutOfOrder => false;
}
private sealed class ToChildBlockJoinScorer : Scorer
{
private readonly Scorer _parentScorer;
private readonly FixedBitSet _parentBits;
private readonly bool _doScores;
private readonly IBits _acceptDocs;
private float _parentScore;
private int _parentFreq = 1;
private int _childDoc = -1;
private int _parentDoc;
public ToChildBlockJoinScorer(Weight weight, Scorer parentScorer, FixedBitSet parentBits, bool doScores, IBits acceptDocs)
: base(weight)
{
_doScores = doScores;
_parentBits = parentBits;
_parentScorer = parentScorer;
_acceptDocs = acceptDocs;
}
public override ICollection<ChildScorer> GetChildren()
{
return new List<ChildScorer> { new ChildScorer(_parentScorer, "BLOCK_JOIN") };
}
public override int NextDoc()
{
//System.out.println("Q.nextDoc() parentDoc=" + parentDoc + " childDoc=" + childDoc);
// Loop until we hit a childDoc that's accepted
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))
{
goto nextChildDocContinue;
}
if (_childDoc < _parentDoc)
{
if (_doScores)
{
_parentScore = _parentScorer.GetScore();
_parentFreq = _parentScorer.Freq;
}
//System.out.println(" " + childDoc);
return _childDoc;
}
else
{
// Degenerate but allowed: parent has no children
}
}
}
if (Debugging.AssertsEnabled) Debugging.Assert(_childDoc < _parentDoc, "childDoc={0} parentDoc={1}", _childDoc, _parentDoc);
_childDoc++;
if (_acceptDocs != null && !_acceptDocs.Get(_childDoc))
{
continue;
}
//System.out.println(" " + childDoc);
return _childDoc;
nextChildDocContinue:;
}
}
/// <summary>
/// Detect mis-use, where provided parent query in fact sometimes returns child documents.
/// </summary>
private void ValidateParentDoc()
{
if (_parentDoc != NO_MORE_DOCS && !_parentBits.Get(_parentDoc))
{
throw new InvalidOperationException(INVALID_QUERY_MESSAGE + _parentDoc);
}
}
public override int DocID => _childDoc;
public override float GetScore()
{
return _parentScore;
}
public override int Freq => _parentFreq;
public override int Advance(int childTarget)
{
if (Debugging.AssertsEnabled) Debugging.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;
}
if (Debugging.AssertsEnabled) Debugging.Assert(_childDoc == -1 || childTarget != _parentDoc, "childTarget={0}", childTarget);
if (_childDoc == -1 || childTarget > _parentDoc)
{
// Advance to new parent:
_parentDoc = _parentScorer.Advance(childTarget);
ValidateParentDoc();
//System.out.println(" advance to parentDoc=" + parentDoc);
if (Debugging.AssertsEnabled) Debugging.Assert(_parentDoc > childTarget);
if (_parentDoc == NO_MORE_DOCS)
{
//System.out.println(" END");
return _childDoc = NO_MORE_DOCS;
}
if (_doScores)
{
_parentScore = _parentScorer.GetScore();
_parentFreq = _parentScorer.Freq;
}
int firstChild = _parentBits.PrevSetBit(_parentDoc - 1);
//System.out.println(" firstChild=" + firstChild);
childTarget = Math.Max(childTarget, firstChild);
}
if (Debugging.AssertsEnabled) Debugging.Assert(childTarget < _parentDoc);
// Advance within children of current parent:
_childDoc = childTarget;
//System.out.println(" " + childDoc);
if (_acceptDocs != null && !_acceptDocs.Get(_childDoc))
{
NextDoc();
}
return _childDoc;
}
public override long GetCost()
{
return _parentScorer.GetCost();
}
}
public override void ExtractTerms(ISet<Term> terms)
{
_parentQuery.ExtractTerms(terms);
}
public override Query Rewrite(IndexReader reader)
{
Query parentRewrite = _parentQuery.Rewrite(reader);
if (parentRewrite != _parentQuery)
{
Query rewritten = new ToChildBlockJoinQuery(_parentQuery, parentRewrite, _parentsFilter, _doScores);
rewritten.Boost = Boost;
return rewritten;
}
return this;
}
public override string ToString(string field)
{
return "ToChildBlockJoinQuery (" + _parentQuery + ")";
}
public override bool Equals(object obj)
{
if (obj is null) return false;
if (obj is ToChildBlockJoinQuery other)
{
return _origParentQuery.Equals(other._origParentQuery) &&
_parentsFilter.Equals(other._parentsFilter) &&
_doScores == other._doScores &&
base.Equals(other);
}
else
{
return false;
}
}
public override int GetHashCode()
{
unchecked
{
int hashCode = base.GetHashCode();
hashCode = (hashCode*397) ^ (_origParentQuery != null ? _origParentQuery.GetHashCode() : 0);
hashCode = (hashCode*397) ^ _doScores.GetHashCode();
hashCode = (hashCode*397) ^ (_parentsFilter != null ? _parentsFilter.GetHashCode() : 0);
return hashCode;
}
}
public override object Clone()
{
return new ToChildBlockJoinQuery((Query) _origParentQuery.Clone(), _parentsFilter, _doScores);
}
}
}