Robert
Robert

Reputation: 520

Using near with elemMatch in Mongoose

I am searching within a collection of Stores. Stores have an embedded collection of outlets with locations. My goal is to return the set of stores that have outlets near a geolocation, and also only return those Outlets within that location.

I can successfully restrict the query to only return Stores have an Outlet at a particular location using 'near'


Store
  .where('isActive').equals(true)
  .where('outlets.location')
  .near({ center: [153.027117, -27.468515], maxDistance: 1000 / 6378137, spherical: true })
  .where('outlets.isActive').equals(true)
  .where('products.productType').equals('53433f1f3e02e39addde1954')
  .where('products.isActive').equals(true)
  .select('name outlets')
  .select({'products': {$elemMatch: {'isActive': true, 'productType': '53433f1f3e02e39addde1954'}}})
  .select('name outlets')
  .execQ()
  .then(function (results) {
    console.log(results);
  })
  .fail(function (err) {
    console.log(err);
  })
  .done();

The problem I have is that the store document returns all the outlets, not just the outlet that matched the geolocation. I've tried using elemMatch within a select like I did with the products;

.select({'outlets': {$elemMatch: {'location': {near:{ center: [153.027117, -27.468515], maxDistance: 10000 / 6378137, spherical: true }}}}})

However it returns an empty array. Can use use the near operator in an elemMatch clause? Am I doing it incorrectly? Is there an more efficient/fast/better way to achieve the goal?

Upvotes: 0

Views: 511

Answers (1)

Neil Lunn
Neil Lunn

Reputation: 151072

I see what you are trying to do here but there seems to be a few flaws in this sort of design. Though not exactly your document structure I see you are trying to do something like this:

{
    "_id" : ObjectId("5344badd519563414f23fdf8"),
    "store" : "Mine",
    "outlets" : [
            {
                    "name" : "somewhere",
                    "loc" : {
                            "type" : "Point",
                            "coordinates" : [
                                    150.975131,
                                    -33.8440366
                            ]
                    }
            },
            {
                    "name" : "else",
                    "loc" : {
                            "type" : "Point",
                            "coordinates" : [
                                    151.3651524,
                                    -33.8389783
                            ]
                    }
            }
    ]
}
{
    "_id" : ObjectId("5344be6f519563414f23fdf9"),
    "store" : "Another",
    "outlets" : [
            {
                    "name" : "else",
                    "loc" : {
                            "type" : "Point",
                            "coordinates" : [
                                    151.3651524,
                                    -33.8389783
                            ]
                    }
            },
            {
                    "name" : "somewhere",
                    "loc" : {
                            "type" : "Point",
                            "coordinates" : [
                                    150.975131,
                                    -33.8440366
                            ]
                    }
            }
    ]
}

So basically you appear to be attempting to nest the outlet locations within an array in a top level document.

What I am referring to a flaw here is that by design, any type of "near" based query is going to return more than 1 result. That does seem logical when you look at the purpose. You can of course modify this to restrict the results by "maxDistance" but generally it will be more than 1.

So the only way is to .limit() the results returned by the cursor to a single "nearest" response. Also note that with some operations those results are not necessarily "sorted" with the "nearest response first.

Now as these results are actually contained within an array of the document, remember that .find() itself does not actually "filter" the results of an array, so of course the document will contain all of the array contents.

If you tried to "project" with a positional $ operator, then the problem falls back to the original point because there is no singular actual match, so it is not possible to return an "index" value for the matching element. If you in fact did try this, you would always get the default index value of 0, so just returning the first element.

If then you thought you could run off to aggregate and and try to actually "de-normalize" the array entries, you would be out of luck because due to the need to use the index at the first stage of any aggregation pipeline statement.

So the summary of this is that embedded entries like this are not suited to this design where you need to do geo-spatial matching on those store locations. The locations are better off in a separate collection:

{
    "_id" : ObjectId("5344bec7519563414f23fdfa"),
    "store": "Mine"
    "name" : "else",
    "loc" : {
            "type" : "Point",
            "coordinates" : [
                    151.3651524,
                    -33.8389783
            ]
    }
}
{
    "_id" : ObjectId("5344bed5519563414f23fdfb"),
    "store": "Mine"
    "name" : "somewhere",
    "loc" : {
            "type" : "Point",
            "coordinates" : [
                    150.975131,
                    -33.8440366
            ]
    }
}

So that would allow you to "limit" the result to the "nearest" match by setting the limit to 1. You can also include any necessary values such as the "store" to be used in your filtering. If you need to you can include other information aside from what you need to filter or otherwise just put the ObjectId values within the array of the original object, or possibly even duplicate for both collections.

But since the very nature of these queries is intended to not only return 1 match, then there is no way you are going to get this to work on embedded documents. So your solution will require some changes in your overall schema.

Upvotes: 1

Related Questions