You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by jp...@apache.org on 2017/12/29 13:37:14 UTC

lucene-solr:master: LUCENE-8109: Propagate information about the minimum score in BooleanQuery.

Repository: lucene-solr
Updated Branches:
  refs/heads/master bc9836fd5 -> 33f421e79


LUCENE-8109: Propagate information about the minimum score in BooleanQuery.


Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/33f421e7
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/33f421e7
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/33f421e7

Branch: refs/heads/master
Commit: 33f421e798d47da6482ed5ff3c55efe7d8f350f8
Parents: bc9836f
Author: Adrien Grand <jp...@gmail.com>
Authored: Fri Dec 29 13:24:23 2017 +0100
Committer: Adrien Grand <jp...@gmail.com>
Committed: Fri Dec 29 14:35:05 2017 +0100

----------------------------------------------------------------------
 lucene/CHANGES.txt                              |   5 +
 .../apache/lucene/search/ConjunctionScorer.java |  17 +-
 .../lucene/search/MaxScoreSumPropagator.java    | 157 +++++++++++++++
 .../apache/lucene/search/ReqOptSumScorer.java   |   8 +-
 .../org/apache/lucene/search/WANDScorer.java    |  20 +-
 .../search/TestMaxScoreSumPropagator.java       | 198 +++++++++++++++++++
 6 files changed, 380 insertions(+), 25 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/33f421e7/lucene/CHANGES.txt
----------------------------------------------------------------------
diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt
index a78770a..0ec795a 100644
--- a/lucene/CHANGES.txt
+++ b/lucene/CHANGES.txt
@@ -64,6 +64,11 @@ Optimizations
 * LUCENE-7993: Phrase queries are now faster if total hit counts are not
   required. (Adrien Grand)
 
