Elasticsearch-Grundlagen

Elasticsearch ist eine Suchmaschine mit JSON-Rest-API, die Lucene verwendet und in Java geschrieben ist. Eine Beschreibung aller Vorteile dieses Motors finden Sie unter die offizielle Seite. Im Folgenden bezeichnen wir Elasticsearch als ES.

Ähnliche Engines werden für komplexe Suchen in einer Dokumentendatenbank verwendet. Suchen Sie beispielsweise unter Berücksichtigung der Morphologie der Sprache oder suchen Sie nach Geokoordinaten.

In diesem Artikel werde ich am Beispiel der Indizierung von Blogbeiträgen über die Grundlagen von ES sprechen. Ich zeige Ihnen, wie Sie Dokumente filtern, sortieren und durchsuchen.

Um nicht vom Betriebssystem abhängig zu sein, werde ich alle Anfragen an ES über CURL stellen. Es gibt auch ein Plugin für Google Chrome namens Sinn.

Der Text enthält Links zur Dokumentation und anderen Quellen. Am Ende finden Sie Links für den schnellen Zugriff auf die Dokumentation. Definitionen unbekannter Begriffe finden Sie in Glossare.

ES installieren

Dazu benötigen wir zunächst Java. Entwickler Empfehlen Installieren Sie Java-Versionen neuer als Java 8 Update 20 oder Java 7 Update 55.

Die ES-Distribution ist verfügbar unter Entwicklerseite. Nach dem Entpacken des Archivs müssen Sie es ausführen bin/elasticsearch. Auch verfügbar Pakete für apt und yum. Gibt es Offizielles Bild für Docker. Mehr zur Installation.

Lassen Sie uns nach der Installation und dem Start die Funktionalität überprüfen:

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

curl -X GET $ES_URL

Wir erhalten so etwas:

{
  "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>Смешная история про котят<p>",
  "tags": [
    "котята",
    "смешная история"
  ],
  "published_at": "2014-09-12T20:44:42+00:00"
}'

Serverantwort:

{
  "_index" : "blog",
  "_type" : "post",
  "_id" : "1",
  "_version" : 1,
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "created" : false
}

ES automatisch erstellt Index Blog und тип Post. Wir können 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, genau wie eine relationale Tabelle. Die Zuordnung wird automatisch generiert, wenn das Dokument indiziert wird:

# Получим mapping всех типов индекса blog
curl -XGET "$ES_URL/blog/_mapping?pretty"

In der Serverantwort habe ich die Werte der Felder des indizierten Dokuments in den Kommentaren hinzugefügt:

