Search code examples
mongodbmongoosegeojson

Mongoose Geo Near Search - How to sort within a given distance?


I am using mongoose and a near query with maxDistance to filter elements close to a given gps location. However, the near query overrides the other sorting. What i would like is to find all elements within maxDistance of a given point, and then order by some other attribute. Here is an example of what i am doing currently:

Schema:

mongoose.Schema({
    name: {
        type: String,
        required: true
    },
    score: {
        type: Number,
        required: true,
        default: 0
    },
    location: {
        type: {
            type: String,
            default: 'Point',
        },
        coordinates: {
            type: [Number]
        }
    },
    ....
});

Query:

model.find({
  "location.coordinates": {
    "$near": {
      "$maxDistance": 1000,
      "$geometry": {
        "type": "Point",
        "coordinates": [
          10,
          10
        ]
      }
    }
  }
}).sort('-score');

Adding a .sort after the find does not help here, and the items are returned in order of near anyway.


Solution

  • In find query you need to use location instead of location.coordinates.

    router.get("/test", async (req, res) => {
      const lat = 59.9165591;
      const lng = 10.7881978;
      const maxDistanceInMeters = 1000;
    
      const result = await model
        .find({
          location: {
            $near: {
              $geometry: {
                type: "Point",
                coordinates: [lng, lat],
              },
              $maxDistance: maxDistanceInMeters,
            },
          },
        })
        .sort("-score");
    
      res.send(result);
    });
    

    For $near to work you need an 2dsphere index on the related collection:

    db.collection.createIndex( { "location" : "2dsphere" } )
    

    In mongodb $near docs it says:

    $near sorts documents by distance. If you also include a sort() for the query, sort() re-orders the matching documents, effectively overriding the sort operation already performed by $near. When using sort() with geospatial queries, consider using $geoWithin operator, which does not sort documents, instead of $near.

    Since you are not interested in sorting by distance, as Nic indicated using $near is unnecessary, better to use $geoWithin like this:

    router.get("/test", async (req, res) => {
      const lat = 59.9165591;
      const lng = 10.7881978;
      const distanceInKilometer = 1;
      const radius = distanceInKilometer / 6378.1;
    
      const result = await model
        .find({
          location: { $geoWithin: { $centerSphere: [[lng, lat], radius] } },
        })
        .sort("-score");
    
      res.send(result);
    });
    

    To calculate radius we divide kilometer to 6378.1, and miles to 3963.2 as described here.

    So this will find the locations inside 1km radius.

    Sample docs:

    [
        {
            "location": {
                "type": "Point",
                "coordinates": [
                    10.7741692,
                    59.9262198
                ]
            },
            "score": 50,
            "_id": "5ea9d4391e468428c8e8f505",
            "name": "Name1"
        },
        {
            "location": {
                "type": "Point",
                "coordinates": [
                    10.7736078,
                    59.9246991
                ]
            },
            "score": 70,
            "_id": "5ea9d45c1e468428c8e8f506",
            "name": "Name2"
        },
        {
            "location": {
                "type": "Point",
                "coordinates": [
                    10.7635027,
                    59.9297932
                ]
            },
            "score": 30,
            "_id": "5ea9d47b1e468428c8e8f507",
            "name": "Name3"
        },
        {
            "location": {
                "type": "Point",
                "coordinates": [
                    10.7635027,
                    59.9297932
                ]
            },
            "score": 40,
            "_id": "5ea9d4971e468428c8e8f508",
            "name": "Name4"
        },
        {
            "location": {
                "type": "Point",
                "coordinates": [
                    10.7768093,
                    59.9287668
                ]
            },
            "score": 90,
            "_id": "5ea9d4bd1e468428c8e8f509",
            "name": "Name5"
        },
        {
            "location": {
                "type": "Point",
                "coordinates": [
                    10.795769,
                    59.9190384
                ]
            },
            "score": 60,
            "_id": "5ea9d4e71e468428c8e8f50a",
            "name": "Name6"
        },
        {
            "location": {
                "type": "Point",
                "coordinates": [
                    10.1715157,
                    59.741873
                ]
            },
            "score": 110,
            "_id": "5ea9d7d216bdf8336094aa92",
            "name": "Name7"
        }
    ]
    

    Output: (within 1km and sorted by descending score)

    [
        {
            "location": {
                "type": "Point",
                "coordinates": [
                    10.7768093,
                    59.9287668
                ]
            },
            "score": 90,
            "_id": "5ea9d4bd1e468428c8e8f509",
            "name": "Name5"
        },
        {
            "location": {
                "type": "Point",
                "coordinates": [
                    10.7736078,
                    59.9246991
                ]
            },
            "score": 70,
            "_id": "5ea9d45c1e468428c8e8f506",
            "name": "Name2"
        },
        {
            "location": {
                "type": "Point",
                "coordinates": [
                    10.795769,
                    59.9190384
                ]
            },
            "score": 60,
            "_id": "5ea9d4e71e468428c8e8f50a",
            "name": "Name6"
        },
        {
            "location": {
                "type": "Point",
                "coordinates": [
                    10.7741692,
                    59.9262198
                ]
            },
            "score": 50,
            "_id": "5ea9d4391e468428c8e8f505",
            "name": "Name1"
        }
    ]