Neben der reinen Volltext-Suche wird die geografische Suche immer wichtiger. Viele von den Diensten heutzutage sind ortsgebunden, was auch Sinn macht, denn wenn ich Hunger habe und nach einer guten Pizzeria suche, nützt es mir nichts, wenn die Suche mir ein Lokal in Berlin vorschlägt und ich gerade in Augsburg unterwegs bin.
Dieser Artikel befasst sich mit den Möglichkeiten, die Apache Solr bietet, um solche Funktionalität zu unterstützen.
Solr und die Geo-Suche
Solr bietet aktuell im Rahmen der geografischen Suche zwei unterschiedliche Varianten an. Bei der ersten Variante handelt es sich um einen geografischen Punkt. Dieser Punkt ist durch einen Breitengrad und einen Längengrad beschrieben.
Die zweite Variante der Suche behandelt die Polygone. Mit Hilfe von Polygonen werden, z.B., geographische Regionen, wie Bundesländer, Städte etc. abgebildet.
Der Punkt
Die erste Variante ist sehr gut dazu geeignet eine Umkreissuche zu realisieren. D.h. so ein Szenario, das am Anfang des Artikels beschrieben wurde, benötigen in der Regel nur die geografischen Positionen von zwei Objekten, dem Ausgangs- und dem Zielpunkt.
Indexierung
Koordinate, speichert man in Solr in einem Feld vom Typ LatLongType, der wiederum den Wert für den Breiten- bzw. Längengrad in separaten Feldern abspeichert. Somit benötigt man im Solr für die Speicherung des geografischen Punktes drei einzelne Felder.
Beispielkonfiguration
<fieldtype name="location" class="solr.LatLongType" subFieldSuffix="_coordinate"/>
<dynamicfield name="*_coordinate" type="tdouble" indexed="true" stored="false"/>
Es reicht, wenn man die beiden Werte für Breiten- und Längengrad getrennt durch Komma angibt. Bei der Indexierung muss jedoch nur das Feld vom Typ LatLongType angesprochen werden.
Beispiel
<field name="store">45.17614,-93.87341</field>
Suche
Bei der Suche kann man nun mit Solr die Umkreissuche basierend auf einer BoundingBox, einem „wirklichen“ Umkreis oder der Entfernung umsetzen. Hierfür gibt es die Funktionen bbox, geofilt und geodist
- bbox – Diese Funktion berechnet ein Quadrat um den Start- bzw. Ausgangspunkt
- geofilt – Diese Funktion berechnet einen Kreis um den Start- bzw. Ausgangspunkt
- geodist – Diese Funktion berechnet den Abstand zwischen einem Zielpunkt (LatLon Koordinate) und dem Start- bzw. Ausgangspunkt
Beispiel
Kommen wir noch einmal auf das Szenario mit der Suche nach einem Lokal zurück und nehmen wir an, dass in meinem Index Restaurants mit ihren Geo-Koordinaten indexiert sind. Meine Suche nach einer Pizzeria im Umkreis von 2 km könnte dann, wie folgt, aussehen:
&q=pizza&fq={!geofilt}&sfield=restaurant_location&pt= 48.3291,10.8578&d=2
Bei dem oben angegebenem Beispiel definiert der Parameter sfield das Feld, in dem die Geo-Koordinate abgespeichert ist, d gibt die Entfernung in km an und pt ist der Start- bzw. Ausgangspunkt.
Einsatzszenarien
Die oben beschriebenen Funktionen können im folgenden Kontext genutzt werden:
- Suche – wie im obigen Beispiel beschrieben, kann ich die Trefferliste auf die Einträge einschränken, die in einer bestimmten Entfernung sind.
- Sortierung – nutzt man diese Funktionen im sort-Parameter, kann man die Trefferliste nach Entfernung sortieren, so dass das nächstliegende Restaurant als erstes angezeigt wird.
- Facetten – Mit den Query Facetten kann man sich eine Geo-Facette erstellen lassen, die die Restaurants in „Ringe“ – bei geofilt – einteilt, d.h. alle Restaurants im Umkreis von 0 bis 2 km, dann alle Restaurants im Umkreis von 2 bis 5 km etc. Ein Beispiel hierfür sieht wie folgt aus:
&sfield= restaurant_location&pt=48.3291,10.8578&facet.query={!geofilt d=2 key=2km}&facet.query={!geofilt d=5 key=5km}&…
Diese Umkreissuche, basierend auf einem geografischen Punkt, ist recht einfach zu konfigurieren und auch schon seit Version 3.1 in Solr integriert.
Das Polygon
Mit der Solr Version 4 wurde nicht nur die SolrCloud eingeführt; es wurde auch ein neues Kapitel in der Geo-Suche aufgeschlagen, denn es wurde ein neuer Feldtyp eingeführt, mit dem man nun auch die Polygone indexieren kann. Dies ist vor allem dann sehr interessant, wenn man beispielsweise Kartenausschnitte (nicht quadratische Vierecke) oder komplexere Formen, wie Städte oder Bundesländer in die Suche einbringen möchte. Der Feldtyp hierfür ist leicht angelegt.
<fieldtype name="location_rpt" class="solr.SpatialRecursivePrefixTreeFieldType" spatialContextFactory="com.spatial4j.core.context.jts.JtsSpatialCon textFactory" distErrPct="0.025" maxDistErr="0.000009" units="degrees"/>
Es gibt hier prinzipiell zwei Möglichkeiten die Flächen zu definieren. Zum einen kann man Rechtecke definieren, in dem man die Werte minX, minY, maxX und maxY angibt. In diesem Fall kann das Property spatialContextFactory in der Typ-Definition auch entfallen. Die andere Variante ist die Angabe des Polygons mittels des WKT (Well-Known-Text) Standards, d.h. alle Punkte des Polygons werden durch Komma getrennt angegeben, wobei der Start- und der Endpunkt identisch sind. Beim Solr muss auf jeden Fall beachtet werden, dass die Punkte gegen den Uhrzeigersinn angegeben werden müssen. Tut man dies nicht, funktionieren die ganzen Flächenfunktionen nicht, die ich noch beschreiben werde.
Beispiel
Indexierung eines Kartenausschnitts mit minX minY maxX maxY
<field name="geo">-74.093 41.042 -69.347 44.558</field>
Beispiel
Indexierung eines Polygons (Dreieck)
<field name="geo">POLYGON((-10 30,-40 40,-10 -20,-10 30))</field>
Die doppelten Klammern sind hier kein Versehen, sondern Absicht, denn der WKT Standard lässt zu, dass man gleichzeitig mehrere Flächen definieren kann bzw. Flächen in denen Flächen ausgeschnitten sind.
Sind solche Flächen nun indexiert, kann man sie für die Suche verwenden. Solr prüft, in wie fern die indexierten Flächen mit einer Fläche, die zur Suchzeit definiert wird, interagiert und schränkt so das Ergebnis entsprechend ein. Hierfür werden folgende Funktionen bereitgestellt:
- Intersects – Diese Funktion prüft, ob sich beide Flächen (über)schneiden, falls Ja zählt das Dokument als Treffer
- IsWithin – Diese Funktion prüft, ob die Fläche des indexierten Dokumentes innerhalb der Fläche ist, die zur Suchzeit angegeben wird
- Contains – diese Funktion prüft, ob die zur Suchzeit angegebene Fläche innerhalb der im Dokument definierten Fläche liegt.
- IsDisjointTo – Diese Funktion prüft, ob sich die Flächen nicht schneiden.
Eine Einschränkung zur Suchzeit kann als FilterQuery dann wie folgt aussehen:
Beispiel Suche Kartenausschnitt mit minX minY maxX maxY
fq=geo:"Intersects(-74.093 41.042 -69.347 44.558)"
Beispiel Suche Polygon (Dreieck)
fq=geo:"IsWithin(POLYGON((-10 30,-40 40,-10 -20,-10 30))) distErrPct=0"
Natürlich kann ich mit diesen Queries – analog zu dem geografischen Punkt – mittels des facet.query Parameter eine eigene Facette generieren, die zum Beispiel die Dokumente nach Bundesland aufteilt.
Eine geografische Suche basierend auf Flächen ist also auch mit relativ geringem Aufwand machbar. Weit schwieriger wird es sein, die richtigen Flächen zu definieren, um auch qualitativ hochwertige Suchen anbieten zu können.
Fazit
Die Geo-Suche steckt, meiner Meinung nach zwar nicht mehr in den Kinderschuhen, aber es gibt hier noch einige TODOs.
Es fehlen beispielsweise noch Funktionalitäten (FunctionQueries), die es erlauben, auch bei den Polygonen das Scoring zu beeinflussen; z.B. nach der Fläche, die entsteht, wenn sich zwei Polygone schneiden.
Positiv ist mir aufgefallen, dass ich den neuen Feldtyp zusammen mit der Funktionalität „Intersects“ nutzen kann, um Geo-Suche auch ohne Geo-Daten durchzuführen. Hierzu gibt es einen schönen Blog zum Thema Schicht- bzw. Arbeitszeitplanung. Dieser Einsatz ermöglicht plötzlich unerwartete Einsatzmöglichkeiten.