Menschen von oben fotografiert, die an einem Tisch sitzen.

adesso Blog

Auf Webseiten wie Zalando und Co. erfolgt die Navigation mit nur wenigen Mausklicks durch ein unendliches Produktportfolio und gesuchte Artikel können durch ein Schlagwort problemlos gefunden werden. Das Geheimnis: Statt sich durch starre Navigationsbäume zu hangeln, kann der User ein paar gewünschte Kriterien - sogenannte Facetten - setzen und bekommt dann die passenden Ergebnisse angezeigt. Diese gesetzten Facetten decken bereits ein breites Spektrum an Nutzerinteressen ab - etwa Preis, Farbe oder Größe. Außerdem wird angezeigt, wie viele Treffer das jeweilige Suchergebnis hat. Die Website filtert also nicht nur bei jedem Klick die Ergebnisliste neu, sondern zählt auch, welche noch nicht gesetzten Facetten auf wie viele Einträge zutreffen.

Soweit schön und gut, aber wie könnt ihr eure eigene Website oder App mit einer solchen facettierten Navigation oder Suche ausstatten? In diesem Zusammenhang liefert die Suchmaschine Elasticsearch eine auf der Programmbibliothek Lucene basierende moderne Lösung. Mit der richtigen Abfrage gefüttert, gibt Elasticsearch alles in einer Antwort zurück, was für eine Darstellung der Facetten benötigt wird. In den folgenden Abschnitten zeige ich euch, auf was es im Einzelnen ankommt.

Elasticsearch füttern

Um eine entsprechende Suche auszuführen, müsst ihr einige Dinge im Vorfeld definieren: Zunächst geht es darum, die Datenstruktur zu kennen, möglichst valide Testdaten vorliegen zu haben und einen Weg zu definieren, wie die Daten in Elasticsearch importiert und verwendet werden können. Erst wenn ihr verstanden habt, wie Elasticsearch-Daten gehandhabt werden, solltet ihr ein individuelles Mapping entwerfen und den Suchindex anlegen.

Für Aktionen rund um die Datenhaltung – etwa bei der Indexierung, Aktualisierung, Reindexierung oder beim Löschen - bietet Elasticsearch eine gut dokumentierte REST API mit einer Reihe von Aufrufen für die einzelnen Aktionen. Diese könnt ihr recht einfach in eure Applikationen einbinden, um sie dann – beispielsweise in Batchjobs oder via Eventstreaming/EventListening – anzusprechen. Abhängig von den eingesetzten Frameworks, Libraries oder der verwendeten Bestandssoftware solltet ihr allerdings nach bereits vorhandenen Lösungen suchen. Der Aufwand, um Prozesse „failsave“ zu machen, ist in der Regel enorm groß.

Der folgende Aufruf zeigt euch, wie mittels POST Request ein Artikel indexiert werden kann:

	
	curl POST /{indexName}/{dokumentTyp}/{datensatzID}
		{
		   „titel“: „Schöner Winterpullover aus Wolle“,
		   „groesse“: „L“,
		   „farbe“: „rot“,
		   „kategorie“: „Pullover“
		}
	

Vielleicht ein Hinweis an dieser Stelle: Beim Anlegen von Datensätzen in Elasticsearch sollte die „Datensatz-ID“ immer eine Referenz zu euren zu indexierenden Einträgen haben. Auf diese Weise könnt ihr auch im Nachhinein entsprechende Updates oder Deletes der einzelnen Daten vornehmen.

Daten richtig verstehen und mappen

Datensätze mit dem oben genannten Beispielaufruf zu indexieren, ist auch ohne weiteres Zutun oder Kenntnisse der Datenstruktur – etwa zu Feldtypen, Feldlängen oder der Information, ob ein einzelnes Feld auch wirklich indexiert werden soll – möglich. Elasticsearch würde in diesem Fall einfach ein Standard-Mapping anwenden, das aber einige Nachteile in Sachen Suchtrefferqualität und Speichergröße des Index mitbringen würde.

Besser ist es, wie im nachfolgenden Auszug dargestellt, wenn ihr ein Mapping in Elasticsearch hinterlegt und auf Basis dessen die Daten beim Anlegen indexiert. Zunächst gilt es, die Datentypen der Felder zu bestimmen und in das Mapping aufzunehmen. Hier könnt ihr zwischen „date“, „long“, „keyword“ oder „text“ unterscheiden, wobei der Datentyp „keyword“ typischerweise für Datenfelder genutzt wird, nach denen gefiltert werden soll. In unserem Auszug wäre das zum Beispiel das Attribut „groesse“.

Durch die Definition von sogenannten Analysern ergeben sich eine Vielzahl von Vorteilen in Sachen „Qualität der Suchergebnisse“: Mit lowercase, german_stemmer oder german_normalization werden auch bei Tippfehlern des Users die korrekten Suchergebnisse angezeigt. Setzt ihr NGram Analyser ein, werden zudem auch die Ergebnisse gefunden, bei denen der User nur einen Teil des Suchbegriffs eingibt.

	
	{
		  "titel":{              
		"index":"true",
		            "store":"true",
		            "type":"text",
		            "search_analyzer":"search_analyzer"
		         },
		 "farbe":{              
		"index":"true",
		            "store":"true",
		            "type":" keyword ",
		            "fields":{                 
		"keyword":{                    
		"type":"keyword",
		                  "ignore_above":50
		               }
		            }
		         },
		 "groesse":{              
		"index":"true",
		            "store":"true",
		            "type":" keyword ",
		            "fields":{                 
		"keyword":{                    
		"type":"keyword",
		                  "ignore_above":50
		               }
		            }
		         },
		 "kategorie":{              
		"index":"true",
		            "store":"true",
		            "type":" keyword ",
		            "fields":{  
		"keyword":{                    
		"type":"keyword",
		                  "ignore_above":200
		               }
		            }
		         }
		}
	

