This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/main by this push:
new 930e6c158e9 SOLR-17951: Optimize re-ranking based on "functions"
930e6c158e9 is described below
commit 930e6c158e9540b16dd0c8fb84972fe6fd9a5abf
Author: Chris Hostetter <[email protected]>
AuthorDate: Wed Apr 29 11:21:06 2026 -0700
SOLR-17951: Optimize re-ranking based on "functions"
---
.../SOLR-17951-optimize-function-rerank.yml | 7 ++
.../apache/solr/search/AbstractReRankQuery.java | 21 +++++
.../org/apache/solr/search/ReRankOperator.java | 20 ++++-
.../apache/solr/search/ReRankQParserPlugin.java | 68 +++++++++++----
.../java/org/apache/solr/search/ReRankScaler.java | 19 ++---
.../solr/search/TestReRankQParserPlugin.java | 99 +++++++++++++++++++---
6 files changed, 190 insertions(+), 44 deletions(-)
diff --git a/changelog/unreleased/SOLR-17951-optimize-function-rerank.yml
b/changelog/unreleased/SOLR-17951-optimize-function-rerank.yml
new file mode 100644
index 00000000000..6331428370f
--- /dev/null
+++ b/changelog/unreleased/SOLR-17951-optimize-function-rerank.yml
@@ -0,0 +1,7 @@
+title: Optimize re-ranking based on "functions"
+type: added
+authors:
+- name: hossman
+links:
+- name: SOLR-17951
+ url: https://issues.apache.org/jira/browse/SOLR-17951
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 a786e849108..5c026e55762 100644
--- a/solr/core/src/java/org/apache/solr/search/AbstractReRankQuery.java
+++ b/solr/core/src/java/org/apache/solr/search/AbstractReRankQuery.java
@@ -16,6 +16,7 @@
*/
package org.apache.solr.search;
+import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
@@ -59,6 +60,26 @@ public abstract class AbstractReRankQuery extends RankQuery {
this.reRankQueryRescorer = reRankQueryRescorer;
}
+ @VisibleForTesting
+ int getReRankDocs() {
+ return reRankDocs;
+ }
+
+ @VisibleForTesting
+ Rescorer getRescorer() {
+ return reRankQueryRescorer;
+ }
+
+ @VisibleForTesting
+ ReRankOperator getReRankOperator() {
+ return reRankOperator;
+ }
+
+ @VisibleForTesting
+ ReRankScaler getReRankScaler() {
+ return reRankScaler;
+ }
+
@Override
public RankQuery wrap(Query _mainQuery) {
if (_mainQuery != null) {
diff --git a/solr/core/src/java/org/apache/solr/search/ReRankOperator.java
b/solr/core/src/java/org/apache/solr/search/ReRankOperator.java
index 0b6583e095e..b3b5582836c 100644
--- a/solr/core/src/java/org/apache/solr/search/ReRankOperator.java
+++ b/solr/core/src/java/org/apache/solr/search/ReRankOperator.java
@@ -17,12 +17,24 @@
package org.apache.solr.search;
import java.util.Locale;
+import java.util.function.DoubleBinaryOperator;
import org.apache.solr.common.SolrException;
-public enum ReRankOperator {
- ADD,
- MULTIPLY,
- REPLACE;
+public enum ReRankOperator implements DoubleBinaryOperator {
+ ADD((firstPass, secondPass) -> firstPass + secondPass),
+ MULTIPLY((firstPass, secondPass) -> firstPass * secondPass),
+ REPLACE((firstPass, secondPass) -> secondPass);
+
+ private final DoubleBinaryOperator op;
+
+ private ReRankOperator(final DoubleBinaryOperator op) {
+ this.op = op;
+ }
+
+ @Override
+ public double applyAsDouble(final double firstPass, final double secondPass)
{
+ return op.applyAsDouble(firstPass, secondPass);
+ }
public static ReRankOperator get(String p) {
if (p != null) {
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 53373c03de5..33bb7f2a35b 100644
--- a/solr/core/src/java/org/apache/solr/search/ReRankQParserPlugin.java
+++ b/solr/core/src/java/org/apache/solr/search/ReRankQParserPlugin.java
@@ -18,9 +18,15 @@ package org.apache.solr.search;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
+import java.util.function.DoubleBinaryOperator;
+import org.apache.lucene.queries.function.FunctionQuery;
+import org.apache.lucene.queries.function.FunctionScoreQuery;
+import org.apache.lucene.search.DoubleValuesSource;
+import org.apache.lucene.search.DoubleValuesSourceRescorer;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.QueryRescorer;
+import org.apache.lucene.search.Rescorer;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.SolrParams;
@@ -63,6 +69,26 @@ public class ReRankQParserPlugin extends QParserPlugin {
return new ReRankQParser(query, localParams, params, req);
}
+ /**
+ * Helper method for constructing a {@link Rescorer} from a {@link
#RERANK_QUERY}, {@link
+ * #RERANK_WEIGHT}, and {@link #RERANK_OPERATOR}.
+ *
+ * <p>By default, this returns a customized {@link QueryRescorer}, unless
the {@link
+ * #RERANK_QUERY} is a known type that can more efficiently be re-ranked
using a customized {@link
+ * DoubleValuesSourceRescorer}.
+ */
+ private static Rescorer createRescorer(
+ final Query reRankQuery, final double reRankWeight, final ReRankOperator
reRankOperator) {
+ assert null != reRankQuery;
+ return switch (reRankQuery) {
+ case FunctionQuery functionQuery -> new ReRankDoubleValuesSourceRescorer(
+ functionQuery.getValueSource().asDoubleValuesSource(), reRankWeight,
reRankOperator);
+ case FunctionScoreQuery functionQuery -> new
ReRankDoubleValuesSourceRescorer(
+ functionQuery.getSource(), reRankWeight, reRankOperator);
+ default -> new ReRankQueryRescorer(reRankQuery, reRankWeight,
reRankOperator);
+ };
+ }
+
private static class ReRankQParser extends QParser {
private boolean isExplainResults() {
@@ -135,7 +161,7 @@ public class ReRankQParserPlugin extends QParserPlugin {
reRankScale,
reRankScaleWeight,
reRankOperator,
- new ReRankQueryRescorer(reRankQuery, 1, ReRankOperator.REPLACE),
+ createRescorer(reRankQuery, 1, ReRankOperator.REPLACE),
explainResults);
if (reRankScaler.scaleScores()) {
@@ -148,6 +174,28 @@ public class ReRankQParserPlugin extends QParserPlugin {
}
}
+ private static final class ReRankDoubleValuesSourceRescorer extends
DoubleValuesSourceRescorer {
+ final DoubleBinaryOperator scoreCombiner;
+
+ public ReRankDoubleValuesSourceRescorer(
+ final DoubleValuesSource valuesSource,
+ final double reRankWeight,
+ final ReRankOperator reRankOperator) {
+ super(valuesSource);
+ this.scoreCombiner =
+ (score, value) -> reRankOperator.applyAsDouble(score, reRankWeight *
value);
+ }
+
+ @Override
+ protected float combine(
+ final float firstPassScore, final boolean valuePresent, final double
sourceValue) {
+ if (valuePresent) {
+ return (float) scoreCombiner.applyAsDouble(firstPassScore,
sourceValue);
+ }
+ return firstPassScore;
+ }
+ }
+
private static final class ReRankQueryRescorer extends QueryRescorer {
final BiFloatFunction scoreCombiner;
@@ -160,20 +208,8 @@ public class ReRankQParserPlugin extends QParserPlugin {
public ReRankQueryRescorer(
Query reRankQuery, double reRankWeight, ReRankOperator reRankOperator)
{
super(reRankQuery);
- switch (reRankOperator) {
- case ADD:
- scoreCombiner = (score, second) -> (float) (score + reRankWeight *
second);
- break;
- case MULTIPLY:
- scoreCombiner = (score, second) -> (float) (score * reRankWeight *
second);
- break;
- case REPLACE:
- scoreCombiner = (score, second) -> (float) (reRankWeight * second);
- break;
- default:
- scoreCombiner = null;
- throw new IllegalArgumentException("Unexpected: reRankOperator=" +
reRankOperator);
- }
+ scoreCombiner =
+ (score, second) -> (float) reRankOperator.applyAsDouble(score,
reRankWeight * second);
}
@Override
@@ -226,7 +262,7 @@ public class ReRankQParserPlugin extends QParserPlugin {
super(
defaultQuery,
reRankDocs,
- new ReRankQueryRescorer(reRankQuery, reRankWeight, reRankOperator),
+ createRescorer(reRankQuery, reRankWeight, reRankOperator),
reRankScaler,
reRankOperator);
this.reRankQuery = reRankQuery;
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 3a03e13fc65..1cf34fffeae 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,7 @@ 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.Rescorer;
import org.apache.lucene.search.ScoreDoc;
public class ReRankScaler {
@@ -35,7 +35,7 @@ public class ReRankScaler {
protected boolean explainResults;
protected ReRankOperator reRankOperator;
protected ReRankScalerExplain reRankScalerExplain;
- private QueryRescorer replaceRescorer;
+ private Rescorer replaceRescorer;
private Set<Integer> reRankSet;
private double reRankScaleWeight;
@@ -44,7 +44,7 @@ public class ReRankScaler {
String reRankScale,
double reRankScaleWeight,
ReRankOperator reRankOperator,
- QueryRescorer replaceRescorer,
+ Rescorer replaceRescorer,
boolean explainResults)
throws SyntaxError {
@@ -99,7 +99,7 @@ public class ReRankScaler {
}
}
- public QueryRescorer getReplaceRescorer() {
+ public Rescorer getReplaceRescorer() {
return replaceRescorer;
}
@@ -237,16 +237,7 @@ public class ReRankScaler {
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;
- }
+ return (float) reRankOperator.applyAsDouble(orginalScore,
reRankScaleWeight * reRankScore);
}
public static final class ReRankScalerExplain {
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 e6856a19e93..f3149af82bc 100644
--- a/solr/core/src/test/org/apache/solr/search/TestReRankQParserPlugin.java
+++ b/solr/core/src/test/org/apache/solr/search/TestReRankQParserPlugin.java
@@ -16,16 +16,24 @@
*/
package org.apache.solr.search;
+import static org.hamcrest.CoreMatchers.instanceOf;
+
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
+import org.apache.lucene.search.DoubleValuesSourceRescorer;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryRescorer;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.request.SolrRequestInfo;
+import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.util.SolrMetricTestUtils;
import org.junit.Before;
import org.junit.BeforeClass;
@@ -63,6 +71,66 @@ public class TestReRankQParserPlugin extends SolrTestCaseJ4 {
assertEquals(ReRankQParserPlugin.RERANK_OPERATOR, "reRankOperator");
}
+ public void testIntrospection() throws Exception {
+ final SolrQueryResponse rsp = new SolrQueryResponse();
+ try (SolrQueryRequest req = req(params("r_f", "{!func}field(test_ti)",
"r_q", "id:1^=10"))) {
+ SolrRequestInfo.setRequestInfo(new SolrRequestInfo(req, rsp));
+
+ { // Sanity check defaults w/simple rank query
+ final AbstractReRankQuery q = parseAndCast("{!rerank
reRankQuery=$r_q}", req);
+ assertEquals(ReRankQParserPlugin.RERANK_DOCS_DEFAULT,
q.getReRankDocs());
+ assertEquals(ReRankOperator.ADD, q.getReRankOperator());
+ assertThat(q.getRescorer(), instanceOf(QueryRescorer.class));
+ assertFalse(q.getReRankScaler().scaleScores());
+ }
+
+ { // Check defaults with function rank query (using an optimized value
source based rescorer)
+ final AbstractReRankQuery q = parseAndCast("{!rerank
reRankQuery=$r_f}", req);
+ assertEquals(ReRankQParserPlugin.RERANK_DOCS_DEFAULT,
q.getReRankDocs());
+ assertEquals(ReRankOperator.ADD, q.getReRankOperator());
+ assertThat(q.getRescorer(),
instanceOf(DoubleValuesSourceRescorer.class));
+ assertFalse(q.getReRankScaler().scaleScores());
+ }
+
+ { // check a re-ranker w/rescaling
+ final AbstractReRankQuery q =
+ parseAndCast(
+ "{!rerank reRankQuery=$r_q reRankOperator=replace
reRankScale='0-1'}", req);
+ assertEquals(ReRankQParserPlugin.RERANK_DOCS_DEFAULT,
q.getReRankDocs());
+ assertEquals(ReRankOperator.REPLACE, q.getReRankOperator());
+ assertThat(q.getRescorer(), instanceOf(QueryRescorer.class));
+ assertTrue(q.getReRankScaler().scaleScores());
+ assertTrue(q.getReRankScaler().scaleReRankScores());
+ assertFalse(q.getReRankScaler().scaleMainScores());
+ assertEquals(0, q.getReRankScaler().getReRankQueryMin());
+ assertEquals(1, q.getReRankScaler().getReRankQueryMax());
+ assertThat(q.getReRankScaler().getReplaceRescorer(),
instanceOf(QueryRescorer.class));
+ }
+
+ { // check a function re-ranker w/rescaling
+ final AbstractReRankQuery q =
+ parseAndCast(
+ "{!rerank reRankQuery=$r_f reRankOperator=multiply
reRankScale='1-2' reRankMainScale=0-3}",
+ req);
+ assertEquals(ReRankQParserPlugin.RERANK_DOCS_DEFAULT,
q.getReRankDocs());
+ assertEquals(ReRankOperator.MULTIPLY, q.getReRankOperator());
+ assertThat(q.getRescorer(),
instanceOf(DoubleValuesSourceRescorer.class));
+ assertTrue(q.getReRankScaler().scaleScores());
+ assertTrue(q.getReRankScaler().scaleReRankScores());
+ assertTrue(q.getReRankScaler().scaleMainScores());
+ assertEquals(1, q.getReRankScaler().getReRankQueryMin());
+ assertEquals(2, q.getReRankScaler().getReRankQueryMax());
+ assertEquals(0, q.getReRankScaler().getMainQueryMin());
+ assertEquals(3, q.getReRankScaler().getMainQueryMax());
+ assertThat(
+ q.getReRankScaler().getReplaceRescorer(),
instanceOf(DoubleValuesSourceRescorer.class));
+ }
+
+ } finally {
+ SolrRequestInfo.clearRequestInfo();
+ }
+ }
+
@Test
public void testRerankReturnOriginalScore() throws Exception {
@@ -115,7 +183,7 @@ public class TestReRankQParserPlugin extends SolrTestCaseJ4
{
+ ReRankQParserPlugin.RERANK_DOCS
+ "=200}");
params.add("q", "term_s:YYYY");
- params.add("rqq", "{!edismax bf=$bff}*:*");
+ params.add("rqq", random().nextBoolean() ? "{!edismax bf=$bff}*:*" :
"{!func}sum(1.0,$bff)");
params.add("bff", "field(test_ti)");
params.add("start", "0");
params.add("rows", "6");
@@ -189,7 +257,7 @@ public class TestReRankQParserPlugin extends SolrTestCaseJ4
{
+ ReRankQParserPlugin.RERANK_DOCS
+ "=200}");
params.add("q", "term_s:YYYY");
- params.add("rqq", "{!edismax bf=$bff}*:*");
+ params.add("rqq", random().nextBoolean() ? "{!edismax bf=$bff}*:*" :
"{!func}sum(1.0,$bff)");
params.add("bff", "field(test_ti)");
params.add("start", "0");
params.add("rows", "6");
@@ -252,7 +320,7 @@ public class TestReRankQParserPlugin extends SolrTestCaseJ4
{
+ ReRankQParserPlugin.RERANK_DOCS
+ "=200}");
params.add("q", "term_s:YYYY");
- params.add("rqq", "{!edismax bf=$bff}*:*");
+ params.add("rqq", random().nextBoolean() ? "{!edismax bf=$bff}*:*" :
"{!func}sum(1.0,$bff)");
params.add("bff", "field(test_ti)");
params.add("start", "0");
params.add("rows", "6");
@@ -290,7 +358,11 @@ public class TestReRankQParserPlugin extends
SolrTestCaseJ4 {
+ "=200}";
params.add("rq", rerankQueryByOp.apply(operation));
params.add("q", "term_s:YYYY^=0.1"); // force score=0.1
- params.add("rqq", "{!edismax bf=$bff}*:*"); // returns 1 + $bff
+ params.add(
+ "rqq",
+ random().nextBoolean()
+ ? "{!edismax bf=$bff}*:*"
+ : "{!func}sum(1.0,$bff)"); // returns 1 + $bff
params.add("bff", "field(test_ti)"); // test_ti=5000 for item 3
params.add("start", "0");
params.add("rows", "6");
@@ -1362,7 +1434,7 @@ public class TestReRankQParserPlugin extends
SolrTestCaseJ4 {
+ "=200}");
params.add("q", "term_t:YYYY");
params.add("fl", "id,score");
- params.add("rqq", "{!edismax bf=$bff}*:*");
+ params.add("rqq", random().nextBoolean() ? "{!edismax bf=$bff}*:*" :
"{!func}sum(1.0,$bff)");
params.add("bff", "field(test_ti)");
params.add("start", "0");
params.add("rows", "6");
@@ -1403,7 +1475,7 @@ public class TestReRankQParserPlugin extends
SolrTestCaseJ4 {
+ "=200}");
params.add("q", "term_t:YYYY");
params.add("fl", "id,score");
- params.add("rqq", "{!edismax bf=$bff}*:*");
+ params.add("rqq", random().nextBoolean() ? "{!edismax bf=$bff}*:*" :
"{!func}sum(1.0,$bff)");
params.add("bff", "field(test_ti)");
params.add("start", "0");
params.add("rows", "4");
@@ -1482,7 +1554,7 @@ public class TestReRankQParserPlugin extends
SolrTestCaseJ4 {
+ "=4}");
params.add("q", "term_t:YYYY");
params.add("fl", "id,score");
- params.add("rqq", "{!edismax bf=$bff}*:*");
+ params.add("rqq", random().nextBoolean() ? "{!edismax bf=$bff}*:*" :
"{!func}sum(1.0,$bff)");
params.add("bff", "field(test_ti)");
params.add("start", "0");
params.add("rows", "6");
@@ -1523,7 +1595,7 @@ public class TestReRankQParserPlugin extends
SolrTestCaseJ4 {
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("rqq", random().nextBoolean() ? "{!edismax bf=$bff}*:*" :
"{!func}sum(1.0,$bff)");
params.add("bff", "field(test_ti)");
params.add("start", "0");
params.add("rows", "6");
@@ -1556,7 +1628,7 @@ public class TestReRankQParserPlugin extends
SolrTestCaseJ4 {
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("rqq", random().nextBoolean() ? "{!edismax bf=$bff}*:*" :
"{!func}sum(1.0,$bff)");
params.add("bff", "field(test_ti)");
params.add("start", "0");
params.add("rows", "6");
@@ -1596,7 +1668,7 @@ public class TestReRankQParserPlugin extends
SolrTestCaseJ4 {
+ "=4}");
params.add("q", "term_t:YYYY");
params.add("fl", "id,score");
- params.add("rqq", "{!edismax bf=$bff}*:*");
+ params.add("rqq", random().nextBoolean() ? "{!edismax bf=$bff}*:*" :
"{!func}sum(1.0,$bff)");
params.add("bff", "field(test_ti)");
params.add("start", "0");
params.add("rows", "6");
@@ -1744,4 +1816,11 @@ public class TestReRankQParserPlugin extends
SolrTestCaseJ4 {
assertTrue(explainResponse.contains("10.0 = scaled main query score
between: 10-20"));
}
+
+ private static AbstractReRankQuery parseAndCast(final String query, final
SolrQueryRequest req)
+ throws Exception {
+ final Query q = QParser.getParser(query, req).getQuery();
+ assertThat(q, instanceOf(AbstractReRankQuery.class));
+ return (AbstractReRankQuery) q;
+ }
}