Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get matched sub documents by Geowithin from mongodb?

What I am trying to do here is that I want only those subdocument which is within the provided latitude and longitude but if only one subdocument inside my document matches and others do not it should only return me the document with that particular document. but it is returning me all subdocument too can someone you guys help me out. my document is like this

{
         "_id": "5ae04fd45f104a5980cf7e0e",
          "name": "Rehan",
         "email": "[email protected]",
         "status": true,
         "created_at": "2018-04-25T09:52:20.266Z",
         "parking_space": [
             {
                 "_id": "5ae05dce5f104a5980cf7e0f",
                 "parking_name": "my space 1",
                 "restriction": "no",
                 "hourly_rate": "3",
                 "location": {
                    "type": "Point",
                    "coordinates": [
                        86.84470799999997,
                        42.7052881
                    ]
                },
            },
            {
                "_id": "5ae06d4d5f104a5980cf7e52",
                "parking_name": "my space 2",
                "restriction": "no",
                "hourly_rate": "6",
                "location": {
                    "type": "Point",
                    "coordinates": [
                        76.7786787,
                        30.7352527
                    ]
                },
            }
        ],
    },
    {
        "_id": "5ae2f8148d51db4937b9df02",
        "name": "nellima",
        "email": "[email protected]",
        "status": true,
        "created_at": "2018-04-27T10:14:44.598Z",
        "parking_space": [
            {

                "_id": "5ae2f89d8d51db4937b9df04",
               "parking_name": "my space 3",
                "restriction": "no",
                "hourly_rate": "60",
              "location": {
                    "type": "Point",
                    "coordinates": [
                        76.7786787,
                        30.7352527
                    ]
                },
            }
        ],
    },
   }

I am applying this query.

User.find({
        "parking_space.location": {
            "$geoWithin": {
                "$centerSphere": [
                    [76.7786787, 30.7352527], 7 / 3963.2
                ]
            }
        },
    }, function(err, park_places) {
        if (err) {
            return res.send({
                data: err,
                status: false
            });
        } else {
            return res.send({
                data: park_places,
                status: true,
                msg: "Parking data according to location"
            });
        }
    });

and i am trying to get data like this.

{
         "_id": "5ae04fd45f104a5980cf7e0e",
          "name": "Rehan",
         "email": "[email protected]",
         "status": true,
         "created_at": "2018-04-25T09:52:20.266Z",
         "parking_space": [
            {
                "_id": "5ae06d4d5f104a5980cf7e52",
                "parking_name": "my space 2",
                "restriction": "no",
                "hourly_rate": "6",
                "location": {
                    "type": "Point",
                    "coordinates": [
                        76.7786787,
                        30.7352527
                    ]
                },
            }
        ],
    },
    {
        "_id": "5ae2f8148d51db4937b9df02",
        "name": "nellima",
        "email": "[email protected]",
        "status": true,
        "created_at": "2018-04-27T10:14:44.598Z",
        "parking_space": [
            {

                "_id": "5ae2f89d8d51db4937b9df04",
               "parking_name": "my space 3",
                "restriction": "no",
                "hourly_rate": "60",
              "location": {
                    "type": "Point",
                    "coordinates": [
                        76.7786787,
                        30.7352527
                    ]
                },
            }
        ],
    },
   }

is it possible to get data like this.

like image 555
Priyank lohan Avatar asked Apr 27 '18 11:04

Priyank lohan


2 Answers

In the case of what you are trying to do here, the far better option is to actually use the $geoNear aggregation pipeline stage to determine the "nearest" matches within your constraints instead. Notably your criteria actually asks for $geoWithin for a 7 mile radius by the application of the math applied. So this really is better expressed using $geoNear, and it's options actually allow you to do what you want.

