Volltextsuche mit ElasticSearch
Seite 2: Abfragen und Schemafreiheit
Abfragen von Statusinformation
Läuft ElasticSearch, lassen sich JSON-Dokumente mit einem HTTP-POST- oder HTTP-PUT-Befehl zur Indizierung übergeben. Um beispielweise eine deutsche Post-Adresse als Dokumententyp man im Index addresses zu indizieren, reicht der unten zu sehende cURL-Befehl. Jede Ressource in ElasticSearch lässt sich über eine REST-konforme URL angesprechen. So setzt sich die URL für ein in ElasticSearch abgelegtes Dokument dabei nach folgendem Schema zusammen:
http://hostname:port/indexname/dokumententyp/dokumentenid
curl -X POST http://localhost:9200/addresses/german -d '{
"empfaenger": "Otto Normalverbraucher",
"strasse": "HauptstraĂźe",
"hausnummer" : "1",
"stadt" : "Katzenelnbogen",
"postleitzahl" : 56368,
"land" : "Deutschland"
}'
{
"ok" : true,
"_index" : "addresses",
"_type" : "german",
"_id" : "PNsR08kgT-uJ_amlS4GGpA",
"_version": 1
}
ElasticSearch quittiert den Aufruf mit dem im Beispiel aufgeführten Dokument, aus dem sich der Erfolg der Operation, der Name des verwendeten Index und Dokumententyps sowie ID und Version des Dokuments entnehmen lässt.
Da das Dokument mit POST und ohne Angabe einer ID übergeben wurde, erzeugt ElasticSearch selbst eine. Alternativ lässt sich die Dokumenten-ID auch selbst vergeben, wofür das Dokument aber mit PUT an ElasticSearch zu übermitteln ist, wie folgender Codeausschnitt zeigt:
curl -X PUT http://localhost:9200/addresses/german/1234 -d '{
"empfaenger": "Otto Normalverbraucher",
"strasse": "HauptstraĂźe",
"hausnummer" : "1",
"stadt" : "Katzenelnbogen",
"postleitzahl" : 56368,
"land" : "Deutschland"
}'
{
"ok" : true,
"_index" : "addresses",
"_type" : "german",
"_id" : "1234",
"_version" : 1
}
Unter Angabe der ID kann ein Dokument durch ein erneutes PUT überschrieben, durch ein GET gelesen, dessen Existenz mit HEAD abgefragt und durch DELETE gelöscht werden.
curl -X PUT http://localhost:9200/addresses/german/1234 -d '{
"empfaenger": "Otto Normalverbraucher",
"strasse": "HauptstraĂźe",
"hausnummer" : "19",
"stadt" : "Katzenelnbogen",
"postleitzahl" : 56368,
"land" : "Deutschland"
}'
curl -X GET http://localhost:9200/addresses/german/1234?pretty
{
"_index" : "addresses",
"_type" : "german",
"_id" : "1234",
"_version" : 4,
"exists" : true, "_source" : {
"empfaenger": "Otto Normalverbraucher",
"strasse": "HauptstraĂźe",
"hausnummer" : "19",
"stadt" : "Katzenelnbogen",
"postleitzahl" : 56368,
"land" : "Deutschland"
}
curl -X DELETE http://localhost:9200/addresses/german/1234?pretty
{
"ok" : true,
"found" : true,
"_index" : "addresses",
"_type" : "german",
"_id" : "1234",
"_version" : 5
}
Da jedes Dokument einen eindeutigen Bezeichner hat, lässt sich ElasticSearch je nach Anwendungsfall auch als Key-Value-Store nutzen.
Schemafrei und doch nicht
Einer der Vorzüge von ElasticSearch ist seine Schemafreiheit. Das bedeutet, dass keine explizite und ausformulierte Beschreibung der zu verarbeitenden Dokumente in Hinsicht auf Datentypen und weitere Informationen angegeben werden muss, sondern sich die Daten direkt an das jeweilige System übergeben lassen. Besonders beim Verarbeiten semistrukturierter Daten kann sich diese Eigenschaft als nützlich erweisen. ElasticSearch arbeitet jedoch nicht vollkommen ohne Schema, sondern verfügt über die Fähigkeit, eins per Heuristik über die übergebenen Daten abzuleiten. Ein aus Zeichen bestehendes Feld wird als String erkannt, Datumsangaben erfolgen meist in ein paar Grundformaten und Zahlen deuten auf einen numerischen Wert hin. Der folgende Codeausschnitt zeigt das von ElasticSearch für Adressen abgeleitete Schema. In ihm werden die Postleitzahl als Long-Wert und die Hausnummer, obwohl auch nur aus Zahlen bestehend, als String betrachtet, da sie in Anführungszeichen eingefasst wurde.
curl -X GET http://localhost:9200/addresses/german/_mapping?pretty
{
"german" : {
"properties" : {
"empfaenger" : { "type" : "string" },
"hausnummer" : { "type" : "string" },
"land" : { "type" : "string" },
"postleitzahl" : { "type" : "long" },
"stadt" : { "type" : "string" },
"strasse" : { "type" : "string" }
}
}
}
Statt ElasticSearch ein Schema ableiten zu lassen, kann der Nutzer auch selbst eins erstellen. Dies erlaubt vergleichsweise mehr Kontrolle darüber, wie ElasticSearch mit den übergebenen Dokumenten umgeht. Die Spannweite der unterstützten Datentypen reicht von mehreren Zahlenformaten über Datums- und Zeitangaben bis hin zu Geodaten. Im vorliegenden Beispiel wäre es möglich, die Postleitzahl als String zu definieren, um auch Kombinationen wie 03476 richtig zu behandeln, die Hausnummer jedoch als Zahl, um sie später für Vergleiche nutzen zu können.
Ein eigenes Mapping erlaubt jedoch nicht nur, die verwendeten Datentypen selbst zu bestimmen, sondern auch wie die Daten durch Einstellungen und darunter liegend durch Lucene verarbeitet werden. Nach Begriffen in der exakten Schreibweise zu suchen, ist nur selten sinnvoll. Beispielsweise wäre es wünschenswert, den Beispielempfänger Otto Normalverbraucher auch bei unvollständiger Eingabe und unabhängig von Groß- und Kleinschreibung zu finden. Die hierfür notwendige Vorverarbeitung übernehmen Analyzer, die Eingangstexte in einzelne Token aufspalten und sie vor dem eigentlichen Speichern in den Indizes von ElasticSearch filtern und transformieren können. Mit Analyzern lassen sich zum Beispiel Texte in einzelne Wörter zerteilen, diese in Kleinbuchstaben umwandeln und Stoppwörter wie "der", "die" und "das" herausfiltern, ehe die Token gespeichert werden.
So ließe sich der Beispielempfänger sowohl bei der Suche nach "otto" als auch nach "normalverbraucher" finden. ElasticSearch stellt eine Reihe solcher Werkzeuge bereit und erlaubt zudem die Definition eigener Varianten. Jeder Analyzer setzt sich aus einem Tokenizer und einem oder mehreren nachgeschalteten Filtern zusammen. Der Tokenizer spaltet die Eingabe in einzelne Elemente auf, und die Filter transformieren diese Token, bevor sie im Index abgelegt werden. Ohne explizite Angabe eines Analyzers kommt ein Standard-Analyzer zum Einsatz.
curl -X PUT http://localhost:9200/addresses -d '{
"settings": {
"analysis" : {
"analyzer" : {
"3gram" : {
"tokenizer" : "engram",
"filter" : ["lowercase"]
}
},
"tokenizer" : {
"engram" : {
"type" : "NGram",
"min_gram" : 3,
"max_gram" : 10
}
}
}
},
"mappings": {
"german": {
"_all": { "enabled": false },
"_source" : { "enabled" : true },
"properties" : {
"empfaenger" : {
"type" : "string",
"index_analyzer" : "3gram",
"search_analyzer" : "standard"
},
"hausnummer" : { "type" : "string" },
"land" : { "type" : "string" },
"postleitzahl" : { "type" : "long" },
"stadt" : { "type" : "string", "store" : "yes" },
"strasse" : { "type" : "string" }
}
}
}
}
Das obige Beispiel zeigt die manuelle Erzeugung eines Indexes mit einem selbst erstellten Mapping für deutsche Adressen. Es unterscheidet sich vom von ElasticSearch erstellten Schema zum einen durch die abweichenden Datentypen für Hausnummer und Postleitzahl. Zum anderen werden für das Feld "empfaenger" zwei Analyzer konfiguriert. "index_analyzer" legt den zur Indizierung verwendeten Analyzer fest, in diesem Fall den zuvor konfigurierten 3gram. Für Suchanfragen im Feld selbst kommt der erwähnte Standard-Analyzer zum Einsatz. Bei Bedarf lässt sich ein Index wie folgt löschen:
curl -X DELETE localhost:9200/adresses
Über die Mapping-Einstellungen lässt sich nicht nur auf Schema und Indizierung Einfluss nehmen, sondern unter anderem das Speichern von Daten steuern oder festlegen, mit welcher Gewichtung Felder zur Bewertung von Suchergebnissen beitragen.