Grundlagen von Elasticsearch

Elasticsearch ist eine Suchmaschine mit JSON REST API, die Lucene verwendet und in Java geschrieben ist. Eine Beschreibung aller Vorteile dieser Engine ist verfügbar auf der offiziellen Website. Im weiteren Verlauf des Textes werden wir Elasticsearch als ES bezeichnen.

Solche Engines werden bei komplexen Suchen in Dokumentendatenbanken eingesetzt. Zum Beispiel für Suchen unter Berücksichtigung der Morphologie von Sprachen oder Suche nach Geokoordinaten.

In diesem Artikel werde ich die Grundlagen von ES am Beispiel der Indizierung von Blogbeiträgen erklären. Ich werde zeigen, wie man Dokumente filtert, sortiert und sucht.

Um nicht von einem Betriebssystem abhängig zu sein, werde ich alle Anfragen an ES mit CURL durchführen. Es gibt auch ein Plugin für Google Chrome mit dem Namen sense.

Im Text sind Verweise auf die Dokumentation und andere Quellen eingebaut. Am Ende finden sich Links für den schnellen Zugriff auf die Dokumentation. Die Definitionen unbekannter Begriffe können im Glossar.

Installation von ES

Dazu benötigen wir zunächst Java. Die Entwickler empfehlen die Installation einer Java-Version, die neuer ist als Java 8 Update 20 oder Java 7 Update 55.

Das ES-Distributionspaket ist verfügbar auf der Website des Herstellers. Nach dem Entpacken des Archivs muss man bin/elasticsearchstarten. Es sind auch Pakete für apt und yum. Es gibt ein offizielles Image für Docker. Mehr zur Installation.

Nach der Installation und dem Start überprüfen wir die Funktionsfähigkeit:

# для удобства запомним адрес в переменную
#export ES_URL=$(docker-machine ip dev):9200
export ES_URL=localhost:9200

curl -X GET $ES_URL

Wir erhalten ungefähr diese 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" : "You Know, for Search"
}

Indizierung

Fügen wir einen Beitrag zu ES hinzu:

# Добавим документ c id 1 типа post в индекс blog.
# ?pretty указывает, что вывод должен быть человеко-читаемым.

curl -XPUT "$ES_URL/blog/post/1?pretty" -d'
{
  "title": "Веселые котята",
  "content": "<p>Eine 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 einen Index blog und einen Typ post erstellt. Man kann eine bedingte Analogie ziehen: ein Index ist eine Datenbank und ein Typ ist eine Tabelle in dieser Datenbank. Jeder Typ hat sein eigenes Schema — Mapping, ähnlich 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 in den Kommentaren die Werte der Felder des indizierten Dokuments hinzugefügt:

{
  "blog" : {
    "mappings" : {
      "post" : {
        "properties" : {
          /* "content": "<p>Eine lustige Geschichte über Kätzchen<p>", */
          "content" : {
            "type" : "string"
          },
          /* "published_at": "2014-09-12T20:44:42+00:00" */
          "published_at" : {
            "type" : "date",
            "format" : "strict_date_optional_time||epoch_millis"
          },
          /* "tags": ["котята", "смешная история"] */
          "tags" : {
            "type" : "string"
          },
          /*  "title": "Веселые котята" */
          "title" : {
            "type" : "string"
          }
        }
      }
    }
  }
}

Es ist wichtig zu beachten, 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 enthält, obwohl sie im Mapping gleich dargestellt werden.
Später werden wir ausführlicher über das Mapping sprechen.

Anfragen

Abrufen eines Dokuments nach seiner 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>Eine 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 erschienen neue Schlüssel: _version und _source. Insgesamt beziehen sich alle Schlüssel, die mit _ beginnen, auf Dienstprogramme.

Schlüssel _version zeigt die Version des Dokuments an. Dies ist erforderlich, um den Mechanismus der optimistischen Sperren zu verwenden. Wenn wir beispielsweise ein Dokument mit Version 1 ändern möchten, senden wir das geänderte Dokument und geben an, dass dies eine Änderung des Dokuments mit Version 1 ist. Falls jemand anderes ebenfalls das Dokument mit Version 1 bearbeitet hat und seine Änderungen vor uns gesendet hat, wird ES unsere Änderungen nicht akzeptieren, da es das Dokument mit Version 2 speichert.

Schlüssel _source enthält das Dokument, das wir indiziert haben. ES verwendet diesen Wert nicht für Suchvorgänge, da für die Suche Indizes verwendet werden. Um Speicherplatz zu sparen, speichert ES das komprimierte Originaldokument. Wenn wir nur die ID und nicht das gesamte Originaldokument benötigen, kann die Speicherung der Quelle 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>Eine 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"
}

