Efektywne przeszukiwanie danych GIS w MongoDB

Kojarzycie MongoDB? Baza NoSQL operująca na dokumentach. Jak to NoSQL, ma swoje ograniczenia, ale jak już da się jej do czegoś użyć, to jest diabelnie szybka. Odkryłem ostatnio, że ma jeden bardzo fajny feature, wsparcie dla danych i operacji GIS!

GIS, nie wdając się w definicję pojęcia, służy do przechowywania i obróbki danych geograficznych lub przestrzennych. Dane to na przykład punkty, wielokąty, linie i co tam jeszcze się da.

Jakie operacje można wykonywać?

  • przeszukiwać przestrzennie, znajdując punkty w bliskiej odległości
  • znajdować przecięcia zbiorów – na przykład nakładające się wielokąty
  • przynależność do obszaru wyznaczonego przez wielokąty

Jeśli kiedykolwiek macie zamiar robić coś z użyciem mapy, na przykład w aplikacji mobilnej, użycie MongoDB do przygotowywania danych jest dobrym wyborem. Uwierzcie mi, że nie chcecie trzymać danych geograficznych w zwykłym SQLu. Tego się nie da efektywnie przechowywać.

W zasadzie jedyną alternatywą jest SQLowy PostGIS, czyli rozszerzenie do PostgresSQL oraz zamknięte systemy GISowe. Tutaj zasoby wiedzy będą większe, ale wydajnościowo MongoDB bije to na głowę.

Realne zastosowanie.

Pokażę przykład, który ma realne zastosowanie i który działa. Problem jest sformułowany następująco.

Na podstawie pozycji geograficznej, sprawdź przynależność do określonego obszaru.

Czyli potrzebny jest jakiś zbiór polygonów, który będziemy przeszukiwać. W przykładzie wystąpi zbiór Gmin Polski z Państwowego Rejestru Granic. Można go pobrać stąd

http://www.gis-support.pl/baza-wiedzy/dane/dane-do-pobrania/

W archiwum jest zbiór w formacie ShapeFile i kilka plików pobocznych, potrzebne są wszystkie. Trzeba je przerobić do formatu strawnego dla mongoimport, czyli GeoJSON z punktami w WGS84. Aby to uzyskać potrzebujemy pakietu GDAL, a konkretnie to zawartego w nim programu ogr2ogr

ogr2ogr -f "GeoJSON" -a_srs gminy.prj gminy.json gminy.shp -t_srs EPSG:4326

Parametr -a_srs odpowiada za podanie wejściowego systemu projekcji, w tym przypadku używanego w Polsce systemu PUWG 1992. Parametr -t_srs odpowiada za ustawienie wyjściowego systemu projekcji na WGS84, o kodzie EPSG:4326. Kody EPSG jednoznacznie identyfikują systemy projekcji, a jest ich na całym świecie całkiem dużo.

Jeśli mamy zamiar te dane potem wyświetlać na mapie, musimy uprościć wielokąty. Do samego przeszukiwania nie jest to konieczne. Plik wynikowy ma jakieś 230MB i próba wyrysowania tego na mapie nie jest dobrym pomysłem. Na szczęście GDAL potrafi upraszczać wielokąty, redukując liczbę wierzchołków. Parametrem jest maksymalna odległość od krzywej. Im większy parametr, tym bardziej uproszczone wielokąty.

ogr2ogr -f "GeoJSON" -simplify=0.01 gminy_simplified.json gminy.json

Czas na zaimportowanie danych do MongoDB, zakładam wersję nieuproszczoną. Potrzebujemy jeszcze jednego programu, by przefiltrować nieco dane. Oraz oczywiście samej bazy MongoDB oraz jej narzędzi.
jq --compact-output ".features" gminy.json >gminy_compacted.json
mongoimport --db JakaGmina -c gminy --jsonArray --file gminy_compacted.json

Jeśli z jakiegoś powodu mongoimport wykrzacza się przy n procent, dodajemy parametr –batchSize=100.

Odpalamy MongoDB!

Wydajemy polecenie mongo i otwieramy konsolę bazy danych. Teraz wypada powiedzieć słowo o języku zapytań. Mamy do czynienia z interpreterem JavaScript i w tym języku możemy pisać zapytania oraz procedury.

Należy wywalić niepotrzebne pola z properties, wszystko poza jpt_nazwa_ i jpt_kod_je.

use JakaGmina //przełączenie bazy danych, nazwa wybrana przy imporcie
doc = db.gminy.find().forEach(function(doc){
unset = {};
for (k in doc.properties) {
  if (k === 'jpt_nazwa_') continue;
  if (k === 'jpt_kod_je') continue;
  unset['properties.'+k] = 1;
}
db.gminy.update(doc, {$unset: unset})});

Najprostsze zapytania poniżej.
db.gminy.find(); //znajdź wszystkie dokumenty w kolekcji
db.gminy.find({'properties.jpt_nazwa_':'Warszawa'}); //znajdź gminę o nazwie Warszawa

Ok, a co z danymi przestrzennymi? Jak je przeszukiwać? Jest dostępnych kilka operatorów, my użyjemy $geoIntersects.
db.gminy.find({
	geometry:
	{
		$geoIntersects:
		{
			$geometry:
			{
			type:'Point', coordinates:[20.9249538,52.4017916]
			}
		}
	}
})

W ten sposób znajdujemy gminę, w której znajduje się punkt. Wynikiem zapytania powinna być gmina Legionowo.

Jeśli już wykonaliście to polecenie, to pewnie jesteście zaskoczeni „czemu tak wolno, to miało zapieprzać!”. Wszystko ok, brakuje nam jeszcze indeksu na danych geograficznych. Potrzebujemy założyć indeks na polu geometry. Niestety, dane wymagają jednej ręcznej poprawki. Gmina o nazwie „Rozdrażew” ma w środku dziurę i jeden powielony punkt. Prawy górny róg.

Próba założenia indeksu bez poprawki, nie powiedzie się. Jako, że to tylko jedna gmina, nie szukałem sposobu na automatyczne poprawianie danych

db.gminy.update({'properties.jpt_nazwa_':'Rozdrażew'},{$unset: {'geometry.coordinates.1': null}});
db.gminy.update({'properties.jpt_nazwa_':'Rozdrażew'},{$pull: {'geometry.coordinates': null}});
db.gminy.update({'properties.jpt_nazwa_':'Rozdrażew'},{$unset: {"geometry.coordinates.0.21" : null }})
db.gminy.update({'properties.jpt_nazwa_':'Rozdrażew'},{$pull: {'geometry.coordinates.0': null}});

db.gminy.ensureIndex({geometry: '2dsphere'}); //dodaj indeks 2dsphere do pola geometry

Teraz wykonaj ponownie przeszukanie. Szybko? Nawet bardzo. Zamiast kilku sekund, milisekundy.

Jak mi się uda znaleźć chwilę, to postaram się udostępnić kod, który wykorzystuje MongoDB do rysowania na mapie.

One thought on “Efektywne przeszukiwanie danych GIS w MongoDB”

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *