Mongo collections - How to reduce an array of values when performing .find()?


#1

I have a publication where I need to return items sorted by most highly rated. The item in question has the following model:

{
  "name": "Some Item",
  "ratings": [
    { "userId": "abc123", "rating": 5 },
    { "userId": "abc456", "rating": 3 },
  ]
}

Is there some way I can use Collection.find() to calculate the average rating for each item and sort by the average rating in descending order?


#2

With $elemMatch you can reduce the array.


#3

I don’t exactly see how $elemMatch can do a reduction. Got any examples?


#4

This looks like the sort of thing I’d be doing with the aggregation pipeline. Is there a reason for not using that?

Edit: Depending on the number of documents you expect to be processed you could tackle this in code, but it’s not something I would recommend.


#5

Sorry

Sorry, think I got it wrong, thought you needed the highest one per document. I went wrong when you wrote: “items sorted by most highly rated”. So I expected that you wanted as output:

{
  "name": "Some Item",
  "ratings": [
    { "userId": "abc123", "rating": 5 },
  ]
}

Here is an example of that:
https://docs.mongodb.com/manual/reference/operator/projection/elemMatch/

Note that in Meteor you would need to put it in the fields object, the example itself won’t work:

Items.find( {  },
              { fields: { students: { $elemMatch: { school: 102 } } } } )

Now I understand you need the average of the ratings first. And work with that value.

To your question, I think you want this:

  • Get average of rating, so 5+3 = 8 / 2 = 4, correct?
  • Then sort by that value

Aggregate / projection

Here is an example:

Not 100% sure but likely it does not work in MiniMongo on the client.

Practical

In Mongo I would go for this by just storing that average in a field, so you get:

{
  "name": "Some Item",
  "ratings": [
    { "userId": "abc123", "rating": 5 },
    { "userId": "abc456", "rating": 3 },
  ],
  "averageRating": 4,
}

Then it all becomes easy off course for sorting etcetera. You can update that field in your code where you insert/update this document to keep it up to date.

You can also use the aggregations etc. as @robfallows mentions, that’s also a nice technique. In this case, when you re-use this value, I would store it though since it makes your code very simple. Calculating the average on insert is simple. And it’s a single document so there are no relations which might modify the average.

And it’s very simple to test, easier than having to test your publication as you can do this in a closed unit. The logic is in one place, the rest of the code for sorting is all very simple to understand.

Performance

Also on performance will be fine because only when there is an update to the ratings you need to calculate. When you use an aggregation it will be calculating constantly. This will be a one time calculation, store it, and done.


#6

This seems like it might be overkill, and maybe it’s best that when the user rates an item, I calculate the average rating at that time and just store it in the collection.