MongoDB: Búsquedas geoespaciales (localización geográfica)

Índice

Introducción

Actualmente gracias a los dispositivos móviles y a las características de geoposicionamiento de HTML5 es posible dotar a nuestras aplicaciones de interesantes valores añadidos para el usuario final ofreciendo todo tipo de información en base a su ubicación.

Gracias a MongoDB, si nuestra aplicación almacena las coordenadas geográficas (longitud, latitud) o bien de una dirección la cual puede ser traducida a coordenadas usando por ejemplo el API de Google Maps, podremos realizar búsquedas de información en base a su localización geográfica.

MongoDB nos permite realizar los siguientes tipos de consultas basadas en el posicionamiento:

  1. Consultas de proximidad, en donde obtendremos los documentos ordenados por proximidad (de más cercano a más distante) a un punto geográfico.
  2. Consultas de acotadas, en donde obtendremos los documentos que están dentro de un área (rectángulo, círculo o polígono).

Idependientemente del tipo de consulta, necesitamos indexar el campo que contiene las coordenadas geográficas, con formato: longitud, latitud.

db.collection.ensureIndex({<location field>: "2d"})

Consultas de proximidad

Este tipo de consultas por defecto devuelve los 100 primeros documentos más próximos a una coordenada ordenados de mayor a menor proximidad.

Los formatos genéricos de consulta son los siguientes (observe el uso de la palabra clave near):

db.collection.find( {<location field>: {$near: [x, y]}})

Además, usando el comando geoNear obtenemos más información (la distancia de cada documento a ese punto, la distancia máxima y la distancia media).

db.runCommand({geoNear: <coleccion>, near: [x, y]})

También es posible limitar los resultados de la consulta de manera que sólo devuelva los documentos que se encuentren a una distancia máxima en metros.

db.coleccion.find({<location field> : {$near : [x, y] , $maxDistance : <distance>}})

db.runCommand({geoNear: <coleccion>, near: [x, y], maxDistance: <distance>})

Consultas de acotadas

Con este tipo de consultas podemos obtener los documentos que están dentro de un círculo, rectángulo o polígono.

El formato genérico de consulta el siguiente (observe el uso de la palabra clave within):

db.collection.find({<location field>: {"$within" : {<shape> : <shape dimensions>}}})

El valor del argumento shape variará dependiendo del tipo de figura geométrica (círculo, rectángulo o polígono).

Círculo

Para obtener los documentos cuyas coordenadas caen dentro de la circunferencia con centro en las coordenadas [x, y] y radio <radio>

db.coleccion.find({<location field>: {"$within":{ "$center": [ [x, y], <radio>] }}});

Rectángulo

Para obtener los documentos cuyas coordenadas caen dentro del rectángulo delimitado por las coordenadas respectivas, esquina inferior izquierda (x1, y1), esquina superior derecha (x2, y2).

db.coleccion.find({<location field>: {"$within": {"$box": [[x1, y1], [x2, x2]]}}})

Polígono

Para obtener los documentos cuyas coordenadas caen dentro del polígono delimitado por una serie de coordenadas.

db.coleccion.find({<location field>: {"$within": {"$polygon": [[x1, y1], [x2, x2], ..., [xN, yN]]}}})

Ejemplos usando el Shell de MongoDB

Imagenemos que nuestra aplicación trabaja con datos sobre distintos productos en donde almacenamos la siguiente información:

  • _id: Identificador del producto.
  • title: Título del producto.
  • tags: Etiquetas asociadas al producto en base a su naturaleza.
  • price: Precio en euros.
  • createdDate: Fecha en la que el producto fué dado de alta en nuestro sistema.
  • state: Estado del producto (activo, vendido, etc)
  • user: Identificador del usuario que dio de alta el producto (imagenemos una web con usuarios registrados).
  • location: Posición geográfica dónde puede ser recogido el producto. Dicha localización se compone del pais, las coordenadas geográficas y la dirección completa.

