scoredoc2
diff --git a/solr/core/src/java/org/apache/solr/search/QueryRescorer.java b/solr/core/src/java/org/apache/solr/search/QueryRescorer.java
new file mode 100644
index 0000000..3502bf4
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/search/QueryRescorer.java
@@ -0,0 +1,207 @@
+/*
+ * 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.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.search.*;
+import org.apache.lucene.util.ArrayUtil;
+
+/**
+ * A {@link Rescorer} that uses a provided Query to assign scores to the first-pass hits.
+ *
+ * @lucene.experimental
+ */
+public abstract class QueryRescorer extends Rescorer {
+
+  private final Query query;
+
+  /** Sole constructor, passing the 2nd pass query to assign scores to the 1st pass hits. */
+  public QueryRescorer(Query query) {
+    this.query = query;
+  }
+
+  /**
+   * Implement this in a subclass to combine the first pass and second pass scores. If
+   * secondPassMatches is false then the second pass query failed to match a hit from the first pass
+   * query, and you should ignore the secondPassScore.
+   */
+  protected abstract float combine(
+      float firstPassScore, boolean secondPassMatches, float secondPassScore);
+
+  @Override
+  public TopDocs rescore(IndexSearcher searcher, TopDocs firstPassTopDocs, int topN)
+      throws IOException {
+
+    // convert Lucene ScoreDoc list to Solr ScoreDoc2 list (the latter supports index attribute)
+    // and record original order in each hit's index attribute
+    // NB code below relies on hits being in docId order so the order is about to be lost
+    ArrayList<ScoreDoc2> solrHits = new ArrayList<ScoreDoc2>();
+    for(int i=0; i<firstPassTopDocs.scoreDocs.length; i++) {
+      ScoreDoc2 hit = (ScoreDoc2) firstPassTopDocs.scoreDocs[i];
+      hit.index=i;
+      solrHits.add(hit);
+    }
+
+    ScoreDoc2[] hits = new ScoreDoc2[solrHits.size()];
+    hits = solrHits.toArray(hits);
+
+    Arrays.sort(
+        hits,
+        new Comparator<ScoreDoc2>() {
+          @Override
+          public int compare(ScoreDoc2 a, ScoreDoc2 b) {
+            return a.doc - b.doc;
+          }
+        });
+
+    List<LeafReaderContext> leaves = searcher.getIndexReader().leaves();
+
+    Query rewritten = searcher.rewrite(query);
+    Weight weight = searcher.createWeight(rewritten, ScoreMode.COMPLETE, 1);
+
+    // Now merge sort docIDs from hits, with reader's leaves:
+    int hitUpto = 0;
+    int readerUpto = -1;
+    int endDoc = 0;
+    int docBase = 0;
+    Scorer scorer = null;
+
+    while (hitUpto < hits.length) {
+      ScoreDoc2 hit = hits[hitUpto];
+      int docID = hit.doc;
+      LeafReaderContext readerContext = null;
+      while (docID >= endDoc) {
+        readerUpto++;
+        readerContext = leaves.get(readerUpto);
+        endDoc = readerContext.docBase + readerContext.reader().maxDoc();
+      }
+
+      if (readerContext != null) {
+        // We advanced to another segment:
+        docBase = readerContext.docBase;
+        scorer = weight.scorer(readerContext);
+      }
+
+      if (scorer != null) {
+        int targetDoc = docID - docBase;
+        int actualDoc = scorer.docID();
+        if (actualDoc < targetDoc) {
+          actualDoc = scorer.iterator().advance(targetDoc);
+        }
+
+        if (actualDoc == targetDoc) {
+          // Query did match this doc:
+          hit.score = combine(hit.score, true, scorer.score());
+        } else {
+          // Query did not match this doc:
+          assert actualDoc > targetDoc;
+          hit.score = combine(hit.score, false, 0.0f);
+        }
+      } else {
+        // Query did not match this doc:
+        hit.score = combine(hit.score, false, 0.0f);
+      }
+
+      hitUpto++;
+    }
+
+    Comparator<ScoreDoc2> sortDocComparator =
+        new Comparator<ScoreDoc2>() {
+          @Override
+          public int compare(ScoreDoc2 a, ScoreDoc2 b) {
+            // Sort by score descending, then original order as
+            // recorded in index attribute above
+            if (a.score > b.score) {
+              return -1;
+            } else if (a.score < b.score) {
+              return 1;
+            } else if (a.index > b.index) {
+              return 1;
+            } else {
+              return -1;
+            }
+          }
+        };
+
+    if (topN < hits.length) {
+      ArrayUtil.select(hits, 0, hits.length, topN, sortDocComparator);
+      ScoreDoc2[] subset = new ScoreDoc2[topN];
+      System.arraycopy(hits, 0, subset, 0, topN);
+      hits = subset;
+    }
+
+    Arrays.sort(hits, sortDocComparator);
+
+    return new TopDocs(firstPassTopDocs.totalHits, hits);
+  }
+
+  @Override
+  public Explanation explain(IndexSearcher searcher, Explanation firstPassExplanation, int docID)
+      throws IOException {
+    Explanation secondPassExplanation = searcher.explain(query, docID);
+
+    Number secondPassScore =
+        secondPassExplanation.isMatch() ? secondPassExplanation.getValue() : null;
+
+    float score;
+    if (secondPassScore == null) {
+      score = combine(firstPassExplanation.getValue().floatValue(), false, 0.0f);
+    } else {
+      score =
+          combine(firstPassExplanation.getValue().floatValue(), true, secondPassScore.floatValue());
+    }
+
+    Explanation first =
+        Explanation.match(
+            firstPassExplanation.getValue(), "first pass score", firstPassExplanation);
+
+    Explanation second;
+    if (secondPassScore == null) {
+      second = Explanation.noMatch("no second pass score");
+    } else {
+      second = Explanation.match(secondPassScore, "second pass score", secondPassExplanation);
+    }
+
+    return Explanation.match(
+        score, "combined first and second pass score using " + getClass(), first, second);
+  }
+
+  /**
+   * Sugar API, calling {#rescore} using a simple linear combination of firstPassScore + weight *
+   * secondPassScore
+   */
+  public static TopDocs rescore(
+      IndexSearcher searcher, TopDocs topDocs, Query query, final double weight, int topN)
+      throws IOException {
+    return new QueryRescorer(query) {
+      @Override
+      protected float combine(
+          float firstPassScore, boolean secondPassMatches, float secondPassScore) {
+        float score = firstPassScore;
+        if (secondPassMatches) {
+          score += weight * secondPassScore;
+        }
+        return score;
+      }
+    }.rescore(searcher, topDocs, topN);
+  }
+}
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 5bf5ce4..5294cf7 100644
--- a/solr/core/src/java/org/apache/solr/search/ReRankQParserPlugin.java
+++ b/solr/core/src/java/org/apache/solr/search/ReRankQParserPlugin.java
@@ -20,7 +20,6 @@
 import java.lang.invoke.MethodHandles;
 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;