+* LUCENE-8109: Boolean queries propagate information about the minimum
+  competitive score in order to make collection faster if there are disjunctions
+  or phrase queries as sub queries, which know how to leverage this information
+  to run faster. (Adrien Grand)
+
 ======================= Lucene 7.3.0 =======================
 
 API Changes

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/33f421e7/lucene/core/src/java/org/apache/lucene/search/ConjunctionScorer.java
----------------------------------------------------------------------
diff --git a/lucene/core/src/java/org/apache/lucene/search/ConjunctionScorer.java b/lucene/core/src/java/org/apache/lucene/search/ConjunctionScorer.java
index 2a8b6e6..75895fc 100644
--- a/lucene/core/src/java/org/apache/lucene/search/ConjunctionScorer.java
+++ b/lucene/core/src/java/org/apache/lucene/search/ConjunctionScorer.java
@@ -27,6 +27,7 @@ class ConjunctionScorer extends Scorer {
   final DocIdSetIterator disi;
   final Scorer[] scorers;
   final Collection<Scorer> required;
+  final MaxScoreSumPropagator maxScorePropagator;
 
   /** Create a new {@link ConjunctionScorer}, note that {@code scorers} must be a subset of {@code required}. */
   ConjunctionScorer(Weight weight, Collection<Scorer> required, Collection<Scorer> scorers) {
@@ -35,6 +36,7 @@ class ConjunctionScorer extends Scorer {
     this.disi = ConjunctionDISI.intersectScorers(required);
     this.scorers = scorers.toArray(new Scorer[scorers.size()]);
     this.required = required;
+    this.maxScorePropagator = new MaxScoreSumPropagator(scorers);
   }
 
   @Override
@@ -63,22 +65,13 @@ class ConjunctionScorer extends Scorer {
 
   @Override
   public float maxScore() {
-    // We iterate in the same order as #score() so no need to worry
-    // about floating-point errors: we would do the same errors in
-    // #score()
-    double sum = 0d;
-    for (Scorer scorer : scorers) {
-      sum += scorer.maxScore();
-    }
-    return (float) sum;
+    return maxScorePropagator.maxScore();
   }
 
   @Override
   public void setMinCompetitiveScore(float score) {
-    if (scorers.length == 1) {
-      scorers[0].setMinCompetitiveScore(score);
-    }
-    // TODO: handle the case when there are multiple scoring clauses too
+    // Propagate to sub clauses.
+    maxScorePropagator.setMinCompetitiveScore(score);
   }
 
   @Override

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/33f421e7/lucene/core/src/java/org/apache/lucene/search/MaxScoreSumPropagator.java
----------------------------------------------------------------------
diff --git a/lucene/core/src/java/org/apache/lucene/search/MaxScoreSumPropagator.java b/lucene/core/src/java/org/apache/lucene/search/MaxScoreSumPropagator.java
new file mode 100644
index 0000000..27c8933
--- /dev/null
+++ b/lucene/core/src/java/org/apache/lucene/search/MaxScoreSumPropagator.java
@@ -0,0 +1,157 @@
+/*
+ * 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.
+ */
+package org.apache.lucene.search;
+
+import java.util.Collection;
+
+import org.apache.lucene.util.InPlaceMergeSorter;
+import org.apache.lucene.util.MathUtil;
+
+/**
+ * Utility class to propagate scoring information in {@link BooleanQuery}, which
+ * compute the score as the sum of the scores of its matching clauses.
+ * This helps propagate information about the maximum produced score
+ */
+final class MaxScoreSumPropagator {
+
+  /**
+   * Return an array which, at index i, stores the sum of all entries of
+   * {@code v} except the one at index i.
+   */
+  private static double[] computeSumOfComplement(float[] v) {
+    // We do not use subtraction on purpose because it would defeat the
+    // upperbound formula that we use for sums.
+    // Naive approach would be O(n^2), but we can do O(n) by computing the
+    // sum for i<j and i>j and then sum them.
+    double[] sum1 = new double[v.length];
+    for (int i = 1; i < sum1.length; ++i) {
+      sum1[i] = sum1[i-1] + v[i-1];
+    }
+
+    double[] sum2 = new double[v.length];
+    for (int i = sum2.length - 2; i >= 0; --i) {
+      sum2[i] = sum2[i+1] + v[i+1];
+    }
+
+    double[] result = new double[v.length];
+    for (int i = 0; i < result.length; ++i) {
+      result[i] = sum1[i] + sum2[i];
+    }
+    return result;
+  }
+
+  private final int numClauses;
+  private final float maxScore;
+  private final Scorer[] scorers;
+  private final double[] sumOfOtherMaxScores;
+
+  MaxScoreSumPropagator(Collection<? extends Scorer> scorerList) {
+    numClauses = scorerList.size();
+    scorers = scorerList.toArray(new Scorer[numClauses]);
+    // We'll need max scores multiple times so we cache them
+    float[] maxScores = new float[numClauses];
+    for (int i = 0; i < numClauses; ++i) {
+      maxScores[i] = scorers[i].maxScore();
+    }
+    // Sort by decreasing max score
+    new InPlaceMergeSorter() {
+      @Override
+      protected void swap(int i, int j) {
+        Scorer tmp = scorers[i];
+        scorers[i] = scorers[j];
+        scorers[j] = tmp;
+        float tmpF = maxScores[i];
+        maxScores[i] = maxScores[j];
+        maxScores[j] = tmpF;
+      }
+      @Override
+      protected int compare(int i, int j) {
+        return Float.compare(maxScores[j], maxScores[i]);
+      }
+    }.sort(0, scorers.length);
+
+    sumOfOtherMaxScores = computeSumOfComplement(maxScores);
+    if (numClauses == 0) {
+      maxScore = 0;
+    } else {
+      maxScore = sumUpperBound(maxScores[0] + sumOfOtherMaxScores[0]);
+    }
+  }
+
+  public float maxScore() {
+    return maxScore;
+  }
+
+  public void setMinCompetitiveScore(float minScoreSum) {
+    for (int i = 0; i < numClauses; ++i) {
+      double sumOfOtherMaxScores = this.sumOfOtherMaxScores[i];
+      float minCompetitiveScore = getMinCompetitiveScore(minScoreSum, sumOfOtherMaxScores);
+      if (minCompetitiveScore <= 0) {
+        // given that scorers are sorted by decreasing max score, next scorers will
+        // have 0 as a minimum competitive score too
+        break;
+      }
+      scorers[i].setMinCompetitiveScore(minCompetitiveScore);
+    }
+  }
+
+  /**
+   * Return the minimum score that a Scorer must produce in order for a hit to
+   * be competitive.
+   */
+  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 (sumUpperBound(minScore + sumOfOtherMaxScores) > minScoreSum) {
+      // 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;
+    }
+    return Math.max(minScore, 0f);
+  }
+
+  private float sumUpperBound(double sum) {
+    if (numClauses <= 2) {
+      // When there are only two clauses, the sum is always the same regardless
+      // of the order.
+      return (float) sum;
+    }
+
+    // The error of sums depends on the order in which values are summed up. In
+    // order to avoid this issue, we compute an upper bound of the value that
+    // the sum may take. If the max relative error is b, then it means that two
+    // sums are always within 2*b of each other.
+    // For conjunctions, we could skip this error factor since the order in which
+    // scores are summed up is predictable, but in practice, this wouldn't help
+    // much since the delta that is introduced by this error factor is usually
+    // cancelled by the float cast.
+    double b = MathUtil.sumRelativeErrorBound(numClauses);
+    return (float) ((1.0 + 2 * b) * sum);
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/33f421e7/lucene/core/src/java/org/apache/lucene/search/ReqOptSumScorer.java
----------------------------------------------------------------------
diff --git a/lucene/core/src/java/org/apache/lucene/search/ReqOptSumScorer.java b/lucene/core/src/java/org/apache/lucene/search/ReqOptSumScorer.java
index 6b2ace8..b91d253 100644
--- a/lucene/core/src/java/org/apache/lucene/search/ReqOptSumScorer.java
+++ b/lucene/core/src/java/org/apache/lucene/search/ReqOptSumScorer.java
@@ -18,6 +18,7 @@ package org.apache.lucene.search;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 
 /** A Scorer for queries with a required part and an optional part.
@@ -35,6 +36,7 @@ class ReqOptSumScorer extends Scorer {
   private boolean optIsRequired;
   private final DocIdSetIterator approximation;
   private final TwoPhaseIterator twoPhase;
+  final MaxScoreSumPropagator maxScorePropagator;
 
   /** Construct a <code>ReqOptScorer</code>.
    * @param reqScorer The required scorer. This must match.
@@ -51,6 +53,7 @@ class ReqOptSumScorer extends Scorer {
     this.optScorer = optScorer;
 
     this.reqMaxScore = reqScorer.maxScore();
+    this.maxScorePropagator = new MaxScoreSumPropagator(Arrays.asList(reqScorer, optScorer));
 
     final TwoPhaseIterator reqTwoPhase = reqScorer.twoPhaseIterator();
     this.optTwoPhase = optScorer.twoPhaseIterator();
@@ -208,14 +211,17 @@ class ReqOptSumScorer extends Scorer {
 
   @Override
   public float maxScore() {
-    return reqScorer.maxScore() + optScorer.maxScore();
+    return maxScorePropagator.maxScore();
   }
 
   @Override
   public void setMinCompetitiveScore(float minScore) {
+    // Potentially move to a conjunction
     if (optIsRequired == false && minScore > reqMaxScore) {
       optIsRequired = true;
     }
+    // And also propagate to sub clauses.
+    maxScorePropagator.setMinCompetitiveScore(minScore);
   }
 
   @Override

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/33f421e7/lucene/core/src/java/org/apache/lucene/search/WANDScorer.java
----------------------------------------------------------------------
diff --git a/lucene/core/src/java/org/apache/lucene/search/WANDScorer.java b/lucene/core/src/java/org/apache/lucene/search/WANDScorer.java
index f5f647e..314ccf1 100644
--- a/lucene/core/src/java/org/apache/lucene/search/WANDScorer.java
+++ b/lucene/core/src/java/org/apache/lucene/search/WANDScorer.java
@@ -26,8 +26,6 @@ import java.util.Collection;
 import java.util.List;
 import java.util.OptionalInt;
 
-import org.apache.lucene.util.MathUtil;
-
 /**
  * This implements the WAND (Weak AND) algorithm for dynamic pruning
  * described in "Efficient Query Evaluation using a Two-Level Retrieval
@@ -122,7 +120,7 @@ final class WANDScorer extends Scorer {
   int tailSize;
 
   final long cost;
-  final float maxScore;
+  final MaxScoreSumPropagator maxScorePropagator;
 
   WANDScorer(Weight weight, Collection<Scorer> scorers) {
     super(weight);
@@ -145,12 +143,10 @@ final class WANDScorer extends Scorer {
     // Use a scaling factor of 0 if all max scores are either 0 or +Infty
     this.scalingFactor = scalingFactor.orElse(0);
     
-    double maxScoreSum = 0;
     for (Scorer scorer : scorers) {
       DisiWrapper w = new DisiWrapper(scorer);
       float maxScore = scorer.maxScore();
       w.maxScore = scaleMaxScore(maxScore, this.scalingFactor);
-      maxScoreSum += maxScore;
       addLead(w);
     }
 
@@ -159,12 +155,7 @@ final class WANDScorer extends Scorer {
       cost += w.cost;
     }
     this.cost = cost;
-    // The error of sums depends on the order in which values are summed up. In
-    // order to avoid this issue, we compute an upper bound of the value that
-    // the sum may take. If the max relative error is b, then it means that two
-    // sums are always within 2*b of each other.
-    double maxScoreRelativeErrorBound = MathUtil.sumRelativeErrorBound(scorers.size());
-    this.maxScore = (float) ((1.0 + 2 * maxScoreRelativeErrorBound) * maxScoreSum);
+    this.maxScorePropagator = new MaxScoreSumPropagator(scorers);
   }
 
   // returns a boolean so that it can be called from assert
@@ -195,10 +186,15 @@ final class WANDScorer extends Scorer {
 
   @Override
   public void setMinCompetitiveScore(float minScore) {
+    // Let this disjunction know about the new min score so that it can skip
+    // over clauses that produce low scores.
     assert minScore >= 0;
     long scaledMinScore = scaleMinScore(minScore, scalingFactor);
     assert scaledMinScore >= minCompetitiveScore;
     minCompetitiveScore = scaledMinScore;
+
+    // And also propagate to sub clauses.
+    maxScorePropagator.setMinCompetitiveScore(minScore);
   }
 
   @Override
@@ -386,7 +382,7 @@ final class WANDScorer extends Scorer {
 
   @Override
   public float maxScore() {
-    return maxScore;
+    return maxScorePropagator.maxScore();
   }
 
   @Override

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/33f421e7/lucene/core/src/test/org/apache/lucene/search/TestMaxScoreSumPropagator.java
----------------------------------------------------------------------
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestMaxScoreSumPropagator.java b/lucene/core/src/test/org/apache/lucene/search/TestMaxScoreSumPropagator.java
new file mode 100644
index 0000000..e52a490
--- /dev/null
+++ b/lucene/core/src/test/org/apache/lucene/search/TestMaxScoreSumPropagator.java
@@ -0,0 +1,198 @@
+/*
+ * 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.
+ */
+package org.apache.lucene.search;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.lucene.util.LuceneTestCase;
+import org.apache.lucene.util.TestUtil;
+
+public class TestMaxScoreSumPropagator extends LuceneTestCase {
+
+  private static class FakeScorer extends Scorer {
+    
+    final float maxScore;
+    float minCompetitiveScore;
+    
+    FakeScorer(float maxScore) {
+      super(null);
+      this.maxScore = maxScore;
+    }
+
+    @Override
+    public int docID() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public float score() throws IOException {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public DocIdSetIterator iterator() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public float maxScore() {
+      return maxScore;
+    }
+
+    @Override
+    public void setMinCompetitiveScore(float minCompetitiveScore) {
+      this.minCompetitiveScore = minCompetitiveScore;
+    }
+  }
+
+  public void test0Clause() {
+    MaxScoreSumPropagator p = new MaxScoreSumPropagator(Collections.emptyList());
+    assertEquals(0f, p.maxScore(), 0f);
+    p.setMinCompetitiveScore(0f); // no exception
+    p.setMinCompetitiveScore(0.5f); // no exception
+  }
+
+  public void test1Clause() {
+    FakeScorer a = new FakeScorer(1);
+
+    MaxScoreSumPropagator p = new MaxScoreSumPropagator(Collections.singletonList(a));
+    assertEquals(1f, p.maxScore(), 0f);
+    p.setMinCompetitiveScore(0f);
+    assertEquals(0f, a.minCompetitiveScore, 0f);
+    p.setMinCompetitiveScore(0.5f);
+    assertEquals(0.5f, a.minCompetitiveScore, 0f);
+    p.setMinCompetitiveScore(1f);
+    assertEquals(1f, a.minCompetitiveScore, 0f);
+  }
+
+  public void test2Clauses() {
+    FakeScorer a = new FakeScorer(1);
+    FakeScorer b = new FakeScorer(2);
+
+    MaxScoreSumPropagator p = new MaxScoreSumPropagator(Arrays.asList(a, b));
+    assertEquals(3f, p.maxScore(), 0f);
+
+    p.setMinCompetitiveScore(1f);
+    assertEquals(0f, a.minCompetitiveScore, 0f);
+    assertEquals(0f, b.minCompetitiveScore, 0f);
+    
+    p.setMinCompetitiveScore(2f);
+    assertEquals(0f, a.minCompetitiveScore, 0f);
+    assertEquals(1f, b.minCompetitiveScore, 0f);
+    
+    p.setMinCompetitiveScore(2.5f);
+    assertEquals(0.5f, a.minCompetitiveScore, 0f);
+    assertEquals(1.5f, b.minCompetitiveScore, 0f);
+    
+    p.setMinCompetitiveScore(3f);
+    assertEquals(1f, a.minCompetitiveScore, 0f);
+    assertEquals(2f, b.minCompetitiveScore, 0f);
+  }
+
+  public void test3Clauses() {
+    FakeScorer a = new FakeScorer(1);
+    FakeScorer b = new FakeScorer(2);
+    FakeScorer c = new FakeScorer(1.5f);
+
+    MaxScoreSumPropagator p = new MaxScoreSumPropagator(Arrays.asList(a, b, c));
+    assertEquals(4.5f, p.maxScore(), 0f);
+
+    p.setMinCompetitiveScore(1f);
+    assertEquals(0f, a.minCompetitiveScore, 0f);
+    assertEquals(0f, b.minCompetitiveScore, 0f);
+    assertEquals(0f, c.minCompetitiveScore, 0f);
+    
+    p.setMinCompetitiveScore(2f);
+    assertEquals(0f, a.minCompetitiveScore, 0f);
+    assertEquals(0f, b.minCompetitiveScore, 0f);
+    assertEquals(0f, c.minCompetitiveScore, 0f);
+    
+    p.setMinCompetitiveScore(3f);
+    assertEquals(0f, a.minCompetitiveScore, 0f);
+    assertEquals(0.5f, b.minCompetitiveScore, 0f);
+    assertEquals(0f, c.minCompetitiveScore, 0f);
+
+    p.setMinCompetitiveScore(4f);
+    assertEquals(0.5f, a.minCompetitiveScore, 0f);
+    assertEquals(1.5f, b.minCompetitiveScore, 0f);
+    assertEquals(1f, c.minCompetitiveScore, 0f);
+  }
+
+  public void test2ClausesRandomScore() {
+    for (int iter = 0; iter < 10; ++iter) {
+      FakeScorer a = new FakeScorer(random().nextFloat());
+      FakeScorer b = new FakeScorer(Math.nextUp(a.maxScore()) + random().nextFloat());
+
+      MaxScoreSumPropagator p = new MaxScoreSumPropagator(Arrays.asList(a, b));
+      assertEquals(a.maxScore() + b.maxScore(), p.maxScore(), 0f);
+      assertMinCompetitiveScore(Arrays.asList(a, b), p, Math.nextUp(a.maxScore()));
+      assertMinCompetitiveScore(Arrays.asList(a, b), p, (a.maxScore() + b.maxScore()) / 2);
+      assertMinCompetitiveScore(Arrays.asList(a, b), p, Math.nextDown(a.maxScore() + b.maxScore()));
+      assertMinCompetitiveScore(Arrays.asList(a, b), p, a.maxScore() + b.maxScore());
+    }
+  }
+
+  public void testNClausesRandomScore() {
+    for (int iter = 0; iter < 100; ++iter) {
+      List<FakeScorer> scorers = new ArrayList<>();
+      int numScorers = TestUtil.nextInt(random(), 3, 4 << random().nextInt(8));
+      double sumOfMaxScore = 0;
+      for (int i = 0; i < numScorers; ++i) {
+        float maxScore = random().nextFloat();
+        scorers.add(new FakeScorer(maxScore));
+        sumOfMaxScore += maxScore;
+      }
+
+      MaxScoreSumPropagator p = new MaxScoreSumPropagator(scorers);
+      assertTrue(p.maxScore() >= (float) sumOfMaxScore);
+      for (int i = 0; i < 10; ++i) {
+        final float minCompetitiveScore = random().nextFloat() * numScorers;
+        assertMinCompetitiveScore(scorers, p, minCompetitiveScore);
+        // reset
+        for (FakeScorer scorer : scorers) {
+          scorer.minCompetitiveScore = 0;
+        }
+      }
+    }
+  }
+
+  private void assertMinCompetitiveScore(Collection<FakeScorer> scorers, MaxScoreSumPropagator p, float minCompetitiveScore) {
+    p.setMinCompetitiveScore(minCompetitiveScore);
+
+    for (FakeScorer scorer : scorers) {
+      if (scorer.minCompetitiveScore == 0f) {
+        // no propagation is performed, still visiting all hits
+        break;
+      }
+      double scoreSum = scorer.minCompetitiveScore;
+      for (FakeScorer scorer2 : scorers) {
+        if (scorer2 != scorer) {
+          scoreSum += scorer2.maxScore();
+        }
+      }
+      assertTrue(
+          "scoreSum=" + scoreSum + ", minCompetitiveScore=" + minCompetitiveScore,
+          (float) scoreSum <= minCompetitiveScore);
+    }
+  }
+
+}