Geocoding (Teil 2) mit Elasticsearch und OpenStreetMap
In meinem ersten Blog-Post erwähnte ich, dass mir die Benutzung von OpenStreetMap (OSM) für eine Georeferenzierung mit Elasticsearch als zu aufwändig erschien, jedoch lies mich dieses Thema nie so richtig los. Nach weiteren Internetrecherchen stieß ich schließlich auf ein Elasticsearch Plug-in für Osmosis [1], das Nicolas Colomer entwickelte. Osmosis [2] ist ein Java Applikation, die OSM Daten lesen und beispielsweise in eine Datenbank importieren kann.
Erzeugen des Elasticsearch-Index
Hierzu ging ich genau wie in der Anleitung [1] beschrieben vor. Für Schritt 1 musste ich jedoch das Verzeichnis noch anlegen und die Variable JAVACMD_OPTIONS hatte zu viele Anführungszeichen. Hier meine erweiterten Linux-Anweisungen:
# Osmosis 0.43 installation
wget -P /tmp http://dev.openstreetmap.org/~bretth/osmosis-build/osmosis-0.43-RELEASE.tgz
# BEGIN - meine Anpassungen
mkdir /opt/osmosis-0.43
tar -zxvf /tmp/osmosis-0.43-RELEASE.tgz -C /opt/osmosis-0.43
echo "JAVACMD_OPTIONS=\"-server -Xmx2G\"" > /etc/osmosis # Put your JVM params there
# END - meine Anpassungen
export OSMOSIS_HOME=/opt/osmosis-0.43
export PATH=$PATH:$OSMOSIS_HOME/bin
Bei Punkt 3 habe ich die gesamten OSM Daten von Deutschland [3] heruntergeladen, das Paket ist 1,8 GByte groß. Der fertige Elasticsearch Index hat eine Größe von 11,7 GByte.
Wie exakt sind die OpenStreetMap Informationen?
Ein paar Experimente mit unterschiedlichen Suchanfragen zeigten, dass es bei Hausnummern Ungenauigkeiten gibt. So kann es beispielsweise vorkommen, dass 3 beieinanderliegende Reihenhäuser, die die Nummern 9,11 und 13 haben mit 9-13 markiert wurden. Wer also Geodaten benötigt, die über eine Genauigkeit bis auf Hausnummernebene verfügen, wird vermutlich zu einem professionellen Anbieter wechseln müssen. Des Weiteren sind POIs wie Schulen besser gepflegt als bspw. Restaurants, da diese sich auch nicht so schnell ändern. Für viele Anwendungsgebiete müssten meiner Meinung nach jedoch die OSM Daten ausreichen, zumal dies eine ausgesprochen preiswerte Variante ist.
Wozu ist die Kombination OpenStreetMap und Elasticsearch zu gebrauchen?
Ich könnte mir folgende Einsatzszenarien vorstellen:
- Postleitzahlen (PLZ) Umkreissuche, die im ersten Teil vorgestellt habe. Jedoch könnte man mit Geo-Koordinaten auf Ebene von Strassen arbeiten.
- Es wäre denkbar Adressdaten aus einem CRM System zu überprüfen. Oder man überprüft beim Ausfüllen eines Online-Formulars, ob die Adressdaten gültig sind.
- Man könnte Straßennamen per Auto-Complete beim Ausfüllen eines Online-Formulars anbieten.
- Immobilien-Portale wie ImmobilienScout24 könnten Orte von Interesse (bspw. Schulen) in Umgebung einer Wohnung anzeigen.
- ...
Ein paar Beispiele für Elasticsearch Geo-Suchanfragen
Ermitteln von Strassen, die sich in einem bestimmten Abstand (geo_distance_range Filter von Elasticsearch) zu einer GPS Koordinate befinden:
curl -XPOST "http://localhost:9200/_search?size=1&pretty=true" -d'
{
"fields":[
"tags.name",
"tags.addr:city",
"tags.addr:street",
"tags.addr:housenumber",
"centroid"
],
"query":{
"filtered":{
"query":{
"match_all":{
}
},
"filter":{
"and":[
{
"geo_distance_range":{
"from":"10m",
"to":"40m",
"centroid":{
"lat":48.98110473912166,
"lon":8.410755143832603
}
}
},
{
"term":{
"_type":"way"
}
}
]
}
}
}
}'
Das Ergebnis (es wird nur das erste Element abgefrag)t:
{
"took": 18,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"failed": 0
},
"hits": {
"total": 9,
"max_score": 1,
"hits": [
{
"_index": "osm",
"_type": "way",
"_id": "240351635",
"_score": 1,
"fields": {
"tags.addr:housenumber": "16",
"tags.addr:city": "Karlsruhe",
"tags.addr:street": "Am Rüppurrer Schloss",
"centroid": [
8.411199259007871,
48.98118619135338
]
}
}
]
}
}
Ermitteln der Postleitzahl und GPS Koordinaten anhand Ort und Strasse:
curl -XPOST "http://localhost:9200/_search?size=1&pretty=true" -d'
{
"fields":[
"tags.addr:postcode",
"centroid"
],
"query":{
"filtered":{
"query":{
"match_all":{
}
},
"filter":{
"and":[
{
"term":{
"tags.addr:city":"Karlsruhe"
}
},
{
"term":{
"tags.addr:street":"Waldstraße"
}
}
]
}
}
}
}'
Das Ergebnis sieht so aus:
{
"took": 6,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"failed": 0
},
"hits": {
"total": 13,
"max_score": 1,
"hits": [
{
"_index": "osm",
"_type": "node",
"_id": "60019232",
"_score": 1,
"fields": {
"tags.addr:postcode": "76133",
"centroid": [
8.3984971,
49.0107008
]
}
}
]
}
}
Nun suchen wir Schulen mit geo_bounding_box, diese befinden sich in einem Rechteck, das über zwei Geo-Koordinaten definiert wird. Es wird wieder nur 1 Element zurückgegeben, damit der Blog-Post nicht zu lang wird.
curl -XPOST "http://localhost:9200/_search?size=1&pretty=true" -d'
{
"fields":[
"tags.name",
"tags.addr:postcode"
],
"query":{
"filtered":{
"query":{
"match_all":{
}
},
"filter":{
"and":[
{
"term":{
"tags.amenity":"school"
}
},
{
"geo_bounding_box":{
"centroid":{
"top_left":"48.98110473912166,8.410755143832603",
"bottom_right":"48.935201915135394,8.429190727359979"
}
}
}
]
}
}
}
}'
Das Ergebnis sieht so aus:
{
"took": 6,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"failed": 0
},
"hits": {
"total": 6,
"max_score": 1,
"hits": [
{
"_index": "osm",
"_type": "node",
"_id": "29416336",
"_score": 1,
"fields": {
"tags.name": "Philipp-Thiebauth-Schule"
}
}
]
}
}
Oder eine Abfrage, die mir alle Hausnummern einer Strasse zurückliefert:
curl -XPOST "http://134.119.3.210:9200/_search?size=1&pretty=true" -d'
{
"fields":[
"tags.name",
"tags.addr:city",
"tags.addr:street",
"tags.addr:housenumber",
"centroid"
],
"sort":[
{
"tags.addr:housenumber":"asc"
}
],
"query":{
"filtered":{
"query":{
"match_all":{
}
},
"filter":{
"and":[
{
"term":{
"tags.addr:postcode":"76199"
}
},
{
"term":{
"tags.addr:city":"Karlsruhe"
}
},
{
"term":{
"tags.addr:street":"Am Rüppurrer Schloss"
}
},
{
"term":{
"_type":"way"
}
}
]
}
}
}
}'
Hier das Ergebnis (wieder nur 1 Element):
{
"took": 6,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"failed": 0
},
"hits": {
"total": 19,
"max_score": null,
"hits": [
{
"_index": "osm",
"_type": "way",
"_id": "87759641",
"_score": null,
"fields": {
"tags.addr:housenumber": "1",
"tags.addr:city": "Karlsruhe",
"tags.addr:street": "Am Rüppurrer Schloss",
"centroid": [
8.40511215753399,
48.98134597514813
]
},
"sort": [
"1"
]
}
]
}
}
Fazit
Elasticsearch eignet sich sehr gut für die Abfrage von Geo-Daten und ist im Vergleich zu SQL Datenbanken wohl einfacher. Bei meinen Tests ist mir des Weiteren aufgefallen, dass bspw. für Karlsruhe die OSM Daten nicht vollständig vorhanden waren. Ein erneutes Indizieren der OSM Daten von Baden-Württemberg konnte das Problem schließlich lösen. Die Firma Geofabrik extrahiert täglich aus allen OpenStreetMap-Daten lokale Daten bspw. für Deutschland, da kann es schon einmal vorkommen, dass so ein Batch nicht ganz korrekt läuft. Für einen professionellen Einsatz mag es sich lohnen, die Firma Geofabrik zu kontaktieren. Hier noch 2 nützliche Links. Über diesen [4] erhält man eine Liste mit allen Tags, die in OSM verfügbar sind. Und über diesen [5], wie gut die einzelnen Tags gepflegt sind. Viel Spaß beim Experimentieren!