User.aggregate([
  { "$geoNear": {
    "near": {
      "type": "Point",
      "coordinates": [76.7786787, 30.7352527]
    },
    "spherical": true,
    "distanceField": "distance",
    "distanceMultiplier": 0.000621371,
    "maxDistance": 7 * 1609.34,
    "includeLocs": "location"
  }},
  { "$addFields": {
    "parking_space": {
      "$filter": {
        "input": "$parking_space",
        "cond": {
          "$eq": ["$location", "$$this.location"]
        }
      }
    }
  }}
],function(err,park_places) {
  // rest of your code.
})

This would produce a result that looks like this:

{
        "_id" : "5ae04fd45f104a5980cf7e0e",
        "name" : "Rehan",
        "email" : "[email protected]",
        "status" : true,
        "created_at" : "2018-04-25T09:52:20.266Z",
        "parking_space" : [
                {
                        "_id" : "5ae06d4d5f104a5980cf7e52",
                        "parking_name" : "my space 2",
                        "restriction" : "no",
                        "hourly_rate" : "6",
                        "location" : {
                                "type" : "Point",
                                "coordinates" : [
                                        76.7786787,
                                        30.7352527
                                ]
                        }
                }
        ],
        "distance" : 0,
        "location" : {
                "type" : "Point",
                "coordinates" : [
                        76.7786787,
                        30.7352527
                ]
        }
}
{
        "_id" : "5ae2f8148d51db4937b9df02",
        "name" : "nellima",
        "email" : "[email protected]",
        "status" : true,
        "created_at" : "2018-04-27T10:14:44.598Z",
        "parking_space" : [
                {
                        "_id" : "5ae2f89d8d51db4937b9df04",
                        "parking_name" : "my space 3",
                        "restriction" : "no",
                        "hourly_rate" : "60",
                        "location" : {
                                "type" : "Point",
                                "coordinates" : [
                                        76.7786787,
                                        30.7352527
                                ]
                        }
                }
        ],
        "distance" : 0,
        "location" : {
                "type" : "Point",
                "coordinates" : [
                        76.7786787,
                        30.7352527
                ]
        }
}

We are using two stages here in the aggregation pipeline, so to explain what each is actually doing:

First the $geoNear performs the query given the location provided here in GeoJSON format for comparison within the "near" option, and this is of course the primary constraint. The "spherical" option is generally required for "2dsphere" indexes, and that is the type of index you actually want for your data. The "distanceField" is the other mandatory argument, and it specifies the name of the property which will actually record the distance from the queried point for the location the "document" was matched on.

The other options are the parts that make this work for what you want to do here. Firstly there is the "distanceMultiplier", which is actually "optional" here as it simply governs the value which will be output in the property specified by "distanceField". The value we are using here will adjust the meters returned as the "distance" into miles, which is what you are generally looking at. This actually does not have any other impact on the rest of the options, but since the "distanceField" is mandatory, we want to show an "expected" numeric value.

The next option is the other main "filter" to mimic your $geoWithin statement. The "maxDistance" option sets an upper limit on "how far away" a matched location can be. In this case we give it 7 for the miles where we multiply by 1609.34 which is how many meters are in a mile. Note the "distanceMultiplier" has no effect on this number, so any "conversion" must be done here as well.

The final option here is the "includeLocs", which is actually the most important option here aside from the distance constraint. This is the actual part that tells us the "location data" which was actually used for the "nearest match" from the array of locations contained in the document. What is defined here is of course the property which will be used to store this data in the documents returned from this pipeline stage. You can see the additional "location" property added to each document reflecting this.

So that pipeline stage has actually identified the matching "location" data, but this does not actually identify which array member was actually matched, explicitly. So in order to actually return the information for the specific array member we can use $filter for the comparison.

The operation of course is a simple comparison of the "matched location" to the actual "location" data of each array member. Since there will only ever be one match, you could alternately use things like $indexOfArray and $arrayElemAt to do the comparison and extract only the "single" result, but $filter is generally the most self explanatory operation that is easy to understand.


The whole point of the restriction on the radius can be demonstrated by a few short changes to the conditions. So if we move the location away slightly:

  { "$geoNear": {
    "near": {
      "type": "Point",
      "coordinates": [76.7786787, 30.6352527]    // <-- different location
    },
    "spherical": true,
    "distanceField": "distance",
    "distanceMultiplier": 0.000621371,
    "maxDistance": 7 * 1609.34,
    "includeLocs": "location"
  }},

This is still within the radius as reported within the output as specified for "distanceField":

 "distance" : 6.917030204982402,

But if you change that radius to be less than the reported number:

  { "$geoNear": {
    "near": {
      "type": "Point",
      "coordinates": [76.7786787, 30.6352527]    // <-- different location
    },
    "spherical": true,
    "distanceField": "distance",
    "distanceMultiplier": 0.000621371,
    "maxDistance": 6.91 * 1609.34,      // <--- smaller radius
    "includeLocs": "location"
  }},

Then the query would return neither of the documents presented in the question. Thus you can see how this setting governs the same boundaries as is implemented with the $geoWithin query, and of course we can now identify the matched sub-document.


Multiple matches

As a final note on the subject we can see how the "includeLocs" option can be used to identify the matching entry for a location within an array of a parent document. Whilst this should suit the use case here, the clear limitation is in matching mutliple locations within a range.

So "multiple" matches is simply beyond the scope of the $geoNear or other geospatial operations with MongoDB. An alternate case would be instead $unwind the array content after the intial $geoNear and then the $geoWithin stage in order to "filter" those multiple matches:

User.aggregate([
  { "$geoNear": {
    "near": {
      "type": "Point",
      "coordinates": [76.7786787, 30.7352527]
    },
    "spherical": true,
    "distanceField": "distance",
    "distanceMultiplier": 0.000621371,
    "maxDistance": 7 * 1609.34,
  }},
  { "$unwind": "$parking_space" },
  { "$match": {
    "parking_space.location": {
      "$geoWithin": {
        "$centerSphere": [
          [76.7786787, 30.7352527], 7 / 3963.2
        ]
      }
    }
  }}
],function(err,park_places) {
  // rest of your code.
})

It's probably better to actually use the $geoNear stage here, and we really just do the same thing without needing the "includeLocs" option. However if you really want to then there is nothing wrong with simply using the $geoWithin on either side of the $unwind stage:

User.aggregate([
  { "$match": {
    "parking_space.location": {
      "$geoWithin": {
        "$centerSphere": [
          [76.7786787, 30.7352527], 7 / 3963.2
        ]
      }
    }
  }}
  { "$unwind": "$parking_space" },
  { "$match": {
    "parking_space.location": {
      "$geoWithin": {
        "$centerSphere": [
          [76.7786787, 30.7352527], 7 / 3963.2
        ]
      }
    }
  }}
],function(err,park_places) {
  // rest of your code.
})

The reason this is okay is because whilst $geoWithin works most "optimally" when it can actually use the geospatial index defined on the collection, it actually does not require the index in order to return results.

Hence in either case after the "initial query" returns the "document" which contains at least one match for the condition, we simply $unwind the array content and then apply the same constraints all over again to filter out those array entries, now as documents. If you want the "array" to be returned, then you can always $group and $push the elements back into array form.

By contrast the $geoNear pipeline stage must be used as the very first pipeline stage only. That is the only place it can use an index, and therefore it is not possible to use it at later stages. But of course the "nearest distance" information is probably useful to you and therefore worthwhile actually including within the query results and conditions.

like image 68
Neil Lunn Avatar answered Nov 14 '22 21:11

Neil Lunn


User.aggregate([
        {
            path: '$parking_space',
            preserveNullAndEmptyArrays: true
        },
        { $geoNear: {
          near: { type: 'Point', 'parking_space.location.coordinates': [76.7786787, 30.7352527] },
          distanceField: 'dist',
          maxDistance: 7 / 3963.2,
          spherical: true
        } },
])
like image 25
SL H4CK3R Avatar answered Nov 14 '22 23:11

SL H4CK3R