{
  "blog" : {
    "mappings" : {
      "post" : {
        "properties" : {
          /* "content": "<p>Смешная история про котят<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 erwähnenswert, dass ES nicht zwischen einem einzelnen Wert und einem Array von Werten unterscheidet. Beispielsweise enthält das Feld „Titel“ einfach einen Titel und das Feld „Tags“ ein Array von Zeichenfolgen, obwohl diese im Mapping auf die gleiche Weise dargestellt werden.
Wir werden später mehr über das Mapping sprechen.

Anfragen

Abrufen eines Dokuments anhand 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" : "Веселые котята",
    "content" : "<p>Смешная история про котят<p>",
    "tags" : [ "котята", "смешная история" ],
    "published_at" : "2014-09-12T20:44:42+00:00"
  }
}

In der Antwort wurden neue Schlüssel angezeigt: _version и _source. Im Allgemeinen beginnen alle Schlüssel mit _ gelten als offiziell.

Schlüssel _version zeigt die Dokumentversion an. Es ist erforderlich, damit der optimistische Verriegelungsmechanismus funktioniert. Wir möchten beispielsweise ein Dokument ändern, das Version 1 hat. Wir reichen das geänderte Dokument ein und geben an, dass es sich um eine Bearbeitung eines Dokuments mit Version 1 handelt. Wenn jemand anderes vor uns auch ein Dokument mit Version 1 bearbeitet und Änderungen eingereicht hat, dann ES wird unsere Änderungen nicht akzeptieren, weil Es speichert das Dokument mit Version 2.

Schlüssel _source enthält das von uns indizierte Dokument. ES verwendet diesen Wert nicht für Suchvorgänge, weil Für die Suche werden Indizes verwendet. Um Platz zu sparen, speichert ES ein komprimiertes Quelldokument. Wenn wir nur die ID und nicht das gesamte Quelldokument benötigen, können wir den Quellspeicher deaktivieren.

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" : "Веселые котята",
  "content" : "<p>Смешная история про котят<p>",
  "tags" : [ "котята", "смешная история" ],
  "published_at" : "2014-09-12T20:44:42+00:00"
}

Sie können auch 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" : "Веселые котята"
  }
}

Lassen Sie uns noch ein paar Beiträge indizieren und komplexere Abfragen ausführen.

curl -XPUT "$ES_URL/blog/post/2" -d'
{
  "title": "Веселые щенки",
  "content": "<p>Смешная история про щенков<p>",
  "tags": [
    "щенки",
    "смешная история"
  ],
  "published_at": "2014-08-12T20:44:42+00:00"
}'
curl -XPUT "$ES_URL/blog/post/3" -d'
{
  "title": "Как у меня появился котенок",
  "content": "<p>Душераздирающая история про бедного котенка с улицы<p>",
  "tags": [
    "котята"
  ],
  "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" : "Веселые котята",
        "published_at" : "2014-09-12T20:44:42+00:00"
      },
      "sort" : [ 1410554682000 ]
    } ]
  }
}

Wir haben uns für den letzten Beitrag entschieden. size begrenzt die Anzahl der auszustellenden Dokumente. total zeigt die Gesamtzahl der Dokumente an, die der Anfrage entsprechen. sort in der Ausgabe enthält ein Array von Ganzzahlen, nach denen sortiert wird. Diese. Das Datum wurde in eine Ganzzahl umgewandelt. Weitere Informationen zum Sortieren finden Sie in Dokumentation.

Filter und Abfragen

ES unterscheidet seit Version 2 nicht mehr zwischen Filtern und Abfragen Das Konzept der Kontexte wird eingeführt.
Ein Abfragekontext unterscheidet sich von einem Filterkontext dadurch, dass die Abfrage einen _score generiert und nicht zwischengespeichert wird. Ich werde Ihnen später zeigen, was _score ist.

Nach Datum filtern

Wir nutzen die Anfrage Angebot im Zusammenhang mit Filter:

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

Filtern nach Tags

Wir gebrauchen Begriffsabfrage So suchen Sie nach Dokument-IDs, die ein bestimmtes 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" : "Веселые котята",
        "tags" : [ "котята", "смешная история" ]
      }
    }, {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "3",
      "_score" : 1.0,
      "_source" : {
        "title" : "Как у меня появился котенок",
        "tags" : [ "котята" ]
      }
    } ]
  }
}

Volltextsuche

Drei unserer Dokumente enthalten im Inhaltsfeld Folgendes:

  • <p>Смешная история про котят<p>
  • <p>Смешная история про щенков<p>
  • <p>Душераздирающая история про бедного котенка с улицы<p>

Wir gebrauchen Übereinstimmungsabfrage So suchen Sie nach Dokument-IDs, die ein bestimmtes 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 im Inhaltsfeld nach „Geschichten“ suchen, werden wir nichts finden, weil Der Index enthält nur die Originalwörter, nicht deren Wortstämme. Um eine qualitativ hochwertige Suche durchführen zu können, müssen Sie den Analysator konfigurieren.

Feld _score zeigt Relevanz. Wenn die Anfrage in einem Filterkontext ausgeführt wird, ist der _score-Wert immer gleich 1, was eine vollständige Übereinstimmung mit dem Filter bedeutet.

Analysatoren

Analysatoren werden benötigt, um den Quelltext in eine Reihe von Tokens umzuwandeln.
Analysatoren bestehen aus einem Tokenisierer und mehrere optional TokenFilter. Dem Tokenizer können mehrere vorangestellt sein CharFilters. Tokenizer zerlegen die Quellzeichenfolge in Token, beispielsweise Leerzeichen und Satzzeichen. TokenFilter kann Token ändern, löschen oder neue hinzufügen, beispielsweise nur den Wortstamm belassen, Präpositionen entfernen und Synonyme hinzufügen. CharFilter – ändert die gesamte Quellzeichenfolge, schneidet beispielsweise HTML-Tags aus.

ES hat mehrere Standardanalysatoren. Zum Beispiel ein Analysator russisch.

Lassen Sie uns verwenden Bienen und sehen wir uns an, wie die Standard- und russischen Analysegeräte 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" : "веселые",
    "start_offset" : 0,
    "end_offset" : 7,
    "type" : "<ALPHANUM>",
    "position" : 0
  }, {
    "token" : "истории",
    "start_offset" : 8,
    "end_offset" : 15,
    "type" : "<ALPHANUM>",
    "position" : 1
  }, {
    "token" : "про",
    "start_offset" : 16,
    "end_offset" : 19,
    "type" : "<ALPHANUM>",
    "position" : 2
  }, {
    "token" : "котят",
    "start_offset" : 20,
    "end_offset" : 25,
    "type" : "<ALPHANUM>",
    "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" : "весел",
    "start_offset" : 0,
    "end_offset" : 7,
    "type" : "<ALPHANUM>",
    "position" : 0
  }, {
    "token" : "истор",
    "start_offset" : 8,
    "end_offset" : 15,
    "type" : "<ALPHANUM>",
    "position" : 1
  }, {
    "token" : "кот",
    "start_offset" : 20,
    "end_offset" : 25,
    "type" : "<ALPHANUM>",
    "position" : 3
  } ]
}

Der Standardanalysator teilte die Zeichenfolge durch Leerzeichen auf und wandelte alles in Kleinbuchstaben um. Der russische Analysator entfernte unwichtige Wörter, wandelte sie in Kleinbuchstaben um und beließ den Wortstamm.

Mal sehen, welche Tokenizer, TokenFilters, CharFilters der russische Analysator 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 отсутствуют */
    }
  }
}

Beschreiben wir unseren auf Russisch basierenden Analysator, der HTML-Tags ausschneidet. Nennen wir es Standard, weil Standardmäßig wird ein Analysator mit diesem Namen verwendet.

{
  "filter": {
    "ru_stop": {
      "type":       "stop",
      "stopwords":  "_russian_"
    },
    "ru_stemmer": {
      "type":       "stemmer",
      "language":   "russian"
    }
  },
  "analyzer": {
    "default": {
      /* добавляем удаление html тегов */
      "char_filter": ["html_strip"],
      "tokenizer":  "standard",
      "filter": [
        "lowercase",
        "ru_stop",
        "ru_stemmer"
      ]
    }
  }
}

Zuerst werden alle HTML-Tags aus der Quellzeichenfolge entfernt, dann wird sie vom Tokenizer-Standard in Token aufgeteilt, die resultierenden Token werden in Kleinbuchstaben verschoben, unbedeutende Wörter werden entfernt und die verbleibenden Token bleiben der Wortstamm.

Einen Index erstellen

Oben haben wir den Standardanalysator beschrieben. Dies gilt für alle Zeichenfolgenfelder. Unser Beitrag enthält eine Reihe von Tags, sodass die Tags auch vom Analysator verarbeitet werden. Weil Wir suchen nach Beiträgen, die genau mit einem Tag übereinstimmen, und müssen dann die Analyse für das Tag-Feld deaktivieren.

Erstellen wir einen Index blog2 mit einem Analysator und Mapping, in dem die Analyse des Tag-Felds 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"
        }
      }
    }
  }
}'

