Podstawy Elasticsearch

Elasticsearch to wyszukiwarka z interfejsem API json rest, korzystająca z Lucene i napisana w Javie. Opis wszystkich zalet tego silnika znajdziesz na stronie Oficjalna strona. W dalszej części Elasticsearch będziemy nazywać ES.

Podobne silniki służą do skomplikowanych przeszukiwań w bazie dokumentów. Na przykład szukaj z uwzględnieniem morfologii języka lub szukaj według współrzędnych geograficznych.

W tym artykule opowiem o podstawach ES na przykładzie indeksowania wpisów na blogu. Pokażę Ci jak filtrować, sortować i przeszukiwać dokumenty.

Aby nie polegać na systemie operacyjnym, wszystkie żądania będę wysyłać do ES za pomocą CURL. Istnieje również wtyczka do przeglądarki Google Chrome o nazwie rozsądek.

W tekście znajdują się linki do dokumentacji i innych źródeł. Na końcu znajdują się linki umożliwiające szybki dostęp do dokumentacji. Definicje nieznanych terminów można znaleźć w glosariusze.

Instalowanie ES

Aby to zrobić, potrzebujemy najpierw Java. Deweloperzy polecić zainstaluj wersje Java nowsze niż Java 8 update 20 lub Java 7 update 55.

Dystrybucja ES jest dostępna pod adresem strona dewelopera. Po rozpakowaniu archiwum musisz uruchomić bin/elasticsearch. Także dostępny pakiety dla apt i mniam. Jest oficjalny obraz okna dokowanego. Więcej o instalacji.

Po instalacji i uruchomieniu sprawdźmy funkcjonalność:

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

curl -X GET $ES_URL

Otrzymamy coś takiego:

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

Indeksowanie

Dodajmy post do ES:

# Добавим документ 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"
}'

odpowiedź serwera:

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

ES utworzony automatycznie indeks blogu i Typ post. Możemy narysować analogię warunkową: indeks jest bazą danych, a typ jest tabelą w tej bazie danych. Każdy typ ma swój własny schemat − mapowanie, podobnie jak tabela relacyjna. Mapowanie jest generowane automatycznie podczas indeksowania dokumentu:

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

W odpowiedzi serwera dodałem w komentarzach wartości pól indeksowanego dokumentu:

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

Warto zauważyć, że ES nie rozróżnia pojedynczej wartości od tablicy wartości. Na przykład pole tytułu zawiera po prostu tytuł, a pole tagów zawiera tablicę ciągów znaków, chociaż są one reprezentowane w ten sam sposób w mapowaniu.
Później porozmawiamy więcej o mapowaniu.

wnioski

Pobieranie dokumentu według jego identyfikatora:

# извлечем документ с 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"
  }
}

W odpowiedzi pojawiły się nowe klucze: _version и _source. Ogólnie rzecz biorąc, wszystkie klucze zaczynające się od _ są klasyfikowane jako oficjalne.

klucz _version pokazuje wersję dokumentu. Jest to potrzebne, aby optymistyczny mechanizm blokujący zadziałał. Przykładowo chcemy zmienić dokument, który ma wersję 1. Zgłaszamy zmieniony dokument i wskazujemy, że jest to edycja dokumentu z wersją 1. Jeżeli ktoś inny również redagował dokument w wersji 1 i przed nami zgłosił zmiany, to ES nie zaakceptuje naszych zmian, ponieważ przechowuje dokument w wersji 2.

klucz _source zawiera dokument, który zaindeksowaliśmy. ES nie używa tej wartości do operacji wyszukiwania, ponieważ Indeksy służą do wyszukiwania. Aby zaoszczędzić miejsce, ES przechowuje skompresowany dokument źródłowy. Jeśli potrzebujemy tylko identyfikatora, a nie całego dokumentu źródłowego, możemy wyłączyć pamięć źródłową.

Jeśli nie potrzebujemy dodatkowych informacji, możemy uzyskać jedynie zawartość _source:

curl -XGET "$ES_URL/blog/post/1/_source?pretty"
{
  "title" : "Веселые котята",
  "content" : "<p>Смешная история про котят<p>",
  "tags" : [ "котята", "смешная история" ],
  "published_at" : "2014-09-12T20:44:42+00:00"
}

Możesz także wybrać tylko niektóre pola:

# извлечем только поле title
curl -XGET "$ES_URL/blog/post/1?_source=title&pretty"
{
  "_index" : "blog",
  "_type" : "post",
  "_id" : "1",
  "_version" : 1,
  "found" : true,
  "_source" : {
    "title" : "Веселые котята"
  }
}

Zaindeksujmy jeszcze kilka postów i wykonajmy bardziej złożone zapytania.

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"
}'

Сортировка

# найдем последний пост по дате публикации и извлечем поля 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 ]
    } ]
  }
}

