Skip to content

Commit f674862

Browse files
KhushJainepugh
andauthored
SOLR-18221 Add support for percentile in rollup for streaming expressions (#4504)
Co-authored-by: Eric Pugh <epugh@opensourceconnections.com>
1 parent 12bf80a commit f674862

7 files changed

Lines changed: 171 additions & 25 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
title: "Support 'percentile' (per) metric in rollup for streaming expressions"
2+
type: added
3+
authors:
4+
- name: khushjain
5+
links:
6+
- name: SOLR-18221
7+
url: https://issues.apache.org/jira/browse/SOLR-18221

solr/core/gradle.lockfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ com.ibm.icu:icu4j:77.1=jarValidation,testRuntimeClasspath
3434
com.j256.simplemagic:simplemagic:1.17=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath
3535
com.jayway.jsonpath:json-path:2.9.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath
3636
com.microsoft.onnxruntime:onnxruntime:1.26.0=jarValidation,testRuntimeClasspath
37-
com.tdunning:t-digest:3.3=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath
37+
com.tdunning:t-digest:3.3=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath
3838
commons-cli:commons-cli:1.11.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath
3939
commons-codec:commons-codec:1.21.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath
4040
commons-io:commons-io:2.21.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath

solr/solr-ref-guide/modules/query-guide/pages/stream-decorator-reference.adoc

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1448,7 +1448,7 @@ For faster aggregation over low to moderate cardinality fields, the `facet` func
14481448
* `StreamExpression` (Mandatory)
14491449
* `over`: (Mandatory) A list of fields to group by.
14501450
* `metrics`: (Mandatory) The list of metrics to compute.
1451-
Currently supported metrics are `sum(col)`, `avg(col)`, `min(col)`, `max(col)`, `count(*)`, `missing(col)`, `countDist(col)`.
1451+
Currently supported metrics are `sum(col)`, `avg(col)`, `min(col)`, `max(col)`, `count(*)`, `missing(col)`, `countDist(col)`, `per(col, percentile)`.
14521452

14531453
=== rollup Syntax
14541454

@@ -1467,7 +1467,9 @@ rollup(
14671467
avg(a_f),
14681468
count(*),
14691469
missing(a_i),
1470-
countDist(a_i)
1470+
countDist(a_i),
1471+
per(a_i, 50),
1472+
per(a_f, 75)
14711473
)
14721474
----
14731475

solr/solrj-streaming/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ dependencies {
2525
// declare dependencies we use even though already declared by solrj-core
2626
implementation libs.slf4j.api
2727
implementation libs.apache.commons.math3
28+
implementation libs.tdunning.tdigest
2829

2930
testImplementation project(':solr:test-framework')
3031
testImplementation project(':solr:core')

solr/solrj-streaming/gradle.lockfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ com.google.protobuf:protobuf-java:3.25.8=annotationProcessor,errorprone,jarValid
3030
com.googlecode.json-simple:json-simple:1.1.1=jarValidation,testRuntimeClasspath
3131
com.j256.simplemagic:simplemagic:1.17=jarValidation,testRuntimeClasspath
3232
com.jayway.jsonpath:json-path:2.9.0=jarValidation,testRuntimeClasspath
33-
com.tdunning:t-digest:3.3=jarValidation,testRuntimeClasspath
33+
com.tdunning:t-digest:3.3=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
3434
commons-cli:commons-cli:1.11.0=jarValidation,testRuntimeClasspath
3535
commons-codec:commons-codec:1.21.0=jarValidation,testRuntimeClasspath
3636
commons-io:commons-io:2.21.0=jarValidation,testCompileClasspath,testRuntimeClasspath

solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/stream/metrics/PercentileMetric.java

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
package org.apache.solr.client.solrj.io.stream.metrics;
1818

19+
import com.tdunning.math.stats.AVLTreeDigest;
1920
import java.io.IOException;
2021
import java.util.Locale;
2122
import org.apache.solr.client.solrj.io.Tuple;
@@ -24,27 +25,27 @@
2425
import org.apache.solr.client.solrj.io.stream.expr.StreamFactory;
2526

2627
public class PercentileMetric extends Metric {
27-
private long longMax = -Long.MIN_VALUE;
28-
private double doubleMax = -Double.MAX_VALUE;
29-
private String columnName;
3028

31-
public PercentileMetric(String columnName, int percentile) {
29+
private static final int COMPRESSION = 100;
30+
31+
private String columnName;
32+
private double percentile;
33+
private AVLTreeDigest digest;
3234

35+
public PercentileMetric(String columnName, double percentile) {
3336
init("per", columnName, percentile);
3437
}
3538

3639
public PercentileMetric(StreamExpression expression, StreamFactory factory) throws IOException {
37-
// grab all parameters out
3840
String functionName = expression.getFunctionName();
3941
String columnName = factory.getValueOperand(expression, 0);
40-
int percentile = Integer.parseInt(factory.getValueOperand(expression, 1));
42+
String percentileStr = factory.getValueOperand(expression, 1);
4143

42-
// validate expression contains only what we want.
4344
if (null == columnName) {
4445
throw new IOException(
4546
String.format(
4647
Locale.ROOT,
47-
"Invalid expression %s - expected %s(columnName)",
48+
"Invalid expression %s - expected %s(columnName, percentile)",
4849
expression,
4950
functionName));
5051
}
@@ -53,39 +54,64 @@ public PercentileMetric(StreamExpression expression, StreamFactory factory) thro
5354
String.format(Locale.ROOT, "Invalid expression %s - unknown operands found", expression));
5455
}
5556

57+
double percentile = Double.parseDouble(percentileStr);
5658
init(functionName, columnName, percentile);
5759
}
5860

59-
private void init(String functionName, String columnName, int percentile) {
61+
private void init(String functionName, String columnName, double percentile) {
62+
if (percentile < 0 || percentile > 100) {
63+
throw new IllegalArgumentException("percentile must be between 0 and 100, got " + percentile);
64+
}
6065
this.columnName = columnName;
66+
this.percentile = percentile;
67+
this.digest = new AVLTreeDigest(COMPRESSION);
6168
setFunctionName(functionName);
62-
setIdentifier(functionName, "(", columnName, "," + percentile, ")");
69+
setIdentifier(functionName, "(", columnName, "," + formatPercentile(percentile), ")");
6370
}
6471

65-
@Override
66-
public Number getValue() {
67-
if (longMax == Long.MIN_VALUE) {
68-
return doubleMax;
69-
} else {
70-
return longMax;
72+
private static String formatPercentile(double percentile) {
73+
if (percentile == Math.floor(percentile) && !Double.isInfinite(percentile)) {
74+
return Integer.toString((int) percentile);
7175
}
76+
return String.valueOf(percentile);
7277
}
7378

7479
@Override
75-
public String[] getColumns() {
76-
return new String[] {columnName};
80+
public void update(Tuple tuple) {
81+
Object o = tuple.get(columnName);
82+
if (o instanceof Double d) {
83+
digest.add(d);
84+
} else if (o instanceof Float f) {
85+
digest.add(f.doubleValue());
86+
} else if (o instanceof Integer i) {
87+
digest.add(i.doubleValue());
88+
} else if (o instanceof Long l) {
89+
digest.add(l.doubleValue());
90+
}
7791
}
7892

7993
@Override
80-
public void update(Tuple tuple) {}
94+
public Number getValue() {
95+
if (digest.size() == 0) {
96+
return null;
97+
}
98+
return digest.quantile(percentile / 100.0);
99+
}
81100

82101
@Override
83102
public Metric newInstance() {
84-
return new MaxMetric(columnName);
103+
return new PercentileMetric(columnName, percentile);
104+
}
105+
106+
@Override
107+
public String[] getColumns() {
108+
return new String[] {columnName};
85109
}
86110

87111
@Override
88112
public StreamExpressionParameter toExpression(StreamFactory factory) throws IOException {
89-
return new StreamExpression(getFunctionName()).withParameter(columnName);
113+
return new StreamExpression(getFunctionName())
114+
.withParameter(columnName)
115+
.withParameter(formatPercentile(percentile));
90116
}
91117
}

solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/stream/StreamingTest.java

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.apache.solr.client.solrj.io.comp.MultipleFieldComparator;
4040
import org.apache.solr.client.solrj.io.eq.FieldEqualitor;
4141
import org.apache.solr.client.solrj.io.ops.GroupOperation;
42+
import org.apache.solr.client.solrj.io.stream.expr.StreamExpressionParameter;
4243
import org.apache.solr.client.solrj.io.stream.expr.StreamExpressionParser;
4344
import org.apache.solr.client.solrj.io.stream.expr.StreamFactory;
4445
import org.apache.solr.client.solrj.io.stream.metrics.Bucket;
@@ -49,6 +50,7 @@
4950
import org.apache.solr.client.solrj.io.stream.metrics.Metric;
5051
import org.apache.solr.client.solrj.io.stream.metrics.MinMetric;
5152
import org.apache.solr.client.solrj.io.stream.metrics.MissingMetric;
53+
import org.apache.solr.client.solrj.io.stream.metrics.PercentileMetric;
5254
import org.apache.solr.client.solrj.io.stream.metrics.SumMetric;
5355
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
5456
import org.apache.solr.client.solrj.request.UpdateRequest;
@@ -1855,6 +1857,114 @@ public void testRollupStream() throws Exception {
18551857
}
18561858
}
18571859

1860+
@Test
1861+
public void testPercentileRollupStream() throws Exception {
1862+
1863+
helloDocsUpdateRequest.commit(cluster.getSolrClient(), COLLECTIONORALIAS);
1864+
StreamContext streamContext = new StreamContext();
1865+
SolrClientCache solrClientCache = new SolrClientCache();
1866+
streamContext.setSolrClientCache(solrClientCache);
1867+
1868+
try {
1869+
// Test percentile metrics in RollupStream
1870+
SolrParams sParamsA = params("q", "*:*", "fl", "a_s,a_i,a_f", "sort", "a_s asc");
1871+
CloudSolrStream stream = new CloudSolrStream(solrConnection, COLLECTIONORALIAS, sParamsA);
1872+
1873+
Bucket[] buckets = {new Bucket("a_s")};
1874+
1875+
Metric[] metrics = {
1876+
new PercentileMetric("a_i", 50),
1877+
new PercentileMetric("a_f", 50),
1878+
new PercentileMetric("a_i", 0),
1879+
new PercentileMetric("a_i", 100),
1880+
new PercentileMetric("a_i", 99.9),
1881+
new MinMetric("a_i"),
1882+
new MaxMetric("a_i"),
1883+
};
1884+
1885+
RollupStream rollupStream = new RollupStream(stream, buckets, metrics);
1886+
rollupStream.setStreamContext(streamContext);
1887+
List<Tuple> tuples = getTuples(rollupStream);
1888+
1889+
assertEquals(3, tuples.size());
1890+
1891+
// hello0: a_i = [0, 1, 2, 14], a_f = [1, 2, 5, 10]
1892+
Tuple tuple = tuples.get(0);
1893+
assertEquals("hello0", tuple.getString("a_s"));
1894+
Double peri50 = tuple.getDouble("per(a_i,50)");
1895+
Double perf50 = tuple.getDouble("per(a_f,50)");
1896+
Double peri0 = tuple.getDouble("per(a_i,0)");
1897+
Double peri100 = tuple.getDouble("per(a_i,100)");
1898+
Double peri999 = tuple.getDouble("per(a_i,99.9)");
1899+
Double mini = tuple.getDouble("min(a_i)");
1900+
Double maxi = tuple.getDouble("max(a_i)");
1901+
1902+
assertNotNull(peri50);
1903+
assertNotNull(perf50);
1904+
// 0th percentile should approximate min
1905+
assertEquals(mini, peri0, 0.5);
1906+
// 100th percentile should approximate max
1907+
assertEquals(maxi, peri100, 0.5);
1908+
// 99.9th percentile should be close to max
1909+
assertNotNull(peri999);
1910+
1911+
// hello3: a_i = [3, 10, 12, 13], a_f = [3, 6, 8, 9]
1912+
tuple = tuples.get(1);
1913+
assertEquals("hello3", tuple.getString("a_s"));
1914+
peri50 = tuple.getDouble("per(a_i,50)");
1915+
perf50 = tuple.getDouble("per(a_f,50)");
1916+
peri0 = tuple.getDouble("per(a_i,0)");
1917+
peri100 = tuple.getDouble("per(a_i,100)");
1918+
mini = tuple.getDouble("min(a_i)");
1919+
maxi = tuple.getDouble("max(a_i)");
1920+
1921+
assertNotNull(peri50);
1922+
assertNotNull(perf50);
1923+
assertEquals(mini, peri0, 0.5);
1924+
assertEquals(maxi, peri100, 0.5);
1925+
1926+
// hello4: a_i = [4, 11], a_f = [4, 7]
1927+
tuple = tuples.get(2);
1928+
assertEquals("hello4", tuple.getString("a_s"));
1929+
peri50 = tuple.getDouble("per(a_i,50)");
1930+
perf50 = tuple.getDouble("per(a_f,50)");
1931+
peri0 = tuple.getDouble("per(a_i,0)");
1932+
peri100 = tuple.getDouble("per(a_i,100)");
1933+
mini = tuple.getDouble("min(a_i)");
1934+
maxi = tuple.getDouble("max(a_i)");
1935+
1936+
assertNotNull(peri50);
1937+
assertNotNull(perf50);
1938+
assertEquals(mini, peri0, 0.5);
1939+
assertEquals(maxi, peri100, 0.5);
1940+
1941+
// Test toExpression round-trip
1942+
PercentileMetric pm = new PercentileMetric("a_i", 50);
1943+
StreamExpressionParameter expr = pm.toExpression(streamFactory);
1944+
assertEquals("per(a_i,50)", expr.toString());
1945+
1946+
pm = new PercentileMetric("a_i", 99.9);
1947+
expr = pm.toExpression(streamFactory);
1948+
assertEquals("per(a_i,99.9)", expr.toString());
1949+
1950+
// Test HashRollupStream with percentile
1951+
stream = new CloudSolrStream(solrConnection, COLLECTIONORALIAS, sParamsA);
1952+
Metric[] hashMetrics = {new PercentileMetric("a_i", 50), new PercentileMetric("a_f", 50)};
1953+
1954+
HashRollupStream hashRollupStream = new HashRollupStream(stream, buckets, hashMetrics);
1955+
hashRollupStream.setStreamContext(streamContext);
1956+
tuples = getTuples(hashRollupStream);
1957+
1958+
assertEquals(3, tuples.size());
1959+
for (Tuple t : tuples) {
1960+
assertNotNull(t.getDouble("per(a_i,50)"));
1961+
assertNotNull(t.getDouble("per(a_f,50)"));
1962+
}
1963+
} finally {
1964+
solrClientCache.close();
1965+
}
1966+
}
1967+
18581968
@Test
18591969
public void testDaemonTopicStream() throws Exception {
18601970
Assume.assumeTrue(!useAlias);

0 commit comments

Comments
 (0)