diff --git a/solr/core/src/java/org/apache/solr/search/ReRankScaler.java b/solr/core/src/java/org/apache/solr/search/ReRankScaler.java
index 8410467..319cb48 100644
--- a/solr/core/src/java/org/apache/solr/search/ReRankScaler.java
+++ b/solr/core/src/java/org/apache/solr/search/ReRankScaler.java
@@ -23,7 +23,6 @@
 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 {
diff --git a/solr/core/src/java/org/apache/solr/search/ScoreDoc2.java b/solr/core/src/java/org/apache/solr/search/ScoreDoc2.java
new file mode 100644
index 0000000..97d258b
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/search/ScoreDoc2.java
@@ -0,0 +1,39 @@
+/*
+ * 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;
+
+public class ScoreDoc2 extends org.apache.lucene.search.ScoreDoc {
+
+  public ScoreDoc2(int doc, float score) {
+    super(doc, score);
+  }
+
+  /**
+   * Original index of the doc in the result set.
+   *
+   * Only set/used by {@link QueryRescorer#rescore} and Solr's LTRRescorer.
+   *
+   * Could shardIndex be used for this instead?
+   */
+  public int index = -1;
+
+  // A convenience method for debugging.
+  @Override
+  public String toString() {
+    return "doc=" + doc + " score=" + score + " shardIndex=" + shardIndex + " index=" + index;
+  }
+}