Skip to content

Commit 32bd540

Browse files
sxhinzvcchristophstrobl
authored andcommitted
Add support for $percentile aggregation operator.
Closes #4473 Original Pull Request: #4496
1 parent 11b992c commit 32bd540

File tree

9 files changed

+216
-2
lines changed

9 files changed

+216
-2
lines changed

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperators.java

+105
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515
*/
1616
package org.springframework.data.mongodb.core.aggregation;
1717

18+
import java.util.Arrays;
1819
import java.util.Collections;
20+
import java.util.HashMap;
1921
import java.util.List;
22+
import java.util.Map;
2023

2124
import org.bson.Document;
2225
import org.springframework.util.Assert;
@@ -25,6 +28,7 @@
2528
* Gateway to {@literal accumulator} aggregation operations.
2629
*
2730
* @author Christoph Strobl
31+
* @author Julia Lee
2832
* @since 1.10
2933
* @soundtrack Rage Against The Machine - Killing In The Name
3034
*/
@@ -52,6 +56,7 @@ public static AccumulatorOperatorFactory valueOf(AggregationExpression expressio
5256

5357
/**
5458
* @author Christoph Strobl
59+
* @author Julia Lee
5560
*/
5661
public static class AccumulatorOperatorFactory {
5762

@@ -246,6 +251,20 @@ public ExpMovingAvg alpha(double exponentialDecayValue) {
246251
};
247252
}
248253

254+
/**
255+
* Creates new {@link AggregationExpression} that calculates the requested percentile(s) of the
256+
* associated numeric value expression.
257+
*
258+
* @return new instance of {@link Percentile}.
259+
* @param percentages must not be {@literal null}.
260+
* @since 4.2
261+
*/
262+
public Percentile percentile(Double... percentages) {
263+
Percentile percentile = usesFieldRef() ? Percentile.percentileOf(fieldReference)
264+
: Percentile.percentileOf(expression);
265+
return percentile.percentages(percentages);
266+
}
267+
249268
private boolean usesFieldRef() {
250269
return fieldReference != null;
251270
}
@@ -977,4 +996,90 @@ protected String getMongoMethod() {
977996
return "$expMovingAvg";
978997
}
979998
}
999+
1000+
/**
1001+
* {@link AggregationExpression} for {@code $percentile}.
1002+
*
1003+
* @author Julia Lee
1004+
* @since 4.2
1005+
*/
1006+
public static class Percentile extends AbstractAggregationExpression {
1007+
1008+
private Percentile(Object value) {
1009+
super(value);
1010+
}
1011+
1012+
/**
1013+
* Creates new {@link Percentile}.
1014+
*
1015+
* @param fieldReference must not be {@literal null}.
1016+
* @return new instance of {@link Percentile}.
1017+
*/
1018+
public static Percentile percentileOf(String fieldReference) {
1019+
1020+
Assert.notNull(fieldReference, "FieldReference must not be null");
1021+
Map<String, Object> fields = new HashMap<>();
1022+
fields.put("input", Fields.field(fieldReference));
1023+
fields.put("method", "approximate");
1024+
return new Percentile(fields);
1025+
}
1026+
1027+
/**
1028+
* Creates new {@link Percentile}.
1029+
*
1030+
* @param expression must not be {@literal null}.
1031+
* @return new instance of {@link Percentile}.
1032+
*/
1033+
public static Percentile percentileOf(AggregationExpression expression) {
1034+
1035+
Assert.notNull(expression, "Expression must not be null");
1036+
Map<String, Object> fields = new HashMap<>();
1037+
fields.put("input", expression);
1038+
fields.put("method", "approximate");
1039+
return new Percentile(fields);
1040+
}
1041+
1042+
/**
1043+
* Define the percentile value(s) that must resolve to percentages in the range {@code 0.0 - 1.0} inclusive.
1044+
*
1045+
* @param percentages must not be {@literal null}.
1046+
* @return new instance of {@link Percentile}.
1047+
*/
1048+
public Percentile percentages(Double... percentages) {
1049+
1050+
Assert.notEmpty(percentages, "Percentages must not be null or empty");
1051+
return new Percentile(append("p", Arrays.asList(percentages)));
1052+
}
1053+
1054+
/**
1055+
* Creates new {@link Percentile} with all previously added inputs appending the given one. <br />
1056+
* <strong>NOTE:</strong> Only possible in {@code $project} stage.
1057+
*
1058+
* @param fieldReference must not be {@literal null}.
1059+
* @return new instance of {@link Percentile}.
1060+
*/
1061+
public Percentile and(String fieldReference) {
1062+
1063+
Assert.notNull(fieldReference, "FieldReference must not be null");
1064+
return new Percentile(appendTo("input", Fields.field(fieldReference)));
1065+
}
1066+
1067+
/**
1068+
* Creates new {@link Percentile} with all previously added inputs appending the given one. <br />
1069+
* <strong>NOTE:</strong> Only possible in {@code $project} stage.
1070+
*
1071+
* @param expression must not be {@literal null}.
1072+
* @return new instance of {@link Percentile}.
1073+
*/
1074+
public Percentile and(AggregationExpression expression) {
1075+
1076+
Assert.notNull(expression, "Expression must not be null");
1077+
return new Percentile(appendTo("input", expression));
1078+
}
1079+
1080+
@Override
1081+
protected String getMongoMethod() {
1082+
return "$percentile";
1083+
}
1084+
}
9801085
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArithmeticOperators.java

