The aggregation framework in MongoDB 3.4 and newer offers the $reduce
operator which efficiently calculates the total without the need for extra pipelines. Consider using it as an expression to return the
total ratings and get the number of ratings using $size
. Together with $addFields
, the average can thus be calculated using the arithmetic operator $divide
as in the formula average = total ratings/number of ratings
:
db.collection.aggregate([
{
"$addFields": {
"rating_average": {
"$divide": [
{ // expression returns total
"$reduce": {
"input": "$ratings",
"initialValue": 0,
"in": { "$add": ["$$value", "$$this.rating"] }
}
},
{ // expression returns ratings count
"$cond": [
{ "$ne": [ { "$size": "$ratings" }, 0 ] },
{ "$size": "$ratings" },
1
]
}
]
}
}
}
])
Sample Output
{
"_id" : ObjectId("58ab48556da32ab5198623f4"),
"title" : "The Hobbit",
"ratings" : [
{
"title" : "best book ever",
"rating" : 5.0
},
{
"title" : "good book",
"rating" : 3.5
}
],
"rating_average" : 4.25
}
With older versions, you would need to first apply the $unwind
operator on the ratings
array field first as your initial aggregation pipeline step. This will deconstruct the ratings
array field from the input documents to output a document for each element. Each output document replaces the array with an element value.
The second pipeline stage would be the $group
operator which groups input documents by the _id
and title
keys identifier expression and applies the desired $avg
accumulator expression to each group that calculates the average. There is another accumulator operator $push
that preserves the original ratings array field by returning an array of all values that result from applying an expression to each document in the above group.
The final pipeline step is the $project
operator which then reshapes each document in the stream, such as by adding the new field ratings_average
.
So, if for instance you have a sample document in your collection (as from above and so below):
db.collection.insert({
"title": "The Hobbit",
"ratings": [
{
"title": "best book ever",
"rating": 5
},
{
"title": "good book",
"rating": 3.5
}
]
})
To calculate the ratings array average and projecting the value in another field ratings_average
, you can then apply the following aggregation pipeline:
db.collection.aggregate([
{
"$unwind": "$ratings"
},
{
"$group": {
"_id": {
"_id": "$_id",
"title": "$title"
},
"ratings":{
"$push": "$ratings"
},
"ratings_average": {
"$avg": "$ratings.rating"
}
}
},
{
"$project": {
"_id": 0,
"title": "$_id.title",
"ratings_average": 1,
"ratings": 1
}
}
])
Result:
/* 1 */
{
"result" : [
{
"ratings" : [
{
"title" : "best book ever",
"rating" : 5
},
{
"title" : "good book",
"rating" : 3.5
}
],
"ratings_average" : 4.25,
"title" : "The Hobbit"
}
],
"ok" : 1
}