Die Suche

Nun zur eigentlichen Suche: Hier bietet Elasticsearch natürlich auch REST-Endpunkte an, die ich exemplarisch im nächsten Beispiel definiert habe. Um auch die entsprechenden Dimensionen oder Facetten für die Ergebnisliste zu erhalten, müsst ihr eine gewisse Gruppierung der Ergebnisse durchführen. Für dieses Vorgehen kennt Elasticsearch zwei Verfahren:

  • Facets, ein älteres und einfaches Vorgehen
  • Aggregations, der Nachfolger von Facets

Letzteres kommt auch im unten gezeigten Beispiel zur Anwendung. Der auf das Attribut gesetzte Filter „groesse“, zeigt dann nur noch Ergebnisse für die Größe XL an.

Im Body der Abfrage ist es möglich - neben unserem Suchbegriff - eine Vielzahl an Optionen zu definieren, um das Suchergebnis im Anschluss entsprechend nach euren Vorstellungen zu formatieren. Dazu gehören, wie auch im Beispiel verwendet, Optionen zur Paginierung (from oder size) oder zur Markierung der Suchbegriffe in den einzelnen Suchergebnissen (highlight).

	
	curl POST /{indexName}/_search
		{
		 "query" : {
		   "bool": {
		     "must": [
		        {
		          "multi_match": {
		            "query": "schöner",
		            "fields": ["titel^2"],
		            "operator": "and"
		          }
		        }
		      ]
		     "filter": [
		        {
		          "terms": {
		"groesse": [
		              "XL"
		            ]
		          }
		        },
		        {
		          "terms": {
		"kategorie": [
		"pullover"
		]
		          }
		        }
		     ]
		   }
		 },
		 "_source" : false,
		"from": 0,
		 "size": 8,
		 "highlight": {
		"pre_tags": ["<strong>"],
		"post_tags": ["</strong>"],
		      "fields": {
		        "title": {
		"fragment_size" : 20,
		          "number_of_fragments" : 5,
		"no_match_size": 20
		        }
		      }
		  },
		      "aggs": {
		        "filter.groesse": {
		          "terms": {
		            "field": "groesse",
		            "missing": "n/a",
		            "min_doc_count": 1,
		            "order": {
		              "_count": "desc"
		            }
		          }
		},
		        "filter.kategorie": {
		          "terms": {
		            "field": "kategorie",
		            "missing": "n/a",
		            "min_doc_count": 1,
		            "order": {
		              "_count": "desc"
		            }
		          }
		        },
		        "filter.farbe": {
		          "terms": {
		            "field": "farbe",
		            "missing": "n/a",
		            "min_doc_count": 1,
		            "order": {
		              "_count": "desc"
		            }
		          }
		        }
		      }
		    }
	

Suchergebnisse

Die Sucherergebnisse der zuvor definierten Suchabfrage werden, wie auch andere Standard REST-APIs, als JSON zurückgegeben und enthalten neben den Ergebnissen auch andere interessante Eigenschaften und Informationen. Hier werden natürlich nur folgende Dinge ausgegeben:

  • Datenfelder, die bei der Abfrage angegeben und entsprechend nach eigener Definitionen (fragment_size, number_of_fragments, etc.) formatiert wurden
  • Suchbegriffe, die mit den Highlightern markiert wurden

Auf diese Weise könnt ihr euch ein teures Rendering auf der Client-Seite sparen. Zudem habt ihr damit die verschiedenen Facetten der Attribute vorliegen, die im Suchergebnis zu finden sind und erkennt, wie viele Treffer es zu den einzelnen Facetten gibt.

Fazit

Wie ihr gesehen habt, gibt es standardisierte und gut beschriebene Interfaces von Elasticsearch, um Filter- oder Suchfunktionalitäten zu implementieren. Zudem findet ihr im Internet eine Vielzahl von Anwendungsbeispielen. Ihr solltet den Gesamtaufwand aber keinesfalls unterschätzen, denn das von mir beschriebene Beispiel zeigt schließlich nur einen Auszug eines Cases. Meiner Meinung nach solltet ihr vor einer Implementierung immer evaluieren, ob es vielleicht eine Library oder ein entsprechendes Toolset gibt, das zum eigenen Stack passt und euch Arbeit – beispielweise bei der Indexierung der Daten in Elasticsearch – abnimmt. Auf keinen Fall solltet ihr versuchen, Elasticsearch als eine Art persistenten Speicher zu nutzen. Es existiert nämlich keine 100-prozentige Zusicherung, die Daten verfügbar zu halten – sei es in Problemfällen beim Speichern oder bei Cluster-Switches.

Ihr möchtet mehr über spannende IT-Themen erfahren? Dann werft doch auch einen Blick in unsere bisher erschienenen Blog-Beiträge

Bild Rico Krause

Autor Rico Krause

Rico Krause ist Projektleiter bei adesso in Hamburg, war in der Vergangenheit viele Jahre als Softwareentwickler tätig und hat die Faszination daran auch nicht verloren.

Diese Seite speichern. Diese Seite entfernen.