Elasticsearch — silnik wyszukiwania z json rest api, wykorzystujący Lucene i napisany w Javie. Opis wszystkich zalet tego silnika dostępny jest na . W dalszej części tekstu będziemy nazywać Elasticsearch jako ES.
Podobne silniki są używane przy zaawansowanym wyszukiwaniu w bazach dokumentów. Na przykład, wyszukiwanie z uwzględnieniem morfologii języka lub wyszukiwanie po koordynatach geograficznych.
W tym artykule opowiem o podstawach ES na przykładzie indeksacji postów bloga. Pokażę, jak filtrować, sortować i wyszukiwać dokumenty.
Aby nie zależeć od systemu operacyjnego, wszystkie zapytania do ES będę robił za pomocą CURL. Istnieje również wtyczka do Google Chrome o nazwie .
W tekście umieściłem linki do dokumentacji i innych źródeł. Na końcu znajdują się linki do szybkiego dostępu do dokumentacji. Definicje nieznanych terminów można przeczytać w .
Instalacja ES
Aby to zrobić, najpierw potrzebujemy Javy. Programiści powinni zainstalować wersje Javy nowsze niż Java 8 update 20 lub Java 7 update 55.
Dystrybucja ES jest dostępna na . Po rozpakowaniu archiwum należy uruchomić bin/elasticsearch. Dostępne są również . Jest . .
Po zainstalowaniu i uruchomieniu sprawdzimy działanie:
# для удобства запомним адрес в переменную
#export ES_URL=$(docker-machine ip dev):9200
export ES_URL=localhost:9200
curl -X GET $ES_URLOtrzymamy mniej więcej taką odpowiedź:
{
"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"
}Indeksacja
Dodamy post do ES:
# Добавим документ c id 1 типа post в индекс blog.
# ?pretty указывает, что вывод должен быть человеко-читаемым.
curl -XPUT "$ES_URL/blog/post/1?pretty" -d'
{
"title": "Веселые котята",
"content": "<p>Śmieszna historia o kotkach<p>",
"tags": [
"kotki",
"śmieszna historia"
],
"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 automatycznie utworzył blog i post. Można przeprowadzić uproszczoną analogię: indeks — to baza danych, a typ — tabela w tej bazie. Każdy typ ma swoją schemę — , podobnie jak tabela w relacyjnej bazie. Mapping jest generowany automatycznie podczas indeksacji dokumentu:
# Получим mapping всех типов индекса blog
curl -XGET "$ES_URL/blog/_mapping?pretty"W odpowiedzi serwera dodałem w komentarzu wartości pól zaindeksowanego dokumentu:
{
"blog" : {
"mappings" : {
"post" : {
"properties" : {
/* "content": "<p>Śmieszna historia o kotkach<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": ["kotki", "smieszna historia"] */
"tags" : {
"type" : "string"
},
/* "title": "Wesołe kotki" */
"title" : {
"type" : "string"
}
}
}
}
}
}Należy zauważyć, że ES nie robi różnicy między pojedynczą wartością a tablicą wartości. Na przykład pole title zawiera po prostu nagłówek, a pole tags — tablicę ciągów, chociaż są one przedstawiane w mappingu w ten sam sposób.
Później porozmawiamy o mapowaniu bardziej szczegółowo.
Zapytania
Pobranie dokumentu po jego 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>\u015amieszna historia o kotkach<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"
}
}W odpowiedzi pojawiły się nowe klucze: _version i _source. W ogóle, wszystkie klucze, które zaczynają się od _ są kluczami systemowymi.
Klucz _version pokazuje wersję dokumentu. Jest to potrzebne do działania mechanizmu optymistycznych blokad. Na przykład, chcemy zmienić dokument mający wersję 1. Wysyłamy zmieniony dokument i wskazujemy, że jest to poprawka dokumentu z wersją 1. Jeśli ktoś inny również edytował dokument z wersją 1 i wysłał zmiany przed nami, to ES nie zaakceptuje naszych zmian, ponieważ przechowuje dokument z wersją 2.
Klucz _source zawiera ten dokument, który zindeksowaliśmy. ES nie używa tej wartości do operacji wyszukiwania, ponieważ do wyszukiwania używane są indeksy. Aby zaoszczędzić miejsce, ES przechowuje skompresowany oryginalny dokument. Jeśli potrzebujemy tylko id, a nie całego oryginalnego dokumentu, można wyłączyć przechowywanie źródła.
Jeśli nie potrzebujemy dodatkowych informacji, możemy uzyskać tylko zawartość _source:
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>\u015amieszna historia o kotkach<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"
}
Można również wybrać tylko określone pola:
# извлечем только поле title
curl -XGET "$ES_URL/blog/post/1?_source=title&pretty"{
"_index" : "blog",
"_type" : "post",
"_id" : "1",
"_version" : 1,
"found" : true,
"_source" : {
"title" : "Wesołe kotki"
}
}Zindeksujmy jeszcze kilka postów i wykonajmy bardziej złożone zapytania.
curl -XPUT "$ES_URL/blog/post/2" -d'
{
"title": "Wesołe szczenięta",
"content": "<p>Śmieszna historia o szczeniakach<p>",
"tags": [
"szczeniaki",
"śmieszna historia"
],
"published_at": "2014-08-12T20:44:42+00:00"
}'curl -XPUT "$ES_URL/blog/post/3" -d'
{
"title": "Jak zdobyłem kociaka",
"content": "<p>Wzruszająca historia o biednym kociaku z ulicy<p>",
"tags": [
"kociaki"
],
"published_at": "2014-07-21T20:44:42+00:00"
}'Sortowanie
# найдем последний пост по дате публикации и извлечем поля 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" : "Wesołe kotki",
"published_at" : "2014-09-12T20:44:42+00:00"
},
"sort" : [ 1410554682000 ]
} ]
}
}Wybraliśmy ostatni post. rozmiar ogranicza liczbę dokumentów w wynikach. total pokazuje całkowitą liczbę dokumentów odpowiadających zapytaniu. sort w wynikach zawiera tablicę liczb całkowitych, na podstawie których następuje sortowanie. Tzn. data została skonwertowana na liczbę całkowitą. Więcej o sortowaniu można przeczytać w .
Filtry i zapytania
ES od wersji 2 nie rozróżnia filtrów i zapytań, zamiast tego .
Kontekst zapytania różni się od kontekstu filtra tym, że zapytanie generuje _score i nie jest cachowane. Co to jest _score pokażę później.
Filtrowanie po dacie
Używamy zapytania w kontekście filtru:
# получим посты, опубликованные 1ого сентября или позже
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
"filter": {
"range": {
"published_at": { "gte": "2014-09-01" }
}
}
}'Filtrowanie po tagach
Używamy do wyszukiwania id dokumentów zawierających dane 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" : "Wesołe kotki",
"tags" : [ "kotki", "zabawna historia" ]
}
}, {
"_index" : "blog",
"_type" : "post",
"_id" : "3",
"_score" : 1.0,
"_source" : {
"title" : "Jak dostałem kociaka",
"tags" : [ "kotki" ]
}
} ]
}
}Wyszukiwanie pełnotekstowe
Trzy nasze dokumenty zawierają w polu content następujące:
<p>Śmieszna historia o kotkach<p><p>Śmieszna historia o szczeniakach<p><p>Wzruszająca historia o biednym kociaku z ulicy<p>
Używamy do wyszukiwania id dokumentów zawierających dane 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
} ]
}
}Jednakże, jeśli szukać „historii” w polu content, nie znajdziemy nic, ponieważ indeks zawiera tylko oryginalne słowa, a nie ich podstawy. Aby zapewnić wysoką jakość wyszukiwania, należy skonfigurować analizator.
Pole _score pokazuje . Jeśli zapytanie jest wykonywane w kontekście filtru, wartość _score zawsze będzie wynosiła 1, co oznacza pełne dopasowanie do filtru.
Analizatory
są potrzebne do przekształcenia oryginalnego tekstu w zestaw tokenów.
Analizatory składają się z jednego i kilku opcjonalnych . Tokenizer może być poprzedzony przez kilka . Tokenizer dzieli oryginalny ciąg na tokeny, na przykład po spacji i znakach interpunkcyjnych. TokenFilter może zmieniać tokeny, usuwać lub dodawać nowe, na przykład zostawiać tylko podstawę słowa, usuwać przyimki, dodawać synonimy. CharFilter zmienia oryginalny ciąg w całości, na przykład wycina tagi HTML.
W ES istnieje kilka Na przykład, analizator .
Skorzystamy i zobaczymy, jak analizatory standard i russian przekształcają ciąg „Wesołe historie o kotkach”:
# используем анализатор 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" : "wesołe",
"start_offset" : 0,
"end_offset" : 7,
"type" : "",
"position" : 0
}, {
"token" : "historie",
"start_offset" : 8,
"end_offset" : 15,
"type" : "",
"position" : 1
}, {
"token" : "o",
"start_offset" : 16,
"end_offset" : 19,
"type" : "",
"position" : 2
}, {
"token" : "kotkach",
"start_offset" : 20,
"end_offset" : 25,
"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" : "wesoły",
"start_offset" : 0,
"end_offset" : 7,
"type" : "",
"position" : 0
}, {
"token" : "historia",
"start_offset" : 8,
"end_offset" : 15,
"type" : "",
"position" : 1
}, {
"token" : "kot",
"start_offset" : 20,
"end_offset" : 25,
"type" : "",
"position" : 3
} ]
}Standardowy analizator dzieli tekst na słowa i zmienia je na małe litery. Analizator rosyjski usuwa nieważne słowa, zamienia tekst na małe litery, a pozostawia rdzenie słów.
Zobaczmy, jakie Tokenizery, Filtry Tokenów i Filtry Znaków używa analizator rosyjski:
{
"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 brak */
}
}
}Opiszmy nasz analizator oparty na rosyjskim, który będzie usuwał tagi HTML. Nazwiemy go domyślnym, ponieważ analizator o tej nazwie będzie używany domyślnie.
{
"filter": {
"ru_stop": {
"type": "stop",
"stopwords": "_russian_"
},
"ru_stemmer": {
"type": "stemmer",
"language": "russian"
}
},
"analyzer": {
"default": {
/* dodajemy usuwanie tagów HTML */
"char_filter": ["html_strip"],
"tokenizer": "standard",
"filter": [
"lowercase",
"ru_stop",
"ru_stemmer"
]
}
}
}Najpierw z oryginalnego tekstu usunięte zostaną wszystkie tagi HTML, następnie tekst podzieli się na tokeny za pomocą tokenizera standard, otrzymane tokeny zostaną przekonwertowane na małe litery, usunięte zostaną nieważne słowa, a z pozostałych tokenów pozostanie rdzeń słowa.
Tworzenie indeksu
Powyżej opisaliśmy analizator domyślny. Będzie on stosowany do wszystkich pól tekstowych. Nasz post zawiera tablicę tagów, w związku z tym tagi również zostaną przetworzone przez analizator. Ponieważ szukamy postów według dokładnego dopasowania tagu, konieczne jest wyłączenie analizy dla pola tagów.
Stworzymy indeks blog2 z analizatorem oraz mapowaniem, w którym wyłączono 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"
}
}
}
}
}'Dodamy te same 3 posty do tego indeksu (blog2). Pomińmy ten proces, ponieważ jest on analogiczny do dodawania dokumentów do indeksu blog.
Wyszukiwanie pełnotekstowe z obsługą wyrażeń
Zapoznajmy się z jeszcze jednym rodzajem zapytań:
# найдем документы, в которых встречается слово 'истории'
# 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 ze stemmingiem rosyjskim, to to zapytanie zwróci wszystkie dokumenty, choć w nich pojawia się tylko słowo ‘historia’.
Zapytanie może zawierać znaki specjalne, na przykład:
""jajka sadzone" +(bakłażan | ziemniak) -frittata"Składnia zapytania:
+ oznacza operację AND
| oznacza operację OR
- neguje pojedynczy token
" otacza grupę tokenów, aby oznaczyć frazę do przeszukiwania
* na końcu terminu oznacza zapytanie prefixowe
( i ) wskazują pierwszeństwo
~N po słowie oznacza dystans edycyjny (nietrafność)
~N po frazie oznacza ilość slop# найдем документы без слова 'щенки'
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
"query": {
"simple_query_string": {
"query": "-щенки",
"fields": [
"title^3",
"tags^2",
"content"
]
}
}
}'
# получим 2 поста про котиковLinki
PS
Jeśli interesują Cię podobne artykuły-lekcje, masz pomysły na nowe artykuły lub masz propozycje współpracy, chętnie przyjmę wiadomość na priv lub na maila m.kuzmin+habr@darkleaf.ru.
Źródło: habr.com