En el shell de mongodb damos de alta los siguientes productos (dos productos de Madrid (centro de España) y uno en Granada (sur de España):

db.products.insert({_id:'1L', title: 'Apple computer', tags: ['electronic', 'computer', 'apple'], price: 1800, createdDate: Date(), state: 'active', user: '1L', location: { country: 'Spain', point: {longitude: 40.42348, latitude: -3.540919}, address: 'Calle Honduras 30, Coslada, Madrid, España' } });

 

db.products.insert({_id:'2L', title: 'Apple Magic mouse', tags: ['electronic', 'mouse', 'apple'], price: 50, createdDate: Date(), state: 'active', user: '2L', location: { country: 'Spain', point: {longitude: 40.423036, latitude: -3.558385}, address: 'Calle Chile 10, Coslada, Madrid, España' } });

 

db.products.insert({_id:'3L', title: 'Renault Clio Alice Diesel', tags: ['car', 'renault'], price: 2500, createdDate: Date(), state: 'active', user: '3L', location: { country: 'Spain', point: {longitude: 37.743671, latitude: -2.552276}, address: 'Calle Elvira 10, Galera, Granada, España' } });

Para conocer las coordenadas a partir de una dirección he usado la web http://itouchmap.com/latlong.html.

Para poder realizar consultas geoespaciales necesitamos indexar el campo que contiene las coordenadas geográficas:

db.products.ensureIndex({"location.point": "2d"})

Una vez indexada la información, comenzemos a realizar consultas, por ejemplo, las coordenadas [37.179569,-3.599607] son las de la "Gran Via de Colón" de Granada, España.

db.products.find({"location.point": {$near: [37.179569,-3.599607]}}, {_id:1});
{ "_id" : "3L" }{ "_id" : "1L" }{ "_id" : "2L" }

Para una visualización más abreviada, devuelvo sólo el campo _id, y efectivamente mirando el mapa usando Google Maps vemos que el resultado es correcto.

db.runCommand( { geoNear: 'products', near: [37.179569,-3.599607], distanceMultiplier: (6371 * Math.PI / 180)} );

Observe el campo dis la distancia en kilómetros respecto a las coordenadas de la consulta. (en la sección referencias tienes una web dónde puedes calcular la distancia entre dos coordenadas)
{ "ns" : "test.products", "near" : "1001011111011001001111110000000001100111001101100010", "results" : [ { "dis" : 132.2758550310587, "obj" : { "_id" : "3L", ... "location" : { "country" : "Spain", "point" : { "longitude" : 37.743671, "latitude" : -2.552276 }, "address" : "Calle Elvira 10, Galera, Granada, España" } } }, { "dis" : 360.68620148978886, "obj" : { "_id" : "2L", ... "location" : { "country" : "Spain", "point" : { "longitude" : 40.423036, "latitude" : -3.558385 }, "address" : "Calle Chile 10, Coslada, Madrid, España" } } }, { "dis" : 360.7654724719795, "obj" : { "_id" : "1L", ... "location" : { "country" : "Spain", "point" : { "longitude" : 40.42348, "latitude" : -3.540919 }, "address" : "Calle Honduras 30, Coslada, Madrid, España" } } } ], "stats" : { "time" : 0, "btreelocs" : 0, "nscanned" : 3, "objectsLoaded" : 3, "avgDistance" : 284.575842997609, "maxDistance" : 3.244452435202979 }, "ok" : 1 }

Ahora busquemos aquellos productos en círculo de radio de 150 Km tomando como punto central la "Puerta del Sol de Madrid (centro de España)"

db.products.find({'location.point': {$within: {$centerSphere: [[40.416752,-3.70337], 150/3963.192]}}}, {_id: 1});

El resultado son los dos productos de Madrid, exluyendose el producto de Granada que está más lejos.

{ "_id" : "2L" }{ "_id" : "1L" }

Ahora busquemos aquellos productos que están dentro de un rectángulo:

db.products.find({"location.point": {"$within": {"$box": [[40.421158,-3.547318] , [40.425095,-3.537834]]}}}, {_id: 1});

Al ser un área muy delitida a propósito (http://itouchmap.com/latlong.html), vemos que efectivamente devuelve un único documento.

{ "_id" : "1L" }

Conclusiones:

Gracias a MongoDB podemos dotar de una manera sencilla, un interesante valor añadido de manera gratuita.

Si necesitamos operaciones más detalladas con distancias, rutas, etc, vamos a requerir otras APIS como Google Maps, pero de base, MongoDB de por sí nos provee un interesante abanico de posibilidades.

Referencias:

Bueno, espero que os sea de utilidad, recuerda que puedes profundizar más, esto es sólo una introducción.
Un saludo. Carlos García

Categorías del artículo

Comentarios de los lectores