diff --git a/changelog/unreleased/matchset_scale-function.yml b/changelog/unreleased/matchset_scale-function.yml
new file mode 100644
index 000000000000..1e1674daae22
--- /dev/null
+++ b/changelog/unreleased/matchset_scale-function.yml
@@ -0,0 +1,5 @@
+title: Add matchset_scale function — a matching-set-scoped variant of scale that avoids the full-index traversal for narrowly filtered queries
+type: added
+authors:
+ - name: Gaurav Jayswal
+ nick: gauravjayswal
diff --git a/solr/core/src/java/org/apache/solr/search/ValueSourceParser.java b/solr/core/src/java/org/apache/solr/search/ValueSourceParser.java
index 04e17b78d8e5..67aaae4632c9 100644
--- a/solr/core/src/java/org/apache/solr/search/ValueSourceParser.java
+++ b/solr/core/src/java/org/apache/solr/search/ValueSourceParser.java
@@ -105,6 +105,7 @@
import org.apache.solr.search.function.ConcatStringFunction;
import org.apache.solr.search.function.DualDoubleFunction;
import org.apache.solr.search.function.EqualFunction;
+import org.apache.solr.search.function.MatchSetScaleFloatFunction;
import org.apache.solr.search.function.OrdFieldSource;
import org.apache.solr.search.function.ReverseOrdFieldSource;
import org.apache.solr.search.function.SolrComparisonBoolFunction;
@@ -262,6 +263,17 @@ public ValueSource parse(FunctionQParser fp) throws SyntaxError {
return new ScaleFloatFunction(source, min, max);
}
});
+ addParser(
+ "matchset_scale",
+ new ValueSourceParser() {
+ @Override
+ public ValueSource parse(FunctionQParser fp) throws SyntaxError {
+ ValueSource source = fp.parseValueSource();
+ float min = fp.parseFloat();
+ float max = fp.parseFloat();
+ return new MatchSetScaleFloatFunction(source, min, max);
+ }
+ });
addParser(
"div",
new ValueSourceParser() {
diff --git a/solr/core/src/java/org/apache/solr/search/function/MatchSetScaleFloatFunction.java b/solr/core/src/java/org/apache/solr/search/function/MatchSetScaleFloatFunction.java
new file mode 100644
index 000000000000..7b3fc8fed983
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/search/function/MatchSetScaleFloatFunction.java
@@ -0,0 +1,237 @@
+/*
+ * 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.function;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.index.ReaderUtil;
+import org.apache.lucene.queries.function.FunctionValues;
+import org.apache.lucene.queries.function.ValueSource;
+import org.apache.lucene.queries.function.docvalues.FloatDocValues;
+import org.apache.lucene.search.DocIdSetIterator;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.Query;
+import org.apache.solr.handler.component.ResponseBuilder;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.request.SolrRequestInfo;
+import org.apache.solr.search.DocSet;
+import org.apache.solr.search.SolrIndexSearcher;
+
+/**
+ * Linearly scales {@code source} into {@code [targetMin, targetMax]} using the observed min/max of
+ * {@code source} over the current request's matching DocSet.
+ *
+ *
Differs from Lucene's {@code ScaleFloatFunction} in two ways:
+ *
+ *
+ *
Bounds are computed over only the request's matching set (intersection of {@code q} and all
+ * {@code fq}s), not every doc in every segment. For narrowly filtered queries this can be
+ * orders of magnitude faster.
+ *
Output is clamped to {@code [targetMin, targetMax]}.
+ *
+ *
+ *
Falls back to a full index scan when a Solr request context is not available — e.g. when
+ * invoked from Lucene-level tests or embedded tool usage.
+ */
+public class MatchSetScaleFloatFunction extends ValueSource {
+ protected final ValueSource source;
+ protected final float targetMin;
+ protected final float targetMax;
+
+ public MatchSetScaleFloatFunction(ValueSource source, float targetMin, float targetMax) {
+ this.source = source;
+ this.targetMin = targetMin;
+ this.targetMax = targetMax;
+ }
+
+ @Override
+ public String description() {
+ return "matchset_scale(" + source.description() + "," + targetMin + "," + targetMax + ")";
+ }
+
+ private static final class Bounds {
+ float min;
+ float max;
+ }
+
+ @Override
+ public void createWeight(Map