knuspermagier.de
Hallo. Ich bins! Philipp!

Side-Project-Tagebuch: recex (2)
Facettensuche mit RediSearch

Ihr kennt das. Man sucht beim Onlinehändler seines Vertrauens nach Schuhen, gibt einen Suchbegriff ein und neben den Suchergebnissen erscheinen weitere Filteroptionen, mit denen man das Ergebnis weiter einschränken kann. Damit man direkt bescheid weiß, steht auch direkt daneben, wie viele Suchergebnisse es für diesen Filter noch gibt.

Diesen Quatsch nennt man gemeinhin Facettensuche und an und für sich ist das eigentlich ganz praktisch. Leider ist es nicht mal eben durch den Aufruf von zwei PHP-Funktionen implementiert, daher habe ich bisher immer davon abgesehen, so etwas bei einem einer Side-Projects einzubauen. Also klar, es ist auch nur in wenigen Fällen sinnvoll, aber zum Beispiel fin, meine Finanzverwaltung, könnte sowas sicher gebrauchen.

Nun beschäftige ich mich seit gestern ja mit meinem neuen Projekt recex und natürlich soll man da auch nach Rezepten suchen können. Von Anfang an war mir da klar, das ich auch direkt nach Zutaten suchen will und nicht nur wildes Matching von Wörtern im Rezepttitel haben möchte.

Glücklicherweise basiert das Backend von recex auf Blogchain — ich kann also mit Fug und Recht behaupten, meine Rezepte in einer Blogchain zu speichern — und das bedeutet, RediSearch ist bereits angebunden, wie schwer kann es also sein, die Suche entsprechend umzustricken?

Index anlegen

Als erstes musste ich einen neuen Index anlegen. Neben Titel und Text (in dem Fall die Zubereitungsschritte des Rezepts) möchte ich auch Tags (“hellofresh“, “wegtuppern“) und Zutaten (“Zwiebel“, “Kartoffel“) indizieren. Leider unterstützt ethanhann/redisearch-php das Tag-Feld noch nicht, also erstelle ich den Index manuell per redis-cli:

FT.CREATE recipes SCHEMA title TEXT text TEXT ingredients TAG SORTABLE tags TAG SORTABLE

Das Indizieren der Rezepte ist wieder easy gemacht, denn dabei sind die TAG-Felder nur Komma-separierte Strings. Das funktioniert also über die PHP-Library

Query

Die Query-Syntax ist leider etwas kompliziert. Das RediSearch-Feature, mit dem wir unsere Facetten bauen heißt Aggregations und ist ziemlich mächtig.

Die Query sieht am Ende etwa so aus:

FT.AGGREGATE recipes "*" APPLY split(@tags) AS tag GROUPBY 1 @tag REDUCE count_distinct 1 @tag AS count SORTBY 1 @count DESC

Die Dokumentation ist zwar eigentlich gut, ich brauchte aber trotzdem einen Moment, bis ich hier auf die richtige Kombination von Wörtern kam. Also:

  • recipes ist der Name vom Index
  • "*" ist die Such-Query. In diesem Fall einfach “alles”
  • APPLY wendet eine Funktion auf das Ergebnis an, in diesem Fall…
  • split(@tags) …wird das Feld tags gesplittet (anhand der Kommas!) und mit AS tag im Verlauf tag genannt.
  • GROUPBY 1 @tag gruppiert nach dem neu erstellten tag-Feld
  • REDUCE count_distinct 1 @tag Zählt die Vorkommnisse jedes Tags und speichert es AS count
  • SORTBY 1 @count DESC sortiert den Spaß absteigend

Etwas verwirrend: Man muss vielen Befehlen (GROUPBY, REDUCE, etc) sagen wie viele Argumente folgen. Daher die ganzen 1-er. Fand ich am Anfang etwas verwirrend, ist aber wohl eine Limitierung des Redis-Command-Syntax.

Leider kann die PHP-Library auch diese Aggregations noch nicht, daher musste ich die Query auch händisch in meinen Code aufnehmen, was ganz fürchterlich aussieht, weil rawCommand jedes Wort einzeln erwartet, warum auch immer:

$result = $redis->rawCommand('FT.AGGREGATE', 'recipes', $query,
    'apply', 'split(@tags)', 'as', 'tag',
    'groupby', '1', '@tag',
    'reduce', 'count_distinct', '1', '@tag', 'as', 'count',
    'SORTBY', '1', '@count', 'desc'
);

Update: Die Library kann doch schon Aggregations, ich war nur zu Blöd es zu finden 😇

Das Ergebnis ist auf jeden Fall ein Array, mit allem, was wir wissen wollen. So einfach! Ist es nicht schön?

Zusammen mit einem kleinen Query-Parser kann man sich sowas bauen, und fröhlich nach Rezepten suchen:

(video file:facet.mov mode:loop)


RediSearch ❤️

Kommentare, Feedback und andere Anmerkungen?
Schreib mir eine E-Mail 🤓