r3wt
r3wt

Reputation: 4732

mongodb with $match, $skip, $limit and $geoNear

I wanted to do this:

model.User.aggregate([
    //step 1 match criteria
    {
        $match: criteria
    },
    //step 2 skip
    { 
        $skip: offset 
    },
    //step 3 limit
    {
        $limit: limit
    },
    //step 4 sort by computed distance
    {
        $geoNear : {
            near: {type: 'Point', coordinates: coords },
            distanceField: 'currentCity.computed_distance',
            includeLocs: 'currentCity.loc',
            spherical: true,
            uniqueDocs: true,
            distanceMultiplier: 3963.2, //convert to miles (this number is the radius of the earth in miles)
        }
    }
],function(err,users){
    if (err) return res.error(err);
    if (!users.length) return res.error('no matched criteria');
    res.apiResponse(users);
});

but the documentation of $geoNear states:

You can only use $geoNear as the first stage of a pipeline.

Reading the documentation, i see that i can simply move $match inside of $geoNear via the query option. likewise, $limit may be placed inside of $geoNear via the limit option. the one problem is, there is no equivalent for the $skip option, so it looks as if facilitating pagination would not be possible? i'm really confused here, why $geoNear can't be the 4th step in the pipeline. The goal of the query is simply to find the best n matches, where n = limit, then sort by nearest in proximity. is this even possible? I am having trouble finding an answer for this specific use case.

I suppose one solution might be to perform a query to select only ids matching documents, convert to list of ids, then do the aggregates with an $in query like so:

model.User.find(criteria).skip(offset).limit(limit).select('_id').exec(function (err, userIds) {
    var ids = [];
    userIds.forEach(function(u){
        ids.push(u._id);
    });

    model.User.aggregate([
        {
            $geoNear : {
                query: { _id: {$in: $ids } },
                near: {type: 'Point', coordinates: coords },
                distanceField: 'currentCity.computed_distance',
                includeLocs: 'currentCity.loc',
                spherical: true,
                uniqueDocs: true,
                distanceMultiplier: 3963.2, //convert to miles (this number is the radius of the earth in miles)
            }
        }
    ],function(err,users){
        if (err) return res.error(err);
        if (!users.length) return res.error('no matched criteria');
        res.apiResponse(users);
    });
}); 

This would work, but ideally i could do it in 1 query if possible. any ideas greatly appreciated.

Upvotes: 1

Views: 2637

Answers (1)

Goofyahead
Goofyahead

Reputation: 5884

One solution is this one:

result = db.cafes.aggregate([{
'$geoNear': {
    'near': {
        'type': 'Point',
        'coordinates': [
            -73.991084,
            40.735863]},
    'spherical': True,
    'distanceField': 'dist',
    'num': 20}
}, {
   '$skip': 10
}])

There is also a better solution with this approach:

ids = [42]

result = db.command(
'geoNear', 'cafes',
near={
    'type': 'Point',
    'coordinates': [
        -73.991084,
        40.735863]},
spherical=True,
minDistance=268,
query={
    '_id': {
        '$nin': ids}},
num=10)

And a really nice explanation on speed and issues over here:

https://emptysqua.re/blog/paging-geo-mongodb/

Upvotes: 1

Related Questions