Skip to content

Commit

Permalink
Add support for $percentile aggregation operator.
Browse files Browse the repository at this point in the history
See #4473
  • Loading branch information
sxhinzvc committed Sep 1, 2023
1 parent c34f8da commit d9798b4
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@
*/
package org.springframework.data.mongodb.core.aggregation;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.bson.Document;
import org.springframework.util.Assert;
Expand All @@ -25,6 +28,7 @@
* Gateway to {@literal accumulator} aggregation operations.
*
* @author Christoph Strobl
* @author Julia Lee
* @since 1.10
* @soundtrack Rage Against The Machine - Killing In The Name
*/
Expand Down Expand Up @@ -52,6 +56,7 @@ public static AccumulatorOperatorFactory valueOf(AggregationExpression expressio

/**
* @author Christoph Strobl
* @author Julia Lee
*/
public static class AccumulatorOperatorFactory {

Expand Down Expand Up @@ -246,6 +251,17 @@ public ExpMovingAvg alpha(double exponentialDecayValue) {
};
}

/**
* Creates new {@link AggregationExpression} that takes the associated numeric value expression and calculates
* the percentile.
*
* @return new instance of {@link Percentile}.
* @since 4.2
*/
public Percentile percentile() {
return usesFieldRef()? Percentile.percentileOf(fieldReference) : Percentile.percentileOf(expression);
}

private boolean usesFieldRef() {
return fieldReference != null;
}
Expand Down Expand Up @@ -977,4 +993,90 @@ protected String getMongoMethod() {
return "$expMovingAvg";
}
}

