Elasticsearch — eine Suchmaschine mit JSON REST API, die Lucene verwendet und in Java geschrieben ist. Eine Beschreibung aller Vorteile dieser Engine finden Sie auf . Im Folgenden werden wir Elasticsearch als ES bezeichnen.
Solche Engines werden für komplexe Suchen in Dokumentendatenbanken eingesetzt. Zum Beispiel für Suchen unter Berücksichtigung der Morphologie der Sprache oder zur Suche anhand von Geo-Koordinaten.
In diesem Artikel erkläre ich die Grundlagen von ES am Beispiel der Indizierung von Blogbeiträgen. Ich zeige, wie man Dokumente filtert, sortiert und sucht.
Um von der Betriebssystemunabhängigkeit zu profitieren, werde ich alle Anfragen an ES mit CURL durchführen. Es gibt auch ein Plugin für Google Chrome mit dem Namen .
Im Text sind Links zur Dokumentation und zu anderen Quellen platziert. Am Ende finden Sie Links für einen schnellen Zugriff auf die Dokumentation. Definitionen unbekannter Begriffe können im .
Installation von ES
Dafür benötigen wir zunächst Java. Die Entwickler sollten eine Java-Version installieren, die neuer ist als Java 8 Update 20 oder Java 7 Update 55.
Das ES-Distributionspaket ist verfügbar auf . Nach dem Entpacken des Archivs müssen Sie bin/elasticsearch. Es sind auch Außerdem gibt es ein . .
Nach der Installation und dem Start prüfen wir die Funktionsfähigkeit:
# для удобства запомним адрес в переменную
#export ES_URL=$(docker-machine ip dev):9200
export ES_URL=localhost:9200
curl -X GET $ES_URLWir erhalten etwa folgende Antwort:
{
"name" : "Heimdall",
"cluster_name" : "elasticsearch",
"version" : {
"number" : "2.2.1",
"build_hash" : "d045fc29d1932bce18b2e65ab8b297fbf6cd41a1",
"build_timestamp" : "2016-03-09T09:38:54Z",
"build_snapshot" : false,
"lucene_version" : "5.4.1"
},
"tagline" : "Wissen Sie, für die Suche"
}Indizierung
Fügen wir einen Beitrag in ES hinzu:
# Добавим документ c id 1 типа post в индекс blog.
# ?pretty указывает, что вывод должен быть человеко-читаемым.
curl -XPUT "$ES_URL/blog/post/1?pretty" -d'
{
"title": "Веселые котята",
"content": "<p>Lustige Geschichte über Kätzchen<p>",
"tags": [
"Kätzchen",
"lustige Geschichte"
],
"published_at": "2014-09-12T20:44:42+00:00"
}'
Antwort des Servers:
{
"_index" : "blog",
"_type" : "post",
"_id" : "1",
"_version" : 1,
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"created" : false
}
ES hat automatisch erstellt blog und post. Man kann eine analogie ziehen: Ein Index ist eine Datenbank, während ein Typ eine Tabelle in dieser Datenbank darstellt. Jeder Typ hat sein eigenes Schema — , genau wie eine relationale Tabelle. Das Mapping wird automatisch bei der Indizierung eines Dokuments generiert:
# Получим mapping всех типов индекса blog
curl -XGET "$ES_URL/blog/_mapping?pretty"In der Antwort des Servers habe ich die Werte der Felder des indizierten Dokuments als Kommentare hinzugefügt:
{
"blog" : {
"mappings" : {
"post" : {
"properties" : {
"content" : {
"type" : "string"
},
"published_at" : {
"type" : "date",
"format" : "strict_date_optional_time||epoch_millis"
},
"tags" : {
"type" : "string"
},
"title" : {
"type" : "string"
}
}
}
}
}
}<p>Lustige Geschichte über Kätzchen<p>{"content":{"type":"string"},"published_at":{"type":"date","format":"strict_date_optional_time||epoch_millis"},"tags":{"type":"string"},"title":{"type":"string"}}Es ist anzumerken, dass ES keinen Unterschied zwischen einem einzelnen Wert und einem Array von Werten macht. Zum Beispiel enthält das Feld title einfach den Titel, während das Feld tags ein Array von Strings darstellt, obwohl sie im Mapping identisch dargestellt werden.
Später werden wir detaillierter über das Mapping sprechen.
Abfragen
Abrufen eines Dokuments über seine ID:
# извлечем документ с id 1 типа post из индекса blog
curl -XGET "$ES_URL/blog/post/1?pretty"{
"_index": "blog",
"_type": "post",
"_id": "1",
"_version": 1,
"found": true,
"_source": {
"title": "\u0412\u0435\u0441\u0435\u043b\u044b\u0435 \u043a\u043e\u0442\u044f\u0442\u0430",
"content": "<p>Lustige Geschichte \u00fcber K\u00e4tzchen<p>",
"tags": [
"\u043a\u043e\u0442\u044f\u0442\u0430",
"\u0441\u043c\u0435\u0448\u043d\u0430\u044f \u0438\u0441\u0442\u043e\u0440\u0438\u044f"
],
"published_at": "2014-09-12T20:44:42+00:00"
}
}In der Antwort sind neue Schlüssel erschienen: _version und _source. Im Allgemeinen beziehen sich alle Schlüssel, die mit _ beginnen, auf systeminterne Funktionen.
Der Schlüssel _version zeigt die Dokumentenversion an. Er ist notwendig für den Mechanismus der optimistischen Sperren. Zum Beispiel, wenn wir ein Dokument mit der Version 1 ändern möchten, senden wir das geänderte Dokument und geben an, dass es sich um eine Bearbeitung des Dokuments mit Version 1 handelt. Sollte jemand anders ebenfalls das Dokument mit Version 1 bearbeitet und seine Änderungen vor uns gesendet haben, wird ES unsere Änderungen nicht akzeptieren, da es das Dokument mit Version 2 speichert.
Der Schlüssel _source enthält das Dokument, das wir indiziert haben. ES verwendet diesen Wert nicht für Suchoperationen, da für die Suche Indizes verwendet werden. Um Speicherplatz zu sparen, speichert ES das komprimierte Quell-Dokument. Falls wir nur die ID und nicht das gesamte Quell-Dokument benötigen, kann die Speicherung des Originals deaktiviert werden.
Wenn wir keine zusätzlichen Informationen benötigen, können wir nur den Inhalt von _source abrufen:
curl -XGET "$ES_URL/blog/post/1/_source?pretty"{
"title": "\u0412\u0435\u0441\u0435\u043b\u044b\u0435 \u043a\u043e\u0442\u044f\u0442\u0430",
"content": "<p>Lustige Geschichte \u00fcber K\u00e4tzchen<p>",
"tags": [
"\u043a\u043e\u0442\u044f\u0442\u0430",
"\u0441\u043c\u0435\u0448\u043d\u0430\u044f \u0438\u0441\u0442\u043e\u0440\u0438\u044f"
],
"published_at": "2014-09-12T20:44:42+00:00"
}
Außerdem können wir nur bestimmte Felder auswählen:
# извлечем только поле title
curl -XGET "$ES_URL/blog/post/1?_source=title&pretty"{
"_index" : "blog",
"_type" : "post",
"_id" : "1",
"_version" : 1,
"found" : true,
"_source" : {
"title" : "Lustige Kätzchen"
}
}Lassen Sie uns noch einige Beiträge indizieren und komplexere Abfragen ausführen.
curl -XPUT "$ES_URL/blog/post/2" -d'
{
"title": "Fröhliche Welpen",
"content": "<p>Eine lustige Geschichte über Welpen<p>",
"tags": [
"Welpen",
"lustige Geschichte"
],
"published_at": "2014-08-12T20:44:42+00:00"
}'curl -XPUT "$ES_URL/blog/post/3" -d'
{
"title": "Wie ich ein Kätzchen bekam",
"content": "<p>Eine bewegende Geschichte über ein armes Kätzchen von der Straße<p>",
"tags": [
"Kätzchen"
],
"published_at": "2014-07-21T20:44:42+00:00"
}'Sortierung
# найдем последний пост по дате публикации и извлечем поля title и published_at
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
"size": 1,
"_source": ["title", "published_at"],
"sort": [{"published_at": "desc"}]
}'{
"took" : 8,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"failed" : 0
},
"hits" : {
"total" : 3,
"max_score" : null,
"hits" : [ {
"_index" : "blog",
"_type" : "post",
"_id" : "1",
"_score" : null,
"_source" : {
"title" : "Fröhliche Kätzchen",
"published_at" : "2014-09-12T20:44:42+00:00"
},
"sort" : [ 1410554682000 ]
} ]
}
}Wir haben den letzten Beitrag ausgewählt. Größe beschränkt die Anzahl der Dokumente in den Ergebnissen. gesamt zeigt die Gesamtzahl der Dokumente an, die der Anfrage entsprechen. sortieren enthält ein Array von Ganzzahlen, nach denen die Sortierung erfolgt. Das heißt, das Datum wurde in eine Ganzzahl umgewandelt. Näheres zur Sortierung finden Sie in .
Filter und Anfragen
Ab Version 2 unterscheidet ES nicht zwischen Filtern und Anfragen, stattdessen .
Der Anfragekontext unterscheidet sich vom Filterkontext dadurch, dass die Anfrage ein _score generiert und nicht zwischengespeichert wird. Was _score ist, werde ich später zeigen.
Datumsfilterung
Verwenden Sie die Anfrage im Kontext filter:
# получим посты, опубликованные 1ого сентября или позже
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
"filter": {
"range": {
"published_at": { "gte": "2014-09-01" }
}
}
}'Tagfilterung
Wir verwenden zum Suchen von Dokumenten-IDs, die das angegebene Wort enthalten:
# найдем все документы, в поле tags которых есть элемент 'котята'
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
"_source": [
"title",
"tags"
],
"filter": {
"term": {
"tags": "котята"
}
}
}'{
"took" : 9,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"failed" : 0
},
"hits" : {
"total" : 2,
"max_score" : 1.0,
"hits" : [ {
"_index" : "blog",
"_type" : "post",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"title" : "Lustige Kätzchen",
"tags" : [ "Kätzchen", "witzige Geschichte" ]
}
}, {
"_index" : "blog",
"_type" : "post",
"_id" : "3",
"_score" : 1.0,
"_source" : {
"title" : "Wie ich einen Kater bekam",
"tags" : [ "Kätzchen" ]
}
} ]
}
}Volltextsuche
Drei unserer Dokumente enthalten im Feld content Folgendes:
<p>Lustige Geschichte über Kätzchen<p><p>Eine lustige Geschichte über Welpen<p><p>Eine bewegende Geschichte über ein armes Kätzchen von der Straße<p>
Wir verwenden zum Suchen von Dokumenten-IDs, die das angegebene Wort enthalten:
# source: false означает, что не нужно извлекать _source найденных документов
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
"_source": false,
"query": {
"match": {
"content": "история"
}
}
}'{
"took" : 13,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"failed" : 0
},
"hits" : {
"total" : 3,
"max_score" : 0.11506981,
"hits" : [ {
"_index" : "blog",
"_type" : "post",
"_id" : "2",
"_score" : 0.11506981
}, {
"_index" : "blog",
"_type" : "post",
"_id" : "1",
"_score" : 0.11506981
}, {
"_index" : "blog",
"_type" : "post",
"_id" : "3",
"_score" : 0.095891505
} ]
}
}Wenn jedoch im Feld Content nach „Geschichten“ gesucht wird, finden wir nichts, da im Index nur die ursprünglichen Wörter und nicht ihre Stämme enthalten sind. Um eine qualitativ hochwertige Suche durchzuführen, muss der Analyzer konfiguriert werden.
Feld _score zeigt . Wenn die Abfrage im Filterkontext ausgeführt wird, beträgt der Wert _score immer 1, was vollständige Übereinstimmung mit dem Filter bedeutet.
Analyzer
sind erforderlich, um den Ausgangstext in ein Token-Set umzuwandeln.
Analysewerkzeuge bestehen aus einem und mehreren optionalen . Der Tokenizer kann mehreren . Der Tokenizer zerlegt den Ausgangstext in Tokens, zum Beispiel anhand von Leerzeichen und Interpunktionszeichen. Der TokenFilter kann Tokens verändern, entfernen oder neue hinzufügen, wie etwa nur den Stamm eines Wortes beizubehalten, Präpositionen zu entfernen oder Synonyme hinzuzufügen. Der CharFilter verändert die gesamte Ausgangszeichenfolge, zum Beispiel durch das Entfernen von HTML-Tags.
In Elasticsearch gibt es mehrere . Zum Beispiel der Analysewerkzeug .
Lassen Sie uns und sehen, wie die Analysewerkzeuge standard und russian die Zeichenfolge „Lustige Geschichten über Kätzchen“ verarbeiten:
# используем анализатор standard
# обязательно нужно перекодировать не ASCII символы
curl -XGET "$ES_URL/_analyze?pretty&analyzer=standard&text=%D0%92%D0%B5%D1%81%D0%B5%D0%BB%D1%8B%D0%B5%20%D0%B8%D1%81%D1%82%D0%BE%D1%80%D0%B8%D0%B8%20%D0%BF%D1%80%D0%BE%20%D0%BA%D0%BE%D1%82%D1%8F%D1%82"{
"tokens" : [ {
"token" : "lustige",
"start_offset" : 0,
"end_offset" : 7,
"type" : "",
"position" : 0
}, {
"token" : "geschichten",
"start_offset" : 8,
"end_offset" : 19,
"type" : "",
"position" : 1
}, {
"token" : "über",
"start_offset" : 20,
"end_offset" : 24,
"type" : "",
"position" : 2
}, {
"token" : "kätzchen",
"start_offset" : 25,
"end_offset" : 33,
"type" : "",
"position" : 3
} ]
}# используем анализатор russian
curl -XGET "$ES_URL/_analyze?pretty&analyzer=russian&text=%D0%92%D0%B5%D1%81%D0%B5%D0%BB%D1%8B%D0%B5%20%D0%B8%D1%81%D1%82%D0%BE%D1%80%D0%B8%D0%B8%20%D0%BF%D1%80%D0%BE%20%D0%BA%D0%BE%D1%82%D1%8F%D1%82"{
"tokens" : [ {
"token" : "fröhlich",
"start_offset" : 0,
"end_offset" : 7,
"type" : "",
"position" : 0
}, {
"token" : "geschichte",
"start_offset" : 8,
"end_offset" : 15,
"type" : "",
"position" : 1
}, {
"token" : "katze",
"start_offset" : 20,
"end_offset" : 25,
"type" : "",
"position" : 3
} ]
}Der Standard-Analyzer hat die Zeichenfolge nach Leerzeichen aufgeteilt und alles in Kleinschreibung umgewandelt, der Analyzer 'russian' hat bedeutungslose Wörter entfernt, in Kleinschreibung übertragen und die Wortstämme beibehalten.
Lassen Sie uns ansehen, welche Tokenizer, TokenFilters und CharFilters der Analyzer 'russian' verwendet:
{
"filter": {
"russian_stop": {
"type": "stop",
"stopwords": "_russian_"
},
"russian_keywords": {
"type": "keyword_marker",
"keywords": []
},
"russian_stemmer": {
"type": "stemmer",
"language": "russian"
}
},
"analyzer": {
"russian": {
"tokenizer": "standard",
/* TokenFilters */
"filter": [
"lowercase",
"russian_stop",
"russian_keywords",
"russian_stemmer"
]
/* CharFilters fehlen */
}
}
}Lassen Sie uns unseren Analyzer auf Basis von 'russian' beschreiben, der HTML-Tags herausfiltert. Wir nennen ihn 'default', da ein Analyzer mit diesem Namen standardmäßig verwendet wird.
{
"filter": {
"de_stop": {
"type": "stop",
"stopwords": "_german_"
},
"de_stemmer": {
"type": "stemmer",
"language": "german"
}
},
"analyzer": {
"default": {
/* Wir fügen die Entfernung von HTML-Tags hinzu */
"char_filter": ["html_strip"],
"tokenizer": "standard",
"filter": [
"lowercase",
"de_stop",
"de_stemmer"
]
}
}
}Zunächst werden alle HTML-Tags aus der ursprünglichen Zeichenkette entfernt, anschließend wird sie vom Tokenizer standard in Tokens zerlegt. Die erhaltenen Tokens werden in Kleinbuchstaben umgewandelt, bedeutungslose Wörter werden entfernt und von den verbleibenden Tokens bleibt die Stammform des Wortes.
Index erstellen
Oben haben wir den Standard-Analyzer beschrieben. Dieser wird auf alle String-Felder angewendet. Unser Post enthält ein Array von Tags, entsprechend werden auch diese vom Analyzer bearbeitet. Da wir Posts nach exakter Übereinstimmung mit dem Tag suchen, muss die Analyse für das Feld tags deaktiviert werden.
Wir erstellen den Index blog2 mit einem Analyzer und einer Zuordnung, in der die Analyse des Felds tags deaktiviert ist:
curl -XPOST "$ES_URL/blog2" -d'
{
"settings": {
"analysis": {
"filter": {
"ru_stop": {
"type": "stop",
"stopwords": "_russian_"
},
"ru_stemmer": {
"type": "stemmer",
"language": "russian"
}
},
"analyzer": {
"default": {
"char_filter": [
"html_strip"
],
"tokenizer": "standard",
"filter": [
"lowercase",
"ru_stop",
"ru_stemmer"
]
}
}
}
},
"mappings": {
"post": {
"properties": {
"content": {
"type": "string"
},
"published_at": {
"type": "date"
},
"tags": {
"type": "string",
"index": "not_analyzed"
},
"title": {
"type": "string"
}
}
}
}
}'Wir fügen dieselben 3 Beiträge zu diesem Index (blog2) hinzu. Ich werde diesen Prozess überspringen, da er dem Hinzufügen von Dokumenten zum Index blog ähnelt.
Volltextsuche mit Unterstützung für Ausdrücke
Lernen wir einen weiteren Abfragetyp kennen:
# найдем документы, в которых встречается слово 'истории'
# query -> simple_query_string -> query содержит поисковый запрос
# поле title имеет приоритет 3
# поле tags имеет приоритет 2
# поле content имеет приоритет 1
# приоритет используется при ранжировании результатов
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
"query": {
"simple_query_string": {
"query": "истории",
"fields": [
"title^3",
"tags^2",
"content"
]
}
}
}'Da wir einen Analysator mit russischem Stemming verwenden, wird diese Anfrage alle Dokumente zurückgeben, auch wenn darin nur das Wort 'Geschichte' vorkommt.
Die Anfrage kann spezielle Zeichen enthalten, zum Beispiel:
""fried eggs" +(eggplant | potato) -frittata"Anfragesyntax:
+ bedeutet UND-Operation
| bedeutet ODER-Operation
- negiert ein einzelnes Token
" umschließt eine Anzahl von Tokens, um eine Phrase für die Suche zu kennzeichnen
* am Ende eines Begriffs bedeutet eine Präfix-Abfrage
( und ) kennzeichnen die Priorität
~N nach einem Wort bedeutet Bearbeitungsdistanz (Fuzziness)
~N nach einer Phrase bedeutet Slop-Menge# найдем документы без слова 'щенки'
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
"query": {
"simple_query_string": {
"query": "-щенки",
"fields": [
"title^3",
"tags^2",
"content"
]
}
}
}'
# получим 2 поста про котиковLinks
PS
Wenn Sie an ähnlichen Artikeln oder Tutorials interessiert sind, Ideen für neue Artikel haben oder Vorschläge zur Zusammenarbeit machen möchten, freue ich mich über eine Nachricht in der persönlichen Nachricht oder an m.kuzmin+habr@darkleaf.ru.
Quelle: habr.com
