LUCENE-10428: Avoid infinite loop under error conditions. (#711)

Co-authored-by: dblock <dblock@dblock.org>
This commit is contained in:
Adrien Grand 2022-03-03 09:42:12 +01:00
parent bb10e62dff
commit 0d35e38b93
3 changed files with 49 additions and 7 deletions

View File

@ -253,6 +253,10 @@ Bug Fixes
* LUCENE-10405: When using the MemoryIndex, binary and Sorted doc values are stored
as BytesRef instead of BytesRefHash so they don't have a limit on size. (Ignacio Vera)
* LUCENE-10428: Queries with a misbehaving score function may no longer cause
infinite loops in their parent BooleanQuery.
(Ankit Jain, Daniel Doubrovkine, Adrien Grand)
Other
---------------------

View File

@ -128,25 +128,40 @@ final class MaxScoreSumPropagator {
}
}
/** Return the minimum score that a Scorer must produce in order for a hit to be competitive. */
/**
* Return the minimum score that a Scorer must produce in order for a hit to be competitive.
*
* <p>The way that boolean queries combine scores of their sub clauses together is by summing up
* the float scores into a double and finally casting back that double back to a float. This
* method undoes this operation by taking the float score sum and subtracting the sum of other
* scores as a double as a first approximation of the minimum score that this clause must have.
*/
private float getMinCompetitiveScore(float minScoreSum, double sumOfOtherMaxScores) {
assert numClauses > 0;
if (minScoreSum <= sumOfOtherMaxScores) {
return 0f;
}
// We need to find a value 'minScore' so that 'minScore + sumOfOtherMaxScores <= minScoreSum'
// TODO: is there an efficient way to find the greatest value that meets this requirement?
float minScore = (float) (minScoreSum - sumOfOtherMaxScores);
int iters = 0;
while (scoreSumUpperBound(minScore + sumOfOtherMaxScores) > minScoreSum) {
for (int iter = 0;
minScore > 0 && scoreSumUpperBound(minScore + sumOfOtherMaxScores) > minScoreSum;
++iter) {
// Important: use ulp of minScoreSum and not minScore to make sure that we
// converge quickly.
minScore -= Math.ulp(minScoreSum);
// this should converge in at most two iterations:
// - one because of the subtraction rounding error
// - one because of the error introduced by sumUpperBound
assert ++iters <= 2 : iters;
if (iter > 2) {
throw new IllegalStateException(
"Could not compute a minimum score for minScore="
+ +minScore
+ ", minScoreSum="
+ minScoreSum
+ ", sumOfOtherMaxScores="
+ sumOfOtherMaxScores
+ ", numClauses="
+ numClauses);
}
}
return Math.max(minScore, 0f);
}

View File

@ -229,4 +229,27 @@ public class TestMaxScoreSumPropagator extends LuceneTestCase {
(float) scoreSum <= minCompetitiveScore);
}
}
/*
In https://issues.apache.org/jira/browse/LUCENE-10428 we have observed an issue in production
causing an infinite loop in MaxScoreSumPropagator.getMinCompetitiveScore. This is likely
caused by calling code that is breaking the assumptions made by MaxScoreSumPropagator,
e.g. a query that produces negative or NaN scores. This test reproduces that scenario,
and asserts that getMinCompetitiveScore aborts after more than 2 iterations.
See https://github.com/apache/lucene/pull/711.
*/
public void testMinCompetitiveScoreIllegalState() throws Exception {
List<FakeScorer> scorers = new ArrayList<>();
scorers.add(new FakeScorer(-0.16903716f));
scorers.add(new FakeScorer(0.62573546f));
scorers.add(new FakeScorer(-0.64014715f));
float minScoreSum = 0.31314075f;
MaxScoreSumPropagator p = new MaxScoreSumPropagator(scorers);
Throwable exception =
assertThrows(IllegalStateException.class, () -> p.setMinCompetitiveScore(minScoreSum));
assertEquals(
"Could not compute a minimum score for minScore=1.1223251, minScoreSum=0.31314072, sumOfOtherMaxScores=-0.8091843128204346, numClauses=3",
exception.getMessage());
}
}