Vediamo cosa sono e come si utilizzano i dati geospaziali in MongoDB.
Dati di tipo geospaziali
MongoDB offre la possibilità di salvare all’interno di un documento dei dati di tipo geospaziale. In particolare sono due i formati supportati:
- il formato GeoJSON consente di rappresentare oggetti geometrici come punti, linee spezzate e poligoni. È il formato consigliato per salvare le coordinate geografiche in quanto nel calcolare misure e distanze tiene in considerazione la forma sferoidale della Terra, ottenendo così calcoli più precisi;
- il formato obsoleto di coordinate bidimensionali può essere invece usato per calcolare le distanze su un piano Euclideo. Le coordinate relative a un punto del piano sono solitamente salvate in un array secondo il formato
[x, y]
.
Oggetti GeoJSON
Un oggetto di tipo GeoJSON ha una struttura ben definita che prevede la presenza di due campi:
- il campo ‘type’ definisce il tipo di oggetto GeoJSON. Alcuni dei valori permessi sono: ‘Point’, ‘LineString’, ‘Polygon’;
- il campo ‘coordinates’ indica quali sono le coordinate. Per convenzione le coordinate vanno specificate indicando prima la longitudine e poi la latitudine, al contrario di quanto avviene con alcune applicazioni di mappe e navigazione.
Per esempio possiamo definire le coordinate di un punto con il seguente documento GeoJSON.
{
"type": "Point",
"coordinates": [
16.184256076812744,
41.88917978666802
]
}
Oppure per una linea spezzata possiamo elencare nell’array ‘coordinates’ le coppie di coordinate dei punti che la formano. Il campo ‘type’ sarà in questo caso uguale a ‘LineString’.
{
"type": "LineString",
"coordinates": [
[
12.45849609375,
41.902277040963696
],
[
11.25,
43.75522505306928
],
[
8.909912109375,
44.41808794374846
],
[
9.140625,
45.44471679159555
]
]
}
Infine per un poligono, avremo un campo ‘type’ uguale a ‘Polygon‘ mentre il campo ‘coordinates’ è un array di array di coordinate GeoJSON LinearRing. Il tipo LinearRing rappresenta delle linee spezzate (LineString) chiuse che hanno almeno quattro coppie di coordinate e specificano la stessa coordinata per il primo e ultimo vertice del poligono.
{
"type": "Polygon",
"coordinates": [
[
[
2.1874809265136714,
41.38324879874266
],
[
2.179584503173828,
41.39174892980349
],
[
2.1742630004882812,
41.39522593585012
],
[
2.1579551696777344,
41.381832002193605
],
[
2.1697998046875,
41.374232293915426
],
[
2.177009582519531,
41.37255766253563
],
[
2.1874809265136714,
41.38324879874266
]
]
]
}
Se per le coordinate di un singolo punto non dovrebbero esserci particolarmente problemi, per definire le coordinate di una linea spezzata o a maggior ragione di un poligono può essere comodo usare uno strumento come GeoJSON.io che consente di definire le forme geometriche direttamente su una mappa.
Tipi geospaziali e MongoDB Compass
Quando si lavora con coordinate geografiche può essere utile e più intuitivo visualizzare la posizione su una mappa.
In MongoDB Compass è possibile selezionare una collezione ed analizzare lo schema per ottenere maggiori informazioni sul tipo di dati.
Se per esempio consideriamo la collezione sample_airbnb.listingsAndReviews
che abbiamo caricato su Atlas nelle prime lezioni, possiamo ottenere i dettagli del campo address.location
e visualizzare le coordinate degli appartamenti direttamente su una mappa.
Indici di tipo 2dsphere
Per poter eseguire delle interrogazioni su campi di tipo GeoJSON è necessario creare prima un indice di tipo 2dsphere
e per farlo è sufficiente usare il metodo db.collection.createIndex()
.
Per esempio, se volessimo creare un indice per il campo address.location
della collezione sample_airbnb.listingsAndReviews
basterebbe eseguire la seguente istruzione (dopo aver ovviamente selezionato il database use sample_airbnb
).
db.listingsAndReviews.createIndex({"address.location": "2dsphere"})
Query su campi di tipo GeoJSON
Una volta creato l’indice, potremo eseguire delle query usando uno degli operatori geospaziali. In questa lezione ne vedremo in particolare tre e trattandosi di operatori sappiamo che il loro nome presenta il prefisso ‘$’.
L’operatore $geoWithin
Il primo operatore che presentiamo è $geoWithin che consente di selezionare i documenti con dati geospaziali completamente inclusi all’interno della forma geometrica specificata.
La sintassi da usare è la seguente.
{
<location field>: {
$geoWithin: {
$geometry: {
type: <"Polygon" or "MultiPolygon"> ,
coordinates: [ <coordinates> ]
}
}
}
}
Vediamo subito un esempio usando il documento GeoJSON definito in precedenza che fa riferimento ad un poligono il quale racchiude una certa area della città di Barcellona. E cerchiamo gli edifici della collezione sample_airbnb.listingsAndReviews
che sono contenuti in quel poligono. Ovviamente possiamo usare il metodo db.collection.find()
come abbiamo fatto nelle precedenti lezioni. In questo caso passiamo un secondo documento per indicare quali devono essere i soli campi da includere nel risultato.
> var polygon = {
"type": "Polygon",
"coordinates": [
[
[
2.1874809265136714,
41.38324879874266
],
[
2.179584503173828,
41.39174892980349
],
[
2.1742630004882812,
41.39522593585012
],
[
2.1579551696777344,
41.381832002193605
],
[
2.1697998046875,
41.374232293915426
],
[
2.177009582519531,
41.37255766253563
],
[
2.1874809265136714,
41.38324879874266
]
]
]
}
> db.listingsAndReviews.find(
{
"address.location": {
$geoWithin: {
$geometry: polygon
}
}
}, {
_id: 0,
name: 1,
"address.street": 1
})
L’operatore $geoIntersects
L’operatore $geoIntersects usa una sintassi simile a quella di $geoWithin.
> db.listingsAndReviews.find(
{
"address.location": {
$geoIntersects: {
$geometry: polygon
}
}
}, {
_id: 0,
name: 1,
"address.street": 1
})
Tuttavia, mentre $geoWithin
cerca documenti con dati geospaziali riferiti a forme che sono contenute interamente in un determinato poligono, $geoIntersects
restituisce anche quelle che si intersecano parzialmente. Se un campo di un documento contiene le coordinate di un punto, il risultato è lo stesso. Diverso è il caso in cui si tratta di campi a cui sono assegnati documenti GeoJSON di tipo ‘Polygon’. In quest’ultima ipotesi, $geoWithin
restituisce solo i poligoni completamente contenuti nella forma geometrica della query del metodo db.collection.find()
, al contrario all’operatore $geoIntersects
basta che un campo intersechi il poligono della query per aggiungere il documento nel risultato da restituire.
L’operatore $near
Un altro operatore estremamente utile è $near
che consente di ottenere i documenti ordinati in senso crescente in base alla distanza da un punto che indichiamo nella query.
Come avremo modo di vedere nei prossimi paragrafi, $near è disponibile anche per le coordinate 2d, per il momento consideriamo però solo i punti espressi in formato GeoJSON.
Per questi ultimi è possibile utilizzare gli operatori opzionali $minDistance e $maxDistance per limitare i risultati in base alla distanza in metri. In particolare $minDistance indica la distanza minima in metri che i documenti devono avere da un punto per essere inclusi nel risultato finale. Invece $maxDistance esprime la distanza massima.
Vediamo allora un esempio considerando sempre la collezione sample_airbnb.listingsAndReviews
e cerchiamo le proprietà che sono distanti almeno 500 metri, ma non più di 3km da Casa Batlló a Barcellona.
> var casaBatllo = {
type: "Point",
coordinates: [2.1380332, 41.3949953]
}
> db.listingsAndReviews.find(
{
"address.location": {
$near: {
$geometry: casaBatllo,
$minDistance: 500,
$maxDistance: 3000
}
}
}
)
Indici di tipo 2d
Per campi di coordinate 2d dovremo creare invece un indice di tipo ‘2d’.
Facendo riferimento alla collezione restaurants del database sample_restaurants che abbiamo caricato su Atlas nell prime lezioni, possiamo creare un indice di tipo ‘2d’ sul campo address.coord
.
db.restaurants.createIndex({"address.coord": "2d"})
Query su campi di coordinate 2d
Una volta definito un indice 2d, possiamo usare l’operatore $near
anche per il sistema di coordinate su un piano. La sintassi è simile all’esempio visto per i documenti GeoJSON. In questo caso non possiamo però esprimere una distanza minima, ma solo una distanza massima espressa in radianti ($maxDistance).
Sotto è riportato un esempio che fa uso dell’operatore $near
. Abbiamo specificato innanzitutto le coordinate di un punto. Con il metodo db.collection.find()
otteniamo poi una lista di ristoranti presenti entro un certo raggio di distanza dal punto di interesse. Il risultato è ordinato in base alla distanza.
> var littleItaly = [
-73.99759769439697,
40.71948548899755
];
> db.restaurants.find(
{
'address.coord': {
$near: littleItaly,
$maxDistance: 0.002
}
}
)
Anche per questo sistema di coordinate possiamo verificare l’inclusione entro una certa forma geometrica con l’operatore $geoWithin
. Solo che in questo caso la sintassi e leggermente diversa.
{
<location field>: {
$geoWithin: { <shape-operator>: <coordinates> }
}
}
E gli operatori disponibili per definire una forma sono:
$box
consente di specificare un rettangolo per il quale dovremo indicare le coordinate del vertice in basso a sinistra e in alto a destra.$polygon
permette di definire un poligono di cui dobbiamo indicare le coordinate dei vertici.$center
dà la possibilità di definire le coordinate del centro di un cerchio e il suo raggio.
Vediamo allora un esempio per cercare i ristoranti della collezione sample_restaurants.restaurants presenti all’interno di un poligono di cui specifichiamo le coordinate dei vertici.
> var littleItaly = [
[
-73.9989709854126,
40.7214044998681
],
[
-73.99974346160887,
40.719290332250566
],
[
-74.00150299072266,
40.71805432623303
],
[
-73.99588108062744,
40.71603763556807
],
[
-73.99420738220215,
40.72046126415031
],
[
-73.9989709854126,
40.7214044998681
]
]
> db.restaurants.find(
{
'address.coord': {
$geoWithin: { $polygon: littleItaly }
}
}
)
Per concludere segnaliamo che l’operatore $geoIntersects
non è invece supportato per gli indici 2d.
Nella prossima lezione…
Nella prossima lezione parleremo dell’"Aggregation Framework" che offre una serie di operatori per manipolare i documenti ed effettuare operazioni che non sarebbero possibili in MQL.