Wybraliśmy ostatni post. size ogranicza liczbę wystawianych dokumentów. total pokazuje całkowitą liczbę dokumentów pasujących do żądania. sort na wyjściu zawiera tablicę liczb całkowitych, według której przeprowadzane jest sortowanie. Te. data została przekonwertowana na liczbę całkowitą. Więcej informacji na temat sortowania można znaleźć w dokumentacja.

Filtry i zapytania

Zamiast tego ES od wersji 2 nie rozróżnia filtrów i zapytań wprowadzono pojęcie kontekstów.
Kontekst zapytania różni się od kontekstu filtru tym, że zapytanie generuje wynik _score i nie jest buforowane. Później pokażę ci, jaki jest _score.

Filtruj według daty

Korzystamy z prośby zasięg w kontekście filtra:

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

Filtruj według tagów

Używamy zapytanie terminowe aby wyszukać identyfikatory dokumentów zawierających podane słowo:

# найдем все документы, в поле 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" : [ "котята" ]
      }
    } ]
  }
}

Wyszukiwanie pełnotekstowe

Trzy z naszych dokumentów zawierają w polu treści następujące informacje:

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

Używamy dopasowanie zapytania aby wyszukać identyfikatory dokumentów zawierających podane słowo:

# 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
    } ]
  }
}

Jeśli jednak w polu treści będziemy szukać „historii”, nic nie znajdziemy, bo Indeks zawiera tylko oryginalne słowa, bez ich rdzeni. Aby przeprowadzić wyszukiwanie wysokiej jakości, należy skonfigurować analizator.

Pole _score przedstawia znaczenie. Jeśli żądanie jest wykonywane w kontekście filtra, wówczas wartość _score będzie zawsze równa 1, co oznacza pełne dopasowanie do filtra.

Analizatory

Analizatory potrzebne do konwersji tekstu źródłowego na zestaw tokenów.
Analizatory składają się z jednego Tokenizator i kilka opcjonalnych Filtry tokenów. Tokenizer może być poprzedzony kilkoma Filtry znaków. Tokenizatory dzielą ciąg źródłowy na tokeny, takie jak spacje i znaki interpunkcyjne. TokenFilter może zmieniać tokeny, usuwać lub dodawać nowe, na przykład pozostawić tylko rdzeń słowa, usunąć przyimki, dodać synonimy. CharFilter - zmienia cały ciąg źródłowy, np. wycina znaczniki HTML.

ES ma kilka standardowe analizatory. Na przykład analizator Rosyjski.

Użyjmy api i zobaczmy, jak standardowe i rosyjskie analizatory przekształcają ciąg „Śmieszne historie o kociątkach”:

# используем анализатор 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
  } ]
}

Standardowy analizator dzieli ciąg znaków na spacje i konwertuje wszystko na małe litery, rosyjski analizator usuwa nieistotne słowa, konwertuje je na małe litery i pozostawia rdzeń słów.

Zobaczmy, jakiego Tokenizera, TokenFilters, CharFilters używa rosyjski analizator:

{
  "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 отсутствуют */
    }
  }
}

Opiszmy nasz analizator oparty na języku rosyjskim, który wytnie znaczniki HTML. Nazwijmy to domyślnym, ponieważ domyślnie będzie używany analizator o tej nazwie.

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

Najpierw wszystkie tagi HTML zostaną usunięte z ciągu źródłowego, następnie standard tokenizera podzieli go na tokeny, powstałe tokeny zostaną przeniesione na małe litery, nieistotne słowa zostaną usunięte, a pozostałe tokeny pozostaną rdzeniem słowa.

Tworzenie indeksu

Powyżej opisaliśmy domyślny analizator. Będzie dotyczyć wszystkich pól tekstowych. Nasz post zawiera tablicę tagów, więc tagi również zostaną przetworzone przez analizator. Ponieważ Poszukujemy postów według dokładnego dopasowania do tagu, wówczas musimy wyłączyć analizę dla pola tagów.

Stwórzmy indeks blog2 z analizatorem i mapowaniem, w którym wyłączymy analizę pola tagów:

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"
        }
      }
    }
  }
}'

Dodajmy te same 3 posty do tego indeksu (blog2). Pominę ten proces, ponieważ... przypomina to dodawanie dokumentów do indeksu bloga.

Wyszukiwanie pełnotekstowe z obsługą wyrażeń

Przyjrzyjmy się innemu rodzajowi żądania:

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

Ponieważ Używamy analizatora z rdzeniem rosyjskim, wtedy to żądanie zwróci wszystkie dokumenty, chociaż zawierają tylko słowo „historia”.

Żądanie może zawierać znaki specjalne, na przykład:

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

Składnia żądania:

+ 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 поста про котиков

referencje

PS

Jeśli jesteś zainteresowany podobnymi artykułami-lekcjami, masz pomysły na nowe artykuły lub masz propozycje współpracy, chętnie otrzymam wiadomość w wiadomości prywatnej lub na e-mail [email chroniony].

Źródło: www.habr.com