NoobSter
NoobSter

Reputation: 1160

How to get a rating average in Mongoose / node

I have a star rating directive working on the front end for angularjs, I can save a rating to the rating collection. here is my rating schema / model:

var mongoose = require('mongoose');

module.exports = mongoose.model('Rating', {
    bourbonId:   {type: mongoose.Schema.ObjectId, ref: 'Bourbon'},
    userId:   {type: mongoose.Schema.ObjectId, ref: 'User'},
    rating:     {type: Number, required: true},
    ratingId : {type: mongoose.Schema.ObjectId}

});

Here is the item that I need an average rating for:

 'use strict';

var mongoose = require('mongoose'),
    BourbonSchema = null;

module.exports = mongoose.model('Bourbon', {
    BourbonId:    {type: mongoose.Schema.ObjectId},
    name:  {type: String, required: true},
    blog:  {type: String, required: true},
    photo: {type: String, required: true},
    ratings: {type: mongoose.Schema.ObjectId, ref: 'Rating'},
    rating: {type: Number}

});

var Bourbon = mongoose.model('Bourbon', BourbonSchema);
module.exports = Bourbon;

I need to find a way to match by bourbon ID. From looking at stack overflow, it seems using an aggregate function may be the way to go. stack overflow link

Here is the current broken code I have in my controller. I know it's way off, along with failed attempts that i've had using async.map to try and solve this as well:

    'use strict';

var Bourbon = require('../../../models/bourbon'),
    Rating = require('../../../models/rating');
    //async = require('async');

module.exports = {
    description: 'Get Bourbons',
    notes: 'Get Bourbons',
    tags:['bourbons'],
    handler: function(request, reply){
        Bourbon.find(function(err, bourbons){
            Bourbon.findOne(id, 'Rating', function(err, bourbon){
                Rating.aggregate([
                    {$match: {bourbonId: {$in: bourbon.ratings}}},
                    {$group: {bourbonId: bourbon._id, average: {$avg: '$rating'}}}
                ], function(err, result){
                    bourbon.rating = result;
                    reply({bourbons:bourbons});
                    console.log('Bourbs', bourbons);
                });
            });
        });
    }
};

any help would be much appreciated. Beating my head against a brick wall, just throwing random code out now. ..


here's what i've implemented: model:

'use strict';

var mongoose = require('mongoose'),
    BourbonResultSchema = null;


module.exports = mongoose.model('BourbonResult', {
    _Id:    {type: mongoose.Schema.ObjectId, 'ref': 'Bourbon'},
    avgRating: {type: Number}

});


var BourbonResult = mongoose.model('BourbonResult', BourbonResultSchema, null);
module.exports = BourbonResult;

controller:

  'use strict';

var Bourbon = require('../../../models/bourbon'),
    Rating = require('../../../models/rating'),
    BourbonResult = require('../../../models/bourbonResult');
    //async = require('async');

module.exports = {
    description: 'Get Bourbons',
    notes: 'Get Bourbons',
    tags:['bourbons'],
    handler: function(request, reply){

            Rating.aggregate(
                [
                    {'$group':{
                        '_id': '$bourbonId',
                        'avgRating': {'$avg': '$rating'}
                    }}
                ],
                function(err,bourbons){
                    // Map plain results to mongoose document objects
                    bourbons = bourbons.map(function(result){
                        return new BourbonResult(result);
                    });

                    Bourbon.populate(bourbons,{'path': '_id'},function(err,bourbons){
                        reply({bourbons:bourbons});
                        console.log('BourbsRESSSSSS', JSON.stringify(bourbons, undefined, 2));
                    });
                }
            );

    }
};

here's what I get back from the consolelog:

BourbsRESSSSSS [ { _id: 
     { _id: 54acf382894ee2bcdebbc7f5,
       name: 'example2',
       photo: 'http://aries-wineny.com/wp-content/uploads/2014/09/woodford-reserve.jpg',
       blog: 'example2',
       __v: 0 },
    avgRating: 3.3333333333333335 },
  { _id: 
     { _id: 54a77e0fe63c850000f1269c,
       name: 'example',
       photo: 'http://aries-wineny.com/wp-content/uploads/2014/09/woodford-reserve.jpg',
       blog: 'example',
       __v: 0 },
    avgRating: 3 } ]

========================================================================

Perfect!

Upvotes: 3

Views: 6685

Answers (1)

Neil Lunn
Neil Lunn

Reputation: 151112

If what you are trying to do is list the "average" rating against each "Bourbon" here in your output there are probably a couple of approaches. But one of the cleaner ways would be to utilize mongoose populate on a special object model representing the structure of the results from aggregation.

You don't appearhave any other "types" of "Ratings" here other than for "bourbon", so it stands to reason that you just want to aggregate the whole collection.

// Set up a schema and model to match result structure
var bourbonResultSchema = new Schema({
    "_id": { "type": Schema.Types.ObjectId, "ref": "Bourbon" },
    "avgRating": Number
});