/**
* {@link AggregationExpression} for {@code $percentile}.
*
* @author Julia Lee
* @since 4.2
*/
public static class Percentile extends AbstractAggregationExpression {

private Percentile(Object value) {
super(value);
}

/**
* Creates new {@link Percentile}.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link Percentile}.
*/
public static Percentile percentileOf(String fieldReference) {

Assert.notNull(fieldReference, "FieldReference must not be null");
Map<String, Object> fields = new HashMap<>();
fields.put("input", Fields.field(fieldReference));
fields.put("method", "approximate");
return new Percentile(fields);
}

/**
* Creates new {@link Percentile}.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link Percentile}.
*/
public static Percentile percentileOf(AggregationExpression expression) {

Assert.notNull(expression, "Expression must not be null");
Map<String, Object> fields = new HashMap<>();
fields.put("input", expression);
fields.put("method", "approximate");
return new Percentile(fields);
}

/**
* Define the percentile value(s) that must resolve to percentages in the range {@code 0.0 - 1.0} inclusive.
*
* @param percentiles must not be {@literal null}.
* @return new instance of {@link Percentile}.
*/
public Percentile withPercentile(Double... percentiles) {

Assert.notEmpty(percentiles, "Percentiles must not be null or empty");
return new Percentile(append("p", Arrays.asList(percentiles)));
}

/**
* Creates new {@link Percentile} with all previously added inputs appending the given one. <br />
* <strong>NOTE:</strong> Only possible in {@code $project} stage.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link Percentile}.
*/
public Percentile and(String fieldReference) {

Assert.notNull(fieldReference, "FieldReference must not be null");
return new Percentile(appendTo("input", Fields.field(fieldReference)));
}

/**
* Creates new {@link Percentile} with all previously added inputs appending the given one. <br />
* <strong>NOTE:</strong> Only possible in {@code $project} stage.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link Percentile}.
*/
public Percentile and(AggregationExpression expression) {

Assert.notNull(expression, "Expression must not be null");
return new Percentile(appendTo("input", expression));
}

@Override
protected String getMongoMethod() {
return "$percentile";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.CovarianceSamp;
import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.Max;
import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.Min;
import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.Percentile;
import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.StdDevPop;
import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.StdDevSamp;
import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.Sum;
Expand All @@ -41,6 +42,7 @@
* @author Christoph Strobl
* @author Mark Paluch
* @author Mushtaq Ahmed
* @author Julia Lee
* @since 1.10
*/
public class ArithmeticOperators {
Expand Down Expand Up @@ -932,6 +934,17 @@ public Tanh tanh(AngularUnit unit) {
return usesFieldRef() ? Tanh.tanhOf(fieldReference, unit) : Tanh.tanhOf(expression, unit);
}

/**
* Creates new {@link AggregationExpression} that returns the percentile.
*
* @return new instance of {@link Percentile}.
* @since 4.2
*/
public Percentile percentile() {
return usesFieldRef() ? AccumulatorOperators.Percentile.percentileOf(fieldReference)
: AccumulatorOperators.Percentile.percentileOf(expression);
}

private boolean usesFieldRef() {
return fieldReference != null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
* Unit tests for {@link AccumulatorOperators}.
*
* @author Christoph Strobl
* @author Julia Lee
*/
class AccumulatorOperatorsUnitTests {

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

@Test // GH-4473
void rendersPercentileWithFieldReference() {

assertThat(valueOf("score").percentile().withPercentile(0.2).toDocument(Aggregation.DEFAULT_CONTEXT))
.isEqualTo(Document.parse("{ $percentile: { input: \"$score\", method: \"approximate\", p: [0.2] } }"));

assertThat(valueOf("score").percentile().withPercentile(0.3, 0.9).toDocument(Aggregation.DEFAULT_CONTEXT))
.isEqualTo(Document.parse("{ $percentile: { input: \"$score\", method: \"approximate\", p: [0.3, 0.9] } }"));

assertThat(valueOf("score").percentile().and("scoreTwo").withPercentile(0.3, 0.9).toDocument(Aggregation.DEFAULT_CONTEXT))
.isEqualTo(Document.parse("{ $percentile: { input: [\"$score\", \"$scoreTwo\"], method: \"approximate\", p: [0.3, 0.9] } }"));
}

@Test // GH-4473
void rendersPercentileWithExpression() {

assertThat(valueOf(Sum.sumOf("score")).percentile().withPercentile(0.1).toDocument(Aggregation.DEFAULT_CONTEXT))
.isEqualTo(Document.parse("{ $percentile: { input: {\"$sum\": \"$score\"}, method: \"approximate\", p: [0.1] } }"));

assertThat(valueOf("scoreOne").percentile().and(Sum.sumOf("scoreTwo")).withPercentile(0.1).toDocument(Aggregation.DEFAULT_CONTEXT))
.isEqualTo(Document.parse("{ $percentile: { input: [\"$scoreOne\", {\"$sum\": \"$scoreTwo\"}], method: \"approximate\", p: [0.1] } }"));
}

static class Jedi {

String name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.mongodb.core.DocumentTestUtils;
import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.Percentile;
import org.springframework.data.mongodb.core.aggregation.SelectionOperators.Bottom;
import org.springframework.data.mongodb.core.query.Criteria;

Expand All @@ -34,6 +35,7 @@
* @author Oliver Gierke
* @author Thomas Darimont
* @author Gustavo de Geus
* @author Julia Lee
*/
class GroupOperationUnitTests {

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

@Test // GH-4473
void groupOperationWithPercentile() {

GroupOperation groupOperation = Aggregation.group("id").and("scorePercentile",
Percentile.percentileOf("score").withPercentile(0.2));

Document groupClause = extractDocumentFromGroupOperation(groupOperation);

assertThat(groupClause).containsEntry("scorePercentile",
Document.parse("{ $percentile : { input: \"$score\", method: \"approximate\", p: [0.2]}}"));
}

private Document extractDocumentFromGroupOperation(GroupOperation groupOperation) {
Document document = groupOperation.toDocument(Aggregation.DEFAULT_CONTEXT);
Document groupClause = DocumentTestUtils.getAsDocument(document, "$group");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2241,6 +2241,26 @@ void nestedMappedFieldReferenceInArrayField() {
"{ $project: { \"author\" : 1, \"myArray\" : [ \"$ti_t_le\", \"plain - string\", { \"$sum\" : [\"$ti_t_le\", 10] } ] } } ] }"));
}

@Test // GH-4473
void shouldRenderPercentileAggregationExpression() {

Document agg = project()
.and(ArithmeticOperators.valueOf("score").percentile().withPercentile(0.3, 0.9)).as("scorePercentiles")
.toDocument(Aggregation.DEFAULT_CONTEXT);

assertThat(agg).isEqualTo(Document.parse("{ $project: { scorePercentiles: { $percentile: { input: \"$score\", method: \"approximate\", p: [0.3, 0.9] } }} } }"));
}

@Test // GH-4473
void shouldRenderPercentileWithMultipleArgsAggregationExpression() {

Document agg = project()
.and(ArithmeticOperators.valueOf("scoreOne").percentile().withPercentile(0.4).and("scoreTwo")).as("scorePercentiles")
.toDocument(Aggregation.DEFAULT_CONTEXT);

assertThat(agg).isEqualTo(Document.parse("{ $project: { scorePercentiles: { $percentile: { input: [\"$scoreOne\", \"$scoreTwo\"], method: \"approximate\", p: [0.4] } }} } }"));
}

private static Document extractOperation(String field, Document fromProjectClause) {
return (Document) fromProjectClause.get(field);
}
Expand Down

0 comments on commit d9798b4

Please sign in to comment.