Aggregation Expression Operations
On this page
Overview
In this guide, you can learn how to use the Kotlin Sync driver to construct expressions for use in aggregation pipelines. You can perform expression operations with discoverable, typesafe Kotlin methods rather than by using BSON documents. Because these methods follow the fluent interface pattern, you can chain aggregation operations together to create code that is compact and naturally readable.
The operations in this guide use methods from the com.mongodb.client.model.mql package. These methods provide an idiomatic way to use the Query API, the mechanism by which the driver interacts with a MongoDB deployment. To learn more about the Query API, see the Server manual documentation.
How to Use Operations
The examples in this guide assume that you include the following imports in your code:
import com.mongodb.client.model.Aggregates import com.mongodb.client.model.Accumulators import com.mongodb.client.model.Projections import com.mongodb.client.model.Filters import com.mongodb.client.model.mql.MqlValues
To access document fields in an expression, you must reference the
current document being processed by the aggregation pipeline by using the
current()
method. To access the value of a
field, you must use the appropriately typed method, such as
getString()
or getDate()
. When you specify the type for a field,
you ensure that the driver provides only those methods which are
compatible with that type. The following code shows how to reference a
string field called name
:
current().getString("name")
To specify a value in an operation, pass it to the of()
constructor method to
convert it to a valid type. The following code shows how to reference a
value of 1.0
:
of(1.0)
To create an operation, chain a method to your field or value reference. You can build more complex operations by chaining multiple methods.
The following example creates an operation to find patients in New Mexico who have visited the doctor’s office at least once. The operation performs the following actions:
Checks if the size of the
visitDates
array value is greater than0
by using thegt()
methodChecks if the
state
field value is “New Mexico” by using theeq()
method
The and()
method links these operations so that the pipeline stage
matches only documents that meet both criteria.
current() .getArray("visitDates") .size() .gt(of(0)) .and(current() .getString("state") .eq(of("New Mexico")))
While some aggregation stages, such as group()
, accept operations
directly, other stages expect that you first include your operation in a
method such as computed()
or expr()
. These methods, which take
values of type TExpression
, allow you to use your expressions in
certain aggregations.
To complete your aggregation pipeline stage, include your expression in an aggregates builder method. The following list provides examples of how to include your expression in common aggregates builder methods:
match(expr(<expression>))
project(fields(computed("<field name>", <expression>)))
group(<expression>)
To learn more about these methods, see the Transform Your Data with Aggregation guide.
Constructor Methods
You can use these constructor methods to define values for use in Kotlin aggregation expressions.
Method | Description |
---|---|
References the current document being processed by the aggregation pipeline. | |
References the current document being processed by the aggregation pipeline as a map value. | |
Returns an MqlValue type corresponding to the provided primitive. | |
Returns an array of MqlValue types corresponding to the provided array of primitives. | |
Returns an entry value. | |
Returns an empty map value. | |
Returns the null value as exists in the Query API. |
Important
When you provide a value to one of these methods, the driver treats
it literally. For example, of("$x")
represents the string value
"$x"
, rather than a field named x
.
Refer to any of the sections under Operations for examples using these methods.
Operations
The following sections provide information and examples for aggregation expression operations available in the driver. The operations are categorized by purpose and functionality.
Each section has a table that describes aggregation methods available in the driver and corresponding expression operators in the Query API. The method names link to API documentation and the aggregation pipeline operator names link to descriptions and examples in the Server manual documentation. While each method is effectively equivalent to the corresponding aggregation operator, they may differ in expected parameters and implementation.
The example in each section uses the listOf()
method to create a
pipeline from the aggregation stage. Then, each example passes the
pipeline to the aggregate()
method of MongoCollection
.
Note
The driver generates a Query API expression that may be different from the Query API expression provided in each example. However, both expressions will produce the same aggregation result.
Important
The driver does not provide methods for all aggregation pipeline operators in
the Query API. To use an unsupported operation in an
aggregation, you must define the entire expression using the BSON Document
type.
Arithmetic Operations
You can perform an arithmetic operation on a value of type MqlInteger
or
MqlNumber
using the methods described in this section.
Method | Aggregation Pipeline Operator |
---|---|
Suppose you have weather data for a specific year that includes the precipitation measurement (in inches) for each day. You want to find the average precipitation, in millimeters, for each month.
The multiply()
operator multiplies the precipitation
field by
25.4
to convert the field value to millimeters. The avg()
accumulator method
returns the average as the avgPrecipMM
field. The group()
method
groups the values by month given in each document's date
field.
The following code shows the pipeline for this aggregation:
val month = current().getDate("date").month(of("UTC")) val precip = current().getInteger("precipitation") val results = collection.aggregate<Document>( listOf( Aggregates.group( month, Accumulators.avg("avgPrecipMM", precip.multiply(25.4)) ) ) )
The following code provides an equivalent aggregation pipeline in the Query API:
[ { $group: { _id: { $month: "$date" }, avgPrecipMM: { $avg: { $multiply: ["$precipitation", 25.4] } } } } ]
Array Operations
You can perform an array operation on a value of type MqlArray
using the methods described in this section.
Method | Aggregation Pipeline Operator |
---|---|
Suppose you have a collection of movies, each of which contains an array of nested documents for upcoming showtimes. Each nested document contains an array that represents the total number of seats in the theater, where the first array entry is the number of premium seats and the second entry is the number of regular seats. Each nested document also contains the number of tickets that have already been bought for the showtime. A document in this collection might resemble the following:
{ "_id": ..., "movie": "Hamlet", "showtimes": [ { "date": "May 14, 2023, 12:00 PM", "seats": [ 20, 80 ], "ticketsBought": 100 }, { "date": "May 20, 2023, 08:00 PM", "seats": [ 10, 40 ], "ticketsBought": 34 }] }
The filter()
method displays only the results matching the provided
predicate. In this case, the predicate uses sum()
to calculate the
total number of seats and compares that value to the number of ticketsBought
by using the lt()
method. The project()
method stores these
filtered results as a new availableShowtimes
array field.
Tip
You must specify the type of values that an array contains when using
the getArray()
method to work with the values as any specific
type. For example, you must specify that an array contains integers
to perform calculations with those integers elsewhere in
your application.
The example in this section specifies that the seats
array
contains values of type MqlDocument
so that it can extract nested
fields from each array entry.
The following code shows the pipeline for this aggregation:
val showtimes = current().getArray<MqlDocument>("showtimes") val results = collection.aggregate<Document>( listOf( Aggregates.project( Projections.fields( Projections.computed("availableShowtimes", showtimes .filter { showtime -> val seats = showtime.getArray<MqlInteger>("seats") val totalSeats = seats.sum { n -> n } val ticketsBought = showtime.getInteger("ticketsBought") val isAvailable = ticketsBought.lt(totalSeats) isAvailable }) ) ) ) )
Note
To improve readability, the previous example assigns intermediary values to
the totalSeats
and isAvailable
variables. If you don't assign
these intermediary values to variables, the code still produces
equivalent results.
The following code provides an equivalent aggregation pipeline in the Query API:
[ { $project: { availableShowtimes: { $filter: { input: "$showtimes", as: "showtime", cond: { $lt: [ "$$showtime.ticketsBought", { $sum: "$$showtime.seats" } ] } } } } } ]
Boolean Operations
You can perform a boolean operation on a value of type MqlBoolean
using the methods described in this section.
Suppose you want to classify very low or high weather temperature readings (in degrees Fahrenheit) as extreme.
The or()
operator checks to see if temperatures are extreme by comparing
the temperature
field to predefined values by using the lt()
and
gt()
methods. The project()
method records this result in the
extremeTemp
field.
The following code shows the pipeline for this aggregation:
val temperature = current().getInteger("temperature") val results = collection.aggregate<Document>( listOf( Aggregates.project( Projections.fields( Projections.computed( "extremeTemp", temperature .lt(of(10)) .or(temperature.gt(of(95))) ) ) ) ) )
The following code provides an equivalent aggregation pipeline in the Query API:
[ { $project: { extremeTemp: { $or: [ { $lt: ["$temperature", 10] }, { $gt: ["$temperature", 95] } ] } } } ]
Comparison Operations
You can perform a comparison operation on a value of type MqlValue
using the methods described in this section.
Tip
The cond()
method is similar to the ternary operator in Kotlin and you
can use it for simple branches based on boolean values. Use
the switchOn()
methods for more complex comparisons such as performing
pattern matching on the value type or other arbitrary checks on the value.
Method | Aggregation Pipeline Operator |
---|---|
The following example shows a pipeline that matches all the documents
where the location
field has the value "California"
:
val location = current().getString("location") val results = collection.aggregate<Document>( listOf( Aggregates.match( Filters.expr(location.eq(of("California"))) ) ) )
The following code provides an equivalent aggregation pipeline in the Query API:
[ { $match: { location: { $eq: "California" } } } ]
Conditional Operations
You can perform a conditional operation using the methods described in this section.
Method | Aggregation Pipeline Operator |
---|---|
Suppose you have a collection of customers with their membership information. Originally, customers were either members or not. Over time, membership levels were introduced and used the same field. The information stored in this field can be one of a few different types, and you want to create a standardized value indicating their membership level.
The switchOn()
method checks each clause in order. If the value matches the
type indicated by the clause, then the clause determines the string value
corresponding to the membership level. If the original value is a string, it
represents the membership level and that value is used. If the data type is a
boolean, it returns either Gold
or Guest
for the membership level. If
the data type is an array, it returns the most recent string in the array which
matches the most recent membership level. If the member
field is an
unknown type, the switchOn()
method provides a default value of Guest
.
The following code shows the pipeline for this aggregation:
val member = current().getField("member") val results = collection.aggregate<Document>( listOf( Aggregates.project( Projections.fields( Projections.computed("membershipLevel", member.switchOn { field -> field .isString { s -> s } .isBoolean { b -> b.cond(of("Gold"), of("Guest")) } .isArray { a -> a.last() } .defaults { d -> of("Guest") } }) ) ) ) )
The following code provides an equivalent aggregation pipeline in the Query API:
[ { $project: { membershipLevel: { $switch: { branches: [ { case: { $eq: [ { $type: "$member" }, "string" ] }, then: "$member" }, { case: { $eq: [ { $type: "$member" }, "bool" ] }, then: { $cond: { if: "$member", then: "Gold", else: "Guest" } } }, { case: { $eq: [ { $type: "$member" }, "array" ] }, then: { $last: "$member" } } ], default: "Guest" } } } } ]
Convenience Operations
You can apply custom functions to values of type
MqlValue
using the methods described in this section.
To improve readability and allow for code reuse, you can move redundant
code into static methods. However, you cannot directly chain
static methods in Kotlin. The passTo()
method lets you chain values
into custom static methods.
Method | Aggregation Pipeline Operator |
---|---|
No corresponding operator |
Suppose you want to determine how a class is performing against some benchmarks. You want to find the average final grade for each class and compare it against the benchmark values.
The following custom method gradeAverage()
takes an array of documents and
the name of an integer field shared across those documents. It calculates the
average of that field across all the documents in the provided array and
determines the average of that field across all the elements in
the provided array. The evaluate()
method compares a provided value to
two provided range limits and generates a response string based on
how the values compare:
fun gradeAverage(students: MqlArray<MqlDocument>, fieldName: String): MqlNumber { val sum = students.sum { student -> student.getInteger(fieldName) } val avg = sum.divide(students.size()) return avg } fun evaluate(grade: MqlNumber, cutoff1: MqlNumber, cutoff2: MqlNumber): MqlString { val message = grade.switchOn { on -> on .lte(cutoff1) { g -> of("Needs improvement") } .lte(cutoff2) { g -> of("Meets expectations") } .defaults { g -> of("Exceeds expectations") } } return message }
Tip
Using the passTo()
method allows you to reuse
your custom methods for other aggregations. For example, you can
use the gradeAverage()
method to find the average of grades for
groups of students filtered by entry year or district, not just their
class. Similarly, you could use the evaluate()
method to evaluate
an individual student's performance or an entire school's performance.
The passArrayTo()
method takes an array of all students and calculates the
average score by using the gradeAverage()
method. Then, the
passNumberTo()
method uses the evaluate()
method to determine how the
classes are performing. This example stores the result as the evaluation
field using the project()
method.
The following code shows the pipeline for this aggregation:
val students = current().getArray<MqlDocument>("students") val results = collection.aggregate<Document>( listOf( Aggregates.project( Projections.fields( Projections.computed("evaluation", students .passArrayTo { s -> gradeAverage(s, "finalGrade") } .passNumberTo { grade -> evaluate(grade, of(70), of(85)) }) ) ) ) )
The following code provides an equivalent aggregation pipeline in the Query API:
[ { $project: { evaluation: { $switch: { branches: [ { case: { $lte: [ { $avg: "$students.finalGrade" }, 70 ] }, then: "Needs improvement" }, { case: { $lte: [ { $avg: "$students.finalGrade" }, 85 ] }, then: "Meets expectations" } ], default: "Exceeds expectations" } } } } ]
Conversion Operations
You can perform a conversion operation to convert between certain MqlValue
types using the methods described in this section.
Method | Aggregation Pipeline Operator |
---|---|
No corresponding operator | |
No corresponding operator | |
Suppose you want to have a collection of student data that includes their graduation years, which are stored as strings. You want to calculate the year of their five-year reunion and store this value in a new field.
The parseInteger()
method converts the graduationYear
to an integer
so that add()
can calculate the reunion year. The addFields()
method
stores this result as a new reunionYear
field.
The following code shows the pipeline for this aggregation:
val students = current().getArray<MqlDocument>("students") val results = collection.aggregate<Document>( listOf( Aggregates.project( Projections.fields( Projections.computed("evaluation", students .passArrayTo { s -> gradeAverage(s, "finalGrade") } .passNumberTo { grade -> evaluate(grade, of(70), of(85)) }) ) ) ) )
The following code provides an equivalent aggregation pipeline in the Query API:
[ { $addFields: { reunionYear: { $add: [ { $toInt: "$graduationYear" }, 5 ] } } } ]
Date Operations
You can perform a date operation on a value of type MqlDate
using the methods described in this section.
Method | Aggregation Pipeline Operator |
---|---|
Suppose you have data about package deliveries and want to match
deliveries that occurred on any Monday in the "America/New_York"
time
zone.
If the deliveryDate
field contains any string values representing
valid dates, such as "2018-01-15T16:00:00Z"
or "Jan 15, 2018, 12:00
PM EST"
, you can use the parseDate()
method to convert the strings
into date types.
The dayOfWeek()
method determines which day of the week that a date
is, then converts it to a number. The number assignment uses 0
to mean
Sunday when using the "America/New_York"
timezone. The eq()
method compares this value to 2
, or Monday.
The following code shows the pipeline for this aggregation:
val deliveryDate = current().getString("deliveryDate") val results = collection.aggregate<Document>( listOf( Aggregates.match( Filters.expr( deliveryDate .parseDate() .dayOfWeek(of("America/New_York")) .eq(of(2)) ) ) ) )
The following code provides an equivalent aggregation pipeline in the Query API:
[ { $match: { $expr: { $eq: [ { $dayOfWeek: { date: { $dateFromString: { dateString: "$deliveryDate" } }, timezone: "America/New_York" }}, 2 ] } } } ]
Document Operations
You can perform a document operation on a value of type MqlDocument
using the methods described in this section.
Method | Aggregation Pipeline Operator |
---|---|
No corresponding operator | |
Suppose you have a collection of legacy customer data which includes
addresses as child documents under the mailing.address
field. You want
to find all the customers who live in Washington state. A
document in this collection might resemble the following:
{ "_id": ..., "customer.name": "Mary Kenneth Keller", "mailing.address": { "street": "601 Mongo Drive", "city": "Vasqueztown", "state": "CO", "zip": 27017 } }
The getDocument()
method retrieves the mailing.address
field as a
document so the nested state
field can be retrieved with the
getString()
method. The eq()
method checks if the value of the
state
field is "WA"
.
The following code shows the pipeline for this aggregation:
val address = current().getDocument("mailing.address") val results = collection.aggregate<Document>( listOf( Aggregates.match( Filters.expr( address .getString("state") .eq(of("WA")) ) ) ) )
The following code provides an equivalent aggregation pipeline in the Query API:
[ { $match: { $expr: { $eq: [{ $getField: { input: { $getField: { input: "$$CURRENT", field: "mailing.address"}}, field: "state" }}, "WA" ] }}}]
Map Operations
You can perform a map operation on a value of either type MqlMap
or
MqlEntry
using the methods described in this section.
Method | Aggregation Pipeline Operator |
---|---|
No corresponding operator | |
No corresponding operator | |
No corresponding operator | |
No corresponding operator | |
No corresponding operator | |
No corresponding operator | |
No corresponding operator | |
No corresponding operator | |
No corresponding operator |
Suppose you have a collection of inventory data where each document represents an individual item you're responsible for supplying. Each document contains a field that is a map of all your warehouses and how many copies they have in their inventory of the item. You want to determine the total number of copies of items you have across all warehouses. A document in this collection might resemble the following:
{ "_id": ..., "item": "notebook" "warehouses": [ { "Atlanta", 50 }, { "Chicago", 0 }, { "Portland", 120 }, { "Dallas", 6 } ] }
The entries()
method returns the map entries in the warehouses
field as an array. The sum()
method calculates the total value of items
based on the values in the array retrieved with the getValue()
method.
This example stores the result as the new totalInventory
field using the
project()
method.
The following code shows the pipeline for this aggregation:
val warehouses = current().getMap<MqlNumber>("warehouses") val results = collection.aggregate<Document>( listOf( Aggregates.project( Projections.fields( Projections.computed("totalInventory", warehouses .entries() .sum { v -> v.getValue() }) ) ) ) )
The following code provides an equivalent aggregation pipeline in the Query API:
[ { $project: { totalInventory: { $sum: { $getField: { $objectToArray: "$warehouses" }, } } } } ]
String Operations
You can perform a string operation on a value of type MqlString
using the methods described in this section.
Method | Aggregation Pipeline Operator |
---|---|
Suppose you want to generate lowercase usernames for employees of a company from the employees' last names and employee IDs.
The append()
method combines the lastName
and employeeID
fields into
a single username, while the toLower()
method makes the entire username
lowercase. This example stores the result as a new username
field using
the project()
method.
The following code shows the pipeline for this aggregation:
val lastName = current().getString("lastName") val employeeID = current().getString("employeeID") val results = collection.aggregate<Document>( listOf( Aggregates.project( Projections.fields( Projections.computed( "username", lastName .append(employeeID) .toLower() ) ) ) ) )
The following code provides an equivalent aggregation pipeline in the Query API:
[ { $project: { username: { $toLower: { $concat: ["$lastName", "$employeeID"] } } } } ]
Type-Checking Operations
You can perform a type-check operation on a value of type MqlValue
using the methods described in this section.
These methods do not return boolean values. Instead, you provide a default value
that matches the type specified by the method. If the checked value
matches the method type, the checked value is returned. Otherwise, the supplied
default value is returned. To program branching logic based on the
data type, see switchOn()
.
Method | Aggregation Pipeline Operator |
---|---|
No corresponding operator | |
No corresponding operator | |
No corresponding operator | |
No corresponding operator | |
No corresponding operator | |
No corresponding operator | |
No corresponding operator | |
No corresponding operator |
Suppose you have a collection of rating data. An early version of the review schema allowed users to submit negative reviews without a star rating. You want convert any of these negative reviews without a star rating to have the minimum value of 1 star.
The isNumberOr()
method returns either the value of rating
, or
a value of 1
if rating
is not a number or is null. The
project()
method returns this value as a new numericalRating
field.
The following code shows the pipeline for this aggregation:
val rating = current().getField("rating") val results = collection.aggregate<Document>( listOf( Aggregates.project( Projections.fields( Projections.computed( "numericalRating", rating .isNumberOr(of(1)) ) ) ) ) )
The following code provides an equivalent aggregation pipeline in the Query API:
[ { $project: { numericalRating: { $cond: { if: { $isNumber: "$rating" }, then: "$rating", else: 1 } } } } ]