Calculated field with Meteor and MongoDB

My dating app has this function where I rank user based on an algorithm which takes their last login date, and some other ‘factors’.

I thought this was gonna be easy. Well, it would’ve been if I wanted to take the entire collection to the client, the use JS to calculate the field, sort, and display to user.

Problem is, I can’t load the entire collection. That’s thousands of users. I have to sort BEFORE I return the result. Which means I have to sort at the database level before I return the results, which was quite challenging.


First the context is, I have a ‘paginated’ with a load-more button page, which uses template-level subscription to reactively subscribe and retrieve data incrementally (for fast loading!!):

Template.SearchMain.onCreated(function() {
  var self = this;

  // initialize or reset the limit variables
  Session.set('limit', 9);

  self.autorun(function() {
    // make sure user is initialized already
    var user = Meteor.user;
    if (user) {
      // set query based on profile.ideal, and sorting based on last login and online status
      // return pipeline object ready to be ran through aggregate
      var pipeline = SearchesHelper.searchUsersPipeline();

      // make sure it did return before proceeding
      if (pipeline) {
        pipeline.push({ $limit: Session.get('limit') });
        // make subscription based on options
        self.subscriptionHandle = self.subscribe('searchUsers', pipeline);

      Tracker.afterFlush(function() {
        $('#loadingUsers').waitMe({ text: '載入中...', bg: 'transparent'});

The pipeline is where it got challenging:

SearchesHelper.searchUsersPipeline = function() {
  var user = Meteor.user();

  if (user) {
    var yearNow = new Date().getFullYear();
    var earliestDob = new Date(new Date().setFullYear(yearNow - user.profile.ideal.ageTo - 1));
    var latestDob = new Date(new Date().setFullYear(yearNow - user.profile.ideal.ageFrom));

    var query = {
      // opposite gender of current user only
      'profile.gender': user.profile.gender == 'male' ? 'female' : 'male',

      // active members only
      userStatus: 'active',

      // filter by profile.ideal.ageFrom and ageTo
      'profile.dob': {$gte: earliestDob, $lte: latestDob}

    // filter out users already passed on, or got passed on, or matches that were cancelled
    var userId = user._id;
    var skipUserIds = [];
    var userIsSenderOrReceiverQuery = { $or: [{receiverId: userId}, {senderId: userId}] };
    var matchIsPassedOrCancelledQuery = { $or: [{passed: true}, {cancelled: true}] };
    var skipMatchesCursor = Matches.find({
      $and: [userIsSenderOrReceiverQuery, matchIsPassedOrCancelledQuery]
    // pass the sender if user is receiver, vice versa
    skipMatchesCursor.forEach(function(match) {
      userId == match.receiverId ? skipUserIds.push(match.senderId) : skipUserIds.push(match.receiverId);
    // exclude user self as well
    // skip all users in skipUserIds array
    query['_id'] = {$nin: skipUserIds};

    // filter city
    if ( != 'city_nopref')
      query[''] =;

    // filter education level
    if ( != 'education_nopref') {
      var educationLevels = ['primary', 'secondary', 'associate', 'bachelor', 'masters', 'doctors'];
      var idealLevel = educationLevels.indexOf(;
      var acceptEduArray = educationLevels.slice(idealLevel, educationLevels.length);
      query[''] = { $in: acceptEduArray };

    // filter children
    if (user.profile.ideal.children != 'children_nopref')
      query['profile.children'] = 'none';

    // filter smoke
    var smokingLevels = ['never_smoke', 'quit_smoke', 'seldom_smoke', 'sometime_smoke', 'always_smoke'];
    if (user.profile.ideal.smoke == 'sometime_smoke') {
      query['profile.smoke'] = { $ne: smokingLevels[4] };
    else if (user.profile.ideal.smoke == 'no_smoke') {
      query['profile.smoke'] = { $in: [smokingLevels[0], smokingLevels[1]] }

    // filter drink
    var drinkingLevels = ['never_drink', 'quit_drink', 'seldom_drink', 'sometime_drink', 'always_drink'];
    if (user.profile.ideal.drink == 'sometime_drink') {
      query['profile.drink'] = { $ne: drinkingLevels[4] };
    else if (user.profile.ideal.drink == 'no_drink') {
      query['profile.drink'] = { $in: [drinkingLevels[0], drinkingLevels[1]] }

    // set how many days would substract one point
    var msPerScore = 86400000 * 2;
    // Diff between now and lastLogin, in MS
    var dateDiffOperator = { $subtract: [ new Date(), "$" ] };
    // return dateScore from lastLogin date
    var dateScoreOperator = { $subtract: [10, { $divide: [dateDiffOperator, msPerScore] }] };
    // return totalScore from weighted geometric average of adminScore and DateScore
    var totalScoreOperator = { $multiply: [dateScoreOperator, "$adminScore"]};

    var project = {
      username: 1,
      profile: 1,
      status: 1,
      userStatus: 1,
      todayMatch: 1,
      starEnd: 1,
      adminScore: 1,
      dateScore: dateScoreOperator,
      totalScore: totalScoreOperator

    var sort = { 'totalScore': -1 };

    return [
      { $match: query },
      { $project: project },
      { $sort: sort }

Basically the logic is this.

First the query, I filter out all the search results based on user’s preference (if they want a partner that is certain age range, doesn’t smoke, etc).

Then I have to make a projection. This is the most challenging part. I have to dig through mongoDB’s API to find out their arithmetic APIs. Also apparently some didn’t work on my version of meteor mongo, so things like floor, square, which I wanted to use, was not available. Perhaps that’s available by now. Anyways I decided to get by with basic multiplications and subtractions.

Once I calculated the ‘dateScore’ and ‘totalScore’ field, then the easiest part and the end, sort by totalScore in descending order.


meteorhacks:aggregate (it’s needed to use the mongo aggregate function within Meteor. Not sure if that’s still needed in the future, maybe meteor will integrate that into the meteor mongo)


This task was really stretching my mongoDB abilities. I had taken the MongoDB University 12-week course, which went through all of this, but putting it in practice and learning is completely different.

Nevertheless I was glad that I knew about the mongoDB aggregation pipeline, and know how to dig deeper to find the stuff I needed to complete this task.

Leave a Reply

Your email address will not be published. Required fields are marked *