Wie facettierte Suche in Elasticsearch wirklich funktioniert (und wo sie scheitert)

von Luca Cavallini Technische Anleitungen
Wie facettierte Suche in Elasticsearch wirklich funktioniert (und wo sie scheitert)

Eine Facette sind zwei Fragen, nicht eine

Eine Suchseite mit Facetten stellt gleichzeitig zwei verschiedene Fragen. Die erste ist offensichtlich: Welche Dokumente passen zur aktuellen Suche und zu den bereits angewendeten Filtern. Die zweite wird einmal für jeden verfügbaren Filterwert gestellt: Wie viele Dokumente würden passen, wenn dieser Wert zusätzlich ausgewählt wäre. Das Kontrollkästchen „Rot (12)“ ist die Antwort auf die zweite Frage, berechnet, während die erste beantwortet wird.

Das meiste von dem, was die facettierte Suche schwierig macht, in Elasticsearch wie anderswo, kommt von dieser zweiten Frage. Eine Liste der passenden Dokumente zu erhalten, ist nicht kompliziert. Neben jedem Wert jedes Filters bei jedem Seitenaufruf eine ehrliche Zahl zu liefern, ohne dass die Antwortzeit mit der Zahl der Facetten explodiert, ist das eigentliche Engineering-Problem.

Der Grundbaustein: eine terms-Aggregation über eine gefilterte Menge

Elasticsearch beantwortet beide Fragen mit einer einzigen Abfrage. Die Query-Klausel grenzt die Dokumentmenge ein (Volltext-Treffer plus die bereits aktiven Filter), und eine Gruppe von Aggregationen, die an dieselbe Abfrage gehängt ist, berechnet für jedes Facettenfeld, wie viele Dokumente dieser gefilterten Menge in jeden Wert fallen. Eine terms-Aggregation auf einem Feld color liefert einen Bucket pro eindeutigem Wert mit seiner Dokumentzahl; eine range- oder histogram-Aggregation tut dasselbe für numerische Felder wie den Preis. All das geschieht innerhalb derselben Abfrageausführung, auf derselben gefilterten Menge, in einem einzigen Roundtrip.

Das ist der Punkt, den man leicht übersieht, wenn man nur SQL kennt: Eine Aggregation ist hier keine zweite Abfrage auf das Ergebnis der ersten. Sie läuft während derselben Anfrage auf den doc values und den Postings des Index, und deshalb bedeuten zwanzig weitere Facetten auf einer Seite nicht zwanzig weitere vollständige Durchläufe der passenden Dokumente. Es bedeutet zwanzig weitere Bucket-Berechnungen auf Daten, die die Engine ohnehin berühren musste, um die Abfrage zu beantworten.

Filterkontext, nicht Query-Kontext

Die Facetten und die Filter, die durch Anklicken angewendet werden, sollten fast immer im Filterkontext stehen, nicht im Query-Kontext. Ein term-Filter auf brand: acme muss keinen Relevanzwert berechnen, er braucht eine Ja-oder-Nein-Antwort, und Elasticsearch cached Filterklauseln deutlich aggressiver als bewertete Query-Klauseln. Eine Suchseite mit Facetten ausschließlich aus bool-must-Klauseln zu bauen, die alle bewertet werden, ist ein verbreiteter Weg, sie ohne jeden Nutzen langsamer zu machen als nötig: Niemand sortiert eine Kategorieseite nach „Relevanz für den Filter Rot“. Filter gehören in filter, die Volltextsuche gehört in must oder in eine eigene match-Abfrage, und diese Trennung ist nicht kosmetisch: Sie ändert, was gecacht und was bewertet wird.

Die Falle der Mehrfachauswahl

Das Detail, an dem mehr Implementierungen der facettierten Suche scheitern als an jedem anderen, ist dieses: Wenn Sie die Zahl für eine Facette berechnen, müssen Sie den Filter dieser Facette selbst aus der Dokumentmenge ausschließen, gegen die Sie zählen, alle anderen aktiven Filter aber beibehalten. Andernfalls zeigt jede Facette, nach der Sie bereits gefiltert haben, eine Zahl, die der Zahl der aktuell angezeigten Ergebnisse entspricht, weil Sie die Schnittmenge mit einer Bedingung zählen, die bereits vollständig angewendet ist. Klicken Sie auf „Rot“, würde jeder andere Wert unter „Farbe“ fälschlich dieselbe Zahl wie die Gesamtsumme anzeigen, statt wie viele Produkte es tatsächlich gibt, die rot sind und zugleich diesen Wert haben.

