blob: d2e03e8ead3b86a913035c726691b00046343aa7 [file] [log] [blame]
package org.apache.lucene.search;
/**
* 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 org.apache.lucene.index.TermPositions;
import java.io.IOException;
import java.util.HashMap;
final class SloppyPhraseScorer extends PhraseScorer {
private int slop;
private PhrasePositions repeats[];
private PhrasePositions tmpPos[]; // for flipping repeating pps.
private boolean checkedRepeats;
SloppyPhraseScorer(Weight weight, TermPositions[] tps, int[] offsets, Similarity similarity,
int slop, byte[] norms) {
super(weight, tps, offsets, similarity, norms);
this.slop = slop;
}
/**
* Score a candidate doc for all slop-valid position-combinations (matches)
* encountered while traversing/hopping the PhrasePositions.
* <br> The score contribution of a match depends on the distance:
* <br> - highest score for distance=0 (exact match).
* <br> - score gets lower as distance gets higher.
* <br>Example: for query "a b"~2, a document "x a b a y" can be scored twice:
* once for "a b" (distance=0), and once for "b a" (distance=2).
* <br>Possibly not all valid combinations are encountered, because for efficiency
* we always propagate the least PhrasePosition. This allows to base on
* PriorityQueue and move forward faster.
* As result, for example, document "a b c b a"
* would score differently for queries "a b c"~4 and "c b a"~4, although
* they really are equivalent.
* Similarly, for doc "a b c b a f g", query "c b"~2
* would get same score as "g f"~2, although "c b"~2 could be matched twice.
* We may want to fix this in the future (currently not, for performance reasons).
*/
@Override
protected final float phraseFreq() throws IOException {
int end = initPhrasePositions();
float freq = 0.0f;
boolean done = (end<0);
while (!done) {
PhrasePositions pp = pq.pop();
int start = pp.position;
int next = pq.top().position;
boolean tpsDiffer = true;
for (int pos = start; pos <= next || !tpsDiffer; pos = pp.position) {
if (pos<=next && tpsDiffer)
start = pos; // advance pp to min window
if (!pp.nextPosition()) {
done = true; // ran out of a term -- done
break;
}
PhrasePositions pp2 = null;
tpsDiffer = !pp.repeats || (pp2 = termPositionsDiffer(pp))==null;
if (pp2!=null && pp2!=pp) {
pp = flip(pp,pp2); // flip pp to pp2
}
}
int matchLength = end - start;
if (matchLength <= slop)
freq += getSimilarity().sloppyFreq(matchLength); // score match
if (pp.position > end)
end = pp.position;
pq.add(pp); // restore pq
}
return freq;
}
// flip pp2 and pp in the queue: pop until finding pp2, insert back all but pp2, insert pp back.
// assumes: pp!=pp2, pp2 in pq, pp not in pq.
// called only when there are repeating pps.
private PhrasePositions flip(PhrasePositions pp, PhrasePositions pp2) {
int n=0;
PhrasePositions pp3;
//pop until finding pp2
while ((pp3=pq.pop()) != pp2) {
tmpPos[n++] = pp3;
}
//insert back all but pp2
for (n--; n>=0; n--) {
pq.insertWithOverflow(tmpPos[n]);
}
//insert pp back
pq.add(pp);
return pp2;
}
/**
* Init PhrasePositions in place.
* There is a one time initialization for this scorer:
* <br>- Put in repeats[] each pp that has another pp with same position in the doc.
* <br>- Also mark each such pp by pp.repeats = true.
* <br>Later can consult with repeats[] in termPositionsDiffer(pp), making that check efficient.
* In particular, this allows to score queries with no repetitions with no overhead due to this computation.
* <br>- Example 1 - query with no repetitions: "ho my"~2
* <br>- Example 2 - query with repetitions: "ho my my"~2
* <br>- Example 3 - query with repetitions: "my ho my"~2
* <br>Init per doc w/repeats in query, includes propagating some repeating pp's to avoid false phrase detection.
* @return end (max position), or -1 if any term ran out (i.e. done)
* @throws IOException
*/
private int initPhrasePositions() throws IOException {
int end = 0;
// no repeats at all (most common case is also the simplest one)
if (checkedRepeats && repeats==null) {
// build queue from list
pq.clear();
for (PhrasePositions pp = first; pp != null; pp = pp.next) {
pp.firstPosition();
if (pp.position > end)
end = pp.position;
pq.add(pp); // build pq from list
}
return end;
}
// position the pp's
for (PhrasePositions pp = first; pp != null; pp = pp.next)
pp.firstPosition();
// one time initializatin for this scorer
if (!checkedRepeats) {
checkedRepeats = true;
// check for repeats
HashMap<PhrasePositions, Object> m = null;
for (PhrasePositions pp = first; pp != null; pp = pp.next) {
int tpPos = pp.position + pp.offset;
for (PhrasePositions pp2 = pp.next; pp2 != null; pp2 = pp2.next) {
int tpPos2 = pp2.position + pp2.offset;
if (tpPos2 == tpPos) {
if (m == null)
m = new HashMap<PhrasePositions, Object>();
pp.repeats = true;
pp2.repeats = true;
m.put(pp,null);
m.put(pp2,null);
}
}
}
if (m!=null)
repeats = m.keySet().toArray(new PhrasePositions[0]);
}
// with repeats must advance some repeating pp's so they all start with differing tp's
if (repeats!=null) {
for (int i = 0; i < repeats.length; i++) {
PhrasePositions pp = repeats[i];
PhrasePositions pp2;
while ((pp2 = termPositionsDiffer(pp)) != null) {
if (!pp2.nextPosition()) // out of pps that do not differ, advance the pp with higher offset
return -1; // ran out of a term -- done
}
}
}
// build queue from list
pq.clear();
for (PhrasePositions pp = first; pp != null; pp = pp.next) {
if (pp.position > end)
end = pp.position;
pq.add(pp); // build pq from list
}
if (repeats!=null) {
tmpPos = new PhrasePositions[pq.size()];
}
return end;
}
/**
* We disallow two pp's to have the same TermPosition, thereby verifying multiple occurrences
* in the query of the same word would go elsewhere in the matched doc.
* @return null if differ (i.e. valid) otherwise return the higher offset PhrasePositions
* out of the first two PPs found to not differ.
*/
private PhrasePositions termPositionsDiffer(PhrasePositions pp) {
// efficiency note: a more efficient implementation could keep a map between repeating
// pp's, so that if pp1a, pp1b, pp1c are repeats term1, and pp2a, pp2b are repeats
// of term2, pp2a would only be checked against pp2b but not against pp1a, pp1b, pp1c.
// However this would complicate code, for a rather rare case, so choice is to compromise here.
int tpPos = pp.position + pp.offset;
for (int i = 0; i < repeats.length; i++) {
PhrasePositions pp2 = repeats[i];
if (pp2 == pp)
continue;
int tpPos2 = pp2.position + pp2.offset;
if (tpPos2 == tpPos)
return pp.offset > pp2.offset ? pp : pp2; // do not differ: return the one with higher offset.
}
return null;
}
}