Upon revisiting this with a fresh perspective, the solution becomes glaringly obvious. The main point you have already mentioned is your desire to discover the "intersection" of two queries within a single response.
Another angle to approach this is by considering that you want all the points constrained by the first query to act as the "input" for the second query, and so forth as needed. Essentially, this is what an intersection accomplishes, although the logic is taken quite literally.
To achieve this, utilize MongoDB's aggregation framework to sequentially connect the matching queries. For example, let's consider the following sample documents:
{ "loc" : { "type" : "Point", "coordinates" : [ 4, 4 ] } }
{ "loc" : { "type" : "Point", "coordinates" : [ 8, 8 ] } }
{ "loc" : { "type" : "Point", "coordinates" : [ 12, 12 ] } }
Now, here is the chained aggregation pipeline comprising just two queries:
db.geotest.aggregate([
{ "$match": {
"loc": {
"$geoWithin": {
"$box": [ [0,0], [10,10] ]
}
}
}},
{ "$match": {
"loc": {
"$geoWithin": {
"$box": [ [5,5], [20,20] ]
}
}
}}
])
In logical terms, the first query identifies the points falling within the boundaries of the initial box or the first two items. These results are then fed into the second query. As the new box limits start at [5,5]
, the first point gets excluded. Additionally, the third point was already eliminated, but if the box restrictions were reversed, only the middle document would remain in the result.
The functioning of this operation is unique to the $geoWithin
query operator compared to other geo functions:
$geoWithin does not require a geospatial index. However, a geospatial index will enhance query performance. Both 2dsphere and 2d geospatial indexes support $geoWithin.
The outcomes have their pros and cons. While it's advantageous to perform this type of operation without an existing index, once the aggregation pipeline alters the collection results post the primary query operation, no further index can be leveraged. Thus, any indexing benefits are nullified upon merging the "set" results from operations beyond the initial Polygon/MultiPolygon, as supported.
Hence, I suggest computing the intersection bounds "externally" to the MongoDB query. Despite the aggregation framework's ability to accomplish this through its "chained" pipeline nature and diminishing intersection sizes, optimal performance can be achieved with a single query encompassing the correct bounds that effectively utilizes all the index advantages.
There exist various methods to achieve this, but for reference, here is an implementation utilizing the JSTS library, which serves as a JavaScript rendition of the popular JTS library for Java. There might be other libraries or language translations available, but this one offers straightforward GeoJSON parsing capabilities and pre-built methods for actions like deriving intersection bounds:
var async = require('async');
util = require('util'),
jsts = require('jsts'),
mongo = require('mongodb'),
MongoClient = mongo.MongoClient;
var parser = new jsts.io.GeoJSONParser();
var polys= [
{
type: 'Polygon',
coordinates: [[
[ 0, 0 ], [ 0, 10 ], [ 10, 10 ], [ 10, 0 ], [ 0, 0 ]
]]
},
{
type: 'Polygon',
coordinates: [[
[ 5, 5 ], [ 5, 20 ], [ 20, 20 ], [ 20, 5 ], [ 5, 5 ]
]]
}
];
var points = [
{ type: 'Point', coordinates: [ 4, 4 ] },
{ type: 'Point', coordinates: [ 8, 8 ] },
{ type: 'Point', coordinates: [ 12, 12 ] }
];
MongoClient.connect('mongodb://localhost/test',function(err,db) {
db.collection('geotest',function(err,geo) {
if (err) throw err;
async.series(
[
// Insert some data
function(callback) {
var bulk = geo.initializeOrderedBulkOp();
bulk.find({}).remove();
async.each(points,function(point,callback) {
bulk.insert({ "loc": point });
callback();
},function(err) {
bulk.execute(callback);
});
},
// Execute each query version
function(callback) {
async.parallel(
[
// Aggregation
function(callback) {
var pipeline = [];
polys.forEach(function(poly) {
pipeline.push({
"$match": {
"loc": {
"$geoWithin": {
"$geometry": poly
}
}
}
});
});
geo.aggregate(pipeline,callback);
},
// Employ external set resolution
function(callback) {
var geos = polys.map(function(poly) {
return parser.read( poly );
});
var bounds = geos[0];
for ( var x=1; x<geos.length; x++ ) {
bounds = bounds.intersection( geos[x] );
}
var coords = parser.write( bounds );
geo.find({
"loc": {
"$geoWithin": {
"$geometry": coords
}
}
}).toArray(callback);
}
],
callback
);
}
],
function(err,results) {
if (err) throw err;
console.log(
util.inspect( results.slice(-1), false, 12, true ) );
db.close();
}
);
});
});
Incorporate full GeoJSON "Polygon" representations as such translates to what JTS comprehends and works with. It's likely that any input for a real application would follow this format rather than opting for conveniences like $box
.
While achieving this task via the aggregation framework or parallel queries combining result sets is feasible, albeit the aggregation framework might handle it better than external result set merging, superior outcomes stem from initially calculating the bounds.