Elasticsearch — vyhledávací engine s json rest api, využívající Lucene a napsaný v Javě. Popis všech výhod tohoto enginu je dostupný na . Dále v textu budeme nazývat Elasticsearch jako ES.
Podobné enginy se používají při složitém vyhledávání v databázi dokumentů. Například při vyhledávání s ohledem na morfologii jazyka nebo vyhledávání podle geo souřadnic.
V tomto článku se podíváme na základy ES na příkladu indexace příspěvků blogu. Ukážu, jak filtrovat, třídit a vyhledávat dokumenty.
Abych byl nezávislý na operačním systému, všechny dotazy na ES budu provádět pomocí CURL. Existuje také plugin pro Google Chrome s názvem .
V textu jsou odkazy na dokumentaci a další zdroje. Na konci jsou umístěny odkazy pro rychlý přístup k dokumentaci. Definice neznámých termínů si můžete přečíst v .
Instalace ES
K tomu nejprve potřebujeme Javu. Vývojáři nainstalovat verze Javy novější než Java 8 update 20 nebo Java 7 update 55.
Distribuce ES je k dispozici na . Po rozbalení archivu je třeba spustit bin/elasticsearch. Jsou také dostupné . Existuje . .
Po instalaci a spuštění zkontrolujeme funkčnost:
# для удобства запомним адрес в переменную
#export ES_URL=$(docker-machine ip dev):9200
export ES_URL=localhost:9200
curl -X GET $ES_URLObdržíme přibližně takovou odpověď:
{
"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"
}Indexace
Přidáme příspěvek do ES:
# Добавим документ c id 1 типа post в индекс blog.
# ?pretty указывает, что вывод должен быть человеко-читаемым.
curl -XPUT "$ES_URL/blog/post/1?pretty" -d'
{
"title": "Веселые котята",
"content": "<p>Vtipný příběh o koťatech<p>",
"tags": [
"koťata",
"vtipný příběh"
],
"published_at": "2014-09-12T20:44:42+00:00"
}'
odpověď serveru:
{
"_index" : "blog",
"_type" : "post",
"_id" : "1",
"_version" : 1,
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"created" : false
}
ES automaticky vytvořil blog a post. Můžeme provést přirozenou analógií: index je databáze a typ je tabulka v této DB. Každý typ má svou vlastní schéma — , stejně jako relační tabulka. Mapping se generuje automaticky při indexaci dokumentu:
# Получим mapping всех типов индекса blog
curl -XGET "$ES_URL/blog/_mapping?pretty"V odpovědi serveru jsem přidal v komentářích hodnoty polí indexovaného dokumentu:
{
"blog" : {
"mappings" : {
"post" : {
"properties" : {
/* "content": "<p>Vtipný příběh o koťatech<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": ["kotě", "vtipný příběh"] *\/
"tags" : {
"type" : "string"
},
/* "title": "Veselé kočky" *\/
"title" : {
"type" : "string"
}
}
}
}
}
}Je třeba poznamenat, že ES nerozlišuje mezi jednotlivou hodnotou a polem hodnot. Například pole title obsahuje prostý nadpis, zatímco pole tags je pole řetězců, přestože jsou v mapování prezentovány stejně.
Později se budeme o mapování bavit detailněji.
Požadavky
Vyhledání dokumentu podle jeho 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>Vtipn\u00fd p\u0159\u00edb\u011bh o ko\u0165atech<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"
}
}V odpovědi se objevily nové klíče: _version a _source. Všechny klíče, které začínají na _ patří k systémovým.
Klíč _version ukazuje verzi dokumentu. Je důležitý pro fungování mechanismu optimistických zámků. Například chceme změnit dokument s verzí 1. Odesíláme změněný dokument a uvádíme, že se jedná o úpravu dokumentu s verzí 1. Pokud někdo jiný také upravoval dokument s verzí 1 a odeslal změny před námi, ES naše změny nepřijme, protože uchovává dokument s verzí 2.
Klíč _source obsahuje dokument, který jsme indexovali. ES tuto hodnotu nepoužívá pro vyhledávací operace, protože pro vyhledávání se používají indexy. Pro šetření místa ES uchovává komprimovaný původní dokument. Pokud potřebujeme pouze id, a ne celý původní dokument, můžeme vypnout uchovávání zdroje.
Pokud nepotřebujeme další informace, můžeme získat pouze obsah _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>Vtipn\u00fd p\u0159\u00edb\u011bh o ko\u0165atech<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"
}
Můžeme také vybrat pouze určité pole:
# извлечем только поле title
curl -XGET "$ES_URL/blog/post/1?_source=title&pretty"{
"_index" : "blog",
"_type" : "post",
"_id" : "1",
"_version" : 1,
"found" : true,
"_source" : {
"title" : "Veselé koťátka"
}
}Pojďme indexovat ještě několik příspěvků a provést složitější dotazy.
curl -XPUT "$ES_URL/blog/post/2" -d'
{
"title": "Veselé štěňata",
"content": "<p>Vtipný příběh o štěňatech<p>",
"tags": [
"štěňata",
"vtipný příběh"
],
"published_at": "2014-08-12T20:44:42+00:00"
}'curl -XPUT "$ES_URL/blog/post/3" -d'
{
"title": "Jak jsem dostal kočku",
"content": "<p>Dojemný příběh o chudém koťátku z ulice<p>"
"tags": [
"koťata"
],
"published_at": "2014-07-21T20:44:42+00:00"
}'Třídění
# найдем последний пост по дате публикации и извлечем поля 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" : "Veselé koťátka",
"published_at" : "2014-09-12T20:44:42+00:00"
},
"sort" : [ 1410554682000 ]
} ]
}
}Vybrali jsme poslední příspěvek. size omezuje počet dokumentů ve výstupu. total ukazuje celkový počet dokumentů, které vyhovují dotazu. sort ve výstupu obsahuje pole celých čísel, podle kterých se provádí třídění. Tedy datum se převedlo na celé číslo. Více informací o třídění najdete v .
Filtry a dotazy
ES od verze 2 nerozlišuje mezi filtry a dotazy, místo toho .
Kontext dotazu se liší od kontextu filtru tím, že dotaz vytváří _score a není ukládán do mezipaměti. Co je _score, ukážu později.
Filtrace podle data
Používáme dotaz v kontextu filter:
# получим посты, опубликованные 1ого сентября или позже
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
"filter": {
"range": {
"published_at": { "gte": "2014-09-01" }
}
}
}'Filtrace podle štítků
Používáme pro vyhledávání id dokumentů, které obsahují dané slovo:
# найдем все документы, в поле 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" : "Veselé kočky",
"tags" : [ "kočky", "vtipný příběh" ]
}
}, {
"_index" : "blog",
"_type" : "post",
"_id" : "3",
"_score" : 1.0,
"_source" : {
"title" : "Jak jsem dostal koťátko",
"tags" : [ "kočky" ]
}
} ]
}
}Plnotextové vyhledávání
Tři naše dokumenty obsahují v poli content následující:
<p>Vtipný příběh o koťatech<p><p>Vtipný příběh o štěňatech<p><p>Dojemný příběh o chudém koťátku z ulice<p>
Používáme pro vyhledávání id dokumentů, které obsahují dané slovo:
# 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
} ]
}
}Pokud však vyhledáváme "příběhy" v poli obsah, nenajdeme nic, protože v indexu se nacházejí pouze původní slova, nikoli jejich základy. Pro kvalitní vyhledávání je třeba nastavit analyzátor.
Pole _score ukazuje . Pokud je dotaz prováděn v kontextu filtru, hodnota _score bude vždy rovna 1, což znamená úplné shodě s filtrem.
Analýzátory
jsou potřebné pro převod původního textu na sadu tokenů.
Analýzátory sestávají z jednoho a několika volitelných . Tokenizer může předcházet několika . Tokenizer dělí původní řetězec na tokeny, například podle mezer a interpunkčních znamének. TokenFilter může měnit tokeny, odstraňovat nebo přidávat nové, například ponechat pouze základ slova, odstranit předložky, přidat synonyma. CharFilter mění celý původní řetězec, například vyřezává HTML tagy.
V ES existuje několik . Například analyzátor .
Využijeme a podíváme se, jak analyzátory standard a russian převádějí řetězec "Veselé příběhy o kočkách":
# используем анализатор 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" : "veselé",
"start_offset" : 0,
"end_offset" : 7,
"type" : "",
"position" : 0
}, {
"token" : "příběhy",
"start_offset" : 8,
"end_offset" : 15,
"type" : "",
"position" : 1
}, {
"token" : "o",
"start_offset" : 16,
"end_offset" : 18,
"type" : "",
"position" : 2
}, {
"token" : "kočkách",
"start_offset" : 19,
"end_offset" : 26,
"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" : "vesel",
"start_offset" : 0,
"end_offset" : 7,
"type" : "",
"position" : 0
}, {
"token" : "histor",
"start_offset" : 8,
"end_offset" : 15,
"type" : "",
"position" : 1
}, {
"token" : "kot",
"start_offset" : 20,
"end_offset" : 25,
"type" : "",
"position" : 3
} ]
}Standardní analyzátor rozdělil řetězec podle mezer a převedl všechno na malé písmeno. Analyzátor russian odstranil bezvýznamná slova, převedl na malá písmena a ponechal základy slov.
Podívejme se, jaké Tokenizer, TokenFilters a CharFilters používá analyzátor russian:
{
"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 nejsou přítomny */
}
}
}Popíšeme svůj analyzátor založený na russian, který odstraní HTML tagy. Nazveme ho default, protože analyzátor s tímto názvem bude používán jako výchozí.
{
"filter": {
"ru_stop": {
"type": "stop",
"stopwords": "_russian_"
},
"ru_stemmer": {
"type": "stemmer",
"language": "russian"
}
},
"analyzer": {
"default": {
/* přidáváme odstranění HTML tagů */
"char_filter": ["html_strip"],
"tokenizer": "standard",
"filter": [
"lowercase",
"ru_stop",
"ru_stemmer"
]
}
}
}Nejprve budou z původního řetězce odstraněny všechny HTML tagy, poté bude rozdělen na tokeny standardním tokenizerem, získané tokeny přejdou do malých písmen, bezvýznamná slova budou odstraněna a zůstane pouze základ slova.
Vytvoření indexu
Výše jsme popsali analyzátor default. Ten bude aplikován na všechna textová pole. Náš příspěvek obsahuje pole tagů, a tedy budou tagy také zpracovány analyzátorem. Protože hledáme příspěvky podle přesného odpovídání tagu, je nutné vypnout analýzu pro pole tags.
Vytvoříme index blog2 s analyzátorem a mapováním, kde je analýza pole tags vypnuta:
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"
}
}
}
}
}'Přidáme stejné 3 příspěvky do tohoto indexu (blog2). Tento proces opomenu, protože je analogický přidávání dokumentů do indexu blog.
Plnotextové vyhledávání s podporou výrazů
Seznámíme se s dalším typem dotazů:
# найдем документы, в которых встречается слово 'истории'
# 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"
]
}
}
}'Vzhledem k tomu, že používáme analyzátor s ruským stemmingem, tento dotaz vrátí všechny dokumenty, i když obsahují pouze slovo ‘historie’.
Dotaz může obsahovat speciální znaky, například:
""fried eggs" +(eggplant | potato) -frittata"Syntaxe dotazu:
+ znamená operaci AND
| znamená operaci OR
- neguje jeden token
" obalí skupinu tokenů, aby znamenala frázi pro hledání
* na konci termínu znamená dotaz s prefixem
( a ) znamenají precedence
~N za slovem znamená vzdálenost editace (fuzziness)
~N za frází znamená množství sloupce# найдем документы без слова 'щенки'
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
"query": {
"simple_query_string": {
"query": "-щенки",
"fields": [
"title^3",
"tags^2",
"content"
]
}
}
}'
# получим 2 поста про котиковOdkazy
PS
Pokud máte zájem o podobné články-návody, máte nápady na nové články nebo máte návrhy na spolupráci, budu rád za zprávu na soukromou zprávu nebo na e-mail m.kuzmin+habr@darkleaf.ru.
Zdroj: habr.com