Die übliche Korrektur besteht darin, pro Facettenfeld eine Aggregation zu bauen, jede eingepackt in eine eigene filter-Aggregation, die alle aktiven Filter außer dem für dieses Feld wieder anwendet, alle auf derselben Ebene unter derselben übergeordneten Abfrage. Konkret: Die Basisabfrage trägt bereits die Volltextsuche und alle Filter, die gerade nicht gezählt werden, und die Aggregation jeder Facette fügt die anderen ausgewählten Filter hinzu, abzüglich ihres eigenen. Das sind mehr Aggregationen pro Abfrage, nicht mehr Abfragen, und es ist der Teil, den man ausdrücklich testen sollte, statt ihn nach Augenmaß zu prüfen, denn eine Seite kann in einer Demo mit drei Produkten völlig korrekt aussehen und stillschweigend falsche Zahlen anzeigen, sobald echte Daten mit echten Überschneidungen geladen sind.

Es gibt eine zweite, damit verbundene Entscheidung, die man bewusst treffen sollte: was eine Facettengruppe bedeutet, wenn mehrere Werte unter derselben Bezeichnung gemeinsam ausgewählt werden. Wenn ein Shop „Karmesin“, „Scharlach“ und „Bordeaux“ unter einem einzigen Filter „Rot“ zusammenfasst, bedeutet die Auswahl zweier dieser Unterwerte „hat dies UND das“ oder „hat dies ODER das“? Eine Zahl, die die UND-Frage beantwortet, während der Klick die ODER-Frage anwendet (oder umgekehrt), ist die häufigste Ursache für einen Filter, der eine Zahl anzeigt und eine andere zurückgibt. Beide müssen mit genau derselben Logik berechnet werden, was meist bedeutet, die Gruppe von Anfang an als eine einzige Vereinigungsbedingung zu behandeln, statt Zahlen pro Wert zu addieren, die sich überschneiden oder dasselbe Produkt doppelt zählen können.

Warum ohne Aggregations-Engine alles zusammenbricht, mit einer echten Zahl

Der Grund, warum es Suchmaschinen mit Facetten gibt, statt alles in SQL zu erledigen, zeigt sich deutlich, sobald man versucht, exakte Zahlen pro Wert auf einer relationalen Datenbank in großem Maßstab zu erhalten. Auf einem Testkatalog mit 200.000 Produkten, der die in PrestaShop integrierte, auf die Datenbank beschränkte Suchmaschine nutzte, mit rund 2.250 Gruppen von Attributen und Merkmalen, führte die Berechnung einer exakten Zahl „wie viele Ergebnisse pro Wert“ mit einer COUNT(DISTINCT ...)-Abfrage pro Facette dazu, dass eine Kategorieseite mit einem aktiven Filter 43 Sekunden brauchte. Das Abschalten der exakten Zahlen für diese Facetten (Anzeige der Wertliste ohne Zahl) brachte dieselbe Seite auf knapp 12 Sekunden herunter, was zeigt, welcher Teil der ursprünglichen Kosten reiner Zählaufwand war und nicht die Suche selbst.

Ein verwandter Fehler, der bei einem internen Code-Review eines Moduls für facettierte Suche entdeckt wurde, bestand darin, den Bereich einer numerischen Facette (für einen Schieberegler „Preis“ oder „Gewicht“) zu berechnen, indem alle passenden Werte in den Speicher der Anwendung geladen und Minimum und Maximum dort berechnet wurden, statt die Datenbank oder die Suchmaschine damit zu beauftragen. Bei einem Katalog, in dem das etwa zwei Millionen Zeilen betraf, brauchte allein dieser Schritt 11 Sekunden und 965 MB Speicher pro Anfrage, was in manchen Konfigurationen ausreichte, um das Speicherlimit eines Shared Hostings zu sprengen und eine Fehlerseite statt eines Schiebereglers auszuliefern. Dieselbe Berechnung in ein GROUP BY der Datenbank oder in die range-Aggregation der Suchmaschine zu verlagern, machte daraus wieder eine normale, schnelle Abfrage, denn genau diese Art aggregierter Berechnung sollen diese Systeme in einem einzigen Durchgang auf indizierten Daten erledigen, und nicht nachträglich im Anwendungscode.

