You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by jb...@apache.org on 2023/06/26 21:21:43 UTC

[solr] branch branch_9x updated: SOLR-16827: Add min/max scaling to the reranker (#1692) (#1723)

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

jbernste pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/branch_9x by this push:
     new ce3b412ef66 SOLR-16827: Add min/max scaling to the reranker (#1692) (#1723)
ce3b412ef66 is described below

commit ce3b412ef664c5c5837042fc19cf3b8e2aa3240a
Author: Joel Bernstein <jb...@apache.org>
AuthorDate: Mon Jun 26 17:21:36 2023 -0400

    SOLR-16827: Add min/max scaling to the reranker (#1692) (#1723)
---
 .../apache/solr/search/AbstractReRankQuery.java    |  27 +-
 .../org/apache/solr/search/ReRankCollector.java    |  56 ++-
 .../apache/solr/search/ReRankQParserPlugin.java    |  82 +++-
 .../java/org/apache/solr/search/ReRankScaler.java  | 470 ++++++++++++++++++
 .../java/org/apache/solr/search/ReRankWeight.java  |  25 +-
 .../solr/search/TestReRankQParserPlugin.java       | 534 +++++++++++++++++++++
 .../query-guide/pages/query-re-ranking.adoc        |  20 +
 7 files changed, 1200 insertions(+), 14 deletions(-)

diff --git a/solr/core/src/java/org/apache/solr/search/AbstractReRankQuery.java b/solr/core/src/java/org/apache/solr/search/AbstractReRankQuery.java
index f0ac6ed41e6..f2eb6de2c3b 100644
--- a/solr/core/src/java/org/apache/solr/search/AbstractReRankQuery.java
+++ b/solr/core/src/java/org/apache/solr/search/AbstractReRankQuery.java
@@ -38,6 +38,21 @@ public abstract class AbstractReRankQuery extends RankQuery {
   protected final int reRankDocs;
   protected final Rescorer reRankQueryRescorer;
   protected Set<BytesRef> boostedPriority;
+  protected ReRankOperator reRankOperator;
+  protected ReRankScaler reRankScaler;
+
+  public AbstractReRankQuery(
+      Query mainQuery,
+      int reRankDocs,
+      Rescorer reRankQueryRescorer,
+      ReRankScaler reRankScaler,
+      ReRankOperator reRankOperator) {
+    this.mainQuery = mainQuery;
+    this.reRankDocs = reRankDocs;
+    this.reRankQueryRescorer = reRankQueryRescorer;
+    this.reRankScaler = reRankScaler;
+    this.reRankOperator = reRankOperator;
+  }
 
   public AbstractReRankQuery(Query mainQuery, int reRankDocs, Rescorer reRankQueryRescorer) {
     this.mainQuery = mainQuery;
@@ -71,7 +86,14 @@ public abstract class AbstractReRankQuery extends RankQuery {
     }
 
     return new ReRankCollector(
-        reRankDocs, len, reRankQueryRescorer, cmd, searcher, boostedPriority);
+        reRankDocs,
+        len,
+        reRankQueryRescorer,
+        cmd,
+        searcher,
+        boostedPriority,
+        reRankScaler,
+        reRankOperator);
   }
 
   @Override
@@ -89,7 +111,8 @@ public abstract class AbstractReRankQuery extends RankQuery {
   public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost)
       throws IOException {
     final Weight mainWeight = mainQuery.createWeight(searcher, scoreMode, boost);
-    return new ReRankWeight(mainQuery, reRankQueryRescorer, searcher, mainWeight);
+    return new ReRankWeight(
+        mainQuery, reRankQueryRescorer, searcher, mainWeight, reRankScaler, reRankOperator);
   }
 
   @Override
diff --git a/solr/core/src/java/org/apache/solr/search/ReRankCollector.java b/solr/core/src/java/org/apache/solr/search/ReRankCollector.java
index 47d4b5fda73..17f206de646 100644
--- a/solr/core/src/java/org/apache/solr/search/ReRankCollector.java
+++ b/solr/core/src/java/org/apache/solr/search/ReRankCollector.java
@@ -51,6 +51,23 @@ public class ReRankCollector extends TopDocsCollector<ScoreDoc> {
   private final Rescorer reRankQueryRescorer;
   private final Sort sort;
   private final Query query;
+  private ReRankScaler reRankScaler;
+  private ReRankOperator reRankOperator;
+
+  public ReRankCollector(
+      int reRankDocs,
+      int length,
+      Rescorer reRankQueryRescorer,
+      QueryCommand cmd,
+      IndexSearcher searcher,
+      Set<BytesRef> boostedPriority,
+      ReRankScaler reRankScaler,
+      ReRankOperator reRankOperator)
+      throws IOException {
+    this(reRankDocs, length, reRankQueryRescorer, cmd, searcher, boostedPriority);
+    this.reRankScaler = reRankScaler;
+    this.reRankOperator = reRankOperator;
+  }
 
   public ReRankCollector(
       int reRankDocs,
@@ -111,13 +128,22 @@ public class ReRankCollector extends TopDocsCollector<ScoreDoc> {
       }
 
       ScoreDoc[] mainScoreDocs = mainDocs.scoreDocs;
+      ScoreDoc[] mainScoreDocsClone =
+          (reRankScaler != null && reRankScaler.scaleScores())
+              ? deepCloneAndZeroOut(mainScoreDocs)
+              : null;
       ScoreDoc[] reRankScoreDocs = new ScoreDoc[Math.min(mainScoreDocs.length, reRankDocs)];
       System.arraycopy(mainScoreDocs, 0, reRankScoreDocs, 0, reRankScoreDocs.length);
 
       mainDocs.scoreDocs = reRankScoreDocs;
 
+      // If we're scaling scores use the replace rescorer because we just want the re-rank score.
       TopDocs rescoredDocs =
-          reRankQueryRescorer.rescore(searcher, mainDocs, mainDocs.scoreDocs.length);
+          reRankScaler != null && reRankScaler.scaleScores()
+              ? reRankScaler
+                  .getReplaceRescorer()
+                  .rescore(searcher, mainDocs, mainDocs.scoreDocs.length)
+              : reRankQueryRescorer.rescore(searcher, mainDocs, mainDocs.scoreDocs.length);
 
       // Lower howMany to return if we've collected fewer documents.
       howMany = Math.min(howMany, mainScoreDocs.length);
@@ -140,6 +166,11 @@ public class ReRankCollector extends TopDocsCollector<ScoreDoc> {
       }
 
       if (howMany == rescoredDocs.scoreDocs.length) {
+        if (reRankScaler != null && reRankScaler.scaleScores()) {
+          rescoredDocs.scoreDocs =
+              reRankScaler.scaleScores(
+                  mainScoreDocsClone, rescoredDocs.scoreDocs, reRankScoreDocs.length);
+        }
         return rescoredDocs; // Just return the rescoredDocs
       } else if (howMany > rescoredDocs.scoreDocs.length) {
         // We need to return more then we've reRanked, so create the combined page.
@@ -153,9 +184,20 @@ public class ReRankCollector extends TopDocsCollector<ScoreDoc> {
             0,
             rescoredDocs.scoreDocs.length); // overlay the re-ranked docs.
         rescoredDocs.scoreDocs = scoreDocs;
+        if (reRankScaler != null && reRankScaler.scaleScores()) {
+          rescoredDocs.scoreDocs =
+              reRankScaler.scaleScores(
+                  mainScoreDocsClone, rescoredDocs.scoreDocs, reRankScoreDocs.length);
+        }
         return rescoredDocs;
       } else {
         // We've rescored more then we need to return.
+
+        if (reRankScaler != null && reRankScaler.scaleScores()) {
+          rescoredDocs.scoreDocs =
+              reRankScaler.scaleScores(
+                  mainScoreDocsClone, rescoredDocs.scoreDocs, rescoredDocs.scoreDocs.length);
+        }
         ScoreDoc[] scoreDocs = new ScoreDoc[howMany];
         System.arraycopy(rescoredDocs.scoreDocs, 0, scoreDocs, 0, howMany);
         rescoredDocs.scoreDocs = scoreDocs;
@@ -166,6 +208,18 @@ public class ReRankCollector extends TopDocsCollector<ScoreDoc> {
     }
   }
 
+  private ScoreDoc[] deepCloneAndZeroOut(ScoreDoc[] scoreDocs) {
+    ScoreDoc[] scoreDocs1 = new ScoreDoc[scoreDocs.length];
+    for (int i = 0; i < scoreDocs.length; i++) {
+      ScoreDoc scoreDoc = scoreDocs[i];
+      if (scoreDoc != null) {
+        scoreDocs1[i] = new ScoreDoc(scoreDoc.doc, scoreDoc.score);
+        scoreDoc.score = 0f;
+      }
+    }
+    return scoreDocs1;
+  }
+
   public static class BoostedComp implements Comparator<ScoreDoc> {
     IntFloatHashMap boostedMap;
 
diff --git a/solr/core/src/java/org/apache/solr/search/ReRankQParserPlugin.java b/solr/core/src/java/org/apache/solr/search/ReRankQParserPlugin.java
index 12204f2a196..2ae72837218 100644
--- a/solr/core/src/java/org/apache/solr/search/ReRankQParserPlugin.java
+++ b/solr/core/src/java/org/apache/solr/search/ReRankQParserPlugin.java
@@ -21,6 +21,7 @@ import org.apache.lucene.search.MatchAllDocsQuery;
 import org.apache.lucene.search.Query;
 import org.apache.lucene.search.QueryRescorer;
 import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.util.StrUtils;
 import org.apache.solr.request.SolrQueryRequest;
@@ -47,6 +48,9 @@ public class ReRankQParserPlugin extends QParserPlugin {
   public static final String RERANK_OPERATOR = "reRankOperator";
   public static final String RERANK_OPERATOR_DEFAULT = "add";
 
+  public static final String RERANK_SCALE = "reRankScale";
+  public static final String RERANK_MAIN_SCALE = "reRankMainScale";
+
   @Override
   public QParser createParser(
       String query, SolrParams localParams, SolrParams params, SolrQueryRequest req) {
@@ -78,7 +82,27 @@ public class ReRankQParserPlugin extends QParserPlugin {
       ReRankOperator reRankOperator =
           ReRankOperator.get(localParams.get(RERANK_OPERATOR, RERANK_OPERATOR_DEFAULT));
 
-      return new ReRankQuery(reRankQuery, reRankDocs, reRankWeight, reRankOperator);
+      String mainScale = localParams.get(RERANK_MAIN_SCALE);
+      String reRankScale = localParams.get(RERANK_SCALE);
+      boolean debugQuery = params.getBool(CommonParams.DEBUG_QUERY, false);
+      double reRankScaleWeight = reRankWeight;
+
+      ReRankScaler reRankScaler =
+          new ReRankScaler(
+              mainScale,
+              reRankScale,
+              reRankScaleWeight,
+              reRankOperator,
+              new ReRankQueryRescorer(reRankQuery, 1, ReRankOperator.REPLACE),
+              debugQuery);
+
+      if (reRankScaler.scaleScores()) {
+        // Scaler applies the weighting instead of the rescorer
+        reRankWeight = 1;
+      }
+
+      return new ReRankQuery(
+          reRankQuery, reRankDocs, reRankWeight, reRankOperator, reRankScaler, debugQuery);
     }
   }
 
@@ -124,7 +148,7 @@ public class ReRankQParserPlugin extends QParserPlugin {
   private static final class ReRankQuery extends AbstractReRankQuery {
     private final Query reRankQuery;
     private final double reRankWeight;
-    private final ReRankOperator reRankOperator;
+    private final boolean debugQuery;
 
     @Override
     public int hashCode() {
@@ -133,7 +157,8 @@ public class ReRankQParserPlugin extends QParserPlugin {
           + reRankQuery.hashCode()
           + (int) reRankWeight
           + reRankDocs
-          + reRankOperator.hashCode();
+          + reRankOperator.hashCode()
+          + reRankScaler.hashCode();
     }
 
     @Override
@@ -146,18 +171,26 @@ public class ReRankQParserPlugin extends QParserPlugin {
           && reRankQuery.equals(rrq.reRankQuery)
           && reRankWeight == rrq.reRankWeight
           && reRankDocs == rrq.reRankDocs
-          && reRankOperator.equals(rrq.reRankOperator);
+          && reRankOperator.equals(rrq.reRankOperator)
+          && reRankScaler.equals(rrq.reRankScaler);
     }
 
     public ReRankQuery(
-        Query reRankQuery, int reRankDocs, double reRankWeight, ReRankOperator reRankOperator) {
+        Query reRankQuery,
+        int reRankDocs,
+        double reRankWeight,
+        ReRankOperator reRankOperator,
+        ReRankScaler reRankScaler,
+        boolean debugQuery) {
       super(
           defaultQuery,
           reRankDocs,
-          new ReRankQueryRescorer(reRankQuery, reRankWeight, reRankOperator));
+          new ReRankQueryRescorer(reRankQuery, reRankWeight, reRankOperator),
+          reRankScaler,
+          reRankOperator);
       this.reRankQuery = reRankQuery;
       this.reRankWeight = reRankWeight;
-      this.reRankOperator = reRankOperator;
+      this.debugQuery = debugQuery;
     }
 
     @Override
@@ -168,15 +201,46 @@ public class ReRankQParserPlugin extends QParserPlugin {
       sb.append(" mainQuery='").append(mainQuery.toString()).append("' ");
       sb.append(RERANK_QUERY).append("='").append(reRankQuery.toString()).append("' ");
       sb.append(RERANK_DOCS).append('=').append(reRankDocs).append(' ');
-      sb.append(RERANK_WEIGHT).append('=').append(reRankWeight).append(' ');
+      if (reRankScaler.scaleScores()) {
+        // The reRankScaler applies the weight
+        sb.append(RERANK_WEIGHT)
+            .append('=')
+            .append(reRankScaler.getReRankScaleWeight())
+            .append(' ');
+      } else {
+        sb.append(RERANK_WEIGHT).append('=').append(reRankWeight).append(' ');
+      }
+      if (reRankScaler.getReRankScalerExplain().getReRankScale() != null) {
+        sb.append(RERANK_SCALE)
+            .append('=')
+            .append(reRankScaler.getReRankScalerExplain().getReRankScale())
+            .append(' ');
+      }
+      if (reRankScaler.getReRankScalerExplain().getMainScale() != null) {
+        sb.append(RERANK_MAIN_SCALE)
+            .append('=')
+            .append(reRankScaler.getReRankScalerExplain().getMainScale())
+            .append(' ');
+      }
       sb.append(RERANK_OPERATOR).append('=').append(reRankOperator.toLower()).append('}');
       return sb.toString();
     }
 
     @Override
     protected Query rewrite(Query rewrittenMainQuery) throws IOException {
-      return new ReRankQuery(reRankQuery, reRankDocs, reRankWeight, reRankOperator)
+      return new ReRankQuery(
+              reRankQuery, reRankDocs, reRankWeight, reRankOperator, reRankScaler, debugQuery)
           .wrap(rewrittenMainQuery);
     }
+
+    @Override
+    public boolean getCache() {
+      if (reRankScaler.scaleScores() && debugQuery) {
+        // Caching breaks explain when reRankScaling is used.
+        return false;
+      } else {
+        return super.getCache();
+      }
+    }
   }
 }
diff --git a/solr/core/src/java/org/apache/solr/search/ReRankScaler.java b/solr/core/src/java/org/apache/solr/search/ReRankScaler.java
new file mode 100644
index 00000000000..686faa4ae1d
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/search/ReRankScaler.java
@@ -0,0 +1,470 @@
+/*
+ * 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.solr.search;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import org.apache.lucene.search.Explanation;
+import org.apache.lucene.search.QueryRescorer;
+import org.apache.lucene.search.ScoreDoc;
+
+public class ReRankScaler {
+
+  protected int mainQueryMin = -1;
+  protected int mainQueryMax = -1;
+  protected int reRankQueryMin = -1;
+  protected int reRankQueryMax = -1;
+  protected boolean debugQuery;
+  protected ReRankOperator reRankOperator;
+  protected ReRankScalerExplain reRankScalerExplain;
+  private QueryRescorer replaceRescorer;
+  private Set<Integer> reRankSet;
+  private double reRankScaleWeight;
+
+  public ReRankScaler(
+      String mainScale,
+      String reRankScale,
+      double reRankScaleWeight,
+      ReRankOperator reRankOperator,
+      QueryRescorer replaceRescorer,
+      boolean debugQuery)
+      throws SyntaxError {
+
+    this.reRankScaleWeight = reRankScaleWeight;
+    this.debugQuery = debugQuery;
+    this.reRankScalerExplain = new ReRankScalerExplain(mainScale, reRankScale);
+    this.replaceRescorer = replaceRescorer;
+    if (reRankOperator != ReRankOperator.ADD
+        && reRankOperator != ReRankOperator.MULTIPLY
+        && reRankOperator != ReRankOperator.REPLACE) {
+      throw new SyntaxError("ReRank scaling only supports ADD, Multiply and Replace operators");
+    } else {
+      this.reRankOperator = reRankOperator;
+    }
+
+    if (reRankScalerExplain.getMainScale() != null) {
+      String[] mainMinMax = reRankScalerExplain.getMainScale().split("-");
+      this.mainQueryMin = Integer.parseInt(mainMinMax[0]);
+      this.mainQueryMax = Integer.parseInt(mainMinMax[1]);
+    }
+
+    if (reRankScalerExplain.getReRankScale() != null) {
+      String[] reRankMinMax = reRankScalerExplain.getReRankScale().split("-");
+      this.reRankQueryMin = Integer.parseInt(reRankMinMax[0]);
+      this.reRankQueryMax = Integer.parseInt(reRankMinMax[1]);
+    }
+  }
+
+  @Override
+  public int hashCode() {
+    return Integer.hashCode(mainQueryMax)
+        + Integer.hashCode(mainQueryMin)
+        + Integer.hashCode(reRankQueryMin)
+        + Integer.hashCode(reRankQueryMax)
+        + reRankOperator.toString().hashCode();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (o instanceof ReRankScaler) {
+      ReRankScaler _reRankScaler = (ReRankScaler) o;
+      if (mainQueryMin == _reRankScaler.mainQueryMin
+          && mainQueryMax == _reRankScaler.mainQueryMax
+          && reRankQueryMin == _reRankScaler.reRankQueryMin
+          && reRankQueryMax == _reRankScaler.reRankQueryMax
+          && reRankOperator.equals(_reRankScaler.reRankOperator)) {
+        return true;
+      } else {
+        return false;
+      }
+    } else {
+      return false;
+    }
+  }
+
+  public QueryRescorer getReplaceRescorer() {
+    return replaceRescorer;
+  }
+
+  public int getMainQueryMin() {
+    return mainQueryMin;
+  }
+
+  public int getMainQueryMax() {
+    return mainQueryMax;
+  }
+
+  public int getReRankQueryMin() {
+    return reRankQueryMin;
+  }
+
+  public int getReRankQueryMax() {
+    return reRankQueryMax;
+  }
+
+  public ReRankScalerExplain getReRankScalerExplain() {
+    return this.reRankScalerExplain;
+  }
+
+  public double getReRankScaleWeight() {
+    return this.reRankScaleWeight;
+  }
+
+  public boolean scaleScores() {
+    if (scaleMainScores() || scaleReRankScores()) {
+      return true;
+    } else {
+      return false;
+    }
+  }
+
+  public boolean scaleMainScores() {
+    if (mainQueryMin > -1 && mainQueryMax > -1) {
+      return true;
+    } else {
+      return false;
+    }
+  }
+
+  public boolean scaleReRankScores() {
+    if (reRankQueryMin > -1 && reRankQueryMax > -1) {
+      return true;
+    } else {
+      return false;
+    }
+  }
+
+  public ScoreDoc[] scaleScores(ScoreDoc[] originalDocs, ScoreDoc[] rescoredDocs, int howMany) {
+
+    Map<Integer, Float> originalScoreMap = new HashMap<>();
+    Map<Integer, Float> scaledOriginalScoreMap = null;
+    Map<Integer, Float> scaledRescoredMap = null;
+    Map<Integer, Float> rescoredMap = new HashMap<>();
+
+    for (ScoreDoc scoreDoc : originalDocs) {
+      originalScoreMap.put(scoreDoc.doc, scoreDoc.score);
+    }
+
+    if (scaleMainScores()) {
+      MinMaxExplain mainExplain = getMinMaxExplain(mainQueryMin, mainQueryMax, originalScoreMap);
+      scaledOriginalScoreMap = minMaxScaleScores(originalScoreMap, mainExplain);
+      reRankScalerExplain.setMainScaleExplain(mainExplain);
+    } else {
+      scaledOriginalScoreMap = originalScoreMap;
+    }
+
+    this.reRankSet = debugQuery ? new HashSet<>() : null;
+
+    for (int i = 0; i < howMany; i++) {
+      ScoreDoc rescoredDoc = rescoredDocs[i];
+      int doc = rescoredDoc.doc;
+      if (debugQuery) {
+        reRankSet.add(doc);
+      }
+      float score = rescoredDoc.score;
+      if (score > 0) {
+        rescoredMap.put(doc, score);
+      }
+    }
+
+    if (scaleReRankScores()) {
+      MinMaxExplain reRankExplain = getMinMaxExplain(reRankQueryMin, reRankQueryMax, rescoredMap);
+      scaledRescoredMap = minMaxScaleScores(rescoredMap, reRankExplain);
+      reRankScalerExplain.setReRankScaleExplain(reRankExplain);
+    } else {
+      scaledRescoredMap = rescoredMap;
+    }
+
+    ScoreDoc[] scaledScoreDocs = new ScoreDoc[originalDocs.length];
+    int index = 0;
+    for (Map.Entry<Integer, Float> entry : scaledOriginalScoreMap.entrySet()) {
+      int doc = entry.getKey();
+      float scaledScore = entry.getValue();
+      ScoreDoc scoreDoc = null;
+      if (scaledRescoredMap.containsKey(doc)) {
+        scoreDoc =
+            new ScoreDoc(
+                doc,
+                combineScores(
+                    scaledScore, scaledRescoredMap.get(doc), reRankScaleWeight, reRankOperator));
+      } else {
+        scoreDoc = new ScoreDoc(doc, scaledScore);
+      }
+
+      scaledScoreDocs[index++] = scoreDoc;
+    }
+
+    Comparator<ScoreDoc> sortDocComparator =
+        new Comparator<ScoreDoc>() {
+          @Override
+          public int compare(ScoreDoc a, ScoreDoc b) {
+            // Sort by score descending, then docID ascending:
+            if (a.score > b.score) {
+              return -1;
+            } else if (a.score < b.score) {
+              return 1;
+            } else {
+              // This subtraction can't overflow int
+              // because docIDs are >= 0:
+              return a.doc - b.doc;
+            }
+          }
+        };
+
+    Arrays.sort(scaledScoreDocs, sortDocComparator);
+    return scaledScoreDocs;
+  }
+
+  public static float combineScores(
+      float orginalScore,
+      float reRankScore,
+      double reRankScaleWeight,
+      ReRankOperator reRankOperator) {
+    switch (reRankOperator) {
+      case ADD:
+        return (float) (orginalScore + reRankScaleWeight * reRankScore);
+      case REPLACE:
+        return (float) (reRankScaleWeight * reRankScore);
+      case MULTIPLY:
+        return (float) (orginalScore * reRankScaleWeight * reRankScore);
+      default:
+        return -1;
+    }
+  }
+
+  public static final class ReRankScalerExplain {
+    private MinMaxExplain mainScaleExplain;
+    private MinMaxExplain reRankScaleExplain;
+    private String mainScale;
+    private String reRankScale;
+
+    public ReRankScalerExplain(String mainScale, String reRankScale) {
+      this.mainScale = mainScale;
+      this.reRankScale = reRankScale;
+    }
+
+    public MinMaxExplain getMainScaleExplain() {
+      return mainScaleExplain;
+    }
+
+    public MinMaxExplain getReRankScaleExplain() {
+      return reRankScaleExplain;
+    }
+
+    public void setMainScaleExplain(MinMaxExplain mainScaleExplain) {
+      this.mainScaleExplain = mainScaleExplain;
+    }
+
+    public void setReRankScaleExplain(MinMaxExplain reRankScaleExplain) {
+      this.reRankScaleExplain = reRankScaleExplain;
+    }
+
+    public boolean reRankScale() {
+      return (getMainScale() != null || getReRankScale() != null);
+    }
+
+    public String getMainScale() {
+      return this.mainScale;
+    }
+
+    public String getReRankScale() {
+      return this.reRankScale;
+    }
+
+    public Explanation explain() {
+      if (getMainScale() != null && getReRankScale() != null) {
+        return Explanation.noMatch(
+            "ReRankScaler Explain",
+            mainScaleExplain.explain("first pass scale"),
+            reRankScaleExplain.explain("second pass scale"));
+      } else if (getMainScale() != null) {
+        return mainScaleExplain.explain("first pass scale");
+      } else if (getReRankScale() != null) {
+        return reRankScaleExplain.explain("second pass scale");
+      }
+      return null;
+    }
+  }
+
+  public static final class MinMaxExplain {
+    public final float scaleMin;
+    public final float scaleMax;
+    public final float localMin;
+    public final float localMax;
+    private float diff;
+
+    public MinMaxExplain(float scaleMin, float scaleMax, float localMin, float localMax) {
+      this.scaleMin = scaleMin;
+      this.scaleMax = scaleMax;
+      this.localMin = localMin;
+      this.localMax = localMax;
+      this.diff = scaleMax - scaleMin;
+    }
+
+    public Explanation explain(String message) {
+      return Explanation.noMatch(
+          message,
+          Explanation.match(localMin, "min score"),
+          Explanation.match(localMax, "max score"));
+    }
+
+    public float scale(float score) {
+      if (localMin == localMax) {
+        return (scaleMin + scaleMax) / 2;
+      } else {
+        float scaledScore = (score - localMin) / (localMax - localMin);
+        if (scaleMin != 0 || scaleMax != 1) {
+          scaledScore = (diff * scaledScore) + scaleMin;
+          return scaledScore;
+        } else {
+          return scaledScore;
+        }
+      }
+    }
+  }
+
+  public Explanation explain(
+      int doc, Explanation mainQueryExplain, Explanation reRankQueryExplain) {
+    float reRankScore = reRankQueryExplain.getDetails()[1].getValue().floatValue();
+    float mainScore = mainQueryExplain.getValue().floatValue();
+    if (reRankSet.contains(doc)) {
+      if (scaleMainScores() && scaleReRankScores()) {
+        if (reRankScore > 0) {
+          MinMaxExplain mainScaleExplain = reRankScalerExplain.getMainScaleExplain();
+          MinMaxExplain reRankScaleExplain = reRankScalerExplain.getReRankScaleExplain();
+          float scaledMainScore = mainScaleExplain.scale(mainScore);
+          float scaledReRankScore = reRankScaleExplain.scale(reRankScore);
+          float combinedScaleScore =
+              combineScores(scaledMainScore, scaledReRankScore, reRankScaleWeight, reRankOperator);
+
+          return Explanation.match(
+              combinedScaleScore,
+              "combined scaled first and second pass score",
+              Explanation.match(
+                  scaledMainScore,
+                  "first pass score scaled between: " + reRankScalerExplain.getMainScale(),
+                  reRankQueryExplain.getDetails()[0],
+                  Explanation.match(mainScaleExplain.localMin, "min first pass score"),
+                  Explanation.match(mainScaleExplain.localMax, "max first pass score")),
+              Explanation.match(
+                  scaledReRankScore,
+                  "second pass score scaled between: " + reRankScalerExplain.getReRankScale(),
+                  reRankQueryExplain.getDetails()[1],
+                  Explanation.match(reRankScaleExplain.localMin, "min second pass score"),
+                  Explanation.match(reRankScaleExplain.localMax, "max second pass score")),
+              Explanation.match(reRankScaleWeight, "rerank weight"));
+
+        } else {
+          MinMaxExplain mainScaleExplain = reRankScalerExplain.getMainScaleExplain();
+          float scaledMainScore = mainScaleExplain.scale(mainScore);
+          return Explanation.match(
+              scaledMainScore,
+              "combined scaled first and second pass score",
+              Explanation.match(
+                  scaledMainScore,
+                  "scaled first pass score",
+                  reRankQueryExplain.getDetails()[0],
+                  Explanation.match(mainScaleExplain.localMin, "min first pass score"),
+                  Explanation.match(mainScaleExplain.localMax, "max first pass score")),
+              reRankQueryExplain.getDetails()[1]);
+        }
+      } else if (scaleMainScores() && !scaleReRankScores()) {
+        MinMaxExplain mainScaleExplain = reRankScalerExplain.getMainScaleExplain();
+        float scaledMainScore = mainScaleExplain.scale(mainScore);
+        float combinedScaleScore =
+            combineScores(scaledMainScore, reRankScore, reRankScaleWeight, reRankOperator);
+        return Explanation.match(
+            combinedScaleScore,
+            "combined scaled first and unscaled second pass score ",
+            Explanation.match(
+                scaledMainScore,
+                "first pass score scaled between: " + reRankScalerExplain.getMainScale(),
+                reRankQueryExplain.getDetails()[0],
+                Explanation.match(mainScaleExplain.localMin, "min first pass score"),
+                Explanation.match(mainScaleExplain.localMax, "max first pass score")),
+            reRankQueryExplain.getDetails()[1],
+            Explanation.match(reRankScaleWeight, "rerank weight"));
+
+      } else if (!scaleMainScores() && scaleReRankScores()) {
+        if (reRankScore > 0) {
+          MinMaxExplain reRankScaleExplain = reRankScalerExplain.getReRankScaleExplain();
+          float scaledReRankScore = reRankScaleExplain.scale(reRankScore);
+          float combinedScaleScore =
+              combineScores(mainScore, scaledReRankScore, reRankScaleWeight, reRankOperator);
+          return Explanation.match(
+              combinedScaleScore,
+              "combined unscaled first and scaled second pass score ",
+              reRankQueryExplain.getDetails()[0],
+              Explanation.match(
+                  scaledReRankScore,
+                  "second pass score scaled between:" + reRankScalerExplain.reRankScale,
+                  reRankQueryExplain.getDetails()[1],
+                  Explanation.match(reRankScaleExplain.localMin, "min second pass score"),
+                  Explanation.match(reRankScaleExplain.localMax, "max sceond pass score")),
+              Explanation.match(reRankScaleWeight, "rerank weight"));
+        } else {
+          return null;
+        }
+      } else {
+        // If we get here nothing has been scaled so return null
+        return null;
+      }
+    } else {
+      if (scaleMainScores()) {
+        MinMaxExplain mainScaleExplain = reRankScalerExplain.getMainScaleExplain();
+        float scaledMainScore = mainScaleExplain.scale(mainScore);
+        return Explanation.match(
+            scaledMainScore,
+            "scaled main query score between: " + reRankScalerExplain.mainScale,
+            mainQueryExplain,
+            Explanation.match(mainScaleExplain.localMin, "min main query score"),
+            Explanation.match(mainScaleExplain.localMax, "max main query score"));
+      } else {
+        return null;
+      }
+    }
+  }
+
+  public static MinMaxExplain getMinMaxExplain(
+      float scaleMin, float scaleMax, Map<Integer, Float> docScoreMap) {
+    float localMin = Float.MAX_VALUE;
+    float localMax = Float.MIN_VALUE;
+
+    for (float score : docScoreMap.values()) {
+      localMin = Math.min(localMin, score);
+      localMax = Math.max(localMax, score);
+    }
+    return new MinMaxExplain(scaleMin, scaleMax, localMin, localMax);
+  }
+
+  public static Map<Integer, Float> minMaxScaleScores(
+      Map<Integer, Float> docScoreMap, MinMaxExplain explain) {
+
+    Map<Integer, Float> scaledScores = new HashMap<>();
+    for (Map.Entry<Integer, Float> entry : docScoreMap.entrySet()) {
+      int doc = entry.getKey();
+      float score = entry.getValue();
+      scaledScores.put(doc, explain.scale(score));
+    }
+
+    return scaledScores;
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/search/ReRankWeight.java b/solr/core/src/java/org/apache/solr/search/ReRankWeight.java
index 1d74b6e8bd0..594c285b5b1 100644
--- a/solr/core/src/java/org/apache/solr/search/ReRankWeight.java
+++ b/solr/core/src/java/org/apache/solr/search/ReRankWeight.java
@@ -30,18 +30,39 @@ public class ReRankWeight extends FilterWeight {
 
   private final IndexSearcher searcher;
   private final Rescorer reRankQueryRescorer;
+  private final ReRankScaler reRankScaler;
+  private final ReRankOperator reRankOperator;
 
   public ReRankWeight(
-      Query mainQuery, Rescorer reRankQueryRescorer, IndexSearcher searcher, Weight mainWeight)
+      Query mainQuery,
+      Rescorer reRankQueryRescorer,
+      IndexSearcher searcher,
+      Weight mainWeight,
+      ReRankScaler reRankScaler,
+      ReRankOperator reRankOperator)
       throws IOException {
     super(mainQuery, mainWeight);
     this.searcher = searcher;
     this.reRankQueryRescorer = reRankQueryRescorer;
+    this.reRankScaler = reRankScaler;
+    this.reRankOperator = reRankOperator;
   }
 
   @Override
   public Explanation explain(LeafReaderContext context, int doc) throws IOException {
     final Explanation mainExplain = in.explain(context, doc);
-    return reRankQueryRescorer.explain(searcher, mainExplain, context.docBase + doc);
+    final Explanation reRankExplain =
+        reRankQueryRescorer.explain(searcher, mainExplain, context.docBase + doc);
+    if (reRankScaler != null && reRankScaler.scaleScores()) {
+      Explanation reScaleExplain =
+          reRankScaler.explain(context.docBase + doc, mainExplain, reRankExplain);
+      if (reScaleExplain != null) {
+        // Can be null if only reRankScore is scaled and is not a reRank match
+        return reScaleExplain;
+      } else {
+        return reRankExplain;
+      }
+    }
+    return reRankExplain;
   }
 }
diff --git a/solr/core/src/test/org/apache/solr/search/TestReRankQParserPlugin.java b/solr/core/src/test/org/apache/solr/search/TestReRankQParserPlugin.java
index b25d725c0a7..f551ba82fca 100644
--- a/solr/core/src/test/org/apache/solr/search/TestReRankQParserPlugin.java
+++ b/solr/core/src/test/org/apache/solr/search/TestReRankQParserPlugin.java
@@ -16,6 +16,7 @@
  */
 package org.apache.solr.search;
 
+import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
 import java.util.function.Function;
@@ -1076,4 +1077,537 @@ public class TestReRankQParserPlugin extends SolrTestCaseJ4 {
         "//result/doc[9]/str[@name='id'][.='7']",
         "//result/doc[10]/str[@name='id'][.='8']");
   }
+
+  @Test
+  public void testReRankScaler() throws Exception {
+
+    ReRankScaler reRankScaler = new ReRankScaler("0-1", "5-100", 1, ReRankOperator.ADD, null, true);
+    assertTrue(reRankScaler.scaleReRankScores());
+    assertTrue(reRankScaler.scaleMainScores());
+    assertEquals(reRankScaler.getMainQueryMin(), 0);
+    assertEquals(reRankScaler.getMainQueryMax(), 1);
+    assertEquals(reRankScaler.getReRankQueryMin(), 5);
+    assertEquals(reRankScaler.getReRankQueryMax(), 100);
+
+    Map<Integer, Float> scores = new HashMap<>();
+    scores.put(1, 180.25f);
+    scores.put(2, 90.125f);
+    scores.put(3, (180.25f + 90.125f) / 2); // halfway
+    ReRankScaler.MinMaxExplain minMaxExplain = ReRankScaler.getMinMaxExplain(0, 1, scores);
+    Map<Integer, Float> scaled = ReRankScaler.minMaxScaleScores(scores, minMaxExplain);
+    assertEquals(scaled.size(), 3);
+    assertTrue(scaled.containsKey(1));
+    assertTrue(scaled.containsKey(2));
+    assertTrue(scaled.containsKey(3));
+    float scaled1 = scaled.get(1);
+    float scaled2 = scaled.get(2);
+    float scaled3 = scaled.get(3);
+    assertEquals(scaled1, 1.0f, 0);
+    assertEquals(scaled2, 0.0f, 0);
+    // scaled3 should be halfway between scaled1 and scaled2
+    assertEquals((scaled1 + scaled2) / 2, scaled3, 0);
+    minMaxExplain = ReRankScaler.getMinMaxExplain(50, 100, scores);
+    scaled = ReRankScaler.minMaxScaleScores(scores, minMaxExplain);
+    scaled1 = scaled.get(1);
+    scaled2 = scaled.get(2);
+    scaled3 = scaled.get(3);
+    assertEquals(scaled1, 100.0f, 0);
+    assertEquals(scaled2, 50.0f, 0);
+    // scaled3 should be halfway between scaled1 and scaled2
+    assertEquals((scaled1 + scaled2) / 2, scaled3, 0);
+
+    scores.put(1, 10f);
+    scores.put(2, 10f);
+    scores.put(3, 10f);
+    minMaxExplain = ReRankScaler.getMinMaxExplain(0, 1, scores);
+
+    scaled = ReRankScaler.minMaxScaleScores(scores, minMaxExplain);
+    scaled1 = scaled.get(1);
+    scaled2 = scaled.get(2);
+    scaled3 = scaled.get(3);
+    assertEquals(scaled1, .5f, 0);
+    assertEquals(scaled2, .5f, 0);
+    assertEquals(scaled3, .5f, 0);
+  }
+
+  @Test
+  public void testReRankScaleQueries() throws Exception {
+
+    assertU(delQ("*:*"));
+    assertU(commit());
+
+    String[] doc = {
+      "id", "1", "term_t", "YYYY", "group_s", "group1", "test_ti", "5", "test_tl", "10", "test_tf",
+      "2000"
+    };
+    assertU(adoc(doc));
+    assertU(commit());
+    String[] doc1 = {
+      "id",
+      "2",
+      "term_t",
+      "YYYY YYYY",
+      "group_s",
+      "group1",
+      "test_ti",
+      "50",
+      "test_tl",
+      "100",
+      "test_tf",
+      "200"
+    };
+    assertU(adoc(doc1));
+
+    String[] doc2 = {
+      "id", "3", "term_t", "YYYY YYYY YYYY", "test_ti", "5000", "test_tl", "100", "test_tf", "200"
+    };
+    assertU(adoc(doc2));
+    assertU(commit());
+    String[] doc3 = {
+      "id",
+      "4",
+      "term_t",
+      "YYYY YYYY YYYY YYYY",
+      "test_ti",
+      "500",
+      "test_tl",
+      "1000",
+      "test_tf",
+      "2000"
+    };
+    assertU(adoc(doc3));
+
+    String[] doc4 = {
+      "id",
+      "5",
+      "term_t",
+      "YYYY YYYY YYYY YYYY YYYY",
+      "group_s",
+      "group2",
+      "test_ti",
+      "4",
+      "test_tl",
+      "10",
+      "test_tf",
+      "2000"
+    };
+    assertU(adoc(doc4));
+    assertU(commit());
+    String[] doc5 = {
+      "id",
+      "6",
+      "term_t",
+      "YYYY YYYY YYYY YYYY YYYY YYYY",
+      "group_s",
+      "group2",
+      "test_ti",
+      "10",
+      "test_tl",
+      "100",
+      "test_tf",
+      "200"
+    };
+    assertU(adoc(doc5));
+    assertU(commit());
+
+    ModifiableSolrParams params = new ModifiableSolrParams();
+    params.add(
+        "rq",
+        "{!"
+            + ReRankQParserPlugin.NAME
+            + " "
+            + ReRankQParserPlugin.RERANK_MAIN_SCALE
+            + "=10-20 "
+            + ReRankQParserPlugin.RERANK_SCALE
+            + "=10-20 "
+            + ReRankQParserPlugin.RERANK_WEIGHT
+            + "=1 "
+            + ReRankQParserPlugin.RERANK_QUERY
+            + "=$rqq "
+            + ReRankQParserPlugin.RERANK_DOCS
+            + "=200}");
+    params.add("q", "term_t:YYYY");
+    params.add("fl", "id,score");
+    params.add("rqq", "{!edismax bf=$bff}*:*");
+    params.add("bff", "field(test_ti)");
+    params.add("start", "0");
+    params.add("rows", "6");
+    params.add("df", "text");
+    params.add("debugQuery", "true");
+    assertQ(
+        req(params),
+        "*[count(//doc)=6]",
+        "//result/doc[1]/str[@name='id'][.='3']",
+        "//result/doc[1]/float[@name='score'][.='37.70526']",
+        "//result/doc[2]/str[@name='id'][.='6']",
+        "//result/doc[2]/float[@name='score'][.='30.012009']",
+        "//result/doc[3]/str[@name='id'][.='4']",
+        "//result/doc[3]/float[@name='score'][.='29.82389']",
+        "//result/doc[4]/str[@name='id'][.='5']",
+        "//result/doc[4]/float[@name='score'][.='29.527113']",
+        "//result/doc[5]/str[@name='id'][.='2']",
+        "//result/doc[5]/float[@name='score'][.='25.665672']",
+        "//result/doc[6]/str[@name='id'][.='1']",
+        "//result/doc[6]/float[@name='score'][.='20.002003']");
+
+    // Test with fewer rows then matching docs.
+    params = new ModifiableSolrParams();
+    params.add(
+        "rq",
+        "{!"
+            + ReRankQParserPlugin.NAME
+            + " "
+            + ReRankQParserPlugin.RERANK_MAIN_SCALE
+            + "=10-20 "
+            + ReRankQParserPlugin.RERANK_SCALE
+            + "=10-20 "
+            + ReRankQParserPlugin.RERANK_WEIGHT
+            + "=1 "
+            + ReRankQParserPlugin.RERANK_QUERY
+            + "=$rqq "
+            + ReRankQParserPlugin.RERANK_DOCS
+            + "=200}");
+    params.add("q", "term_t:YYYY");
+    params.add("fl", "id,score");
+    params.add("rqq", "{!edismax bf=$bff}*:*");
+    params.add("bff", "field(test_ti)");
+    params.add("start", "0");
+    params.add("rows", "4");
+    params.add("df", "text");
+    params.add("debugQuery", "true");
+    assertQ(
+        req(params),
+        "*[count(//doc)=4]",
+        "//result/doc[1]/str[@name='id'][.='3']",
+        "//result/doc[1]/float[@name='score'][.='37.70526']",
+        "//result/doc[2]/str[@name='id'][.='6']",
+        "//result/doc[2]/float[@name='score'][.='30.012009']",
+        "//result/doc[3]/str[@name='id'][.='4']",
+        "//result/doc[3]/float[@name='score'][.='29.82389']",
+        "//result/doc[4]/str[@name='id'][.='5']",
+        "//result/doc[4]/float[@name='score'][.='29.527113']");
+
+    // Test no-rerank hits.
+    params = new ModifiableSolrParams();
+    params.add(
+        "rq",
+        "{!"
+            + ReRankQParserPlugin.NAME
+            + " "
+            + ReRankQParserPlugin.RERANK_MAIN_SCALE
+            + "=10-20 "
+            + ReRankQParserPlugin.RERANK_SCALE
+            + "=10-20 "
+            + ReRankQParserPlugin.RERANK_WEIGHT
+            + "=1 "
+            + ReRankQParserPlugin.RERANK_QUERY
+            + "=$rqq "
+            + ReRankQParserPlugin.RERANK_DOCS
+            + "=200}");
+    params.add("q", "term_t:YYYY");
+    params.add("fl", "id,score");
+    params.add("rqq", "{!edismax bf=$bff}BBBBBBBB"); // No hit re-rank.
+    params.add("bff", "field(test_ti)");
+    params.add("start", "0");
+    params.add("rows", "6");
+    params.add("df", "text");
+    params.add("debugQuery", "true");
+
+    assertQ(
+        req(params),
+        "*[count(//doc)=6]",
+        "//result/doc[1]/str[@name='id'][.='6']",
+        "//result/doc[1]/float[@name='score'][.='20.0']",
+        "//result/doc[2]/str[@name='id'][.='5']",
+        "//result/doc[2]/float[@name='score'][.='19.527113']",
+        "//result/doc[3]/str[@name='id'][.='4']",
+        "//result/doc[3]/float[@name='score'][.='18.831097']",
+        "//result/doc[4]/str[@name='id'][.='3']",
+        "//result/doc[4]/float[@name='score'][.='17.705261']",
+        "//result/doc[5]/str[@name='id'][.='2']",
+        "//result/doc[5]/float[@name='score'][.='15.5736']",
+        "//result/doc[6]/str[@name='id'][.='1']",
+        "//result/doc[6]/float[@name='score'][.='10.0']");
+
+    // Test reRank only top 4
+    params = new ModifiableSolrParams();
+    params.add(
+        "rq",
+        "{!"
+            + ReRankQParserPlugin.NAME
+            + " "
+            + ReRankQParserPlugin.RERANK_MAIN_SCALE
+            + "=10-20 "
+            + ReRankQParserPlugin.RERANK_SCALE
+            + "=10-20 "
+            + ReRankQParserPlugin.RERANK_WEIGHT
+            + "=1 "
+            + ReRankQParserPlugin.RERANK_QUERY
+            + "=$rqq "
+            + ReRankQParserPlugin.RERANK_DOCS
+            + "=4}");
+    params.add("q", "term_t:YYYY");
+    params.add("fl", "id,score");
+    params.add("rqq", "{!edismax bf=$bff}*:*");
+    params.add("bff", "field(test_ti)");
+    params.add("start", "0");
+    params.add("rows", "6");
+    params.add("df", "text");
+    assertQ(
+        req(params),
+        "*[count(//doc)=6]",
+        "//result/doc[1]/str[@name='id'][.='3']",
+        "//result/doc[1]/float[@name='score'][.='37.70526']",
+        "//result/doc[2]/str[@name='id'][.='6']",
+        "//result/doc[2]/float[@name='score'][.='30.012009']",
+        "//result/doc[3]/str[@name='id'][.='4']",
+        "//result/doc[3]/float[@name='score'][.='29.82389']",
+        "//result/doc[4]/str[@name='id'][.='5']",
+        "//result/doc[4]/float[@name='score'][.='29.527113']",
+        "//result/doc[5]/str[@name='id'][.='2']",
+        "//result/doc[5]/float[@name='score'][.='15.5736']",
+        "//result/doc[6]/str[@name='id'][.='1']",
+        "//result/doc[6]/float[@name='score'][.='10.0']");
+
+    // Test reRank more than found
+    params = new ModifiableSolrParams();
+    params.add(
+        "rq",
+        "{!"
+            + ReRankQParserPlugin.NAME
+            + " "
+            + ReRankQParserPlugin.RERANK_MAIN_SCALE
+            + "=10-20 "
+            + ReRankQParserPlugin.RERANK_SCALE
+            + "=10-20 "
+            + ReRankQParserPlugin.RERANK_WEIGHT
+            + "=1 "
+            + ReRankQParserPlugin.RERANK_QUERY
+            + "=$rqq "
+            + ReRankQParserPlugin.RERANK_DOCS
+            + "=4}");
+    params.add("q", "term_t:YYYY");
+    params.add("fq", "id:(4 OR 5)");
+    params.add("fl", "id,score");
+    params.add("rqq", "{!edismax bf=$bff}*:*");
+    params.add("bff", "field(test_ti)");
+    params.add("start", "0");
+    params.add("rows", "6");
+    params.add("df", "text");
+    assertQ(
+        req(params),
+        "*[count(//doc)=2]",
+        "//result/doc[1]/str[@name='id'][.='4']",
+        "//result/doc[1]/float[@name='score'][.='30.0']",
+        "//result/doc[2]/str[@name='id'][.='5']",
+        "//result/doc[2]/float[@name='score'][.='30.0']");
+
+    // Test reRank more than found
+    params = new ModifiableSolrParams();
+    params.add(
+        "rq",
+        "{!"
+            + ReRankQParserPlugin.NAME
+            + " "
+            + ReRankQParserPlugin.RERANK_MAIN_SCALE
+            + "=10-20 "
+            + ReRankQParserPlugin.RERANK_SCALE
+            + "=10-20 "
+            + ReRankQParserPlugin.RERANK_WEIGHT
+            + "=1 "
+            + ReRankQParserPlugin.RERANK_QUERY
+            + "=$rqq "
+            + ReRankQParserPlugin.RERANK_DOCS
+            + "=4}");
+    params.add("q", "term_t:YYYY");
+    params.add("fq", "id:(4 OR 5)");
+    params.add("fl", "id,score");
+    params.add("rqq", "{!edismax bf=$bff}*:*");
+    params.add("bff", "field(test_ti)");
+    params.add("start", "0");
+    params.add("rows", "6");
+    params.add("df", "text");
+    params.add("debugQuery", "true");
+    assertQ(
+        req(params),
+        "*[count(//doc)=2]",
+        "//result/doc[1]/str[@name='id'][.='4']",
+        "//result/doc[1]/float[@name='score'][.='30.0']",
+        "//result/doc[2]/str[@name='id'][.='5']",
+        "//result/doc[2]/float[@name='score'][.='30.0']");
+
+    String explainResponse = JQ(req(params));
+    assertTrue(explainResponse.contains("30.0 = combined scaled first and second pass score"));
+
+    assertTrue(explainResponse.contains("10.0 = first pass score scaled between: 10-20"));
+    assertTrue(explainResponse.contains("20.0 = second pass score scaled between: 10-20"));
+
+    assertTrue(explainResponse.contains("20.0 = first pass score scaled between: 10-20"));
+
+    assertTrue(explainResponse.contains("10.0 = second pass score scaled between: 10-20"));
+
+    params = new ModifiableSolrParams();
+    params.add(
+        "rq",
+        "{!"
+            + ReRankQParserPlugin.NAME
+            + " "
+            + ReRankQParserPlugin.RERANK_MAIN_SCALE
+            + "=10-20 "
+            + ReRankQParserPlugin.RERANK_WEIGHT
+            + "=1 "
+            + ReRankQParserPlugin.RERANK_QUERY
+            + "=$rqq "
+            + ReRankQParserPlugin.RERANK_DOCS
+            + "=4}");
+    params.add("q", "term_t:YYYY");
+    params.add("fl", "id,score");
+    params.add("rqq", "{!edismax bf=$bff}*:*");
+    params.add("bff", "field(test_ti)");
+    params.add("start", "0");
+    params.add("rows", "6");
+    params.add("df", "text");
+    params.add("debugQuery", "true");
+    assertQ(
+        req(params),
+        "*[count(//doc)=6]",
+        "//result/doc[1]/str[@name='id'][.='3']",
+        "//result/doc[1]/float[@name='score'][.='5018.705']",
+        "//result/doc[2]/str[@name='id'][.='4']",
+        "//result/doc[2]/float[@name='score'][.='519.8311']",
+        "//result/doc[3]/str[@name='id'][.='6']",
+        "//result/doc[3]/float[@name='score'][.='31.0']",
+        "//result/doc[4]/str[@name='id'][.='5']",
+        "//result/doc[4]/float[@name='score'][.='24.527113']",
+        "//result/doc[5]/str[@name='id'][.='2']",
+        "//result/doc[5]/float[@name='score'][.='15.5736']",
+        "//result/doc[6]/str[@name='id'][.='1']",
+        "//result/doc[6]/float[@name='score'][.='10.0']");
+
+    explainResponse = JQ(req(params));
+    assertTrue(
+        explainResponse.contains(
+            "5018.705 = combined scaled first and unscaled second pass score"));
+    assertTrue(
+        explainResponse.contains(
+            "519.8311 = combined scaled first and unscaled second pass score"));
+    assertTrue(
+        explainResponse.contains("31.0 = combined scaled first and unscaled second pass score "));
+    assertTrue(
+        explainResponse.contains(
+            "24.527113 = combined scaled first and unscaled second pass score"));
+
+    assertTrue(explainResponse.contains("15.5736 = scaled main query score between: 10-20"));
+
+    assertTrue(explainResponse.contains("10.0 = scaled main query score between: 10-20"));
+
+    params = new ModifiableSolrParams();
+    params.add(
+        "rq",
+        "{!"
+            + ReRankQParserPlugin.NAME
+            + " "
+            + ReRankQParserPlugin.RERANK_MAIN_SCALE
+            + "=10-20 "
+            + ReRankQParserPlugin.RERANK_WEIGHT
+            + "=1 "
+            + ReRankQParserPlugin.RERANK_QUERY
+            + "=$rqq "
+            + ReRankQParserPlugin.RERANK_DOCS
+            + "=4}");
+    params.add("q", "term_t:YYYY");
+    params.add("fl", "id,score");
+    params.add("rqq", "{!edismax bf=$bff}id:(3 OR 4 OR 6 OR 5)");
+    params.add("bff", "field(test_ti)");
+    params.add("start", "0");
+    params.add("rows", "6");
+    params.add("df", "text");
+    params.add("debugQuery", "true");
+    assertQ(
+        req(params),
+        "*[count(//doc)=6]",
+        "//result/doc[1]/str[@name='id'][.='3']",
+        "//result/doc[1]/float[@name='score'][.='5018.4053']",
+        "//result/doc[2]/str[@name='id'][.='4']",
+        "//result/doc[2]/float[@name='score'][.='519.5313']",
+        "//result/doc[3]/str[@name='id'][.='6']",
+        "//result/doc[3]/float[@name='score'][.='30.700203']",
+        "//result/doc[4]/str[@name='id'][.='5']",
+        "//result/doc[4]/float[@name='score'][.='24.227316']",
+        "//result/doc[5]/str[@name='id'][.='2']",
+        "//result/doc[5]/float[@name='score'][.='15.5736']",
+        "//result/doc[6]/str[@name='id'][.='1']",
+        "//result/doc[6]/float[@name='score'][.='10.0']");
+
+    assertTrue(
+        explainResponse.contains(
+            "5018.705 = combined scaled first and unscaled second pass score"));
+    assertTrue(
+        explainResponse.contains(
+            "519.8311 = combined scaled first and unscaled second pass score"));
+    assertTrue(
+        explainResponse.contains("31.0 = combined scaled first and unscaled second pass score "));
+    assertTrue(
+        explainResponse.contains(
+            "24.527113 = combined scaled first and unscaled second pass score"));
+
+    assertTrue(explainResponse.contains("15.5736 = scaled main query score between: 10-20"));
+
+    assertTrue(explainResponse.contains("10.0 = scaled main query score between: 10-20"));
+
+    // Use default reRankWeight of 2
+    params = new ModifiableSolrParams();
+    params.add(
+        "rq",
+        "{!"
+            + ReRankQParserPlugin.NAME
+            + " "
+            + ReRankQParserPlugin.RERANK_MAIN_SCALE
+            + "=10-20 "
+            + ReRankQParserPlugin.RERANK_QUERY
+            + "=$rqq "
+            + ReRankQParserPlugin.RERANK_DOCS
+            + "=4}");
+    params.add("q", "term_t:YYYY");
+    params.add("fl", "id,score");
+    params.add("rqq", "{!edismax bf=$bff}id:(3 OR 4 OR 6 OR 5)");
+    params.add("bff", "field(test_ti)");
+    params.add("start", "0");
+    params.add("rows", "6");
+    params.add("df", "text");
+    params.add("debugQuery", "true");
+    assertQ(
+        req(params),
+        "*[count(//doc)=6]",
+        "//result/doc[1]/str[@name='id'][.='3']",
+        "//result/doc[1]/float[@name='score'][.='10019.105']",
+        "//result/doc[2]/str[@name='id'][.='4']",
+        "//result/doc[2]/float[@name='score'][.='1020.2315']",
+        "//result/doc[3]/str[@name='id'][.='6']",
+        "//result/doc[3]/float[@name='score'][.='41.400406']",
+        "//result/doc[4]/str[@name='id'][.='5']",
+        "//result/doc[4]/float[@name='score'][.='28.927517']",
+        "//result/doc[5]/str[@name='id'][.='2']",
+        "//result/doc[5]/float[@name='score'][.='15.5736']",
+        "//result/doc[6]/str[@name='id'][.='1']",
+        "//result/doc[6]/float[@name='score'][.='10.0']");
+
+    explainResponse = JQ(req(params));
+    assertTrue(
+        explainResponse.contains(
+            "10019.105 = combined scaled first and unscaled second pass score"));
+    assertTrue(
+        explainResponse.contains(
+            "1020.2315 = combined scaled first and unscaled second pass score"));
+    assertTrue(
+        explainResponse.contains(
+            "41.400406 = combined scaled first and unscaled second pass score"));
+    assertTrue(
+        explainResponse.contains(
+            "28.927517 = combined scaled first and unscaled second pass score"));
+
+    assertTrue(explainResponse.contains("15.5736 = scaled main query score between: 10-20"));
+
+    assertTrue(explainResponse.contains("10.0 = scaled main query score between: 10-20"));
+  }
 }
diff --git a/solr/solr-ref-guide/modules/query-guide/pages/query-re-ranking.adoc b/solr/solr-ref-guide/modules/query-guide/pages/query-re-ranking.adoc
index fe32836c458..d1eb28e302e 100644
--- a/solr/solr-ref-guide/modules/query-guide/pages/query-re-ranking.adoc
+++ b/solr/solr-ref-guide/modules/query-guide/pages/query-re-ranking.adoc
@@ -69,6 +69,26 @@ This number will be treated as a minimum, and may be increased internally automa
 +
 A multiplicative factor that will be applied to the score from the reRankQuery for each of the top matching documents, before that score is combined with the original score.
 
+`reRankScale`::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: `none`
+|===
++
+Scales the rerank scores between min and max values. The format of this parameter value is `min-max` where
+min and max are positive integers. Example `reRankScale=0-1` rescales the rerank scores between 0 and 1.
+
+`reRankMainScale`::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: `none`
+|===
++
+Scales the main query scores between min and max values. The format of this parameter value is `min-max` where
+min and max are positive integers. Example `reRankMainScale=0-1` rescales the main query scores between 0 and 1.
+
 `reRankOperator`::
 +
 [%autowidth,frame=none]