ArVan
ArVan

Reputation: 4275

Custom fields in MongoDB query result

After being so used to SQL, I have came across this problem with mongoDB. First, I am using mongoose.

Now, the problem. I have a collection named User.

var UserSchema = new Schema ({
    id : ObjectId,
    name : {type : String, trim : true, required : true},
    email: {type:String, trim:true, required: true, index: { unique: true }},
    password: {type:String, required: true, set: passwordToMD5},
    age: {type:Number, min: 18, required: true, default: 18},
    gender: {type: Number, default:0, required: true},

    height: {type: Number, default:180, min: 140, max: 220},
    _eye_color: {type: ObjectId, default: null},
    location: {
            lon: {type: Number, default: 0},
            lat: {type: Number, default: 0}
    },
    status: {type:Number, required: true, default:0}
    },{
        toObject: { virtuals: true },
        toJSON: { virtuals: true },
        collection:"user"});

Now I need to select all users from this collection and sort them by special attribude (say "rank"). This rank is calculated with certain logic depending of their distance from a point, age compared with given age, etc...

So now I was wondering how to select this rank and then use it in sorting? I have tried to use virtuals, they are handy to count additional info, but unfortunately, it is not possible to sort the find() results by a virtual field. Of course I can calculate this rank in a virtual, then select all records, and after that, in callback, do some javascript. But in this case, as I select all the users then sort and then limit, the javascript part might take too long... I was thinking to use mapreduce, but I am not sure it will do what I want. Can someone give me a hint if my task is possible to do in mongoDB/mongoose?

EDIT 1

I have also tried to use aggregation framework, and at first it seemed to be the best solution with the $project ability. But then, when I needed to do rank calculations, I found out that aggregation does not support a lot of mathematical functions like sin, cos and sqrt. And also it was impossible to use pre-defined usual javascript functions in projection. I mean,the function got called, but I was not able to pass current record fields to it.

{$project: {
  distance_from_user: mUtils.getDistance(point, this.location)
}

Inside function the second attr was "undefined".

So I guess it is impossible to do my rank calculations with aggregation framework.

EDIT 2 Ok, I know everyone tells me not to use mapreduce as it is not good for realtime queries, but as I cannot use aggregation, I think I'll try mapreduce. So Let's say I have this map reduce.

function map() {
            emit(1, // Or put a GROUP BY key here
                {name: this.name, // the field you want stats for
                    age: this.age,
                    lat: this.location.lat,
                    lon: this.location.lon,
                    distance:0,
                    rank:0

                });
        }

        function reduce(key, values) {


            return val;
        }

        function finalize(key, value){

            return value;
        }


        var command = {'mapreduce': "user", 'map': map.toString(), 'reduce': reduce.toString(), query:{$and: [{gender: user_params.gender}, {_id: {$ne: current_user_id}}]}, 'out': {inline:1}};

        mongoose.connection.db.executeDbCommand(command, function(error, result){
            if(error) {
                log(error);
                return;
            }
            log(result);
            return;
        });

What should I write in reduce (or maybe change map) to calculate rank for every user?

Upvotes: 0

Views: 1966

Answers (3)

mrówa
mrówa

Reputation: 5771

You are aware of amount of computations such thing would need - if you'd do it every time user logs in, you'll have interesting load peaks when lots of people would log in at shorter amount of time - and your page (interface) would be heavily resources-bound (which is not good).
I'd recommend you something a bit different - keeping ranking for every logged-on user and updating them in intervals: keeping "short session" and "long session" (long session - the one you use in web browser and short - "online, currently using the site") and generating ranks regularly only for "shortly-active" users and rarely for the logged on in the long session. Something like every five minutes. Much more scallable - and if user would be unhappy about him not having his rank counted - you may always tweak the sys to count his ranks on demand.
You might use mapredurce in such case - your map function should only emit the data you need for counting the rank for a given user (like age, lat, long, whatever you need) AND a result (rank) for a tested user (emit it empty). For reduce function you'd need to look at sorting with mapreduce (it highly depends on the way you create the rank) - also you'd count the rank (or some kind of a sub-value) for the other users.

Upvotes: 1

Eric
Eric

Reputation: 2894

It look like a good use case for MongoDB + Hadoop.

This presentation show some of the possibilities of this combination.

Upvotes: 0

Remon van Vliet
Remon van Vliet

Reputation: 18625

The only real solution is to calculate your rank for each document and storing it in the document. Since this value will be constant as long as the values in your document remain constant you can simply calculate this value whenever you update the fields that affect it.

Map/reduce certainly isn't a good solution for this nor is any other type of aggregation. Precalculating your rank and storing it with the document is the only option that scales if you're using MongoDB.

Upvotes: 1

Related Questions