mongodbgroup-byaggregation-frameworkgisgeonear

Mongo aggregation use $sort in conjunction with $geoNear


Context :

I'm trying to build an architecture displaying POIs which can be at different know locations over time.

I have 2 collections,

pois

{
  _id: ObjectId,
  name: string
}

locations

  _id: ObjectId,
  point: {
    type: 'Point',
    coordinates: Array<number>
  },
  poi: ObjectId // Reference to Poi

Use-case :

So I'm trying to build a query which

Ideally, having this output :

[
  {
    _id: ObjectId(AE54232),
    name: 'Some poi',
    location: {
      _id: ObjectId(BFE5423),
      point: {
        type: 'Point',
        coordinates: [3, 50]
      },
      distance: 3
    }
  }
]

Attempt

Reading carefully at the docs, I used this combination :

      // Keep only locations within radius,
      // output 'distance'
      // and sort by distance
      {
        $geoNear: {
          near: nearCenter,
          key: 'point',
          distanceField: 'distance',
          maxDistance: nearRadius,
          spherical: true,
        },
      },
      // Keep only first (assumed 'nearest')
      // location of each poi
      {
        $group: {
          _id: '$poi',
          location: {
            $first: '$$ROOT'
          }
        }
      },
      // Retrieve poi
      {
        $lookup: {
          from: 'pois',
          localField: '_id',
          foreignField: '_id',
          as: 'poi',
        },
      },
      // Flatten poi
      {
        $unwind: {
          path: '$poi',
        },
      },
      // Push poi at the root,
      // and put location inside 'location'
      {
        $replaceRoot: {
          newRoot: {
            $mergeObjects: [
              "$poi",
              { location: "$location" },
            ]
          },
        }
      },

So to sumup :

Trouble

I'm facing a strange behavior where the query basically works; excepts its not sorted by distance : the pois with their location come in an erratic and non-determinist order !

I tried commenting every step one by one, and apparently this is the $first which is causing the "shuffle". Which is surprising since docs states :

$geoNear

Outputs documents in order of nearest to farthest from a specified point.

$first

Returns the value that results from applying an expression to the first document in a group of documents. Only meaningful when documents are in a defined order.

Fix attempts

My thought was $first expects an actual $sort and not an implicit $geoNear sorting; so I tried to intercalate a $sort in between like so :

      {
        $sort: {
          'distance': 1,
        },
      },

in between like so :

But it gave me the exact same result !


The only thing that worked is adding a $sort at the very end like so

      {
        $sort: {
          'location.distance': 1,
        },
      },

But I'm concerned it could have performance issues on large datasets

Question

Is there any way to accomplish that logic

Without loosing $geoNear order ?


Solution

  • If each poi can have few locations then grouping them may change the order so the documents after the grouping are no longer sorted by distance. You can sort by distance after the grouping to solve it:

      {
            $geoNear: {
              near: nearCenter,
              key: 'point',
              distanceField: 'distance',
              maxDistance: nearRadius,
              spherical: true,
            },
          },
          // at this point you have all locations matching the criteria, sorted by `distance`
          // Keep only first (assumed 'nearest')
          // location of each poi
          {
            $group: {
              _id: '$poi',
              location: {
                $first: '$$ROOT'
              }
            }
          },
          // at this point you have one location and its distance from `nearCenter per each `poi`. The grouping can change the order of documents 
          {
            $lookup: {
              from: 'pois',
              localField: '_id',
              foreignField: '_id',
              as: 'poi',
            },
          },
          // until here you retrieved the `poi` as `poi`
          {$sort: {distance: -1}}
          // now the `poi`s are sorted by distance
          {
            $replaceRoot: {
              newRoot: {
                $mergeObjects: [
                  {$first: "$poi"},
                  { location: "$location" },
                ]
              },
            }
          }
          // Now the answer is formatted (no need to $unwind since you have only one item in the array)