MongoDB
 sql >> Base de données >  >> NoSQL >> MongoDB

MongoDB - Intersection géospatiale de deux polygones

Donc, en regardant cela avec un esprit neuf, la réponse me saute aux yeux. L'élément clé que vous avez déjà indiqué est que vous souhaitez trouver "l'intersection" de deux requêtes dans une seule réponse.

Une autre façon de voir cela est que vous voulez que tous les points liés par la première requête soient ensuite "en entrée" pour la deuxième requête, et ainsi de suite si nécessaire. C'est essentiellement ce que fait une intersection, mais la logique est en fait littérale.

Il suffit donc d'utiliser le framework d'agrégation pour enchaîner les requêtes correspondantes. Pour un exemple simple, considérez les documents suivants :

{ "loc" : { "type" : "Point", "coordinates" : [ 4, 4 ] } }
{ "loc" : { "type" : "Point", "coordinates" : [ 8, 8 ] } }
{ "loc" : { "type" : "Point", "coordinates" : [ 12, 12 ] } }

Et le pipeline d'agrégation chaînée, juste deux requêtes :

db.geotest.aggregate([
    { "$match": {
        "loc": {
            "$geoWithin": {
                "$box": [ [0,0], [10,10] ]
            }
        }
    }},
    { "$match": {
        "loc": {
            "$geoWithin": {
                "$box": [ [5,5], [20,20] ]
            }
        }
    }}
])

Donc, si vous considérez cela logiquement, le premier résultat trouvera les points qui se situent dans les limites de la boîte initiale ou des deux premiers éléments. Ces résultats sont ensuite pris en compte par la deuxième requête, et puisque les nouvelles limites de boîte commencent à [5,5] qui exclut le premier point. Le troisième point était déjà exclu, mais si les restrictions de boîte étaient inversées, le résultat serait uniquement le même document central.

Comment cela fonctionne de manière assez unique pour le $geoWithin opérateur de requête par rapport à diverses autres fonctions géographiques :

Les résultats sont donc à la fois bons et mauvais. Bon dans la mesure où vous pouvez effectuer ce type d'opération sans index en place, mais mauvais car une fois que le pipeline d'agrégation a modifié les résultats de la collecte après la première opération de requête, aucun autre index ne peut être utilisé. Ainsi, tout avantage en termes de performances d'un index est perdu lors de la fusion des résultats de "l'ensemble" à partir de tout ce qui se trouve après le polygone/multipolygone initial tel qu'il est pris en charge.

Pour cette raison, je recommanderais toujours de calculer les limites d'intersection "à l'extérieur" de la requête envoyée à MongoDB. Même si le cadre d'agrégation peut le faire en raison de la nature "chaînée" du pipeline, et même si les intersections résultantes deviennent de plus en plus petites, votre meilleure performance est une requête unique avec les limites correctes qui peuvent utiliser tous les avantages de l'index.

Il existe différentes méthodes pour le faire, mais pour référence, voici une implémentation utilisant le JSTS bibliothèque, qui est un port JavaScript du populaire JTS bibliothèque pour Java. Il peut y en avoir d'autres ou d'autres ports de langage, mais cela a une analyse GeoJSON simple et des méthodes intégrées pour des choses telles que l'obtention des limites d'intersection :

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);
          });
        },

        // Run each version of the query
        function(callback) {
          async.parallel(
            [
              // Aggregation
              function(callback) {
                var pipeline = [];
                polys.forEach(function(poly) {
                  pipeline.push({
                    "$match": {
                      "loc": {
                        "$geoWithin": {
                          "$geometry": poly
                        }
                      }
                    }
                  });
                });

                geo.aggregate(pipeline,callback);
              },

              // Using 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();
      }
    );

  });

});

En utilisant les représentations GeoJSON "Polygon" complètes, cela se traduit par ce que JTS peut comprendre et utiliser. Il y a de fortes chances que toute entrée que vous pourriez recevoir pour une application réelle soit également dans ce format plutôt que d'appliquer des commodités telles que $box .

Cela peut donc être fait avec le cadre d'agrégation, ou même des requêtes parallèles fusionnant "l'ensemble" des résultats. Mais bien que le cadre d'agrégation puisse le faire mieux que de fusionner des ensembles de résultats en externe, les meilleurs résultats proviendront toujours du calcul des limites en premier.