Die beiden Zahlen beschreiben denselben Grundfehler auf unterschiedlichen Ebenen: „Wie viele, für jeden möglichen Wert“ einmal pro Wert zu fragen, statt einmal für alle Werte zusammen. Das Aggregations-Framework von Elasticsearch existiert genau dafür, diese Frage in einem einzigen Durchgang auf bereits indizierten Daten zu beantworten. Darin liegt sein eigentlicher Wert, unabhängig vom Relevanz-Ranking der Volltextsuche, und deshalb ist die facettierte Navigation einer der häufigsten Gründe, Elasticsearch einzusetzen, selbst wenn die Suche selbst keine fortgeschrittenen Relevanzfunktionen braucht.

Eine kurze Checkliste, bevor Sie eine facettierte Suche live schalten

  • Filter gehören in den Filterkontext, Volltext in den Query-Kontext. Berechnen Sie keinen Score für ein Kontrollkästchen.
  • Die Zähl-Aggregation jeder Facette muss den aktiven Filter dieser Facette ausschließen und alle anderen beibehalten, sonst lügen Facetten mit Mehrfachauswahl, sobald zwei Filter gemeinsam verwendet werden.
  • Entscheiden Sie von Anfang an, und testen Sie es ausdrücklich, ob eine Gruppe von Werten unter derselben Bezeichnung mit UND oder mit ODER arbeitet, und lassen Sie Zählung und Klick genau dieselbe Logik teilen.
  • Begrenzen Sie die Größe der Aggregationen und beobachten Sie doc_count_error_upper_bound bei Feldern mit hoher Kardinalität; eine unbegrenzte terms-Aggregation, die Tausende Buckets zurückgibt, ist ein eigenständiges Performance-Problem, unabhängig von der oben beschriebenen Frage der Mehrfachauswahl.
  • Berechnen Sie die Zahlen einer Facette niemals, indem Sie rohe Zeilen in den Speicher der Anwendung laden. Wenn Sie sich dabei ertappen, eine Schleife über die Ergebnisse einer Abfrage zu schreiben, um eine Zahl oder einen Bereich zu bilden, gehört diese Schleife in die Aggregation, nicht in Ihren Code.

Wann Elasticsearch nicht die Antwort ist

Nichts davon bedeutet, dass jeder Katalog Elasticsearch braucht. Ein Shop mit einigen tausend Produkten und einer Handvoll Facetten wird den Unterschied einer eigenen Suchmaschine selten bemerken, und der Betrieb eines Clusters hat seine eigenen Betriebskosten: Speicher, Festplatte und jemanden, der die Neuindizierung beherrscht, wenn sich das Schema ändert. Die obigen Zahlen beginnen erst dann zu zählen, wenn die Zahl der Facetten, die Größe des Katalogs oder beides so groß werden, dass sich ein naives Zählen in Sekunden statt in Millisekunden messen lässt. Wenn Sie noch nicht so weit sind, ist eine gut optimierte Datenbankabfrage eine völlig vernünftige Wahl, und die obige Checkliste gilt auch dort: Die UND/ODER-Frage und die Falle der Mehrfachauswahl sind dasselbe Problem, egal, welche Engine antwortet.

Kurz gesagt

Facettierte Suche ist keine Abfrage, sie ist eine Abfrage plus eine Aggregation pro Filter, und fast alle echten Fehler entstehen durch eine Abweichung von diesem Modell: zu bewerten, was ein Filter sein sollte, eine Facette unter Berücksichtigung ihrer eigenen, bereits angewendeten Bedingung zu zählen oder Zählung und Klick mit zwei verschiedenen Definitionen einer Gruppe zu beantworten. Das Aggregations-Framework von Elasticsearch löst das Skalierungsproblem sauber, in einem Durchgang auf indizierten Daten statt mit einer Abfrage pro Facettenwert, und das ist der konkrete Grund, warum die oben genannten Zahlen von 43 und 11 Sekunden auf einer auf die Datenbank beschränkten Engine entstanden sind und nicht auf einer dafür gebauten. Wir erledigen diese Arbeit täglich in cll_essearch, unserem Modul für facettierte Suche für PrestaShop, sowohl auf dem nativen Treiber als auch auf Elasticsearch, OpenSearch und Meilisearch (siehe die Installationsanleitung), wenn Sie dieselbe Logik an einem Onlineshop statt an einem rohen Index angewendet sehen möchten.

Elasticsearch Suche und Facettierte NavigationBlitzschnelle, tippfehlertolerante Suche mit moderner facettierter Navigation im modalen Filterpanel, mit der…99,99 €Produkt ansehen

Kommentar hinterlassen

Kommentare werden vor der Veröffentlichung moderiert.

← Blog

Lade ...
Zum Seitenanfang