Search code examples
mongodbmeteorgeolocationmongodb-queryaggregation-framework

How to use a document property for $maxDistance in a $nearSphere query?


I have a collection which contains documents like this:

{
    "_id" : "cysMrqjootq6YS6WP",
    “profile” : {
        ……
        "deliveryDistance” : 20,
        "address" : {
            "loc" : {
                "type" : "Point",
                "coordinates" : [
                    —2.120361,
                    52.536273
                ]
            }       }
    }
}

And I have a GeoJSON point like:

var referencePoint= {
                "type" : "Point",
                "coordinates" : [
                    —2.120361,
                    52.536273
                ]
            }

I am using Meteor.js, Node.js and MongoDB. I would like to create a query where the maxDistance from this point is the deliveryDistance property from each document into my collection.

If the maxDistance was a fixed value, the query would be:

myCollection.find({
    “profile.address.loc":{
      "$nearSphere": {
        "$geometry": referencePoint,
        "$maxDistance": 20000 //for 20Kms
      }
    }
  })

But this is not the case. For each document, the maxDistance has to be the value of ‘profile.deliveryDistance’. How do I use this value from the document as maxDistance in this query? Is it possible? If not, any other ideas?


Solution

  • You cannot reference existing properties of a document within a .find() query, and at least not within a $near or $nearSphere operation.

    Instead the approach here is to use the aggregation framework and $geoNear. This allows you to calculate the distance from the queried point and then compare if that falls within the "deliveryDistance" in the document.

    So for meteor, you are probably best off installing the meteorhacks aggregate package, and then doing something like this:

    Meteor.publish("aggResults",function(referencePoint) {
       var self = this;
    
       var results = myCollection.aggregate([
           { "$geoNear": {
               "near": referencePoint,
               "distanceField": "distance",
               "spherical": true
           }},
           { "$redact": {
               "$cond": {
                   "if": { "$lt": [ "$distance", "$profile.deliveryDistance" ] },
                   "then": "$$KEEP",
                   "else": "$$PRUNE"
               }
           }}
       ]);
    
       _.each(results,function(result) {
           self.added("client_collection_name",result._id, {
               "profile": result.profile,
               "distance": result.distance           
           });
       });
       self.ready();
    })
    

    If your MongoDB sever was less than version 2.6 ( and would have to be at least 2.4 for geospatial queries ) then you would use $project and $match in place of $redact to filter out the documents that did not fall within the "deliveryDistance":

       var results = myCollection.aggregate([
           { "$geoNear": {
               "near": referencePoint,
               "distanceField": "distance",
               "spherical": true
           }},
           { "$project": {
               "profile": 1,
               "distance": 1,
               "within": { "$lt": [ "$distance", "$profile.distance" ] }
           }},
           { "$match": { "within": true } }
       ]);
    

    But that is the basic case, where you give the server the tools to work out the distance comparison and then return any of those documents.

    The wrapping of aggregate output really depends on which way it is important for you to use the data in your application. This is just one sample of putting the output into a client addressable collection.

    Of course you can also dig into the driver internals to call .aggregate() as shown here, but it's likely not as flexible as using the mentioned meteor package.