// The "null" for the collection is because there will not be any physical storage
var BourbonResult = mongoose.model( "BourbonResult", bourbonResultSchema, null );


// Aggregate an mapping code

Rating.aggregate(
    [
        { "$group": {
            "_id": "$bourbonId",
            "avgRating": { "$avg": { "$ifNull": ["$rating",0 ] } }    
        }}
    ],
    function(err,results) {
        if (err) throw err;

        // Map plain results to mongoose document objects
        results = results.map(function(result) {
            return new BourbonResult(result);
        });

        Bourbon.populate(results,{ "path": "_id" },function(err,results) {
            if (err) throw err;
            reply(results);
            console.log( JSON.stringify( results, undefined, 2 ) );
        })
    }
);

So you define a schema and model that will match the structure of the results returned from aggregate. This is done so you can call .populate() later.

The results returned from aggregate are not mongoose documents, but just plain objects. You then cast all the results to BourbonResult objects by passing them through the .map() method in order to return an array of BourbonResult.

Since these are not mongoose documents, you can call the model method of .populate(), which takes an array of mongoose documents as the first argument. The second "options" argument tells the method which field path to use for the popluation, which is _id as defined earlier to reference the Bourbon model.

In the callback to .populate() the returned results merge both the average score returned from aggregation and the complete Bourbon object within the _id field. If you really wished, you could also run further .populate() statements over each Bourbon object in order to pull in any of it's references. A bit more complicated but possible.

As a note, the "bourbonId" field in the "Bourbon" model is probably a bit redundant. MongoDB always has a unique _id field present and the actual value used by referenced object links is that field unless specified otherwise. Even if you needed to define a reference there as I have done for BourbonResult then you can do that as well.


A complete listing with amended schema examples:

var async = require('async'),
    mongoose = require('mongoose'),
    Schema = mongoose.Schema;

var userSchema = new Schema({
  "name": String
});

var ratingSchema = new Schema({
  "bourbonId": { "type": Schema.Types.ObjectId, "ref": "Bourbon" },
  "userId": { "type": Schema.Types.ObjectId, "ref": "User" },
  "rating": { "type": Number, "required": true }
});

var bourbonSchema = new Schema({
  "name": { "type": String, "required": true },
  "blog": { "type": String, "required": true },
  "photo": { "type": String, "required": true },
  "ratings": [{ "type": Schema.Types.ObjectId, "ref": "Rating" }],
  "rating": { "type": Number }
});

var bourbonResultSchema = new Schema({
  "_id": { "type": Schema.Types.ObjectId },
  "avgRating": Number
});

var User = mongoose.model( "User", userSchema ),
    Rating = mongoose.model( "Rating", ratingSchema ),
    Bourbon = mongoose.model( "Bourbon", bourbonSchema ),
    BourbonResult = mongoose.model(
      "BourbonResult", bourbonResultSchema, null );


mongoose.connect("mongodb://localhost/bourbon");

async.waterfall(
  [
    function(callback) {
      async.each([User,Rating,Bourbon],function(model,callback) {
        model.remove({},callback);
      },
      function(err) {
        callback(err);
      });
    },

    function(callback) {
      Bourbon.create({
        "name": 'test',
        "blog": 'test',
        "photo": 'test'
      },callback);
    },

    function(bourbon,callback) {
      User.create({ "name": 'ted' },function(err,user) {
        if (err) callback(err);
        Rating.create({
          "bourbonId": bourbon,
          "userId": user,
          "rating": 5
        },function(err,rating1) {
          callback(err,user,bourbon,rating1)
        });
      });
    },

    function(user,bourbon,rating1,callback) {
      Rating.create({
        "bourbonId": bourbon,
        "userId": user,
        "rating": 7
      },function(err,rating2) {
        callback(err,bourbon,rating1,rating2);
      });
    },

    function(bourbon,rating1,rating2,callback) {
      Bourbon.findById(bourbon.id,function(err,bourbon) {
        bourbon.ratings.push(rating1,rating2);
        bourbon.save(function(err,bourbon) {
          callback(err)
        });
      });
    },

    function(callback) {
      Rating.aggregate(
        [
          { "$group": {
            "_id": "$bourbonId",
            "avgRating": { "$avg": { "$ifNull": ["$rating", 0 ] } }
          }},
        ],
        function(err,results) {
          console.log(results);

          results = results.map(function(result) {
            return new BourbonResult(result);
          });

          Bourbon.populate(
            results,
            { "path": "_id" },
            function(err,results) {
              console.log(results);
              callback(err);
            }
          )

        }
      );
    }
  ],
  function(err) {
    if (err) throw err;
    mongoose.disconnect();
  }
)

Gives output:

[ { _id: 54af7581efc755470845005c, avgRating: 6 } ]
[ { _id:
     { _id: 54af7581efc755470845005c,
       name: 'test',
       blog: 'test',
       photo: 'test',
       __v: 1,
       ratings: [ 54af7581efc755470845005e, 54af7581efc755470845005f ] },
    avgRating: 6 } ]

Upvotes: 2

Related Questions