4

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.

Daniel Valland
  • 1,057
  • 4
  • 21
  • 45
  • score is an attribute on the model. I included the schema now. What i am trying to do is find all the items within 1000 meters of the given gps coordinates, and then sort by score. I could of course do the sorting outside the query afterwards, but for scalability i would like to find a query solution to sorting... – Daniel Valland Apr 21 '20 at 13:59
  • does `sort('-score')` work for reverse sorting? I've always see that as `sort({score:-1})` – Joe Apr 21 '20 at 18:51
  • Yes, without the near query it works for sorting in descending order. I guess it might have evolved somewhat over the versions (ref: https://stackoverflow.com/a/15081087/2099833), and is not in fashion anymore, but it works well for me as i take in the sort attribute in a get param to allow the client to sort at will, and so it is easier than using the object notation.. – Daniel Valland Apr 21 '20 at 20:08
  • Ok so do you want to first sort with distance and then with `score`? – Ashh Apr 21 '20 at 20:20
  • I only care that the distance is within 1000 meters, after that i want to sort by the score attribute. So assuming two objects, one is 500 meters away with a score of 10, and the other is 900 meters away with a score of 20, the second item will appear first, even thou it is further away. I might be forced to sort after the query, its just that sorting is handled in the query for all other operations, so i would hate to make a special case for near queries... Further, it would force me to load all elements from the query into memory before sorting... very bad if i only wanted X elements... – Daniel Valland Apr 21 '20 at 20:24
  • 1
    Could you show your sample documents. And remove the `coordinates` from the query it must be only `location: { $near: ... }` – Ashh Apr 21 '20 at 20:36
  • The schema is there. Objects are {name: "Bob", score: 10, location: { coordinates: [10,10]} for example. The sort by distance works, but i need a way to apply the sorting after having found all the closest elements within 1km.. – Daniel Valland Apr 25 '20 at 17:08
  • Daniel, I wonder if my answer solved your problem. – SuleymanSah May 03 '20 at 19:16
  • @SuleymanSah sorry for my late reply. Was away for quite a few days. Some very good answers here. Trying the different solutions now. Looks like the bounty went to the highest rated. – Daniel Valland May 04 '20 at 16:41
  • I got half of the bounty :) actually I tried the code in my answer, $geoWithin seems to be better approach here. – SuleymanSah May 04 '20 at 16:45

2 Answers2

6

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"
    }
]
SuleymanSah
  • 17,153
  • 5
  • 33
  • 54
  • 1
    Thanks a lot. Works great :) Quick question, are you aware of a way to set the unit used? Can i indicate that i use km and not miles somewhere? – Daniel Valland May 04 '20 at 17:32
  • @DanielValland sorry I didn't noticed the question about unit. I couln't find anything useful in mongo docs, but you can create an utility helper function to make conversion. – SuleymanSah May 08 '20 at 08:01
1

$near sorts documents by distance which is a waste here. It's probably better to use $geoWithin which does not sort documents. Something like:

model.find({
  "location.coordinates": { 
     $geoWithin: { $center: [ [-74, 40.74], <radius> ] } } }
).sort({score: -1});

The $center docs have some more details.

Nic Cottrell
  • 9,401
  • 7
  • 53
  • 76