+16
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.CovarianceSamp;
2626
import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.Max;
2727
import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.Min;
28+
import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.Percentile;
2829
import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.StdDevPop;
2930
import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.StdDevSamp;
3031
import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.Sum;
@@ -41,6 +42,7 @@
4142
* @author Christoph Strobl
4243
* @author Mark Paluch
4344
* @author Mushtaq Ahmed
45+
* @author Julia Lee
4446
* @since 1.10
4547
*/
4648
public class ArithmeticOperators {
@@ -932,6 +934,20 @@ public Tanh tanh(AngularUnit unit) {
932934
return usesFieldRef() ? Tanh.tanhOf(fieldReference, unit) : Tanh.tanhOf(expression, unit);
933935
}
934936

937+
/**
938+
* Creates new {@link AggregationExpression} that calculates the requested percentile(s) of the
939+
* numeric value.
940+
*
941+
* @return new instance of {@link Percentile}.
942+
* @param percentages must not be {@literal null}.
943+
* @since 4.2
944+
*/
945+
public Percentile percentile(Double... percentages) {
946+
Percentile percentile = usesFieldRef() ? AccumulatorOperators.Percentile.percentileOf(fieldReference)
947+
: AccumulatorOperators.Percentile.percentileOf(expression);
948+
return percentile.percentages(percentages);
949+
}
950+
935951
private boolean usesFieldRef() {
936952
return fieldReference != null;
937953
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java

+3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
* @author Sebastien Gerard
3636
* @author Christoph Strobl
3737
* @author Mark Paluch
38+
* @author Julia Lee
3839
*/
3940
public class MethodReferenceNode extends ExpressionNode {
4041

@@ -228,6 +229,8 @@ public class MethodReferenceNode extends ExpressionNode {
228229
.mappingParametersTo("n", "input"));
229230
map.put("minN", mapArgRef().forOperator("$minN") //
230231
.mappingParametersTo("n", "input"));
232+
map.put("percentile", mapArgRef().forOperator("$percentile") //
233+
.mappingParametersTo("input", "p", "method"));
231234

232235
// TYPE OPERATORS
233236
map.put("type", singleArgRef().forOperator("$type"));

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperatorsUnitTests.java

+24
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
* Unit tests for {@link AccumulatorOperators}.
3232
*
3333
* @author Christoph Strobl
34+
* @author Julia Lee
3435
*/
3536
class AccumulatorOperatorsUnitTests {
3637

@@ -108,6 +109,29 @@ void rendersMinN() {
108109
.isEqualTo(Document.parse("{ $minN: { n: 3, input : \"$price\" } }"));
109110
}
110111

112+
@Test // GH-4473
113+
void rendersPercentileWithFieldReference() {
114+
115+
assertThat(valueOf("score").percentile(0.2).toDocument(Aggregation.DEFAULT_CONTEXT))
116+
.isEqualTo(Document.parse("{ $percentile: { input: \"$score\", method: \"approximate\", p: [0.2] } }"));
117+
118+
assertThat(valueOf("score").percentile(0.3, 0.9).toDocument(Aggregation.DEFAULT_CONTEXT))
119+
.isEqualTo(Document.parse("{ $percentile: { input: \"$score\", method: \"approximate\", p: [0.3, 0.9] } }"));
120+
121+
assertThat(valueOf("score").percentile(0.3, 0.9).and("scoreTwo").toDocument(Aggregation.DEFAULT_CONTEXT))
122+
.isEqualTo(Document.parse("{ $percentile: { input: [\"$score\", \"$scoreTwo\"], method: \"approximate\", p: [0.3, 0.9] } }"));
123+
}
124+
125+
@Test // GH-4473
126+
void rendersPercentileWithExpression() {
127+
128+
assertThat(valueOf(Sum.sumOf("score")).percentile(0.1).toDocument(Aggregation.DEFAULT_CONTEXT))
129+
.isEqualTo(Document.parse("{ $percentile: { input: {\"$sum\": \"$score\"}, method: \"approximate\", p: [0.1] } }"));
130+
131+
assertThat(valueOf("scoreOne").percentile(0.1, 0.2).and(Sum.sumOf("scoreTwo")).toDocument(Aggregation.DEFAULT_CONTEXT))
132+
.isEqualTo(Document.parse("{ $percentile: { input: [\"$scoreOne\", {\"$sum\": \"$scoreTwo\"}], method: \"approximate\", p: [0.1, 0.2] } }"));
133+
}
134+
111135
static class Jedi {
112136

113137
String name;

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java

+19
Original file line numberDiff line numberDiff line change
@@ -1893,6 +1893,25 @@ void facetShouldCreateFacets() {
18931893
assertThat(categorizeByYear).hasSize(3);
18941894
}
18951895

1896+
@Test // GH-4473
1897+
@EnableIfMongoServerVersion(isGreaterThanEqual = "7.0")
1898+
void percentileShouldBeAppliedCorrectly() {
1899+
1900+
mongoTemplate.insert(new DATAMONGO788(15, 16));
1901+
mongoTemplate.insert(new DATAMONGO788(17, 18));
1902+
1903+
Aggregation agg = Aggregation.newAggregation(
1904+
project().and(ArithmeticOperators.valueOf("x").percentile(0.9).and("y"))
1905+
.as("ninetiethPercentile"));
1906+
1907+
AggregationResults<Document> result = mongoTemplate.aggregate(agg, DATAMONGO788.class, Document.class);
1908+
1909+
// MongoDB server returns $percentile as an array of doubles
1910+
List<Document> rawResults = (List<Document>) result.getRawResults().get("results");
1911+
assertThat((List<Object>) rawResults.get(0).get("ninetiethPercentile")).containsExactly(16.0);
1912+
assertThat((List<Object>) rawResults.get(1).get("ninetiethPercentile")).containsExactly(18.0);
1913+
}
1914+
18961915
@Test // DATAMONGO-1986
18971916
void runMatchOperationCriteriaThroughQueryMapperForTypedAggregation() {
18981917

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GroupOperationUnitTests.java

+14
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.springframework.data.domain.Sort;
2626
import org.springframework.data.domain.Sort.Direction;
2727
import org.springframework.data.mongodb.core.DocumentTestUtils;
28+
import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.Percentile;
2829
import org.springframework.data.mongodb.core.aggregation.SelectionOperators.Bottom;
2930
import org.springframework.data.mongodb.core.query.Criteria;
3031

@@ -34,6 +35,7 @@
3435
* @author Oliver Gierke
3536
* @author Thomas Darimont
3637
* @author Gustavo de Geus
38+
* @author Julia Lee
3739
*/
3840
class GroupOperationUnitTests {
3941

@@ -266,6 +268,18 @@ void groupOperationAllowsToAddFieldsComputedViaExpression() {
266268
Document.parse("{ $bottom : { output: [ \"$playerId\", \"$score\" ], sortBy: { \"score\": -1 }}}"));
267269
}
268270

271+
@Test // GH-4473
272+
void groupOperationAllowsAddingFieldWithPercentileAggregationExpression() {
273+
274+
GroupOperation groupOperation = Aggregation.group("id").and("scorePercentile",
275+
Percentile.percentileOf("score").percentages(0.2));
276+
277+
Document groupClause = extractDocumentFromGroupOperation(groupOperation);
278+
279+
assertThat(groupClause).containsEntry("scorePercentile",
280+
Document.parse("{ $percentile : { input: \"$score\", method: \"approximate\", p: [0.2]}}"));
281+
}
282+
269283
private Document extractDocumentFromGroupOperation(GroupOperation groupOperation) {
270284
Document document = groupOperation.toDocument(Aggregation.DEFAULT_CONTEXT);
271285
Document groupClause = DocumentTestUtils.getAsDocument(document, "$group");

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java

+20
Original file line numberDiff line numberDiff line change
@@ -2241,6 +2241,26 @@ void nestedMappedFieldReferenceInArrayField() {
22412241
"{ $project: { \"author\" : 1, \"myArray\" : [ \"$ti_t_le\", \"plain - string\", { \"$sum\" : [\"$ti_t_le\", 10] } ] } } ] }"));
22422242
}
22432243

2244+
@Test // GH-4473
2245+
void shouldRenderPercentileAggregationExpression() {
2246+
2247+
Document agg = project()
2248+
.and(ArithmeticOperators.valueOf("score").percentile(0.3, 0.9)).as("scorePercentiles")
2249+
.toDocument(Aggregation.DEFAULT_CONTEXT);
2250+
2251+
assertThat(agg).isEqualTo(Document.parse("{ $project: { scorePercentiles: { $percentile: { input: \"$score\", method: \"approximate\", p: [0.3, 0.9] } }} } }"));
2252+
}
2253+
2254+
@Test // GH-4473
2255+
void shouldRenderPercentileWithMultipleArgsAggregationExpression() {
2256+
2257+
Document agg = project()
2258+
.and(ArithmeticOperators.valueOf("scoreOne").percentile(0.4).and("scoreTwo")).as("scorePercentiles")
2259+
.toDocument(Aggregation.DEFAULT_CONTEXT);
2260+
2261+
assertThat(agg).isEqualTo(Document.parse("{ $project: { scorePercentiles: { $percentile: { input: [\"$scoreOne\", \"$scoreTwo\"], method: \"approximate\", p: [0.4] } }} } }"));
2262+
}
2263+
22442264
private static Document extractOperation(String field, Document fromProjectClause) {
22452265
return (Document) fromProjectClause.get(field);
22462266
}

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java

+14-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
* @author Oliver Gierke
3434
* @author Christoph Strobl
3535
* @author Divya Srivastava
36+
* @author Julia Lee
3637
*/
3738
public class SpelExpressionTransformerUnitTests {
3839

@@ -1255,7 +1256,19 @@ void shouldTsSecond() {
12551256
void shouldRenderLocf() {
12561257
assertThat(transform("locf(price)")).isEqualTo("{ $locf: \"$price\" }");
12571258
}
1258-
1259+
1260+
@Test // GH-4473
1261+
void shouldRenderPercentile() {
1262+
assertThat(transform("percentile(new String[]{\"$scoreOne\", \"$scoreTwo\" }, new double[]{0.4}, \"approximate\")"))
1263+
.isEqualTo("{ $percentile : { input : [\"$scoreOne\", \"$scoreTwo\"], p : [0.4], method : \"approximate\" }}");
1264+
1265+
assertThat(transform("percentile(score, new double[]{0.4, 0.85}, \"approximate\")"))
1266+
.isEqualTo("{ $percentile : { input : \"$score\", p : [0.4, 0.85], method : \"approximate\" }}");
1267+
1268+
assertThat(transform("percentile(\"$score\", new double[]{0.4, 0.85}, \"approximate\")"))
1269+
.isEqualTo("{ $percentile : { input : \"$score\", p : [0.4, 0.85], method : \"approximate\" }}");
1270+
}
1271+
12591272
private Document transform(String expression, Object... params) {
12601273
return (Document) transformer.transform(expression, Aggregation.DEFAULT_CONTEXT, params);
12611274
}

src/main/antora/modules/ROOT/pages/mongodb/aggregation-framework.adoc

+1-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ At the time of this writing, we provide support for the following Aggregation Op
112112
| `setEquals`, `setIntersection`, `setUnion`, `setDifference`, `setIsSubset`, `anyElementTrue`, `allElementsTrue`
113113

114114
| Group/Accumulator Aggregation Operators
115-
| `addToSet`, `bottom`, `bottomN`, `covariancePop`, `covarianceSamp`, `expMovingAvg`, `first`, `firstN`, `last`, `lastN` `max`, `maxN`, `min`, `minN`, `avg`, `push`, `sum`, `top`, `topN`, `count` (+++*+++), `stdDevPop`, `stdDevSamp`
115+
| `addToSet`, `bottom`, `bottomN`, `covariancePop`, `covarianceSamp`, `expMovingAvg`, `first`, `firstN`, `last`, `lastN` `max`, `maxN`, `min`, `minN`, `avg`, `push`, `sum`, `top`, `topN`, `count` (+++*+++), `percentile`, `stdDevPop`, `stdDevSamp`
116116

117117
| Arithmetic Aggregation Operators
118118
| `abs`, `acos`, `acosh`, `add` (+++*+++ via `plus`), `asin`, `asin`, `atan`, `atan2`, `atanh`, `ceil`, `cos`, `cosh`, `derivative`, `divide`, `exp`, `floor`, `integral`, `ln`, `log`, `log10`, `mod`, `multiply`, `pow`, `round`, `sqrt`, `subtract` (+++*+++ via `minus`), `sin`, `sinh`, `tan`, `tanh`, `trunc`

0 commit comments

Comments
 (0)