/**
* Copyright 2014 National University of Ireland, Galway.
*
* This file is part of the SIREn project. Project and contact information:
*
* https://github.com/rdelbru/SIREn
*
* Licensed 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.sindice.siren.search.node;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.apache.lucene.search.Scorer;
import org.apache.lucene.util.IntsRef;
import org.sindice.siren.search.node.NodeBooleanClause.Occur;
import org.sindice.siren.search.node.NodeBooleanQuery.AbstractNodeBooleanWeight;
/**
* A {@link NodeScorer} that matches a boolean combination of node scorers.
*
* <p>
*
* Uses {@link NodeConjunctionScorer}, {@link NodeDisjunctionScorer},
* {@link NodeReqExclScorer} and {@link NodeReqOptScorer}.
*
* <p>
*
* Code taken from {@link BooleanScorer2} and adapted for the Siren use case.
*/
class NodeBooleanScorer extends NodeScorer {
protected final List<NodeScorer> requiredScorers;
protected final List<NodeScorer> optionalScorers;
protected final List<NodeScorer> prohibitedScorers;
private final Coordinator coordinator;
/**
* The scorer to which all scoring will be delegated, except for computing and
* using the coordination factor.
*/
protected NodeScorer countingSumScorer = null;
/**
* Creates a {@link NodeBooleanScorer} with the given lists of
* required, prohibited and optional scorers. In no required scorers are added,
* at least one of the optional scorers will have to match during the search.
*
* @param weight
* The BooleanWeight to be used.
* @param required
* the list of required scorers.
* @param prohibited
* the list of prohibited scorers.
* @param optional
* the list of optional scorers.
*/
public NodeBooleanScorer(final AbstractNodeBooleanWeight weight,
final List<NodeScorer> required,
final List<NodeScorer> prohibited,
final List<NodeScorer> optional) throws IOException {
super(weight);
coordinator = new Coordinator();
optionalScorers = optional;
requiredScorers = required;
prohibitedScorers = prohibited;
coordinator.init();
countingSumScorer = this.makeCountingSumScorer();
}
private NodeScorer countingDisjunctionSumScorer(final List<NodeScorer> scorers)
throws IOException {
return new NodeDisjunctionScorer(this.getWeight(), scorers) {
@Override
public float scoreInNode()
throws IOException {
final float nodeScore = super.scoreInNode();
coordinator.nrMatchers += super.nrMatchers();
return nodeScore;
}
};
}
private NodeScorer countingConjunctionSumScorer(final List<NodeScorer> requiredScorers)
throws IOException {
// each scorer from the list counted as a single matcher
final int requiredNrMatchers = requiredScorers.size();
return new NodeConjunctionScorer(weight, 1.0f, requiredScorers) {
@Override
public float scoreInNode() throws IOException {
final float nodeScore = super.scoreInNode();
coordinator.nrMatchers += requiredNrMatchers;
// All scorers match, so defaultSimilarity super.score() always has 1 as
// the coordination factor.
// Therefore the sum of the scores of the requiredScorers
// is used as score.
return nodeScore;
}
};
}
/**
* Returns the scorer to be used for match counting and score summing. Uses
* requiredScorers, optionalScorers and prohibitedScorers.
*/
private NodeScorer makeCountingSumScorer()
throws IOException { // each scorer counted as a single matcher
return (requiredScorers.size() == 0) ? this.makeCountingSumScorerNoReq()
: this.makeCountingSumScorerSomeReq();
}
private NodeScorer makeCountingSumScorerNoReq()
throws IOException { // No required scorers
NodeScorer requiredCountingSumScorer;
if (optionalScorers.size() > 1)
requiredCountingSumScorer = this.countingDisjunctionSumScorer(optionalScorers);
else if (optionalScorers.size() == 1)
requiredCountingSumScorer = new SingleMatchScorer(optionalScorers.get(0));
else {
requiredCountingSumScorer = this.countingConjunctionSumScorer(optionalScorers);
}
return this.addProhibitedScorers(requiredCountingSumScorer);
}
private NodeScorer makeCountingSumScorerSomeReq()
throws IOException { // At least one required scorer.
final NodeScorer requiredCountingSumScorer =
(requiredScorers.size() == 1) ? new SingleMatchScorer(requiredScorers.get(0))
: this.countingConjunctionSumScorer(requiredScorers);
if (optionalScorers.isEmpty()) {
return this.addProhibitedScorers(requiredCountingSumScorer);
}
else {
return new NodeReqOptScorer(
this.addProhibitedScorers(requiredCountingSumScorer),
optionalScorers.size() == 1
? new SingleMatchScorer(optionalScorers.get(0))
// require 1 in combined, optional scorer.
: this.countingDisjunctionSumScorer(optionalScorers));
}
}
/**
* Returns the scorer to be used for match counting and score summing. Uses
* the given required scorer and the prohibitedScorers.
*
* @param requiredCountingSumScorer
* A required scorer already built.
*/
private NodeScorer addProhibitedScorers(final NodeScorer requiredCountingSumScorer)
throws IOException {
return (prohibitedScorers.size() == 0)
? requiredCountingSumScorer // no prohibited
: new NodeReqExclScorer(requiredCountingSumScorer,
((prohibitedScorers.size() == 1)
? prohibitedScorers.get(0)
: new NodeDisjunctionScorer(weight, prohibitedScorers)));
}
@Override
public int doc() {
return countingSumScorer.doc();
}
@Override
public float freqInNode() throws IOException {
return coordinator.nrMatchers;
}
@Override
public IntsRef node() {
return countingSumScorer.node();
}
@Override
public boolean nextCandidateDocument() throws IOException {
return countingSumScorer.nextCandidateDocument();
}
@Override
public boolean nextNode() throws IOException {
return countingSumScorer.nextNode();
}
@Override
public float scoreInNode()
throws IOException {
coordinator.nrMatchers = 0;
final float sum = countingSumScorer.scoreInNode();
/*
* TODO: the score is weighted by the number of matched scorer.
* Is this the right place to do it ? Shouldn't it be done inside
* the similarity implementation ?
*/
return sum * coordinator.coordFactors[coordinator.nrMatchers];
}
@Override
public boolean skipToCandidate(final int target) throws IOException {
return countingSumScorer.skipToCandidate(target);
}
@Override
public Collection<ChildScorer> getChildren() {
final ArrayList<ChildScorer> children = new ArrayList<ChildScorer>();
for (final Scorer s : optionalScorers) {
children.add(new ChildScorer(s, Occur.SHOULD.toString()));
}
for (final Scorer s : prohibitedScorers) {
children.add(new ChildScorer(s, Occur.MUST_NOT.toString()));
}
for (final Scorer s : requiredScorers) {
children.add(new ChildScorer(s, Occur.MUST.toString()));
}
return children;
}
@Override
public String toString() {
return "NodeBooleanScorer(" + this.weight + "," + this.doc() + "," +
this.node() + ")";
}
private class Coordinator {
float[] coordFactors = null;
int nrMatchers; // to be increased by score() of match counting scorers.
void init() { // use after all scorers have been added.
coordFactors = new float[optionalScorers.size() + requiredScorers.size() + 1];
for (int i = 0; i < coordFactors.length; i++) {
coordFactors[i] = 1.0f;
}
}
}
/** Count a scorer as a single match. */
private class SingleMatchScorer extends NodeScorer {
private final NodeScorer scorer;
SingleMatchScorer(final NodeScorer scorer) {
super(scorer.getWeight());
this.scorer = scorer;
}
@Override
public float freqInNode() throws IOException {
return scorer.freqInNode();
}
/*
* TODO: Is it useful to cache the score ?
* It would mean (1) to cache an ints ref and (2) an array comparison for
* each call to scoreInNode().
*/
@Override
public float scoreInNode() throws IOException {
final float nodeScore = scorer.scoreInNode();
coordinator.nrMatchers++;
return nodeScore;
}
@Override
public int doc() {
return scorer.doc();
}
@Override
public IntsRef node() {
return scorer.node();
}
@Override
public boolean nextCandidateDocument() throws IOException {
return scorer.nextCandidateDocument();
}
@Override
public boolean nextNode() throws IOException {
return scorer.nextNode();
}
@Override
public boolean skipToCandidate(final int target) throws IOException {
return scorer.skipToCandidate(target);
}
@Override
public String toString() {
return "SingleMatchScorer(" + weight + "," + this.doc() + "," +
this.node() + ")";
}
}
}