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 2023/09/12 13:23:33 UTC

[lucene] branch main updated: Reduce the overhead of ImpactsDISI. (#12490)

This is an automated email from the ASF dual-hosted git repository.

jpountz pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/lucene.git


The following commit(s) were added to refs/heads/main by this push:
     new 37a42219fc7 Reduce the overhead of ImpactsDISI. (#12490)
37a42219fc7 is described below

commit 37a42219fc7e9b2807e69827b1709fe056b7764e
Author: Adrien Grand <jp...@gmail.com>
AuthorDate: Tue Sep 12 15:23:28 2023 +0200

    Reduce the overhead of ImpactsDISI. (#12490)
    
    `ImpactsDISI` is nice: you give it an `ImpactsEnum`, typically coming from the
    `PostingsFormat` and it will automatically skip hits whose score cannot be
    greater than the minimum competitive score. This is the class that yields 10x
    or more speedups on top-level `TermQuery`s compared to exhaustive evaluation.
    
    However, when nested under a disjunction or a conjunction, `ImpactsDISI`
    typically adds more overhead than it enables skipping. The reason is that on a
    disjunction `a OR b`, the minimum competitive score of `a` is the minimum score
    for the disjunction minus the maximum score of `b`. While this sort of
    propagation of minimum competitive scores down the query tree sometimes helps,
    it does hurt more than it helps on average, because `ImpactsDISI` adds quite
    some overhead and the per-clauses minimum scores are usually so low that they
    don't actually enable skipping hits. I looked into reducing this overhead, but
    a big part of it is the additional virtual call, so the only way to get rid of
    this overhead is to not wrap with an `ImpactsDISI` at all.
    
    This means that scorers need a way to know whether they are producing the
    top-level score, or whether they are producing a partial score that then gets
    combined into the top-level score. Term queries would then only wrap with
    `ImpactsDISI` when they produce the top-level score. Note that this does not
    only include top-level term queries, but also conjunctions that have a single
    scoring clause (`a #b`) or combinations of a term query and one or more
    prohibited clauses (`a -b`).
---
 lucene/CHANGES.txt                                 |   2 +
 .../org/apache/lucene/document/FeatureQuery.java   |  55 +++--
 .../lucene/search/BlockMaxConjunctionScorer.java   |   3 -
 .../org/apache/lucene/search/BlockMaxDISI.java     |  94 --------
 .../lucene/search/Boolean2ScorerSupplier.java      |  43 +++-
 .../apache/lucene/search/ConstantScoreScorer.java  |   4 +
 .../apache/lucene/search/DisjunctionMaxQuery.java  |  69 ++++--
 .../apache/lucene/search/DisjunctionMaxScorer.java |  10 +-
 .../apache/lucene/search/DisjunctionScorer.java    |  17 +-
 .../apache/lucene/search/ExactPhraseMatcher.java   |   9 +-
 .../java/org/apache/lucene/search/ImpactsDISI.java |  46 +---
 .../apache/lucene/search/MaxScoreBulkScorer.java   |   9 +-
 .../org/apache/lucene/search/MaxScoreCache.java    |  21 +-
 .../lucene/search/MaxScoreSumPropagator.java       | 191 ---------------
 .../org/apache/lucene/search/PhraseScorer.java     |   6 +-
 .../org/apache/lucene/search/ReqOptSumScorer.java  |   8 -
 .../org/apache/lucene/search/ScorerSupplier.java   |  10 +
 .../apache/lucene/search/SloppyPhraseMatcher.java  |   2 +-
 .../org/apache/lucene/search/SynonymQuery.java     |  12 +-
 .../java/org/apache/lucene/search/TermQuery.java   |  57 ++++-
 .../java/org/apache/lucene/search/TermScorer.java  |  33 ++-
 .../java/org/apache/lucene/search/WANDScorer.java  |  24 +-
 .../src/java/org/apache/lucene/search/Weight.java  |  10 +-
 .../src/java/org/apache/lucene/util/MathUtil.java  |  25 ++
 .../lucene/search/TestBoolean2ScorerSupplier.java  |  91 ++++++++
 .../lucene/search/TestMaxScoreSumPropagator.java   | 255 ---------------------
 .../apache/lucene/search/TestReqOptSumScorer.java  |  12 +-
 .../org/apache/lucene/search/TestWANDScorer.java   | 110 ++++-----
 .../lucene/search/join/TestBlockJoinScorer.java    |  13 +-
 .../lucene/tests/search/AssertingScorer.java       |  11 +-
 .../lucene/tests/search/AssertingWeight.java       |  15 +-
 .../org/apache/lucene/tests/search/CheckHits.java  |  21 +-
 32 files changed, 508 insertions(+), 780 deletions(-)

diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt
index 92a595b9804..a0bdaf972b1 100644
--- a/lucene/CHANGES.txt
+++ b/lucene/CHANGES.txt
@@ -186,6 +186,8 @@ Optimizations
 
 * GITHUB##12371: Lazy computation of similarity score during initializeFromGraph (Jack Wang)
 
+* GITHUB#12490: Faster computation of top-k hits on boolean queries. (Adrien Grand)
+
 Changes in runtime behavior
 ---------------------
 
diff --git a/lucene/core/src/java/org/apache/lucene/document/FeatureQuery.java b/lucene/core/src/java/org/apache/lucene/document/FeatureQuery.java
index 8b1ea8c4725..8c2f16fc110 100644
--- a/lucene/core/src/java/org/apache/lucene/document/FeatureQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/document/FeatureQuery.java
@@ -25,15 +25,16 @@ import org.apache.lucene.index.PostingsEnum;
 import org.apache.lucene.index.Term;
 import org.apache.lucene.index.Terms;
 import org.apache.lucene.index.TermsEnum;
-import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.Explanation;
-import org.apache.lucene.search.ImpactsDISI;
 import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.LeafSimScorer;
 import org.apache.lucene.search.Query;
 import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.ScoreMode;
 import org.apache.lucene.search.Scorer;
+import org.apache.lucene.search.ScorerSupplier;
 import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.search.TermScorer;
 import org.apache.lucene.search.Weight;
 import org.apache.lucene.search.similarities.Similarity.SimScorer;
 import org.apache.lucene.util.BytesRef;
@@ -118,50 +119,48 @@ final class FeatureQuery extends Query {
       }
 
       @Override
-      public Scorer scorer(LeafReaderContext context) throws IOException {
+      public ScorerSupplier scorerSupplier(LeafReaderContext context) throws IOException {
+        final Weight thisWeight = this;
         Terms terms = Terms.getTerms(context.reader(), fieldName);
         TermsEnum termsEnum = terms.iterator();
         if (termsEnum.seekExact(new BytesRef(featureName)) == false) {
           return null;
         }
+        final int docFreq = termsEnum.docFreq();
 
-        final SimScorer scorer = function.scorer(boost);
-        final ImpactsEnum impacts = termsEnum.impacts(PostingsEnum.FREQS);
-        final ImpactsDISI impactsDisi = new ImpactsDISI(impacts, impacts, scorer);
-
-        return new Scorer(this) {
-
-          @Override
-          public int docID() {
-            return impacts.docID();
-          }
-
-          @Override
-          public float score() throws IOException {
-            return scorer.score(impacts.freq(), 1L);
-          }
+        return new ScorerSupplier() {
 
-          @Override
-          public DocIdSetIterator iterator() {
-            return impactsDisi;
-          }
+          private boolean topLevelScoringClause = false;
 
           @Override
-          public int advanceShallow(int target) throws IOException {
-            return impactsDisi.advanceShallow(target);
+          public Scorer get(long leadCost) throws IOException {
+            final SimScorer scorer = function.scorer(boost);
+            final LeafSimScorer simScorer =
+                new LeafSimScorer(scorer, context.reader(), fieldName, false);
+            final ImpactsEnum impacts = termsEnum.impacts(PostingsEnum.FREQS);
+            return new TermScorer(thisWeight, impacts, simScorer, topLevelScoringClause);
           }
 
           @Override
-          public float getMaxScore(int upTo) throws IOException {
-            return impactsDisi.getMaxScore(upTo);
+          public long cost() {
+            return docFreq;
           }
 
           @Override
-          public void setMinCompetitiveScore(float minScore) {
-            impactsDisi.setMinCompetitiveScore(minScore);
+          public void setTopLevelScoringClause() throws IOException {
+            topLevelScoringClause = true;
           }
         };
       }
+
+      @Override
+      public Scorer scorer(LeafReaderContext context) throws IOException {
+        ScorerSupplier supplier = scorerSupplier(context);
+        if (supplier == null) {
+          return null;
+        }
+        return supplier.get(Long.MAX_VALUE);
+      }
     };
   }
 
diff --git a/lucene/core/src/java/org/apache/lucene/search/BlockMaxConjunctionScorer.java b/lucene/core/src/java/org/apache/lucene/search/BlockMaxConjunctionScorer.java
index 78107267c1f..127d6346edd 100644
--- a/lucene/core/src/java/org/apache/lucene/search/BlockMaxConjunctionScorer.java
+++ b/lucene/core/src/java/org/apache/lucene/search/BlockMaxConjunctionScorer.java
@@ -31,7 +31,6 @@ final class BlockMaxConjunctionScorer extends Scorer {
   final Scorer[] scorers;
   final DocIdSetIterator[] approximations;
   final TwoPhaseIterator[] twoPhases;
-  final MaxScoreSumPropagator maxScorePropagator;
   float minScore;
 
   /** Create a new {@link BlockMaxConjunctionScorer} from scoring clauses. */
@@ -40,7 +39,6 @@ final class BlockMaxConjunctionScorer extends Scorer {
     this.scorers = scorersList.toArray(new Scorer[scorersList.size()]);
     // Sort scorer by cost
     Arrays.sort(this.scorers, Comparator.comparingLong(s -> s.iterator().cost()));
-    this.maxScorePropagator = new MaxScoreSumPropagator(Arrays.asList(scorers));
 
     this.approximations = new DocIdSetIterator[scorers.length];
     List<TwoPhaseIterator> twoPhaseList = new ArrayList<>();
@@ -237,7 +235,6 @@ final class BlockMaxConjunctionScorer extends Scorer {
   @Override
   public void setMinCompetitiveScore(float score) throws IOException {
     minScore = score;
-    maxScorePropagator.setMinCompetitiveScore(score);
   }
 
   @Override
diff --git a/lucene/core/src/java/org/apache/lucene/search/BlockMaxDISI.java b/lucene/core/src/java/org/apache/lucene/search/BlockMaxDISI.java
deleted file mode 100644
index 2f59d46acd8..00000000000
--- a/lucene/core/src/java/org/apache/lucene/search/BlockMaxDISI.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * 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;
-
-/**
- * {@link DocIdSetIterator} that skips non-competitive docs by checking the max score of the
- * provided {@link Scorer} for the current block. Call {@link #setMinCompetitiveScore(float)} in
- * order to give this iterator the ability to skip low-scoring documents.
- *
- * @lucene.internal
- */
-public class BlockMaxDISI extends DocIdSetIterator {
-  protected final Scorer scorer;
-  private final DocIdSetIterator in;
-  private float minScore;
-  private float maxScore;
-  private int upTo = -1;
-
-  public BlockMaxDISI(DocIdSetIterator iterator, Scorer scorer) {
-    this.in = iterator;
-    this.scorer = scorer;
-  }
-
-  @Override
-  public int docID() {
-    return in.docID();
-  }
-
-  @Override
-  public int nextDoc() throws IOException {
-    return advance(docID() + 1);
-  }
-
-  @Override
-  public int advance(int target) throws IOException {
-    int doc = advanceImpacts(target);
-    return in.advance(doc);
-  }
-
-  @Override
-  public long cost() {
-    return in.cost();
-  }
-
-  public void setMinCompetitiveScore(float minScore) {
-    this.minScore = minScore;
-  }
-
-  private void moveToNextBlock(int target) throws IOException {
-    upTo = scorer.advanceShallow(target);
-    maxScore = scorer.getMaxScore(upTo);
-  }
-
-  private int advanceImpacts(int target) throws IOException {
-    if (minScore == -1 || target == NO_MORE_DOCS) {
-      return target;
-    }
-
-    if (target > upTo) {
-      moveToNextBlock(target);
-    }
-
-    while (true) {
-      if (maxScore >= minScore) {
-        return target;
-      }
-
-      if (upTo == NO_MORE_DOCS) {
-        return NO_MORE_DOCS;
-      }
-
-      target = upTo + 1;
-
-      moveToNextBlock(target);
-    }
-  }
-}
diff --git a/lucene/core/src/java/org/apache/lucene/search/Boolean2ScorerSupplier.java b/lucene/core/src/java/org/apache/lucene/search/Boolean2ScorerSupplier.java
index bdf085d4669..56b676b88cb 100644
--- a/lucene/core/src/java/org/apache/lucene/search/Boolean2ScorerSupplier.java
+++ b/lucene/core/src/java/org/apache/lucene/search/Boolean2ScorerSupplier.java
@@ -34,6 +34,7 @@ final class Boolean2ScorerSupplier extends ScorerSupplier {
   private final ScoreMode scoreMode;
   private final int minShouldMatch;
   private long cost = -1;
+  private boolean topLevelScoringClause;
 
   Boolean2ScorerSupplier(
       Weight weight,
@@ -83,6 +84,20 @@ final class Boolean2ScorerSupplier extends ScorerSupplier {
     }
   }
 
+  @Override
+  public void setTopLevelScoringClause() throws IOException {
+    topLevelScoringClause = true;
+    if (subs.get(Occur.SHOULD).size() + subs.get(Occur.MUST).size() == 1) {
+      // If there is a single scoring clause, propagate the call.
+      for (ScorerSupplier ss : subs.get(Occur.SHOULD)) {
+        ss.setTopLevelScoringClause();
+      }
+      for (ScorerSupplier ss : subs.get(Occur.MUST)) {
+        ss.setTopLevelScoringClause();
+      }
+    }
+  }
+
   @Override
   public long cost() {
     if (cost == -1) {
@@ -113,7 +128,7 @@ final class Boolean2ScorerSupplier extends ScorerSupplier {
     // pure conjunction
     if (subs.get(Occur.SHOULD).isEmpty()) {
       return excl(
-          req(subs.get(Occur.FILTER), subs.get(Occur.MUST), leadCost),
+          req(subs.get(Occur.FILTER), subs.get(Occur.MUST), leadCost, topLevelScoringClause),
           subs.get(Occur.MUST_NOT),
           leadCost);
     }
@@ -121,7 +136,7 @@ final class Boolean2ScorerSupplier extends ScorerSupplier {
     // pure disjunction
     if (subs.get(Occur.FILTER).isEmpty() && subs.get(Occur.MUST).isEmpty()) {
       return excl(
-          opt(subs.get(Occur.SHOULD), minShouldMatch, scoreMode, leadCost),
+          opt(subs.get(Occur.SHOULD), minShouldMatch, scoreMode, leadCost, topLevelScoringClause),
           subs.get(Occur.MUST_NOT),
           leadCost);
     }
@@ -134,19 +149,19 @@ final class Boolean2ScorerSupplier extends ScorerSupplier {
     if (minShouldMatch > 0) {
       Scorer req =
           excl(
-              req(subs.get(Occur.FILTER), subs.get(Occur.MUST), leadCost),
+              req(subs.get(Occur.FILTER), subs.get(Occur.MUST), leadCost, false),
               subs.get(Occur.MUST_NOT),
               leadCost);
-      Scorer opt = opt(subs.get(Occur.SHOULD), minShouldMatch, scoreMode, leadCost);
+      Scorer opt = opt(subs.get(Occur.SHOULD), minShouldMatch, scoreMode, leadCost, false);
       return new ConjunctionScorer(weight, Arrays.asList(req, opt), Arrays.asList(req, opt));
     } else {
       assert scoreMode.needsScores();
       return new ReqOptSumScorer(
           excl(
-              req(subs.get(Occur.FILTER), subs.get(Occur.MUST), leadCost),
+              req(subs.get(Occur.FILTER), subs.get(Occur.MUST), leadCost, false),
               subs.get(Occur.MUST_NOT),
               leadCost),
-          opt(subs.get(Occur.SHOULD), minShouldMatch, scoreMode, leadCost),
+          opt(subs.get(Occur.SHOULD), minShouldMatch, scoreMode, leadCost, false),
           scoreMode);
     }
   }
@@ -158,7 +173,8 @@ final class Boolean2ScorerSupplier extends ScorerSupplier {
   private Scorer req(
       Collection<ScorerSupplier> requiredNoScoring,
       Collection<ScorerSupplier> requiredScoring,
-      long leadCost)
+      long leadCost,
+      boolean topLevelScoringClause)
       throws IOException {
     if (requiredNoScoring.size() + requiredScoring.size() == 1) {
       Scorer req =
@@ -199,7 +215,7 @@ final class Boolean2ScorerSupplier extends ScorerSupplier {
         Scorer scorer = s.get(leadCost);
         scoringScorers.add(scorer);
       }
-      if (scoreMode == ScoreMode.TOP_SCORES && scoringScorers.size() > 1) {
+      if (scoreMode == ScoreMode.TOP_SCORES && scoringScorers.size() > 1 && topLevelScoringClause) {
         Scorer blockMaxScorer = new BlockMaxConjunctionScorer(weight, scoringScorers);
         if (requiredScorers.isEmpty()) {
           return blockMaxScorer;
@@ -216,12 +232,17 @@ final class Boolean2ScorerSupplier extends ScorerSupplier {
     if (prohibited.isEmpty()) {
       return main;
     } else {
-      return new ReqExclScorer(main, opt(prohibited, 1, ScoreMode.COMPLETE_NO_SCORES, leadCost));
+      return new ReqExclScorer(
+          main, opt(prohibited, 1, ScoreMode.COMPLETE_NO_SCORES, leadCost, false));
     }
   }
 
   private Scorer opt(
-      Collection<ScorerSupplier> optional, int minShouldMatch, ScoreMode scoreMode, long leadCost)
+      Collection<ScorerSupplier> optional,
+      int minShouldMatch,
+      ScoreMode scoreMode,
+      long leadCost,
+      boolean topLevelScoringClause)
       throws IOException {
     if (optional.size() == 1) {
       return optional.iterator().next().get(leadCost);
@@ -238,7 +259,7 @@ final class Boolean2ScorerSupplier extends ScorerSupplier {
       //
       // However, as WANDScorer uses more complex algorithm and data structure, we would like to
       // still use DisjunctionSumScorer to handle exhaustive pure disjunctions, which may be faster
-      if (scoreMode == ScoreMode.TOP_SCORES || minShouldMatch > 1) {
+      if ((scoreMode == ScoreMode.TOP_SCORES && topLevelScoringClause) || minShouldMatch > 1) {
         return new WANDScorer(weight, optionalScorers, minShouldMatch, scoreMode);
       } else {
         return new DisjunctionSumScorer(weight, optionalScorers, scoreMode);
diff --git a/lucene/core/src/java/org/apache/lucene/search/ConstantScoreScorer.java b/lucene/core/src/java/org/apache/lucene/search/ConstantScoreScorer.java
index 759300ecb80..d7a3a812a51 100644
--- a/lucene/core/src/java/org/apache/lucene/search/ConstantScoreScorer.java
+++ b/lucene/core/src/java/org/apache/lucene/search/ConstantScoreScorer.java
@@ -74,6 +74,8 @@ public final class ConstantScoreScorer extends Scorer {
     super(weight);
     this.score = score;
     this.scoreMode = scoreMode;
+    // TODO: Only wrap when it is the top-level scoring clause? See
+    // ScorerSupplier#setTopLevelScoringClause
     this.approximation =
         scoreMode == ScoreMode.TOP_SCORES ? new DocIdSetIteratorWrapper(disi) : disi;
     this.twoPhaseIterator = null;
@@ -95,6 +97,8 @@ public final class ConstantScoreScorer extends Scorer {
     this.score = score;
     this.scoreMode = scoreMode;
     if (scoreMode == ScoreMode.TOP_SCORES) {
+      // TODO: Only wrap when it is the top-level scoring clause? See
+      // ScorerSupplier#setTopLevelScoringClause
       this.approximation = new DocIdSetIteratorWrapper(twoPhaseIterator.approximation());
       this.twoPhaseIterator =
           new TwoPhaseIterator(this.approximation) {
diff --git a/lucene/core/src/java/org/apache/lucene/search/DisjunctionMaxQuery.java b/lucene/core/src/java/org/apache/lucene/search/DisjunctionMaxQuery.java
index 1ab4f3b5081..47e8b8f25e9 100644
--- a/lucene/core/src/java/org/apache/lucene/search/DisjunctionMaxQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/DisjunctionMaxQuery.java
@@ -127,26 +127,69 @@ public final class DisjunctionMaxQuery extends Query implements Iterable<Query>
       return MatchesUtils.fromSubMatches(mis);
     }
 
-    /** Create the scorer used to score our associated DisjunctionMaxQuery */
     @Override
-    public Scorer scorer(LeafReaderContext context) throws IOException {
-      List<Scorer> scorers = new ArrayList<>();
+    public ScorerSupplier scorerSupplier(LeafReaderContext context) throws IOException {
+      List<ScorerSupplier> scorerSuppliers = new ArrayList<>();
       for (Weight w : weights) {
-        // we will advance() subscorers
-        Scorer subScorer = w.scorer(context);
-        if (subScorer != null) {
-          scorers.add(subScorer);
+        ScorerSupplier ss = w.scorerSupplier(context);
+        if (ss != null) {
+          scorerSuppliers.add(ss);
         }
       }
-      if (scorers.isEmpty()) {
-        // no sub-scorers had any documents
+
+      if (scorerSuppliers.isEmpty()) {
         return null;
-      } else if (scorers.size() == 1) {
-        // only one sub-scorer in this segment
-        return scorers.get(0);
+      } else if (scorerSuppliers.size() == 1) {
+        return scorerSuppliers.get(0);
       } else {
-        return new DisjunctionMaxScorer(this, tieBreakerMultiplier, scorers, scoreMode);
+        final Weight thisWeight = this;
+        return new ScorerSupplier() {
+
+          private long cost = -1;
+
+          @Override
+          public Scorer get(long leadCost) throws IOException {
+            List<Scorer> scorers = new ArrayList<>();
+            for (ScorerSupplier ss : scorerSuppliers) {
+              scorers.add(ss.get(leadCost));
+            }
+            return new DisjunctionMaxScorer(thisWeight, tieBreakerMultiplier, scorers, scoreMode);
+          }
+
+          @Override
+          public long cost() {
+            if (cost == -1) {
+              long cost = 0;
+              for (ScorerSupplier ss : scorerSuppliers) {
+                cost += ss.cost();
+              }
+              this.cost = cost;
+            }
+            return cost;
+          }
+
+          @Override
+          public void setTopLevelScoringClause() throws IOException {
+            if (tieBreakerMultiplier == 0) {
+              for (ScorerSupplier ss : scorerSuppliers) {
+                // sub scorers need to be able to skip too as calls to setMinCompetitiveScore get
+                // propagated
+                ss.setTopLevelScoringClause();
+              }
+            }
+          }
+        };
+      }
+    }
+
+    @Override
+    public Scorer scorer(LeafReaderContext context) throws IOException {
+      ScorerSupplier supplier = scorerSupplier(context);
+      if (supplier == null) {
+        return null;
       }
+      supplier.setTopLevelScoringClause();
+      return supplier.get(Long.MAX_VALUE);
     }
 
     @Override
diff --git a/lucene/core/src/java/org/apache/lucene/search/DisjunctionMaxScorer.java b/lucene/core/src/java/org/apache/lucene/search/DisjunctionMaxScorer.java
index affe77193b5..a43077a725e 100644
--- a/lucene/core/src/java/org/apache/lucene/search/DisjunctionMaxScorer.java
+++ b/lucene/core/src/java/org/apache/lucene/search/DisjunctionMaxScorer.java
@@ -75,7 +75,10 @@ final class DisjunctionMaxScorer extends DisjunctionScorer {
 
   @Override
   public int advanceShallow(int target) throws IOException {
-    return disjunctionBlockPropagator.advanceShallow(target);
+    if (disjunctionBlockPropagator != null) {
+      return disjunctionBlockPropagator.advanceShallow(target);
+    }
+    return super.advanceShallow(target);
   }
 
   @Override
@@ -108,8 +111,9 @@ final class DisjunctionMaxScorer extends DisjunctionScorer {
 
   @Override
   public void setMinCompetitiveScore(float minScore) throws IOException {
-    getBlockMaxApprox().setMinCompetitiveScore(minScore);
-    disjunctionBlockPropagator.setMinCompetitiveScore(minScore);
+    if (disjunctionBlockPropagator != null) {
+      disjunctionBlockPropagator.setMinCompetitiveScore(minScore);
+    }
     if (tieBreakerMultiplier == 0) {
       // TODO: we could even remove some scorers from the priority queue?
       for (Scorer scorer : subScorers) {
diff --git a/lucene/core/src/java/org/apache/lucene/search/DisjunctionScorer.java b/lucene/core/src/java/org/apache/lucene/search/DisjunctionScorer.java
index e7c75420b86..74fb42f01b3 100644
--- a/lucene/core/src/java/org/apache/lucene/search/DisjunctionScorer.java
+++ b/lucene/core/src/java/org/apache/lucene/search/DisjunctionScorer.java
@@ -29,7 +29,6 @@ abstract class DisjunctionScorer extends Scorer {
 
   private final DisiPriorityQueue subScorers;
   private final DocIdSetIterator approximation;
-  private final BlockMaxDISI blockMaxApprox;
   private final TwoPhase twoPhase;
 
   protected DisjunctionScorer(Weight weight, List<Scorer> subScorers, ScoreMode scoreMode)
@@ -44,17 +43,7 @@ abstract class DisjunctionScorer extends Scorer {
       this.subScorers.add(w);
     }
     this.needsScores = scoreMode != ScoreMode.COMPLETE_NO_SCORES;
-    if (scoreMode == ScoreMode.TOP_SCORES) {
-      for (Scorer scorer : subScorers) {
-        scorer.advanceShallow(0);
-      }
-      this.blockMaxApprox =
-          new BlockMaxDISI(new DisjunctionDISIApproximation(this.subScorers), this);
-      this.approximation = blockMaxApprox;
-    } else {
-      this.approximation = new DisjunctionDISIApproximation(this.subScorers);
-      this.blockMaxApprox = null;
-    }
+    this.approximation = new DisjunctionDISIApproximation(this.subScorers);
 
     boolean hasApproximation = false;
     float sumMatchCost = 0;
@@ -176,10 +165,6 @@ abstract class DisjunctionScorer extends Scorer {
     return subScorers.top().doc;
   }
 
-  BlockMaxDISI getBlockMaxApprox() {
-    return blockMaxApprox;
-  }
-
   DisiWrapper getSubMatches() throws IOException {
     if (twoPhase == null) {
       return subScorers.topList();
diff --git a/lucene/core/src/java/org/apache/lucene/search/ExactPhraseMatcher.java b/lucene/core/src/java/org/apache/lucene/search/ExactPhraseMatcher.java
index b35f2d868dc..f6bcadb4b8f 100644
--- a/lucene/core/src/java/org/apache/lucene/search/ExactPhraseMatcher.java
+++ b/lucene/core/src/java/org/apache/lucene/search/ExactPhraseMatcher.java
@@ -67,12 +67,15 @@ public final class ExactPhraseMatcher extends PhraseMatcher {
     final ImpactsSource impactsSource =
         mergeImpacts(Arrays.stream(postings).map(p -> p.impacts).toArray(ImpactsEnum[]::new));
 
+    this.impactsApproximation =
+        new ImpactsDISI(approximation, new MaxScoreCache(impactsSource, scorer));
     if (scoreMode == ScoreMode.TOP_SCORES) {
-      this.approximation =
-          this.impactsApproximation = new ImpactsDISI(approximation, impactsSource, scorer);
+      // TODO: only do this when this is the top-level scoring clause
+      // (ScorerSupplier#setTopLevelScoringClause) to save the overhead of wrapping with ImpactsDISI
+      // when it would not help
+      this.approximation = impactsApproximation;
     } else {
       this.approximation = approximation;
-      this.impactsApproximation = new ImpactsDISI(approximation, impactsSource, scorer);
     }
 
     List<PostingsAndPosition> postingsAndPositions = new ArrayList<>();
diff --git a/lucene/core/src/java/org/apache/lucene/search/ImpactsDISI.java b/lucene/core/src/java/org/apache/lucene/search/ImpactsDISI.java
index 20efacc25c9..5ab19747246 100644
--- a/lucene/core/src/java/org/apache/lucene/search/ImpactsDISI.java
+++ b/lucene/core/src/java/org/apache/lucene/search/ImpactsDISI.java
@@ -17,10 +17,6 @@
 package org.apache.lucene.search;
 
 import java.io.IOException;
-import org.apache.lucene.index.Impacts;
-import org.apache.lucene.index.ImpactsEnum;
-import org.apache.lucene.index.ImpactsSource;
-import org.apache.lucene.search.similarities.Similarity.SimScorer;
 
 /**
  * {@link DocIdSetIterator} that skips non-competitive docs thanks to the indexed impacts. Call
@@ -32,7 +28,6 @@ import org.apache.lucene.search.similarities.Similarity.SimScorer;
 public final class ImpactsDISI extends DocIdSetIterator {
 
   private final DocIdSetIterator in;
-  private final ImpactsSource impactsSource;
   private final MaxScoreCache maxScoreCache;
   private float minCompetitiveScore = 0;
   private int upTo = DocIdSetIterator.NO_MORE_DOCS;
@@ -41,14 +36,17 @@ public final class ImpactsDISI extends DocIdSetIterator {
   /**
    * Sole constructor.
    *
-   * @param in wrapped iterator
-   * @param impactsSource source of impacts
-   * @param scorer scorer
+   * @param in the iterator, typically an ImpactsEnum
+   * @param maxScoreCache the cache of maximum scores, typically computed from the same ImpactsEnum
    */
-  public ImpactsDISI(DocIdSetIterator in, ImpactsSource impactsSource, SimScorer scorer) {
+  public ImpactsDISI(DocIdSetIterator in, MaxScoreCache maxScoreCache) {
     this.in = in;
-    this.impactsSource = impactsSource;
-    this.maxScoreCache = new MaxScoreCache(impactsSource, scorer);
+    this.maxScoreCache = maxScoreCache;
+  }
+
+  /** Get the {@link MaxScoreCache}. */
+  public MaxScoreCache getMaxScoreCache() {
+    return maxScoreCache;
   }
 
   /**
@@ -67,28 +65,6 @@ public final class ImpactsDISI extends DocIdSetIterator {
     }
   }
 
-  /**
-   * Implement the contract of {@link Scorer#advanceShallow(int)} based on the wrapped {@link
-   * ImpactsEnum}.
-   *
-   * @see Scorer#advanceShallow(int)
-   */
-  public int advanceShallow(int target) throws IOException {
-    impactsSource.advanceShallow(target);
-    Impacts impacts = impactsSource.getImpacts();
-    return impacts.getDocIdUpTo(0);
-  }
-
-  /**
-   * Implement the contract of {@link Scorer#getMaxScore(int)} based on the wrapped {@link
-   * ImpactsEnum} and {@link Scorer}.
-   *
-   * @see Scorer#getMaxScore(int)
-   */
-  public float getMaxScore(int upTo) throws IOException {
-    return maxScoreCache.getMaxScore(upTo);
-  }
-
   private int advanceTarget(int target) throws IOException {
     if (target <= upTo) {
       // we are still in the current block, which is considered competitive
@@ -96,7 +72,7 @@ public final class ImpactsDISI extends DocIdSetIterator {
       return target;
     }
 
-    upTo = advanceShallow(target);
+    upTo = maxScoreCache.advanceShallow(target);
     maxScore = maxScoreCache.getMaxScoreForLevelZero();
 
     while (true) {
@@ -118,7 +94,7 @@ public final class ImpactsDISI extends DocIdSetIterator {
       } else {
         target = skipUpTo + 1;
       }
-      upTo = advanceShallow(target);
+      upTo = maxScoreCache.advanceShallow(target);
       maxScore = maxScoreCache.getMaxScoreForLevelZero();
     }
   }
diff --git a/lucene/core/src/java/org/apache/lucene/search/MaxScoreBulkScorer.java b/lucene/core/src/java/org/apache/lucene/search/MaxScoreBulkScorer.java
index 0ef5a1d07bd..4cc14cdd0b3 100644
--- a/lucene/core/src/java/org/apache/lucene/search/MaxScoreBulkScorer.java
+++ b/lucene/core/src/java/org/apache/lucene/search/MaxScoreBulkScorer.java
@@ -22,6 +22,7 @@ import java.util.Comparator;
 import java.util.List;
 import org.apache.lucene.util.Bits;
 import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.MathUtil;
 
 final class MaxScoreBulkScorer extends BulkScorer {
 
@@ -37,7 +38,6 @@ final class MaxScoreBulkScorer extends BulkScorer {
   // Index of the first essential scorer, ie. essentialQueue contains all scorers from
   // allScorers[firstEssentialScorer:]. All scorers below this index are non-essential.
   private int firstEssentialScorer;
-  private final MaxScoreSumPropagator maxScorePropagator;
   private final long cost;
   private float minCompetitiveScore;
   private boolean minCompetitiveScoreUpdated;
@@ -59,7 +59,6 @@ final class MaxScoreBulkScorer extends BulkScorer {
       allScorers[i++] = w;
     }
     this.cost = cost;
-    maxScorePropagator = new MaxScoreSumPropagator(scorers);
     essentialQueue = new DisiPriorityQueue(allScorers.length);
     maxScoreSums = new double[allScorers.length];
   }
@@ -227,7 +226,8 @@ final class MaxScoreBulkScorer extends BulkScorer {
       throws IOException {
     double score = essentialScore;
     for (int i = firstEssentialScorer - 1; i >= 0; --i) {
-      float maxPossibleScore = maxScorePropagator.scoreSumUpperBound(score + maxScoreSums[i]);
+      float maxPossibleScore =
+          (float) MathUtil.sumUpperBound(score + maxScoreSums[i], allScorers.length);
       if (maxPossibleScore < minCompetitiveScore) {
         // Hit is not competitive.
         return;
@@ -268,7 +268,7 @@ final class MaxScoreBulkScorer extends BulkScorer {
       final DisiWrapper w = scratch[i];
       double newMaxScoreSum = maxScoreSum + w.maxWindowScore;
       float maxScoreSumFloat =
-          MaxScoreSumPropagator.scoreSumUpperBound(newMaxScoreSum, firstEssentialScorer + 1);
+          (float) MathUtil.sumUpperBound(newMaxScoreSum, firstEssentialScorer + 1);
       if (maxScoreSumFloat < minCompetitiveScore) {
         maxScoreSum = newMaxScoreSum;
         allScorers[firstEssentialScorer] = w;
@@ -324,7 +324,6 @@ final class MaxScoreBulkScorer extends BulkScorer {
     @Override
     public void setMinCompetitiveScore(float minScore) throws IOException {
       MaxScoreBulkScorer.this.minCompetitiveScore = minScore;
-      maxScorePropagator.setMinCompetitiveScore(minScore);
       minCompetitiveScoreUpdated = true;
     }
   }
diff --git a/lucene/core/src/java/org/apache/lucene/search/MaxScoreCache.java b/lucene/core/src/java/org/apache/lucene/search/MaxScoreCache.java
index 70a10ee0561..2d38370e86a 100644
--- a/lucene/core/src/java/org/apache/lucene/search/MaxScoreCache.java
+++ b/lucene/core/src/java/org/apache/lucene/search/MaxScoreCache.java
@@ -31,7 +31,7 @@ import org.apache.lucene.util.ArrayUtil;
  *
  * @lucene.internal
  */
-final class MaxScoreCache {
+public final class MaxScoreCache {
 
   private final ImpactsSource impactsSource;
   private final SimScorer scorer;
@@ -48,6 +48,18 @@ final class MaxScoreCache {
     maxScoreCacheUpTo = new int[0];
   }
 
+  /**
+   * Implement the contract of {@link Scorer#advanceShallow(int)} based on the wrapped {@link
+   * ImpactsSource}.
+   *
+   * @see Scorer#advanceShallow(int)
+   */
+  public int advanceShallow(int target) throws IOException {
+    impactsSource.advanceShallow(target);
+    Impacts impacts = impactsSource.getImpacts();
+    return impacts.getDocIdUpTo(0);
+  }
+
   private void ensureCacheSize(int size) {
     if (maxScoreCache.length < size) {
       int oldLength = maxScoreCache.length;
@@ -65,7 +77,12 @@ final class MaxScoreCache {
     return maxScore;
   }
 
-  float getMaxScore(int upTo) throws IOException {
+  /**
+   * Return the maximum score up to upTo included.
+   *
+   * @see Scorer#getMaxScore(int)
+   */
+  public float getMaxScore(int upTo) throws IOException {
     final int level = getLevel(upTo);
     if (level == -1) {
       return globalMaxScore;
diff --git a/lucene/core/src/java/org/apache/lucene/search/MaxScoreSumPropagator.java b/lucene/core/src/java/org/apache/lucene/search/MaxScoreSumPropagator.java
deleted file mode 100644
index 676dee04e4d..00000000000
--- a/lucene/core/src/java/org/apache/lucene/search/MaxScoreSumPropagator.java
+++ /dev/null
@@ -1,191 +0,0 @@
-/*
- * 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 static org.apache.lucene.search.DocIdSetIterator.NO_MORE_DOCS;
-
-import java.io.IOException;
-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 Scorer[] scorers;
-  private final double[] sumOfOtherMaxScores;
-
-  MaxScoreSumPropagator(Collection<? extends Scorer> scorerList) throws IOException {
-    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) {
-      scorers[i].advanceShallow(0);
-      maxScores[i] = scorers[i].getMaxScore(NO_MORE_DOCS);
-    }
-    // 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);
-  }
-
-  void advanceShallow(int target) throws IOException {
-    for (Scorer s : scorers) {
-      if (s.docID() < target) {
-        s.advanceShallow(target);
-      }
-    }
-  }
-
-  float getMaxScore(int upTo) throws IOException {
-    double maxScore = 0;
-    for (Scorer s : scorers) {
-      if (s.docID() <= upTo) {
-        maxScore += s.getMaxScore(upTo);
-      }
-    }
-    return scoreSumUpperBound(maxScore);
-  }
-
-  void setMinCompetitiveScore(float minScore) throws IOException {
-    if (minScore == 0) {
-      return;
-    }
-    // A double that is less than 'minScore' might still be converted to 'minScore'
-    // when casted to a float, so we go to the previous float to avoid this issue
-    float minScoreDown = Math.nextDown(minScore);
-    for (int i = 0; i < numClauses; ++i) {
-      double sumOfOtherMaxScores = this.sumOfOtherMaxScores[i];
-      float minCompetitiveScore = getMinCompetitiveScore(minScoreDown, 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.
-   *
-   * <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;
-
-    // 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);
-    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
-      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);
-  }
-
-  float scoreSumUpperBound(double sum) {
-    return scoreSumUpperBound(sum, numClauses);
-  }
-
-  static float scoreSumUpperBound(double sum, int numClauses) {
-    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);
-  }
-}
diff --git a/lucene/core/src/java/org/apache/lucene/search/PhraseScorer.java b/lucene/core/src/java/org/apache/lucene/search/PhraseScorer.java
index f1a4b0bbed0..4925de479f8 100644
--- a/lucene/core/src/java/org/apache/lucene/search/PhraseScorer.java
+++ b/lucene/core/src/java/org/apache/lucene/search/PhraseScorer.java
@@ -23,6 +23,7 @@ class PhraseScorer extends Scorer {
 
   final DocIdSetIterator approximation;
   final ImpactsDISI impactsApproximation;
+  final MaxScoreCache maxScoreCache;
   final PhraseMatcher matcher;
   final ScoreMode scoreMode;
   private final LeafSimScorer simScorer;
@@ -39,6 +40,7 @@ class PhraseScorer extends Scorer {
     this.matchCost = matcher.getMatchCost();
     this.approximation = matcher.approximation();
     this.impactsApproximation = matcher.impactsApproximation();
+    this.maxScoreCache = impactsApproximation.getMaxScoreCache();
   }
 
   @Override
@@ -94,12 +96,12 @@ class PhraseScorer extends Scorer {
 
   @Override
   public int advanceShallow(int target) throws IOException {
-    return impactsApproximation.advanceShallow(target);
+    return maxScoreCache.advanceShallow(target);
   }
 
   @Override
   public float getMaxScore(int upTo) throws IOException {
-    return impactsApproximation.getMaxScore(upTo);
+    return maxScoreCache.getMaxScore(upTo);
   }
 
   @Override
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 31e0fa31112..f4730288296 100644
--- a/lucene/core/src/java/org/apache/lucene/search/ReqOptSumScorer.java
+++ b/lucene/core/src/java/org/apache/lucene/search/ReqOptSumScorer.java
@@ -20,7 +20,6 @@ import static org.apache.lucene.search.DocIdSetIterator.NO_MORE_DOCS;
 
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 
 /**
@@ -36,7 +35,6 @@ class ReqOptSumScorer extends Scorer {
   private final DocIdSetIterator approximation;
   private final TwoPhaseIterator twoPhase;
 
-  private final MaxScoreSumPropagator maxScorePropagator;
   private float minScore = 0;
   private float reqMaxScore;
   private boolean optIsRequired;
@@ -53,11 +51,6 @@ class ReqOptSumScorer extends Scorer {
     super(reqScorer.weight);
     assert reqScorer != null;
     assert optScorer != null;
-    if (scoreMode == ScoreMode.TOP_SCORES) {
-      this.maxScorePropagator = new MaxScoreSumPropagator(Arrays.asList(reqScorer, optScorer));
-    } else {
-      this.maxScorePropagator = null;
-    }
     this.reqScorer = reqScorer;
     this.optScorer = optScorer;
 
@@ -303,7 +296,6 @@ class ReqOptSumScorer extends Scorer {
     if (reqMaxScore < minScore) {
       optIsRequired = true;
     }
-    maxScorePropagator.setMinCompetitiveScore(minScore);
   }
 
   @Override
diff --git a/lucene/core/src/java/org/apache/lucene/search/ScorerSupplier.java b/lucene/core/src/java/org/apache/lucene/search/ScorerSupplier.java
index e6af2d6b13c..8053ac94540 100644
--- a/lucene/core/src/java/org/apache/lucene/search/ScorerSupplier.java
+++ b/lucene/core/src/java/org/apache/lucene/search/ScorerSupplier.java
@@ -42,4 +42,14 @@ public abstract class ScorerSupplier {
    * @see DocIdSetIterator#cost
    */
   public abstract long cost();
+
+  /**
+   * Inform this {@link ScorerSupplier} that its returned scorers produce scores that get passed to
+   * the collector, as opposed to partial scores that then need to get combined (e.g. summed up).
+   * Note that this method also gets called if scores are not requested, e.g. because the score mode
+   * is {@link ScoreMode#COMPLETE_NO_SCORES}, so implementations should look at both the score mode
+   * and this boolean to know whether to prepare for reacting to {@link
+   * Scorer#setMinCompetitiveScore(float)} calls.
+   */
+  public void setTopLevelScoringClause() throws IOException {}
 }
diff --git a/lucene/core/src/java/org/apache/lucene/search/SloppyPhraseMatcher.java b/lucene/core/src/java/org/apache/lucene/search/SloppyPhraseMatcher.java
index 259ea8f7e57..ca70a52fbb7 100644
--- a/lucene/core/src/java/org/apache/lucene/search/SloppyPhraseMatcher.java
+++ b/lucene/core/src/java/org/apache/lucene/search/SloppyPhraseMatcher.java
@@ -133,7 +133,7 @@ public final class SloppyPhraseMatcher extends PhraseMatcher {
           @Override
           public void advanceShallow(int target) throws IOException {}
         };
-    impactsApproximation = new ImpactsDISI(approximation, impactsSource, scorer);
+    impactsApproximation = new ImpactsDISI(approximation, new MaxScoreCache(impactsSource, scorer));
   }
 
   @Override
diff --git a/lucene/core/src/java/org/apache/lucene/search/SynonymQuery.java b/lucene/core/src/java/org/apache/lucene/search/SynonymQuery.java
index 7e314870f9c..2ab11ed495a 100644
--- a/lucene/core/src/java/org/apache/lucene/search/SynonymQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/SynonymQuery.java
@@ -333,9 +333,13 @@ public final class SynonymQuery extends Query {
         boosts[i] = termBoosts.get(i);
       }
       ImpactsSource impactsSource = mergeImpacts(impacts.toArray(new ImpactsEnum[0]), boosts);
-      ImpactsDISI impactsDisi = new ImpactsDISI(iterator, impactsSource, simScorer.getSimScorer());
+      MaxScoreCache maxScoreCache = new MaxScoreCache(impactsSource, simScorer.getSimScorer());
+      ImpactsDISI impactsDisi = new ImpactsDISI(iterator, maxScoreCache);
 
       if (scoreMode == ScoreMode.TOP_SCORES) {
+        // TODO: only do this when this is the top-level scoring clause
+        // (ScorerSupplier#setTopLevelScoringClause) to save the overhead of wrapping with
+        // ImpactsDISI when it would not help
         iterator = impactsDisi;
       }
 
@@ -520,6 +524,7 @@ public final class SynonymQuery extends Query {
 
     private final DisiPriorityQueue queue;
     private final DocIdSetIterator iterator;
+    private final MaxScoreCache maxScoreCache;
     private final ImpactsDISI impactsDisi;
     private final LeafSimScorer simScorer;
 
@@ -532,6 +537,7 @@ public final class SynonymQuery extends Query {
       super(weight);
       this.queue = queue;
       this.iterator = iterator;
+      this.maxScoreCache = impactsDisi.getMaxScoreCache();
       this.impactsDisi = impactsDisi;
       this.simScorer = simScorer;
     }
@@ -562,12 +568,12 @@ public final class SynonymQuery extends Query {
 
     @Override
     public float getMaxScore(int upTo) throws IOException {
-      return impactsDisi.getMaxScore(upTo);
+      return maxScoreCache.getMaxScore(upTo);
     }
 
     @Override
     public int advanceShallow(int target) throws IOException {
-      return impactsDisi.advanceShallow(target);
+      return maxScoreCache.advanceShallow(target);
     }
 
     @Override
diff --git a/lucene/core/src/java/org/apache/lucene/search/TermQuery.java b/lucene/core/src/java/org/apache/lucene/search/TermQuery.java
index c7d5ac325b9..58251a3f6d7 100644
--- a/lucene/core/src/java/org/apache/lucene/search/TermQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/TermQuery.java
@@ -114,26 +114,59 @@ public class TermQuery extends Query {
     }
 
     @Override
-    public Scorer scorer(LeafReaderContext context) throws IOException {
+    public ScorerSupplier scorerSupplier(LeafReaderContext context) throws IOException {
       assert termStates == null || termStates.wasBuiltFor(ReaderUtil.getTopLevelContext(context))
           : "The top-reader used to create Weight is not the same as the current reader's top-reader ("
               + ReaderUtil.getTopLevelContext(context);
-      ;
+
       final TermsEnum termsEnum = getTermsEnum(context);
       if (termsEnum == null) {
         return null;
       }
-      LeafSimScorer scorer =
-          new LeafSimScorer(simScorer, context.reader(), term.field(), scoreMode.needsScores());
-      if (scoreMode == ScoreMode.TOP_SCORES) {
-        return new TermScorer(this, termsEnum.impacts(PostingsEnum.FREQS), scorer);
-      } else {
-        return new TermScorer(
-            this,
-            termsEnum.postings(
-                null, scoreMode.needsScores() ? PostingsEnum.FREQS : PostingsEnum.NONE),
-            scorer);
+      final int docFreq = termsEnum.docFreq();
+
+      return new ScorerSupplier() {
+
+        private boolean topLevelScoringClause = false;
+
+        @Override
+        public Scorer get(long leadCost) throws IOException {
+          LeafSimScorer scorer =
+              new LeafSimScorer(simScorer, context.reader(), term.field(), scoreMode.needsScores());
+          if (scoreMode == ScoreMode.TOP_SCORES) {
+            return new TermScorer(
+                TermWeight.this,
+                termsEnum.impacts(PostingsEnum.FREQS),
+                scorer,
+                topLevelScoringClause);
+          } else {
+            return new TermScorer(
+                TermWeight.this,
+                termsEnum.postings(
+                    null, scoreMode.needsScores() ? PostingsEnum.FREQS : PostingsEnum.NONE),
+                scorer);
+          }
+        }
+
+        @Override
+        public long cost() {
+          return docFreq;
+        }
+
+        @Override
+        public void setTopLevelScoringClause() throws IOException {
+          topLevelScoringClause = true;
+        }
+      };
+    }
+
+    @Override
+    public Scorer scorer(LeafReaderContext context) throws IOException {
+      ScorerSupplier supplier = scorerSupplier(context);
+      if (supplier == null) {
+        return null;
       }
+      return supplier.get(Long.MAX_VALUE);
     }
 
     @Override
diff --git a/lucene/core/src/java/org/apache/lucene/search/TermScorer.java b/lucene/core/src/java/org/apache/lucene/search/TermScorer.java
index 51b8068806e..12d59db76a3 100644
--- a/lucene/core/src/java/org/apache/lucene/search/TermScorer.java
+++ b/lucene/core/src/java/org/apache/lucene/search/TermScorer.java
@@ -28,17 +28,18 @@ import org.apache.lucene.index.SlowImpactsEnum;
  */
 public final class TermScorer extends Scorer {
   private final PostingsEnum postingsEnum;
-  private final ImpactsEnum impactsEnum;
   private final DocIdSetIterator iterator;
   private final LeafSimScorer docScorer;
   private final ImpactsDISI impactsDisi;
+  private final MaxScoreCache maxScoreCache;
 
   /** Construct a {@link TermScorer} that will iterate all documents. */
   public TermScorer(Weight weight, PostingsEnum postingsEnum, LeafSimScorer docScorer) {
     super(weight);
     iterator = this.postingsEnum = postingsEnum;
-    impactsEnum = new SlowImpactsEnum(postingsEnum);
-    impactsDisi = new ImpactsDISI(impactsEnum, impactsEnum, docScorer.getSimScorer());
+    ImpactsEnum impactsEnum = new SlowImpactsEnum(postingsEnum);
+    maxScoreCache = new MaxScoreCache(impactsEnum, docScorer.getSimScorer());
+    impactsDisi = null;
     this.docScorer = docScorer;
   }
 
@@ -46,11 +47,21 @@ public final class TermScorer extends Scorer {
    * Construct a {@link TermScorer} that will use impacts to skip blocks of non-competitive
    * documents.
    */
-  TermScorer(Weight weight, ImpactsEnum impactsEnum, LeafSimScorer docScorer) {
+  public TermScorer(
+      Weight weight,
+      ImpactsEnum impactsEnum,
+      LeafSimScorer docScorer,
+      boolean topLevelScoringClause) {
     super(weight);
-    postingsEnum = this.impactsEnum = impactsEnum;
-    impactsDisi = new ImpactsDISI(impactsEnum, impactsEnum, docScorer.getSimScorer());
-    iterator = impactsDisi;
+    postingsEnum = impactsEnum;
+    maxScoreCache = new MaxScoreCache(impactsEnum, docScorer.getSimScorer());
+    if (topLevelScoringClause) {
+      impactsDisi = new ImpactsDISI(impactsEnum, maxScoreCache);
+      iterator = impactsDisi;
+    } else {
+      impactsDisi = null;
+      iterator = impactsEnum;
+    }
     this.docScorer = docScorer;
   }
 
@@ -82,17 +93,19 @@ public final class TermScorer extends Scorer {
 
   @Override
   public int advanceShallow(int target) throws IOException {
-    return impactsDisi.advanceShallow(target);
+    return maxScoreCache.advanceShallow(target);
   }
 
   @Override
   public float getMaxScore(int upTo) throws IOException {
-    return impactsDisi.getMaxScore(upTo);
+    return maxScoreCache.getMaxScore(upTo);
   }
 
   @Override
   public void setMinCompetitiveScore(float minScore) {
-    impactsDisi.setMinCompetitiveScore(minScore);
+    if (impactsDisi != null) {
+      impactsDisi.setMinCompetitiveScore(minScore);
+    }
   }
 
   /** Returns a string representation of this <code>TermScorer</code>. */
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 620858bccc0..7fa2a338653 100644
--- a/lucene/core/src/java/org/apache/lucene/search/WANDScorer.java
+++ b/lucene/core/src/java/org/apache/lucene/search/WANDScorer.java
@@ -25,6 +25,7 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import org.apache.lucene.util.MathUtil;
 
 /**
  * This implements the WAND (Weak AND) algorithm for dynamic pruning described in "Efficient Query
@@ -121,6 +122,8 @@ final class WANDScorer extends Scorer {
   // scaled min competitive score
   private long minCompetitiveScore = 0;
 
+  private final Scorer[] allScorers;
+
   // list of scorers which 'lead' the iteration and are currently
   // positioned on 'doc'. This is sometimes called the 'pivot' in
   // some descriptions of WAND (Weak AND).
@@ -139,7 +142,6 @@ final class WANDScorer extends Scorer {
   int tailSize;
 
   final long cost;
-  final MaxScoreSumPropagator maxScorePropagator;
 
   int upTo; // upper bound for which max scores are valid
 
@@ -156,6 +158,7 @@ final class WANDScorer extends Scorer {
       throw new IllegalArgumentException("minShouldMatch should be < the number of scorers");
     }
 
+    allScorers = scorers.toArray(Scorer[]::new);
     this.minCompetitiveScore = 0;
 
     assert minShouldMatch >= 0 : "minShouldMatch should not be negative, but got " + minShouldMatch;
@@ -185,12 +188,10 @@ final class WANDScorer extends Scorer {
         float maxScore = scorer.getMaxScore(DocIdSetIterator.NO_MORE_DOCS);
         maxScoreSumDouble += maxScore;
       }
-      this.maxScorePropagator = new MaxScoreSumPropagator(scorers);
-      final float maxScoreSum = maxScorePropagator.scoreSumUpperBound(maxScoreSumDouble);
+      final float maxScoreSum = (float) MathUtil.sumUpperBound(maxScoreSumDouble, scorers.size());
       this.scalingFactor = scalingFactor(maxScoreSum);
     } else {
       this.scalingFactor = 0;
-      this.maxScorePropagator = null;
     }
 
     for (Scorer scorer : scorers) {
@@ -245,7 +246,6 @@ final class WANDScorer extends Scorer {
     long scaledMinScore = scaleMinScore(minScore, scalingFactor);
     assert scaledMinScore >= minCompetitiveScore;
     minCompetitiveScore = scaledMinScore;
-    maxScorePropagator.setMinCompetitiveScore(minScore);
   }
 
   @Override
@@ -532,7 +532,11 @@ final class WANDScorer extends Scorer {
   @Override
   public int advanceShallow(int target) throws IOException {
     // Propagate to improve score bounds
-    maxScorePropagator.advanceShallow(target);
+    for (Scorer scorer : allScorers) {
+      if (scorer.docID() < target) {
+        scorer.advanceShallow(target);
+      }
+    }
     if (target <= upTo) {
       return upTo;
     }
@@ -542,7 +546,13 @@ final class WANDScorer extends Scorer {
 
   @Override
   public float getMaxScore(int upTo) throws IOException {
-    return maxScorePropagator.getMaxScore(upTo);
+    double maxScoreSum = 0;
+    for (Scorer scorer : allScorers) {
+      if (scorer.docID() <= upTo) {
+        maxScoreSum += scorer.getMaxScore(upTo);
+      }
+    }
+    return (float) MathUtil.sumUpperBound(maxScoreSum, allScorers.length);
   }
 
   @Override
diff --git a/lucene/core/src/java/org/apache/lucene/search/Weight.java b/lucene/core/src/java/org/apache/lucene/search/Weight.java
index 57930cd5440..54c49d365a1 100644
--- a/lucene/core/src/java/org/apache/lucene/search/Weight.java
+++ b/lucene/core/src/java/org/apache/lucene/search/Weight.java
@@ -164,15 +164,15 @@ public abstract class Weight implements SegmentCacheable {
    */
   public BulkScorer bulkScorer(LeafReaderContext context) throws IOException {
 
-    Scorer scorer = scorer(context);
-    if (scorer == null) {
+    ScorerSupplier scorerSupplier = scorerSupplier(context);
+    if (scorerSupplier == null) {
       // No docs match
       return null;
     }
 
-    // This impl always scores docs in order, so we can
-    // ignore scoreDocsInOrder:
-    return new DefaultBulkScorer(scorer);
+    scorerSupplier.setTopLevelScoringClause();
+
+    return new DefaultBulkScorer(scorerSupplier.get(Long.MAX_VALUE));
   }
 
   /**
diff --git a/lucene/core/src/java/org/apache/lucene/util/MathUtil.java b/lucene/core/src/java/org/apache/lucene/util/MathUtil.java
index 973383585d4..141a4566595 100644
--- a/lucene/core/src/java/org/apache/lucene/util/MathUtil.java
+++ b/lucene/core/src/java/org/apache/lucene/util/MathUtil.java
@@ -167,4 +167,29 @@ public final class MathUtil {
     double u = Math.scalb(1.0, -52);
     return (numValues - 1) * u;
   }
+
+  /**
+   * Return the maximum possible sum across {@code numValues} non-negative doubles, assuming one sum
+   * yielded {@code sum}.
+   *
+   * @see #sumRelativeErrorBound(int)
+   */
+  public static double sumUpperBound(double sum, int numValues) {
+    if (numValues <= 2) {
+      // When there are only two clauses, the sum is always the same regardless
+      // of the order.
+      return 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(numValues);
+    return (1.0 + 2 * b) * sum;
+  }
 }
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestBoolean2ScorerSupplier.java b/lucene/core/src/test/org/apache/lucene/search/TestBoolean2ScorerSupplier.java
index b35969aab86..360d0536e1f 100644
--- a/lucene/core/src/test/org/apache/lucene/search/TestBoolean2ScorerSupplier.java
+++ b/lucene/core/src/test/org/apache/lucene/search/TestBoolean2ScorerSupplier.java
@@ -91,6 +91,7 @@ public class TestBoolean2ScorerSupplier extends LuceneTestCase {
 
     private final long cost;
     private final Long leadCost;
+    private boolean topLevelScoringClause = false;
 
     FakeScorerSupplier(long cost) {
       this.cost = cost;
@@ -119,6 +120,11 @@ public class TestBoolean2ScorerSupplier extends LuceneTestCase {
     public String toString() {
       return "FakeLazyScorer(cost=" + cost + ",leadCost=" + leadCost + ")";
     }
+
+    @Override
+    public void setTopLevelScoringClause() throws IOException {
+      topLevelScoringClause = true;
+    }
   }
 
   public void testConjunctionCost() {
@@ -430,4 +436,89 @@ public class TestBoolean2ScorerSupplier extends LuceneTestCase {
     new Boolean2ScorerSupplier(new FakeWeight(), subs, ScoreMode.COMPLETE, 0)
         .get(20); // triggers assertions as a side-effect
   }
+
+  public void testDisjunctionTopLevelScoringClause() throws Exception {
+    Map<Occur, Collection<ScorerSupplier>> subs = new EnumMap<>(Occur.class);
+    for (Occur occur : Occur.values()) {
+      subs.put(occur, new ArrayList<>());
+    }
+
+    FakeScorerSupplier clause1 = new FakeScorerSupplier(10, 10);
+    subs.get(Occur.SHOULD).add(clause1);
+    FakeScorerSupplier clause2 = new FakeScorerSupplier(10, 10);
+    subs.get(Occur.SHOULD).add(clause2);
+
+    new Boolean2ScorerSupplier(new FakeWeight(), subs, ScoreMode.TOP_SCORES, 0)
+        .setTopLevelScoringClause();
+    assertFalse(clause1.topLevelScoringClause);
+    assertFalse(clause2.topLevelScoringClause);
+  }
+
+  public void testConjunctionTopLevelScoringClause() throws Exception {
+    Map<Occur, Collection<ScorerSupplier>> subs = new EnumMap<>(Occur.class);
+    for (Occur occur : Occur.values()) {
+      subs.put(occur, new ArrayList<>());
+    }
+
+    FakeScorerSupplier clause1 = new FakeScorerSupplier(10, 10);
+    subs.get(Occur.MUST).add(clause1);
+    FakeScorerSupplier clause2 = new FakeScorerSupplier(10, 10);
+    subs.get(Occur.MUST).add(clause2);
+
+    new Boolean2ScorerSupplier(new FakeWeight(), subs, ScoreMode.TOP_SCORES, 0)
+        .setTopLevelScoringClause();
+    assertFalse(clause1.topLevelScoringClause);
+    assertFalse(clause2.topLevelScoringClause);
+  }
+
+  public void testFilterTopLevelScoringClause() throws Exception {
+    Map<Occur, Collection<ScorerSupplier>> subs = new EnumMap<>(Occur.class);
+    for (Occur occur : Occur.values()) {
+      subs.put(occur, new ArrayList<>());
+    }
+
+    FakeScorerSupplier clause1 = new FakeScorerSupplier(10, 10);
+    subs.get(Occur.FILTER).add(clause1);
+    FakeScorerSupplier clause2 = new FakeScorerSupplier(10, 10);
+    subs.get(Occur.FILTER).add(clause2);
+
+    new Boolean2ScorerSupplier(new FakeWeight(), subs, ScoreMode.TOP_SCORES, 0)
+        .setTopLevelScoringClause();
+    assertFalse(clause1.topLevelScoringClause);
+    assertFalse(clause2.topLevelScoringClause);
+  }
+
+  public void testSingleMustScoringClause() throws Exception {
+    Map<Occur, Collection<ScorerSupplier>> subs = new EnumMap<>(Occur.class);
+    for (Occur occur : Occur.values()) {
+      subs.put(occur, new ArrayList<>());
+    }
+
+    FakeScorerSupplier clause1 = new FakeScorerSupplier(10, 10);
+    subs.get(Occur.MUST).add(clause1);
+    FakeScorerSupplier clause2 = new FakeScorerSupplier(10, 10);
+    subs.get(Occur.FILTER).add(clause2);
+
+    new Boolean2ScorerSupplier(new FakeWeight(), subs, ScoreMode.TOP_SCORES, 0)
+        .setTopLevelScoringClause();
+    assertTrue(clause1.topLevelScoringClause);
+    assertFalse(clause2.topLevelScoringClause);
+  }
+
+  public void testSingleShouldScoringClause() throws Exception {
+    Map<Occur, Collection<ScorerSupplier>> subs = new EnumMap<>(Occur.class);
+    for (Occur occur : Occur.values()) {
+      subs.put(occur, new ArrayList<>());
+    }
+
+    FakeScorerSupplier clause1 = new FakeScorerSupplier(10, 10);
+    subs.get(Occur.SHOULD).add(clause1);
+    FakeScorerSupplier clause2 = new FakeScorerSupplier(10, 10);
+    subs.get(Occur.MUST_NOT).add(clause2);
+
+    new Boolean2ScorerSupplier(new FakeWeight(), subs, ScoreMode.TOP_SCORES, 0)
+        .setTopLevelScoringClause();
+    assertTrue(clause1.topLevelScoringClause);
+    assertFalse(clause2.topLevelScoringClause);
+  }
 }
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestMaxScoreSumPropagator.java b/lucene/core/src/test/org/apache/lucene/search/TestMaxScoreSumPropagator.java
deleted file mode 100644
index 0cbe28fa8c3..00000000000
--- a/lucene/core/src/test/org/apache/lucene/search/TestMaxScoreSumPropagator.java
+++ /dev/null
@@ -1,255 +0,0 @@
-/*
- * 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 static org.apache.lucene.search.DocIdSetIterator.NO_MORE_DOCS;
-
-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.index.LeafReaderContext;
-import org.apache.lucene.tests.util.LuceneTestCase;
-import org.apache.lucene.tests.util.TestUtil;
-
-public class TestMaxScoreSumPropagator extends LuceneTestCase {
-
-  private static class FakeWeight extends Weight {
-
-    FakeWeight() {
-      super(new MatchNoDocsQuery());
-    }
-
-    @Override
-    public Explanation explain(LeafReaderContext context, int doc) throws IOException {
-      return null;
-    }
-
-    @Override
-    public Scorer scorer(LeafReaderContext context) throws IOException {
-      return null;
-    }
-
-    @Override
-    public boolean isCacheable(LeafReaderContext ctx) {
-      return false;
-    }
-  }
-
-  private static class FakeScorer extends Scorer {
-
-    final float maxScore;
-    float minCompetitiveScore;
-
-    FakeScorer(float maxScore) throws IOException {
-      super(new FakeWeight());
-      this.maxScore = maxScore;
-    }
-
-    @Override
-    public int docID() {
-      return 0;
-    }
-
-    @Override
-    public float score() {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public DocIdSetIterator iterator() {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public void setMinCompetitiveScore(float minCompetitiveScore) {
-      this.minCompetitiveScore = minCompetitiveScore;
-    }
-
-    @Override
-    public float getMaxScore(int upTo) throws IOException {
-      assert upTo == NO_MORE_DOCS;
-      return maxScore;
-    }
-  }
-
-  public void test0Clause() throws IOException {
-    MaxScoreSumPropagator p = new MaxScoreSumPropagator(Collections.emptyList());
-    p.setMinCompetitiveScore(0f); // no exception
-    p.setMinCompetitiveScore(0.5f); // no exception
-  }
-
-  public void test1Clause() throws IOException {
-    FakeScorer a = new FakeScorer(1);
-
-    MaxScoreSumPropagator p = new MaxScoreSumPropagator(Collections.singletonList(a));
-    assertEquals(1f, p.getMaxScore(NO_MORE_DOCS), 0f);
-    p.setMinCompetitiveScore(0f);
-    assertEquals(0f, a.minCompetitiveScore, 0f);
-    p.setMinCompetitiveScore(0.5f);
-    assertEquals(Math.nextDown(0.5f), a.minCompetitiveScore, 0f);
-    p.setMinCompetitiveScore(1f);
-    assertEquals(Math.nextDown(1f), a.minCompetitiveScore, 0f);
-  }
-
-  public void test2Clauses() throws IOException {
-    FakeScorer a = new FakeScorer(1);
-    FakeScorer b = new FakeScorer(2);
-
-    MaxScoreSumPropagator p = new MaxScoreSumPropagator(Arrays.asList(a, b));
-    assertEquals(3f, p.getMaxScore(NO_MORE_DOCS), 0f);
-
-    p.setMinCompetitiveScore(1f);
-    assertEquals(0f, a.minCompetitiveScore, 0f);
-    assertEquals(0f, b.minCompetitiveScore, 0f);
-
-    p.setMinCompetitiveScore(2f);
-    assertEquals(0f, a.minCompetitiveScore, 0f);
-    assertEquals(Math.nextDown(2f) - 1f, b.minCompetitiveScore, 0f);
-
-    p.setMinCompetitiveScore(2.5f);
-    assertEquals(Math.nextDown(2.5f) - 2f, a.minCompetitiveScore, 0f);
-    assertEquals(Math.nextDown(2.5f) - 1f, b.minCompetitiveScore, 0f);
-
-    p.setMinCompetitiveScore(3f);
-    assertEquals(Math.nextDown(3f) - 2f, a.minCompetitiveScore, 0f);
-    assertEquals(Math.nextDown(3f) - 1f, b.minCompetitiveScore, 0f);
-  }
-
-  public void test3Clauses() throws IOException {
-    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.getMaxScore(NO_MORE_DOCS), 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(Math.nextDown(3f) - 1f - 1.5f, b.minCompetitiveScore, 0f);
-    assertEquals(0f, c.minCompetitiveScore, 0f);
-
-    p.setMinCompetitiveScore(4f);
-    assertEquals(Math.nextDown(4f) - 2f - 1.5f, a.minCompetitiveScore, 0f);
-    assertEquals(Math.nextDown(4f) - 1f - 1.5f, b.minCompetitiveScore, 0f);
-    assertEquals(Math.nextDown(4f) - 1f - 2f, c.minCompetitiveScore, 0f);
-  }
-
-  public void test2ClausesRandomScore() throws IOException {
-    for (int iter = 0; iter < 10; ++iter) {
-      FakeScorer a = new FakeScorer(random().nextFloat());
-      FakeScorer b =
-          new FakeScorer(Math.nextUp(a.getMaxScore(NO_MORE_DOCS)) + random().nextFloat());
-
-      MaxScoreSumPropagator p = new MaxScoreSumPropagator(Arrays.asList(a, b));
-      assertEquals(
-          a.getMaxScore(NO_MORE_DOCS) + b.getMaxScore(NO_MORE_DOCS),
-          p.getMaxScore(NO_MORE_DOCS),
-          0f);
-      assertMinCompetitiveScore(Arrays.asList(a, b), p, Math.nextUp(a.getMaxScore(NO_MORE_DOCS)));
-      assertMinCompetitiveScore(
-          Arrays.asList(a, b), p, (a.getMaxScore(NO_MORE_DOCS) + b.getMaxScore(NO_MORE_DOCS)) / 2);
-      assertMinCompetitiveScore(
-          Arrays.asList(a, b),
-          p,
-          Math.nextDown(a.getMaxScore(NO_MORE_DOCS) + b.getMaxScore(NO_MORE_DOCS)));
-      assertMinCompetitiveScore(
-          Arrays.asList(a, b), p, a.getMaxScore(NO_MORE_DOCS) + b.getMaxScore(NO_MORE_DOCS));
-    }
-  }
-
-  public void testNClausesRandomScore() throws IOException {
-    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.getMaxScore(NO_MORE_DOCS) >= (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)
-      throws IOException {
-    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.getMaxScore(NO_MORE_DOCS);
-        }
-      }
-      assertTrue(
-          "scoreSum=" + scoreSum + ", minCompetitiveScore=" + minCompetitiveScore,
-          (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());
-  }
-}
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestReqOptSumScorer.java b/lucene/core/src/test/org/apache/lucene/search/TestReqOptSumScorer.java
index 3358a3576f8..5d99816037e 100644
--- a/lucene/core/src/test/org/apache/lucene/search/TestReqOptSumScorer.java
+++ b/lucene/core/src/test/org/apache/lucene/search/TestReqOptSumScorer.java
@@ -88,7 +88,9 @@ public class TestReqOptSumScorer extends LuceneTestCase {
     assertEquals(4, scorer.iterator().nextDoc());
     assertEquals(DocIdSetIterator.NO_MORE_DOCS, scorer.iterator().nextDoc());
 
-    scorer = weight.scorer(context);
+    ScorerSupplier ss = weight.scorerSupplier(context);
+    ss.setTopLevelScoringClause();
+    scorer = ss.get(Long.MAX_VALUE);
     scorer.setMinCompetitiveScore(Math.nextDown(1f));
     assertEquals(0, scorer.iterator().nextDoc());
     assertEquals(1, scorer.iterator().nextDoc());
@@ -96,13 +98,17 @@ public class TestReqOptSumScorer extends LuceneTestCase {
     assertEquals(4, scorer.iterator().nextDoc());
     assertEquals(DocIdSetIterator.NO_MORE_DOCS, scorer.iterator().nextDoc());
 
-    scorer = weight.scorer(context);
+    ss = weight.scorerSupplier(context);
+    ss.setTopLevelScoringClause();
+    scorer = ss.get(Long.MAX_VALUE);
     scorer.setMinCompetitiveScore(Math.nextUp(1f));
     assertEquals(1, scorer.iterator().nextDoc());
     assertEquals(4, scorer.iterator().nextDoc());
     assertEquals(DocIdSetIterator.NO_MORE_DOCS, scorer.iterator().nextDoc());
 
-    scorer = weight.scorer(context);
+    ss = weight.scorerSupplier(context);
+    ss.setTopLevelScoringClause();
+    scorer = ss.get(Long.MAX_VALUE);
     assertEquals(0, scorer.iterator().nextDoc());
     scorer.setMinCompetitiveScore(Math.nextUp(1f));
     assertEquals(1, scorer.iterator().nextDoc());
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestWANDScorer.java b/lucene/core/src/test/org/apache/lucene/search/TestWANDScorer.java
index 188b7db34bc..b811b2e7859 100644
--- a/lucene/core/src/test/org/apache/lucene/search/TestWANDScorer.java
+++ b/lucene/core/src/test/org/apache/lucene/search/TestWANDScorer.java
@@ -121,10 +121,10 @@ public class TestWANDScorer extends LuceneTestCase {
                     Occur.SHOULD)
                 .build());
 
-    Scorer scorer =
-        searcher
-            .createWeight(searcher.rewrite(query), ScoreMode.TOP_SCORES, 1)
-            .scorer(searcher.getIndexReader().leaves().get(0));
+    Weight weight = searcher.createWeight(searcher.rewrite(query), ScoreMode.TOP_SCORES, 1);
+    ScorerSupplier ss = weight.scorerSupplier(searcher.getIndexReader().leaves().get(0));
+    ss.setTopLevelScoringClause();
+    Scorer scorer = ss.get(Long.MAX_VALUE);
 
     assertEquals(0, scorer.iterator().nextDoc());
     assertEquals(2 + 1, scorer.score(), 0);
@@ -143,10 +143,9 @@ public class TestWANDScorer extends LuceneTestCase {
 
     assertEquals(DocIdSetIterator.NO_MORE_DOCS, scorer.iterator().nextDoc());
 
-    scorer =
-        searcher
-            .createWeight(searcher.rewrite(query), ScoreMode.TOP_SCORES, 1)
-            .scorer(searcher.getIndexReader().leaves().get(0));
+    ss = weight.scorerSupplier(searcher.getIndexReader().leaves().get(0));
+    ss.setTopLevelScoringClause();
+    scorer = ss.get(Long.MAX_VALUE);
     scorer.setMinCompetitiveScore(4);
 
     assertEquals(3, scorer.iterator().nextDoc());
@@ -157,10 +156,9 @@ public class TestWANDScorer extends LuceneTestCase {
 
     assertEquals(DocIdSetIterator.NO_MORE_DOCS, scorer.iterator().nextDoc());
 
-    scorer =
-        searcher
-            .createWeight(searcher.rewrite(query), ScoreMode.TOP_SCORES, 1)
-            .scorer(searcher.getIndexReader().leaves().get(0));
+    ss = weight.scorerSupplier(searcher.getIndexReader().leaves().get(0));
+    ss.setTopLevelScoringClause();
+    scorer = ss.get(Long.MAX_VALUE);
 
     assertEquals(0, scorer.iterator().nextDoc());
     assertEquals(2 + 1, scorer.score(), 0);
@@ -187,10 +185,10 @@ public class TestWANDScorer extends LuceneTestCase {
             .add(new TermQuery(new Term("foo", "C")), Occur.FILTER)
             .build();
 
-    scorer =
-        searcher
-            .createWeight(searcher.rewrite(query), ScoreMode.TOP_SCORES, 1)
-            .scorer(searcher.getIndexReader().leaves().get(0));
+    weight = searcher.createWeight(searcher.rewrite(query), ScoreMode.TOP_SCORES, 1);
+    ss = weight.scorerSupplier(searcher.getIndexReader().leaves().get(0));
+    ss.setTopLevelScoringClause();
+    scorer = ss.get(Long.MAX_VALUE);
 
     assertEquals(3, scorer.iterator().nextDoc());
     assertEquals(2 + 1, scorer.score(), 0);
@@ -200,10 +198,9 @@ public class TestWANDScorer extends LuceneTestCase {
 
     assertEquals(DocIdSetIterator.NO_MORE_DOCS, scorer.iterator().nextDoc());
 
-    scorer =
-        searcher
-            .createWeight(searcher.rewrite(query), ScoreMode.TOP_SCORES, 1)
-            .scorer(searcher.getIndexReader().leaves().get(0));
+    ss = weight.scorerSupplier(searcher.getIndexReader().leaves().get(0));
+    ss.setTopLevelScoringClause();
+    scorer = ss.get(Long.MAX_VALUE);
 
     scorer.setMinCompetitiveScore(2);
 
@@ -230,10 +227,10 @@ public class TestWANDScorer extends LuceneTestCase {
             .add(new TermQuery(new Term("foo", "C")), Occur.MUST_NOT)
             .build();
 
-    scorer =
-        searcher
-            .createWeight(searcher.rewrite(query), ScoreMode.TOP_SCORES, 1)
-            .scorer(searcher.getIndexReader().leaves().get(0));
+    weight = searcher.createWeight(searcher.rewrite(query), ScoreMode.TOP_SCORES, 1);
+    ss = weight.scorerSupplier(searcher.getIndexReader().leaves().get(0));
+    ss.setTopLevelScoringClause();
+    scorer = ss.get(Long.MAX_VALUE);
 
     assertEquals(0, scorer.iterator().nextDoc());
     assertEquals(2 + 1, scorer.score(), 0);
@@ -246,10 +243,9 @@ public class TestWANDScorer extends LuceneTestCase {
 
     assertEquals(DocIdSetIterator.NO_MORE_DOCS, scorer.iterator().nextDoc());
 
-    scorer =
-        searcher
-            .createWeight(searcher.rewrite(query), ScoreMode.TOP_SCORES, 1)
-            .scorer(searcher.getIndexReader().leaves().get(0));
+    ss = weight.scorerSupplier(searcher.getIndexReader().leaves().get(0));
+    ss.setTopLevelScoringClause();
+    scorer = ss.get(Long.MAX_VALUE);
 
     scorer.setMinCompetitiveScore(3);
 
@@ -303,10 +299,10 @@ public class TestWANDScorer extends LuceneTestCase {
                     .setMinimumNumberShouldMatch(2)
                     .build());
 
-        Scorer scorer =
-            searcher
-                .createWeight(searcher.rewrite(query), ScoreMode.TOP_SCORES, 1)
-                .scorer(searcher.getIndexReader().leaves().get(0));
+        Weight weight = searcher.createWeight(searcher.rewrite(query), ScoreMode.TOP_SCORES, 1);
+        ScorerSupplier ss = weight.scorerSupplier(searcher.getIndexReader().leaves().get(0));
+        ss.setTopLevelScoringClause();
+        Scorer scorer = ss.get(Long.MAX_VALUE);
 
         assertEquals(0, scorer.iterator().nextDoc());
         assertEquals(2 + 1, scorer.score(), 0);
@@ -319,10 +315,9 @@ public class TestWANDScorer extends LuceneTestCase {
 
         assertEquals(DocIdSetIterator.NO_MORE_DOCS, scorer.iterator().nextDoc());
 
-        scorer =
-            searcher
-                .createWeight(searcher.rewrite(query), ScoreMode.TOP_SCORES, 1)
-                .scorer(searcher.getIndexReader().leaves().get(0));
+        ss = weight.scorerSupplier(searcher.getIndexReader().leaves().get(0));
+        ss.setTopLevelScoringClause();
+        scorer = ss.get(Long.MAX_VALUE);
         scorer.setMinCompetitiveScore(4);
 
         assertEquals(3, scorer.iterator().nextDoc());
@@ -333,10 +328,9 @@ public class TestWANDScorer extends LuceneTestCase {
 
         assertEquals(DocIdSetIterator.NO_MORE_DOCS, scorer.iterator().nextDoc());
 
-        scorer =
-            searcher
-                .createWeight(searcher.rewrite(query), ScoreMode.TOP_SCORES, 1)
-                .scorer(searcher.getIndexReader().leaves().get(0));
+        ss = weight.scorerSupplier(searcher.getIndexReader().leaves().get(0));
+        ss.setTopLevelScoringClause();
+        scorer = ss.get(Long.MAX_VALUE);
 
         assertEquals(0, scorer.iterator().nextDoc());
         assertEquals(2 + 1, scorer.score(), 0);
@@ -385,10 +379,10 @@ public class TestWANDScorer extends LuceneTestCase {
                     .setMinimumNumberShouldMatch(2)
                     .build());
 
-        Scorer scorer =
-            searcher
-                .createWeight(searcher.rewrite(query), ScoreMode.TOP_SCORES, 1)
-                .scorer(searcher.getIndexReader().leaves().get(0));
+        Weight weight = searcher.createWeight(searcher.rewrite(query), ScoreMode.TOP_SCORES, 1);
+        ScorerSupplier ss = weight.scorerSupplier(searcher.getIndexReader().leaves().get(0));
+        ss.setTopLevelScoringClause();
+        Scorer scorer = ss.get(Long.MAX_VALUE);
 
         assertEquals(0, scorer.iterator().nextDoc());
         scorer.setMinCompetitiveScore(scorer.score());
@@ -500,10 +494,10 @@ public class TestWANDScorer extends LuceneTestCase {
                 .add(new TermQuery(new Term("foo", "C")), Occur.FILTER)
                 .build();
 
-        Scorer scorer =
-            searcher
-                .createWeight(searcher.rewrite(query), ScoreMode.TOP_SCORES, 1)
-                .scorer(searcher.getIndexReader().leaves().get(0));
+        Weight weight = searcher.createWeight(searcher.rewrite(query), ScoreMode.TOP_SCORES, 1);
+        ScorerSupplier ss = weight.scorerSupplier(searcher.getIndexReader().leaves().get(0));
+        ss.setTopLevelScoringClause();
+        Scorer scorer = ss.get(Long.MAX_VALUE);
 
         assertEquals(1, scorer.iterator().nextDoc());
         assertEquals(2 + 4, scorer.score(), 0);
@@ -513,10 +507,9 @@ public class TestWANDScorer extends LuceneTestCase {
 
         assertEquals(DocIdSetIterator.NO_MORE_DOCS, scorer.iterator().nextDoc());
 
-        scorer =
-            searcher
-                .createWeight(searcher.rewrite(query), ScoreMode.TOP_SCORES, 1)
-                .scorer(searcher.getIndexReader().leaves().get(0));
+        ss = weight.scorerSupplier(searcher.getIndexReader().leaves().get(0));
+        ss.setTopLevelScoringClause();
+        scorer = ss.get(Long.MAX_VALUE);
 
         scorer.setMinCompetitiveScore(2 + 1 + 4);
 
@@ -637,10 +630,8 @@ public class TestWANDScorer extends LuceneTestCase {
                 .add(new TermQuery(new Term("foo", "C")), Occur.MUST_NOT)
                 .build();
 
-        Scorer scorer =
-            searcher
-                .createWeight(searcher.rewrite(query), ScoreMode.TOP_SCORES, 1)
-                .scorer(searcher.getIndexReader().leaves().get(0));
+        Weight weight = searcher.createWeight(searcher.rewrite(query), ScoreMode.TOP_SCORES, 1);
+        Scorer scorer = weight.scorer(searcher.getIndexReader().leaves().get(0));
 
         assertEquals(0, scorer.iterator().nextDoc());
         assertEquals(2 + 1, scorer.score(), 0);
@@ -650,10 +641,9 @@ public class TestWANDScorer extends LuceneTestCase {
 
         assertEquals(DocIdSetIterator.NO_MORE_DOCS, scorer.iterator().nextDoc());
 
-        scorer =
-            searcher
-                .createWeight(searcher.rewrite(query), ScoreMode.TOP_SCORES, 1)
-                .scorer(searcher.getIndexReader().leaves().get(0));
+        ScorerSupplier ss = weight.scorerSupplier(searcher.getIndexReader().leaves().get(0));
+        ss.setTopLevelScoringClause();
+        scorer = ss.get(Long.MAX_VALUE);
 
         scorer.setMinCompetitiveScore(4);
 
diff --git a/lucene/join/src/test/org/apache/lucene/search/join/TestBlockJoinScorer.java b/lucene/join/src/test/org/apache/lucene/search/join/TestBlockJoinScorer.java
index 22f4ae6a5d7..6acca015c0e 100644
--- a/lucene/join/src/test/org/apache/lucene/search/join/TestBlockJoinScorer.java
+++ b/lucene/join/src/test/org/apache/lucene/search/join/TestBlockJoinScorer.java
@@ -30,6 +30,7 @@ import org.apache.lucene.search.MatchAllDocsQuery;
 import org.apache.lucene.search.Query;
 import org.apache.lucene.search.ScoreMode;
 import org.apache.lucene.search.Scorer;
+import org.apache.lucene.search.ScorerSupplier;
 import org.apache.lucene.search.TermQuery;
 import org.apache.lucene.search.Weight;
 import org.apache.lucene.store.Directory;
@@ -90,7 +91,9 @@ public class TestBlockJoinScorer extends LuceneTestCase {
     }
     assertEquals(DocIdSetIterator.NO_MORE_DOCS, scorer.iterator().nextDoc());
 
-    scorer = weight.scorer(context);
+    ScorerSupplier ss = weight.scorerSupplier(context);
+    ss.setTopLevelScoringClause();
+    scorer = ss.get(Long.MAX_VALUE);
     scorer.setMinCompetitiveScore(0f);
     parent = 0;
     for (int i = 0; i < 9; i++) {
@@ -99,11 +102,15 @@ public class TestBlockJoinScorer extends LuceneTestCase {
     }
     assertEquals(DocIdSetIterator.NO_MORE_DOCS, scorer.iterator().nextDoc());
 
-    scorer = weight.scorer(context);
+    ss = weight.scorerSupplier(context);
+    ss.setTopLevelScoringClause();
+    scorer = ss.get(Long.MAX_VALUE);
     scorer.setMinCompetitiveScore(Math.nextUp(0f));
     assertEquals(DocIdSetIterator.NO_MORE_DOCS, scorer.iterator().nextDoc());
 
-    scorer = weight.scorer(context);
+    ss = weight.scorerSupplier(context);
+    ss.setTopLevelScoringClause();
+    scorer = ss.get(Long.MAX_VALUE);
     assertEquals(2, scorer.iterator().nextDoc());
     scorer.setMinCompetitiveScore(Math.nextUp(0f));
     assertEquals(DocIdSetIterator.NO_MORE_DOCS, scorer.iterator().nextDoc());
diff --git a/lucene/test-framework/src/java/org/apache/lucene/tests/search/AssertingScorer.java b/lucene/test-framework/src/java/org/apache/lucene/tests/search/AssertingScorer.java
index 8792535375d..a68ca05400a 100644
--- a/lucene/test-framework/src/java/org/apache/lucene/tests/search/AssertingScorer.java
+++ b/lucene/test-framework/src/java/org/apache/lucene/tests/search/AssertingScorer.java
@@ -35,28 +35,32 @@ public class AssertingScorer extends Scorer {
     FINISHED
   };
 
-  public static Scorer wrap(Random random, Scorer other, ScoreMode scoreMode) {
+  public static Scorer wrap(
+      Random random, Scorer other, ScoreMode scoreMode, boolean canCallMinCompetitiveScore) {
     if (other == null) {
       return null;
     }
-    return new AssertingScorer(random, other, scoreMode);
+    return new AssertingScorer(random, other, scoreMode, canCallMinCompetitiveScore);
   }
 
   final Random random;
   final Scorer in;
   final ScoreMode scoreMode;
+  final boolean canCallMinCompetitiveScore;
 
   IteratorState state = IteratorState.ITERATING;
   int doc;
   float minCompetitiveScore = 0;
   int lastShallowTarget = -1;
 
-  private AssertingScorer(Random random, Scorer in, ScoreMode scoreMode) {
+  private AssertingScorer(
+      Random random, Scorer in, ScoreMode scoreMode, boolean canCallMinCompetitiveScore) {
     super(in.getWeight());
     this.random = random;
     this.in = in;
     this.scoreMode = scoreMode;
     this.doc = in.docID();
+    this.canCallMinCompetitiveScore = canCallMinCompetitiveScore;
   }
 
   public Scorer getIn() {
@@ -77,6 +81,7 @@ public class AssertingScorer extends Scorer {
   @Override
   public void setMinCompetitiveScore(float score) throws IOException {
     assert scoreMode == ScoreMode.TOP_SCORES;
+    assert canCallMinCompetitiveScore;
     assert Float.isNaN(score) == false;
     assert score >= minCompetitiveScore;
     in.setMinCompetitiveScore(score);
diff --git a/lucene/test-framework/src/java/org/apache/lucene/tests/search/AssertingWeight.java b/lucene/test-framework/src/java/org/apache/lucene/tests/search/AssertingWeight.java
index 9dec127789b..574031c6dc4 100644
--- a/lucene/test-framework/src/java/org/apache/lucene/tests/search/AssertingWeight.java
+++ b/lucene/test-framework/src/java/org/apache/lucene/tests/search/AssertingWeight.java
@@ -61,7 +61,7 @@ class AssertingWeight extends FilterWeight {
     if (random.nextBoolean()) {
       final Scorer inScorer = in.scorer(context);
       assert inScorer == null || inScorer.docID() == -1;
-      return AssertingScorer.wrap(new Random(random.nextLong()), inScorer, scoreMode);
+      return AssertingScorer.wrap(new Random(random.nextLong()), inScorer, scoreMode, false);
     } else {
       final ScorerSupplier scorerSupplier = scorerSupplier(context);
       if (scorerSupplier == null) {
@@ -83,6 +83,7 @@ class AssertingWeight extends FilterWeight {
     }
     return new ScorerSupplier() {
       private boolean getCalled = false;
+      private boolean topLevelScoringClause = false;
 
       @Override
       public Scorer get(long leadCost) throws IOException {
@@ -90,7 +91,10 @@ class AssertingWeight extends FilterWeight {
         getCalled = true;
         assert leadCost >= 0 : leadCost;
         return AssertingScorer.wrap(
-            new Random(random.nextLong()), inScorerSupplier.get(leadCost), scoreMode);
+            new Random(random.nextLong()),
+            inScorerSupplier.get(leadCost),
+            scoreMode,
+            topLevelScoringClause);
       }
 
       @Override
@@ -99,6 +103,13 @@ class AssertingWeight extends FilterWeight {
         assert cost >= 0;
         return cost;
       }
+
+      @Override
+      public void setTopLevelScoringClause() throws IOException {
+        assert getCalled == false;
+        topLevelScoringClause = true;
+        inScorerSupplier.setTopLevelScoringClause();
+      }
     };
   }
 
diff --git a/lucene/test-framework/src/java/org/apache/lucene/tests/search/CheckHits.java b/lucene/test-framework/src/java/org/apache/lucene/tests/search/CheckHits.java
index cbcf99c536e..668c308dd0a 100644
--- a/lucene/test-framework/src/java/org/apache/lucene/tests/search/CheckHits.java
+++ b/lucene/test-framework/src/java/org/apache/lucene/tests/search/CheckHits.java
@@ -44,6 +44,7 @@ import org.apache.lucene.search.Scorable;
 import org.apache.lucene.search.ScoreDoc;
 import org.apache.lucene.search.ScoreMode;
 import org.apache.lucene.search.Scorer;
+import org.apache.lucene.search.ScorerSupplier;
 import org.apache.lucene.search.SimpleCollector;
 import org.apache.lucene.search.Sort;
 import org.apache.lucene.search.TopDocs;
@@ -717,7 +718,15 @@ public class CheckHits {
     // Check boundaries and max scores when iterating all matches
     for (LeafReaderContext ctx : searcher.getIndexReader().leaves()) {
       Scorer s1 = w1.scorer(ctx);
-      Scorer s2 = w2.scorer(ctx);
+      ScorerSupplier ss2 = w2.scorerSupplier(ctx);
+      Scorer s2;
+      if (ss2 == null) {
+        s2 = null;
+      } else {
+        // We'll call setMinCompetitiveScore on s2
+        ss2.setTopLevelScoringClause();
+        s2 = ss2.get(Long.MAX_VALUE);
+      }
       if (s1 == null) {
         assertTrue(s2 == null || s2.iterator().nextDoc() == DocIdSetIterator.NO_MORE_DOCS);
         continue;
@@ -765,7 +774,15 @@ public class CheckHits {
     // Now check advancing
     for (LeafReaderContext ctx : searcher.getIndexReader().leaves()) {
       Scorer s1 = w1.scorer(ctx);
-      Scorer s2 = w2.scorer(ctx);
+      ScorerSupplier ss2 = w2.scorerSupplier(ctx);
+      Scorer s2;
+      if (ss2 == null) {
+        s2 = null;
+      } else {
+        // We'll call setMinCompetitiveScore on s2
+        ss2.setTopLevelScoringClause();
+        s2 = ss2.get(Long.MAX_VALUE);
+      }
       if (s1 == null) {
         assertTrue(s2 == null || s2.iterator().nextDoc() == DocIdSetIterator.NO_MORE_DOCS);
         continue;