Fügen wir die gleichen drei Beiträge zu diesem Index hinzu (blog3). Ich werde diesen Vorgang auslassen, weil... Es ähnelt dem Hinzufügen von Dokumenten zum Blog-Index.

Volltextsuche mit Ausdrucksunterstützung

Schauen wir uns eine andere Art von Anfrage an:

# найдем документы, в которых встречается слово 'истории'
# 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"
      ]
    }
  }
}'

Weil Wir verwenden einen Analysator mit russischer Wortstammerkennung, dann werden bei dieser Anfrage alle Dokumente zurückgegeben, obwohl sie nur das Wort „Geschichte“ enthalten.

Die Anfrage kann Sonderzeichen enthalten, zum Beispiel:

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

Anfragesyntax:

+ signifies AND operation
| signifies OR operation
- negates a single token
" wraps a number of tokens to signify a phrase for searching
* at the end of a term signifies a prefix query
( and ) signify precedence
~N after a word signifies edit distance (fuzziness)
~N after a phrase signifies slop amount
# найдем документы без слова 'щенки'
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
  "query": {
    "simple_query_string": {
      "query": "-щенки",
      "fields": [
        "title^3",
        "tags^2",
        "content"
      ]
    }
  }
}'

# получим 2 поста про котиков

Referenzen

PS

Wenn Sie Interesse an ähnlichen Artikel-Lektionen haben, Ideen für neue Artikel haben oder Vorschläge für eine Zusammenarbeit haben, freue ich mich über eine Nachricht in einer persönlichen Nachricht oder per E-Mail [E-Mail geschützt] .

Source: habr.com