Es können auch nur bestimmte Felder ausgewählt werden:

# извлечем только поле title
curl -XGET "$ES_URL/blog/post/1?_source=title&pretty"
{
  "_index" : "blog",
  "_type" : "post",
  "_id" : "1",
  "_version" : 1,
  "found" : true,
  "_source" : {
    "title" : "Fröhliche Kätzchen"
  }
}

Lass uns noch ein paar Beiträge indizieren und komplexere Abfragen durchführen.

curl -XPUT "$ES_URL/blog/post/2" -d'
{
  "title": "Lustige 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 herzzerreißende 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 im Ergebnis. Gesamt zeigt die Gesamtzahl der Dokumente an, die für die Abfrage geeignet sind. sortieren im Ergebnis enthält ein Array von Ganzzahlen, nach denen sortiert wird. Das heißt, das Datum wurde in eine Ganzzahl umgewandelt. Weitere Informationen zur Sortierung finden Sie in Dokumentation.

Filter und Abfragen

ES unterscheidet seit Version 2 nicht mehr zwischen Filtern und Abfragen, stattdessen wird das Konzept der Kontexte eingeführt..
Der Anfragekontext unterscheidet sich vom Filterkontext darin, dass die Anfrage _score generiert und nicht gespeichert wird. Was _score ist, werde ich später zeigen.

Filtern nach Datum

Verwenden Sie die Anfrage Bereich im Kontext von filter:

# получим посты, опубликованные 1ого сентября или позже
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
  "filter": {
    "range": {
      "published_at": { "gte": "2014-09-01" }
    }
  }
}'

Filtern nach Tags

Verwenden Sie Termabfrage zum Suchen nach ID von Dokumenten, 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", "lustige Geschichte" ]
      }
    }, {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "3",
      "_score" : 1.0,
      "_source" : {
        "title" : "Wie ich ein Kätzchen bekam",
        "tags" : [ "Kätzchen" ]
      }
    } ]
  }
}

Volltextsuche

Drei unserer Dokumente enthalten im Feld content Folgendes:

  • <p>Eine lustige Geschichte über Kätzchen<p>
  • <p>Eine lustige Geschichte über Welpen<p>
  • <p>Eine herzzerreißende Geschichte über ein armes Kätzchen von der Straße<p>

Verwenden Sie Match-Abfrage zum Suchen nach ID von Dokumenten, 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 wir jedoch nach "Geschichten" im Feld Inhalt suchen, finden wir nichts, da im Index nur originale Wörter und nicht deren Grundformen gespeichert sind. Um eine qualitativ hochwertige Suche durchzuführen, muss der Analysator konfiguriert werden.

Feld _score zeigt Relevanz. Wenn die Anfrage im Filterkontext ausgeführt wird, beträgt der Wert von _score immer 1, was vollständige Übereinstimmung mit dem Filter bedeutet.

Analysatoren

Analysatoren werden benötigt, um den Ausgangstext in eine Menge von Tokens umzuwandeln.
Analysatoren bestehen aus einem Tokenizer und mehreren optionalen TokenFilters. Ein Tokenizer kann mehreren CharFilters. Der Tokenizer zerlegt die Eingabezeile in Tokens, beispielsweise anhand von Leerzeichen und Satzzeichen. TokenFilter kann Tokens ändern, entfernen oder neue hinzufügen, zum Beispiel nur die Stammform eines Wortes beibehalten, Präpositionen entfernen, Synonyme hinzufügen. CharFilter verändert die ursprüngliche Zeichenkette vollständig, zum Beispiel indem HTML-Tags entfernt werden.

In ES gibt es mehrere Standardanalysatoren. Zum Beispiel der Analysator russian.

