Babar
Babar

Reputation: 1202

Sorting records on number of embedded documents

I am using mongodb and mongoose, And I have a situation where a user creates a rating and a review, what I want is to fetch review of the product whose review has the highest number of votes. Is it possible to do it in mongo?

The structure is

{ "product" : ObjectId("53ccfa53b502542b2f463acd"),
  "_id" : ObjectId("53e8675aea39355818ec4ab2"),
  "vote" : [ ],
  "review" : "Blah Blah Blah",
  "stars" : 3,
  "email" : "[email protected]", "__v" : 0 }

Now I want to show the review that has achieved the most votes, I know if after find() I put sort() and limit() function, it can be achieved through a field present on the same document level however I do not know how to handle multiple records in this case 'vote'....

Upvotes: 0

Views: 40

Answers (2)

Neil Lunn
Neil Lunn

Reputation: 151190

The best thing you can do is to maintain a "voteCount" on the document itself. The reasons will become self apparent in a moment.

You can maintain this as members are added or removed from the array. Let's say you are using an ObjectId and the $push and $pull update operators to do this. So you also you $inc with a bit of query logic to make sure you don't duplicate the "User ObjectId" casting the vote. Assuming a model called "Product":

Product.update(
    {
        "_id": ObjectId("53e8675aea39355818ec4ab2"),
        "votes": { "$ne": ObjectId("53e87caaca37ffa384e5a931") }, // the user ObjectId
    },
    {
        "$push": { "votes": ObjectId("53e87caaca37ffa384e5a931" }, // same Id
        "$inc": { "voteCount": 1 }
    },
    function(err) {

    }
);

And to remove:

Product.update(
    {
        "_id": ObjectId("53e8675aea39355818ec4ab2"),
        "votes": ObjectId("53e87caaca37ffa384e5a931"), // the user ObjectId
    },
    {
        "$pull": { "votes": ObjectId("53e87caaca37ffa384e5a931" }, // same Id
        "$inc": { "voteCount": -1 }
    },
    function(err) {

    }
);

Then it's just a matter of sorting on the field:

Product.find().sort({ "voteCount": -1 }).limit(1).exec(function(err,doc) {


});

But if for some reason you cannot see fit to keep the "voteCount" in the document, then you need to manually "project" this with the aggregation framework. Etiher using the $size aggregate method where you have MongoDB 2.6 or greater:

Product.aggregate(
    [
        { "$project": {
            "product": 1,
            "vote": 1,
            "review": 1,
            "stars": 1,
            "email": 1,
            "voteCount": { "$size": "$vote" }
        }},
        { "$sort": { "voteCount": -1 } },
        { "$limit": 1 }
    ],
    function(err,result) {

    }
);

Or by $unwind on the array and getting the count via $sum for earlier versions:

Product.aggregate(
    [
        { "$unwind": "$vote"
        { "$group": {
            "_id": "$_id",
            "product": { "$first": "$product" },
            "vote": { "$push": "$vote" },
            "review": { "$first": "$review" },
            "stars": { "$first": "$stars" },
            "email": { "$first": "$email" },
            "voteCount": { "$sum": 1 }
        }},
        { "$sort": { "voteCount": -1 } },
        { "$limit": 1 }
    ],
    function(err,result) {

    }
);

The aggregation approach really does not make that much sense to implement unless you really need other calculations than the array length. So best just to keep it in the document.

Upvotes: 2

Leonid Beschastny
Leonid Beschastny

Reputation: 51500

The best way to do so with MongoDB is to add new counter field to explicitly store the number of votes:

{ "product" : ObjectId("53ccfa53b502542b2f463acd"),
  "_id" : ObjectId("53e8675aea39355818ec4ab2"),
  "vote" : [ {...}, {...}, {...} ],
  "vote_count": 3, // <-- new field
  "review" : "Blah Blah Blah",
  "stars" : 3,
  "email" : "[email protected]", "__v" : 0 }

Of course, you have other options, like using Aggregation Pipeline. But adding new field is the best option, because it'll allow you to build an index on this field and do indexed queries.

Upvotes: 1

Related Questions