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

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


The following commit(s) were added to refs/heads/branch_10x by this push:
     new f52271e35d9 SOLR-18328 Add support for standard deviation in rollup 
for streaming expressions (#4691)
f52271e35d9 is described below

commit f52271e35d9753a976de2108331d839d77dde6b7
Author: Khush Jain <[email protected]>
AuthorDate: Mon Aug 10 10:39:15 2026 -0400

    SOLR-18328 Add support for standard deviation in rollup for streaming 
expressions (#4691)
    
    (cherry picked from commit 5c28547dd82775b387b4a6ffa75fb5672318ce85)
---
 .../SOLR-18328-support-std-in-rollup.yml           |  7 +++
 .../pages/stream-decorator-reference.adoc          |  5 +-
 .../client/solrj/io/stream/metrics/StdMetric.java  | 43 +++++++++++++----
 .../solrj/io/stream/StreamExpressionTest.java      | 56 ++++++++++++++++++++++
 .../solr/client/solrj/io/stream/StreamingTest.java | 15 ++++++
 5 files changed, 114 insertions(+), 12 deletions(-)

diff --git a/changelog/unreleased/SOLR-18328-support-std-in-rollup.yml 
b/changelog/unreleased/SOLR-18328-support-std-in-rollup.yml
new file mode 100644
index 00000000000..dbaf0175293
--- /dev/null
+++ b/changelog/unreleased/SOLR-18328-support-std-in-rollup.yml
@@ -0,0 +1,7 @@
+title: "Support 'std' (standard deviation) metric in rollup for streaming 
expressions"
+type: added
+authors:
+  - name: khushjain
+links:
+  - name: SOLR-18328
+    url: https://issues.apache.org/jira/browse/SOLR-18328
diff --git 
a/solr/solr-ref-guide/modules/query-guide/pages/stream-decorator-reference.adoc 
b/solr/solr-ref-guide/modules/query-guide/pages/stream-decorator-reference.adoc
index 9cbc8e440be..d9cadb16c49 100644
--- 
a/solr/solr-ref-guide/modules/query-guide/pages/stream-decorator-reference.adoc
+++ 
b/solr/solr-ref-guide/modules/query-guide/pages/stream-decorator-reference.adoc
@@ -1448,7 +1448,7 @@ For faster aggregation over low to moderate cardinality 
fields, the `facet` func
 * `StreamExpression` (Mandatory)
 * `over`: (Mandatory) A list of fields to group by.
 * `metrics`: (Mandatory) The list of metrics to compute.
-Currently supported metrics are `sum(col)`, `avg(col)`, `min(col)`, 
`max(col)`, `count(*)`, `missing(col)`, `countDist(col)`, `per(col, 
percentile)`.
+Currently supported metrics are `sum(col)`, `avg(col)`, `min(col)`, 
`max(col)`, `count(*)`, `missing(col)`, `countDist(col)`, `per(col, 
percentile)`, `std(col)`.
 
 === rollup Syntax
 
@@ -1469,7 +1469,8 @@ rollup(
    missing(a_i),
    countDist(a_i),
    per(a_i, 50),
-   per(a_f, 75)
+   per(a_f, 75),
+   std(a_i)
 )
 ----
 
diff --git 
a/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/stream/metrics/StdMetric.java
 
b/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/stream/metrics/StdMetric.java
index 57ca2b08cac..697e638406f 100644
--- 
a/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/stream/metrics/StdMetric.java
+++ 
b/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/stream/metrics/StdMetric.java
@@ -23,16 +23,15 @@ import 
org.apache.solr.client.solrj.io.stream.expr.StreamExpression;
 import org.apache.solr.client.solrj.io.stream.expr.StreamExpressionParameter;
 import org.apache.solr.client.solrj.io.stream.expr.StreamFactory;
 
+/**
+ * Metric that computes the sample standard deviation of a numeric column over 
a stream. Consistent
+ * with the {@code std} streaming evaluator.
+ */
 public class StdMetric extends Metric {
-  // How'd the MeanMetric get to be so mean?
-  // Maybe it was born with it.
-  // Maybe it was mayba-mean.
-  //
-  // I'll see myself out.
 
   private String columnName;
-  private double doubleSum;
-  private long longSum;
+  private double sum;
+  private double sumSq;
   private long count;
 
   public StdMetric(String columnName) {
@@ -75,11 +74,28 @@ public class StdMetric extends Metric {
   }
 
   @Override
-  public void update(Tuple tuple) {}
+  public void update(Tuple tuple) {
+    Object o = tuple.get(columnName);
+    double val;
+    if (o instanceof Double d) {
+      val = d;
+    } else if (o instanceof Float f) {
+      val = f.doubleValue();
+    } else if (o instanceof Integer i) {
+      val = i.doubleValue();
+    } else if (o instanceof Long l) {
+      val = l.doubleValue();
+    } else {
+      return;
+    }
+    ++count;
+    sum += val;
+    sumSq += val * val;
+  }
 
   @Override
   public Metric newInstance() {
-    return new MeanMetric(columnName, outputLong);
+    return new StdMetric(columnName, outputLong);
   }
 
   @Override
@@ -87,9 +103,16 @@ public class StdMetric extends Metric {
     return new String[] {columnName};
   }
 
+  /** Returns the sample standard deviation of the values seen so far. */
   @Override
   public Number getValue() {
-    return null;
+    double std =
+        count <= 1 ? 0.0d : Math.sqrt(((count * sumSq) - (sum * sum)) / (count 
* (count - 1.0D)));
+    if (outputLong) {
+      return Math.round(std);
+    } else {
+      return std;
+    }
   }
 
   @Override
diff --git 
a/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/stream/StreamExpressionTest.java
 
b/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/stream/StreamExpressionTest.java
index 4ef5a73ccef..de80b4ae728 100644
--- 
a/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/stream/StreamExpressionTest.java
+++ 
b/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/stream/StreamExpressionTest.java
@@ -1333,6 +1333,62 @@ public class StreamExpressionTest extends 
SolrCloudTestCase {
     assertEquals(saf, 18, 0);
   }
 
+  @Test
+  public void testRollupStdMetric() throws Exception {
+    new UpdateRequest()
+        .add(id, "0", "a_s", "hello0", "a_i", "0", "a_f", "1")
+        .add(id, "2", "a_s", "hello0", "a_i", "2", "a_f", "2")
+        .add(id, "3", "a_s", "hello3", "a_i", "3", "a_f", "3")
+        .add(id, "4", "a_s", "hello4", "a_i", "4", "a_f", "4")
+        .add(id, "1", "a_s", "hello0", "a_i", "1", "a_f", "5")
+        .add(id, "5", "a_s", "hello3", "a_i", "10", "a_f", "6")
+        .add(id, "6", "a_s", "hello4", "a_i", "11", "a_f", "7")
+        .add(id, "7", "a_s", "hello3", "a_i", "12", "a_f", "8")
+        .add(id, "8", "a_s", "hello3", "a_i", "13", "a_f", "9")
+        .add(id, "9", "a_s", "hello0", "a_i", "14", "a_f", "10")
+        .commit(cluster.getSolrClient(), COLLECTIONORALIAS);
+
+    ModifiableSolrParams paramsLoc = new ModifiableSolrParams();
+    String expr =
+        "rollup("
+            + "  search(collection1, q=*:*, fl=\"a_s,a_i,a_f\", sort=\"a_s 
asc\", qt=\"/export\"),"
+            + "  over=\"a_s\", std(a_i), std(a_f), count(*)"
+            + ")";
+    paramsLoc.set("expr", expr);
+    paramsLoc.set("qt", "/stream");
+
+    String url =
+        cluster.getJettySolrRunners().get(0).getBaseUrl().toString() + "/" + 
COLLECTIONORALIAS;
+    TupleStream solrStream = new SolrStream(url, paramsLoc);
+
+    StreamContext context = new StreamContext();
+    solrStream.setStreamContext(context);
+    List<Tuple> tuples = getTuples(solrStream);
+
+    assertEquals(3, tuples.size());
+
+    // hello0: a_i = [0, 1, 2, 14], a_f = [1, 2, 5, 10]
+    Tuple tuple = tuples.get(0);
+    assertEquals("hello0", tuple.getString("a_s"));
+    assertEquals(6.5511, tuple.getDouble("std(a_i)"), 0.001);
+    assertEquals(4.0415, tuple.getDouble("std(a_f)"), 0.001);
+    assertEquals(4, tuple.getDouble("count(*)"), 0.0);
+
+    // hello3: a_i = [3, 10, 12, 13], a_f = [3, 6, 8, 9]
+    tuple = tuples.get(1);
+    assertEquals("hello3", tuple.getString("a_s"));
+    assertEquals(4.5092, tuple.getDouble("std(a_i)"), 0.001);
+    assertEquals(2.6458, tuple.getDouble("std(a_f)"), 0.001);
+    assertEquals(4, tuple.getDouble("count(*)"), 0.0);
+
+    // hello4: a_i = [4, 11], a_f = [4, 7]
+    tuple = tuples.get(2);
+    assertEquals("hello4", tuple.getString("a_s"));
+    assertEquals(4.9497, tuple.getDouble("std(a_i)"), 0.001);
+    assertEquals(2.1213, tuple.getDouble("std(a_f)"), 0.001);
+    assertEquals(2, tuple.getDouble("count(*)"), 0.0);
+  }
+
   @Test
   public void testFacetStream() throws Exception {
 
diff --git 
a/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/stream/StreamingTest.java
 
b/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/stream/StreamingTest.java
index 2ce5c3d7afd..31bf75c7b18 100644
--- 
a/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/stream/StreamingTest.java
+++ 
b/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/stream/StreamingTest.java
@@ -51,6 +51,7 @@ import org.apache.solr.client.solrj.io.stream.metrics.Metric;
 import org.apache.solr.client.solrj.io.stream.metrics.MinMetric;
 import org.apache.solr.client.solrj.io.stream.metrics.MissingMetric;
 import org.apache.solr.client.solrj.io.stream.metrics.PercentileMetric;
+import org.apache.solr.client.solrj.io.stream.metrics.StdMetric;
 import org.apache.solr.client.solrj.io.stream.metrics.SumMetric;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
 import org.apache.solr.client.solrj.request.UpdateRequest;
@@ -1676,6 +1677,8 @@ public class StreamingTest extends SolrCloudTestCase {
         new MaxMetric("a_f"),
         new MeanMetric("a_i"),
         new MeanMetric("a_f"),
+        new StdMetric("a_i"),
+        new StdMetric("a_f"),
         new CountMetric(),
         new MissingMetric("b_f"),
         new CountDistinctMetric("a_i"),
@@ -1700,6 +1703,8 @@ public class StreamingTest extends SolrCloudTestCase {
       Double maxf = tuple.getDouble("max(a_f)");
       Double avgi = tuple.getDouble("avg(a_i)");
       Double avgf = tuple.getDouble("avg(a_f)");
+      Double stdi = tuple.getDouble("std(a_i)");
+      Double stdf = tuple.getDouble("std(a_f)");
       Double count = tuple.getDouble("count(*)");
       Double missingBf = tuple.getDouble("missing(b_f)");
       Double countDistI = tuple.getDouble("countDist(a_i)");
@@ -1714,6 +1719,8 @@ public class StreamingTest extends SolrCloudTestCase {
       assertEquals(10, maxf, 0.001);
       assertEquals(4.25, avgi, 0.001);
       assertEquals(4.5, avgf, 0.001);
+      assertEquals(6.5511, stdi, 0.001);
+      assertEquals(4.0415, stdf, 0.001);
       assertEquals(4, count, 0.001);
       assertEquals(2, missingBf, 0.001);
       assertEquals(4, countDistI, 0.001);
@@ -1729,6 +1736,8 @@ public class StreamingTest extends SolrCloudTestCase {
       maxf = tuple.getDouble("max(a_f)");
       avgi = tuple.getDouble("avg(a_i)");
       avgf = tuple.getDouble("avg(a_f)");
+      stdi = tuple.getDouble("std(a_i)");
+      stdf = tuple.getDouble("std(a_f)");
       count = tuple.getDouble("count(*)");
       missingBf = tuple.getDouble("missing(b_f)");
       countDistI = tuple.getDouble("countDist(a_i)");
@@ -1743,6 +1752,8 @@ public class StreamingTest extends SolrCloudTestCase {
       assertEquals(9, maxf, 0.001);
       assertEquals(9.5, avgi, 0.001);
       assertEquals(6.5, avgf, 0.001);
+      assertEquals(4.5092, stdi, 0.001);
+      assertEquals(2.6458, stdf, 0.001);
       assertEquals(4, count, 0.001);
       assertEquals(3, missingBf, 0.001);
       assertEquals(4, countDistI, 0.001);
@@ -1758,6 +1769,8 @@ public class StreamingTest extends SolrCloudTestCase {
       maxf = tuple.getDouble("max(a_f)");
       avgi = tuple.getDouble("avg(a_i)");
       avgf = tuple.getDouble("avg(a_f)");
+      stdi = tuple.getDouble("std(a_i)");
+      stdf = tuple.getDouble("std(a_f)");
       count = tuple.getDouble("count(*)");
       missingBf = tuple.getDouble("missing(b_f)");
       countDistI = tuple.getDouble("countDist(a_i)");
@@ -1772,6 +1785,8 @@ public class StreamingTest extends SolrCloudTestCase {
       assertEquals(7, maxf, 0.01);
       assertEquals(7.5, avgi, 0.01);
       assertEquals(5.5, avgf, 0.01);
+      assertEquals(4.9497, stdi, 0.01);
+      assertEquals(2.1213, stdf, 0.01);
       assertEquals(2, count, 0.01);
       assertEquals(0, missingBf, 0.01);
       assertEquals(2, countDistI, 0.01);

Reply via email to