Lassen Sie uns API verwenden und sehen, wie die Analysatoren standard und russian die Zeichenfolge "Lustige Geschichten über Kätzchen" umwandeln:

# используем анализатор 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" : 18,
    "type" : "",
    "position" : 1
  }, {
    "token" : "über",
    "start_offset" : 19,
    "end_offset" : 24,
    "type" : "",
    "position" : 2
  }, {
    "token" : "kätzchen",
    "start_offset" : 25,
    "end_offset" : 32,
    "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" : "geschicht",
    "start_offset" : 8,
    "end_offset" : 15,
    "type" : "",
    "position" : 1
  }, {
    "token" : "katze",
    "start_offset" : 20,
    "end_offset" : 25,
    "type" : "",
    "position" : 3
  } ]
}

Der Standardanalysator hat die Zeichenfolge anhand von Leerzeichen aufgeteilt und alles in Kleinbuchstaben umgewandelt. Der Analysator "russian" hat unwichtige Wörter entfernt, in Kleinbuchstaben umgewandelt und die Wortstämme beibehalten.

Sehen wir uns an, welche Tokenizer, TokenFilters und CharFilters der Analysator "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",
      
      "filter": [
        "lowercase",
        "russian_stop",
        "russian_keywords",
        "russian_stemmer"
      ]
      
    }
  }
}

Lassen Sie uns unseren Analysator auf Basis von "russian" beschreiben, der HTML-Tags entfernen wird. Wir nennen ihn "default", da der Analysator mit diesem Namen standardmäßig verwendet wird.

{
  "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"
      ]
    }
  }
}

Zuerst werden alle HTML-Tags aus der ursprünglichen Zeichenfolge entfernt, dann wird sie vom Tokenizer "standard" in Tokens aufgeteilt, die erhaltenen Tokens werden in Kleinbuchstaben umgewandelt, unwichtige Wörter werden entfernt und es bleiben die Wortstämme der verbleibenden Tokens.

Indizes erstellen

Oben haben wir den default-Analysator beschrieben. Dieser wird auf alle Textfelder angewendet. Unser Beitrag enthält ein Array von Tags, entsprechend werden auch die Tags vom Analysator verarbeitet. Da wir nach genauem Tag-Matching suchen, muss die Analyse für das Feld "tags" deaktiviert werden.

Erstellen wir den Index "blog2" mit einem Analysator und Mapping, bei dem die Analyse des Feldes "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 drei Beiträge zu diesem Index (blog2) hinzu. Ich werde diesen Prozess überspringen, da er dem Hinzufügen von Dokumenten zum Index blog ähnlich ist.

Volltextsuche mit Unterstützung für Ausdrücke

Lassen Sie uns einen weiteren Abfragetyp kennenlernen:

# найдем документы, в которых встречается слово 'истории'
# 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 Analyzer mit russischem Stemming verwenden, gibt diese Abfrage alle Dokumente zurück, obwohl nur das Wort ‚Geschichte‘ darin vorkommt.

Abfragen können spezielle Zeichen enthalten, zum Beispiel:

""fried eggs" +(eggplant | potato) -frittata"

Abfragesyntax:

+ bedeutet AND-Operation
| bedeutet OR-Operation
- negiert ein einzelnes Token
" umschließt eine Anzahl von Tokens, um eine Phrase für die Suche darzustellen
* am Ende eines Begriffs bedeutet eine Präfixabfrage
( und ) bedeuten Vorrang
~N nach einem Wort bedeutet Editierdistanz (Unsicherheit)
~N nach einer Phrase bedeutet die Anzahl der Slops
# найдем документы без слова 'щенки'
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 und Tutorials interessiert sind, Ideen für neue Artikel haben oder Vorschläge zur Zusammenarbeit haben, freue ich mich über eine Nachricht in meiner Inbox oder an die E-Mail m.kuzmin+habr@darkleaf.ru.

Quelle: habr.com

Zuverlässiges Hosting für Websites mit DDoS-Schutz kaufen, VPS VDS Server 🔥 Zuverlässiges Hosting für Websites mit DDoS-Schutz kaufen, VPS VDS Server - ProHoster