Join Query mit Apache Solr 4.10
In diesem Artikel erklären wir die Join-Query, die mit Version 4 von Solr eingeführt wurde.
Eine Join-Query wird, genau wie in SQL, dazu verwendet, einzelne Datensätze in Beziehung zu setzen und somit Denormalisierung zu verhindern und komplexere Abfragen in nur einem Schritt ausführen zu können, ohne alle Zwischenergebnis abfragen zu müssen.
Trotz der großen Gemeinsamkeiten mit dem SQL-Pendant gibt es natürlich auch Unterschiede und Besonderheiten.
Solr Join im Detail
Ein Join wird in einer Solr-Query über die lokalen Parameter abgebildet.
Die allgemeine Syntax lautet{!join from=X to=Y}Q, wobei X und Y jeweils Feldnamen und Q eine gültige Solr-Query sind.
Das Ergebnis wird auf folgende Weise ermittelt:
- Als Zwischenergebnis alle Dokumente ermitteln, die auf die Query Q passen
- Als Endergebnis alle Dokumente ermitteln, die im Feld Y einen Wert haben, den die Dokumente aus dem Zwischenergebnis im Feld X haben
Diese Art der Abarbeitung deckt sich mit der von Sub-Selects in SQL:
Eine Join-Query der Form {!join from=X to=Y}Q entspricht daher einer SQL-Abfrage der Form select * from t where t.Y = (select X from t where Q).
Wobei t dem Namen unseres Solr-Cores entspricht.
Beispiele
{!join from=id to=manufacturer_id}companyName:ACME
Finde alle Dokumente, die im Feld manufacturer_id einen Wert haben, den Dokumente im Feld id haben, deren Feld companyName den Wert “ACME” hat.
D.h. finde alle Dokumente, die einem Hersteller zugeordnet sind, dessen Name “ACME” ist.
{!join from=id to=country_id}name:G*
Finde alle Dokumente, die im Feld country_id einen Wert haben, den Dokumente im Feld id haben, deren Feld name einen Wert hat, der mit “G” beginnt.
D.h. finde alle Dokumente, die einem Land zugeordnet sind, das mit “G” beginnt.
{!join from=id to=employee_id}gender=female AND first_name:A*
Finde alle Dokumente, die im Feld employee_id einen Wert haben, den Dokumente im Feld id haben, deren Feld gender den Wert “female” und deren Feld first_name einen Wert hat, der mit “A” beginnt.
D.h. finde alle Dokumente, die einem Mitarbeiter zugeordnet sind, der weiblich ist und deren Vorname mit “A” beginnt.
[call-to-action-consulting /]
Einschränkungen
Keine Verteilung der Daten
Bei den bisherigen Beispielen enthielt der Solr-Index immer Dokumente mit verschiedenen inhaltlichen Bedeutungen, z.B. Hersteller und Produkte, Länder und Städte, Mitarbeiter und Projekte. Dies kann dazu führen, dass das Schema viele Felder vorsieht, die einzelnen Dokumente aber immer nur wenige davon benutzen. Sind bei verschiedenen Dokumentarten gleichnamige Felder mit unterschiedlichen Datentypen oder Konfigurationen notwendig, wird das Problem noch schwieriger. Unübersichtlichkeit und komplizierte Feldnamen können die Folge sein.
Als Ausweg bietet sich die Möglichkeit, die verschiedenen Dokumentarten in jeweils eigene Cores mit eigenen Schemas zu legen und beim Join anzugeben, welche Cores beteiligt sind: {!join from=X to=Y fromCore=Z}Q
Dies sucht die Zwischenergebnisse mittels Query Q im Core Z und verwendet deren Werte aus Feld X, um damit Dokumente des angefragten Cores anhand ihres Feldes Y auszuwählen.
Damit dies funktionieren kann, müssen alle Cores auf demselben Knoten legen. Dazu bietet sich eine Multi-Core Installation von Solr an.
Ein Join über Cores auf verschiedenen Knoten, d.h. über ein Netzwerk, ist dagegen nicht möglich.
Daraus ergibt sich auch gleich die gravierendste Einschränkung: Joins unterstützen kein Sharding.
Sobald die Daten über mehrere Knoten verteilt sind, berücksichtigt der Join immer nur die Daten, die lokal verfügbar sind.
Durch geschicktes Verteilen der Daten lassen sich dadurch immer noch vollständige und richtige Ergebnisse produzieren, allerdings können sich daraus ungünstige Folgen auf die Verteilung der Last ergeben. Ist nicht genau vorhersagbar, welche Joins in Zukunft benötigt werden – z.B. weil Benutzer selbst entscheiden können – wird Sharding unmöglich.
Kein Scoring
Wie aus der Ablaufbeschreibung ersichtlich, werden die Ergebnisse in zwei Schritten ermittelt.
Der erste Schritt führt die Query Q aus und ermittelt alle Zwischenergebnisse. Der zweite Schritt verwendet die Feldwerte des Zwischenergebnisses, um das Endergebnis zu ermitteln. Bei diesem zweiten Schritt werden TermQueries verwendet, da nur noch von Bedeutung ist, ob das potentielle Zieldokument den Wert enthält oder nicht. Daher trägt jedes Dokument im Endergebnis den Score 1,0.
Performance
Wie in der Ablaufbeschreibung dargestellt, wird zunächst das Zwischenergebnis ermittelt, um dann das Endergebnis zu erstellen. Sollte das Zwischenergebnis Dokumente mit vielen verschiedenen Werten enthalten, steigt die Antwortzeit entsprechend an. Besonders kritisch ist es, wenn das Feld from einen sehr großen TermVector besitzt, da bei der Abarbeitung darüber iteriert wird.
Natürlich werden auch bei einem Join alle Caches verwendet, die bei herkömmlichen Queries zur Verfügung stehen, diese können aber definitionsgemäß nicht helfen, wenn eine Query zum ersten Mal ausgeführt wird.
Trotzdem sind auch mit Joins sinnvolle Antwortzeiten möglich, die sich im günstigsten Fall nicht wesentlich von den Antwortzeiten herkömmlicher Queries unterschieden. Trotzdem sollte in jedem Einzelfall genau geprüft werden, ob ein Join die richtige Lösung darstellt.
Einen nennenswerten Zusatzbedarf an Speicher ergibt sich durch Joins nicht.
Komplexere Abfragen mit Solr Join Queries
Bei den bisherigen Beispielen wurden immer nur recht einfache Anfragen formuliert. Vorallem bezogen sich die Einschränkungen immer nur auf das Zwischenergebnis und es gab keine Kriterien für eine weitere Einschräkung der Endergebnisse.
Eine Schwierigkeit dabei ist, dass der gesamte Query-Teil nach der Definition des Joins für die Einschränkung der Zwischenergebnisse verwendet wird.
{!join from=id to=manufacturer_id}name:A* AND price:[* TO 100]
Liest man die Query in der Reihenfolge, in der sie auch ausgeführt wird, nämlich von rechts nach links, erkennt man, dass dies nicht alle Produkte bis 100 Euro von Herstellern, deren Name mit A beginnt liefert. Stattdessen wird nach Herstellern, deren Name mit A beginnt und deren Preis 100 Euro nicht übersteigt, gesucht und dann deren Produkte zurückgeliefert. Eine völlig andere Fragestellung und in diesem Fall auch völlig sinnlos.
Hier können wir einfach die Query umdrehen und so das gewünschte Ergebnis erhalten:
price:[* TO 100] AND {!join from=id to=manufacturer_id}name:A*
Dieser Ansatz funktioniert allerdings nicht mehr, wenn mehrere Joins kombiniert werden sollen.
Hier kann man sich mit Nested Queries behelfen:
q=_query_:{!join from=id to=manufacturer_id}name:a* AND _query_:{!join from=id to=project_id}inception_year:2008
oder jeden Join als einzelne FilterQuery verwenden:
q=*:* fq={!join from=id to=manufacturer_id}name:a* fq={!join from=id to=project_id}inception_year:2008