Elasticsearch — поисковый движок с json rest api, использующий Lucene и написанный на Java. Описание всех преимуществ этого движка доступно на . Далее по тексту будем называть Elasticsearch как ES.
Подобные движки используются при сложном поиске по базе документов. Например, поиск с учетом морфологии языка или поиск по geo координатам.
В этой статье я расскажу про основы ES на примере индексации постов блога. Покажу как фильтровать, сортировать и искать документы.
Чтобы не зависеть от операционной системы, все запросы к ES я буду делать с помощью CURL. Также есть плагин для google chrome под названием .
По тексту расставлены ссылки на документацию и другие источники. В конце размещены ссылки для быстрого доступа к документации. Определения незнакомых терминов можно прочитать в .
Установка ES
Для этого нам сначала потребуется Java. Разработчики установить версии Java новее, чем Java 8 update 20 или Java 7 update 55.
Дистрибутив ES доступен на . После распаковки архива нужно запустить bin/elasticsearch. Также доступны . Ապրանքներ . .
После установки и запуска проверим работоспособность:
# для удобства запомним адрес в переменную
#export ES_URL=$(docker-machine ip dev):9200
export ES_URL=localhost:9200
curl -X GET $ES_URLНам придет приблизительно такой ответ:
{
"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"
}Индексация
Добавим пост в 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"
}'
ответ сервера:
{
"_index" : "blog",
"_type" : "post",
"_id" : "1",
"_version" : 1,
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"created" : false
}
ES автоматически создал blog и post. Можно провести условную аналогию: индекс — это база данных, а тип — таблица в этой БД. Каждый тип имеет свою схему — , также как и реляционная таблица. Mapping генерируется автоматически при индексации документа:
# Получим mapping всех типов индекса blog
curl -XGET "$ES_URL/blog/_mapping?pretty"В ответе сервера я добавил в комментариях значения полей проиндексированного документа:
{
"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 не делает различий между одиночным значением и массивом значений. Например, поле title содержит просто заголовок, а поле tags — массив строк, хотя они представлены в mapping одинаково.
Позднее мы поговорим о маппинге более подобно.
Запросы
Извлечение документа по его 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>\u053f\u0561\u057f\u0561\u056f\u0565\u0580\u0563\u0561\u056f\u0561\u0576 \u057a\u0561\u057f\u0574\u0578\u0582\u0569\u0575\u0578\u0582\u0576 \u056f\u056f\u056b\u056f\u0576\u0565\u0580\u056b \u0574\u0561\u057d\u056b\u0576<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"
}
}В ответе появились новые ключи: _version և _source. Вообще, все ключи, начинающиеся с _ относятся к служебным.
Ключ _version показывает версию документа. Он нужен для работы механизма оптимистических блокировок. Например, мы хотим изменить документ, имеющий версию 1. Мы отправляем измененный документ и указываем, что это правка документа с версией 1. Если кто-то другой тоже редактировал документ с версией 1 и отправил изменения раньше нас, то ES не примет наши изменения, т.к. он хранит документ с версией 2.
Ключ _source նա պարունակում է այն փաստաթուղթը, որը մենք ինդեքսավորված ենք: ES այս արժեքը չի օգտագործում որոնման գործողությունների համար, քանի որ որոնման համար օգտագործվում են ինդեքսներ: Լիարժեքության պահպանման համար ES-ն պահում է սեղմված առաջին փաստաթուղթը: Եթե մեզ միայն id է պետք, այլ ոչ թե ամբողջ սկզբնական փաստաթուղթ, ապա կարող ենք անջատել սկիզբի պահպանումը:
Եթե մեզ ավելորդ տեղեկություն չի պետք, կարող ենք ստանալ միայն _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>\u053f\u0561\u057f\u0561\u056f\u0565\u0580\u0563\u0561\u056f\u0561\u0576 \u057a\u0561\u057f\u0574\u0578\u0582\u0569\u0575\u0578\u0582\u0576 \u056f\u056f\u056b\u056f\u0576\u0565\u0580\u056b \u0574\u0561\u057d\u056b\u0576<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"
}
Հնարավոր է նաև ընտրել միայն հատուկ դաշտեր:
# извлечем только поле title
curl -XGET "$ES_URL/blog/post/1?_source=title&pretty"{
"_index" : "blog",
"_type" : "post",
"_id" : "1",
"_version" : 1,
"found" : true,
"_source" : {
"title" : "Քաղցր կատուներ"
}
}Եկեք ինդեքսավորենք ավելի շատ գրառումներ և կատարենք բարդ որոնումներ:
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 ]
} ]
}
}Մենք ընտրել ենք վերջին գրառումը: size սահմանափակում է փաստաթղթի քանակը արդյունքներում: բարելավված ցուցադրում է ընդհանուր փաստաթղթերի թիվը, որոնք համապատասխանում են որոնմանը: դասավորություն արդյունքում պարունակում է ամբողջ թվերի զանգված, ըստ որի կատարվում է դասավորություն: Այսինքն՝ ամսաթիվը վերածվել է ամբողջ թվի: Դասավորության մասին ավելի մանրամասն կարող եք կարդալ .
Ֆիլտրեր և հարցումներ
ES 2-րդ տարբերակից չի տարբերակում ֆիլտրերն ու հարցումները, փոխարենը .
Հարցման կոնտեքստը տարբեր է ֆիլտրի կոնտեքստից, քանի որ հարցումը արտադրում է _score և չի պահվում: Ի՞նչ է _score-ն, դա ցույց կտամ ավելի ուշ:
Ամսաթվի միջոցով ֆիլտրում
Օգտագործում ենք հարցում ֆիլտրի կոնտեքստում։
# получим посты, опубликованные 1ого сентября или позже
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
"filter": {
"range": {
"published_at": { "gte": "2014-09-01" }
}
}
}'Տեգերի միջոցով ֆիլտրում
Մեր կողմից օգտագործվում է փաստաթղթերի id որոնման համար, որոնք պարունակում են նշված բառը:
# найдем все документы, в поле 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" : [ "կատուներ" ]
}
} ]
}
}Նախագծային որոնում
Մեր երեք փաստաթրեքները պարունակում են content դաշտում հետևյալը:
<p>Կատակերգական պատմություն կկիկների մասին<p><p>Մռայլ պատմություն շան երեխաների մասին<p><p>Սրտաճեղող պատմություն բախտի կորած կատվիկի մասին, որը փողոցից է<p>
Մեր կողմից օգտագործվում է փաստաթղթերի id որոնման համար, որոնք պարունակում են նշված բառը:
# 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
} ]
}
}Այլապես, եթե որոնենք «էլեկտրոնային» content դաշտում, մենք ոչինչ չենք գտնելու, քանի որ ինդեքսում գոյություն ունեն միայն բնագիր բառերը, այլ ոչ թե դրանց հիմքերը: որ ընտրելու համար որակապես որոնում, անհրաժեշտ են կարգավորել վերլուծողը:
Դաշտը _score ցուցադրում է . Եթե հարցումը իրականացվում է ֆիլտրի կոնտեքստում, ապա _score արժեքը միշտ կլինի 1, ինչը նշանակում է լիարժեք համապատասխանություն ֆիլտրին:
Վերլուծողներն
պահանջվում են սկզբնական տեքստը տոկենների խմբի վերածելու համար։
Վերլուծիչները բաղկացած են մեկ և մի քանի օպցիոնալ ։ Tokenizer-ը կարող է նախորդորդել մի քանի ։ Tokenizer-ը բաժանում է սկզբնական տողը տոկենների, օրինակ՝ բացատների և նշանների վրա։ TokenFilter-ը կարող է փոխել տոկենները, ջնջել կամ ավելացնել նորերը, օրինակ՝ թողնելով միայն բառի հիմքը, հեռացնելով նախադասությունները, ավելացնելով շատաբանությունները։ CharFilter-ը լիովին փոխում է սկզբնական տողը, օրինակ՝ դուրս է հանում html պիտակները։
ES-ում կան մի քանի ։ Օրինակ, վերլուծիչ .
Նվազագույնը կօգտագործենք և կտաֆիլնենք, թե ինչպես են standard և russian վերլուծիչները զտում «Веселые истории про котят» տողը։
# используем анализатор 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" : "",
"position" : 0
}, {
"token" : "истории",
"start_offset" : 8,
"end_offset" : 15,
"type" : "",
"position" : 1
}, {
"token" : "про",
"start_offset" : 16,
"end_offset" : 19,
"type" : "",
"position" : 2
}, {
"token" : "котят",
"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" : "весел",
"start_offset" : 0,
"end_offset" : 7,
"type" : "",
"position" : 0
}, {
"token" : "истор",
"start_offset" : 8,
"end_offset" : 15,
"type" : "",
"position" : 1
}, {
"token" : "кот",
"start_offset" : 20,
"end_offset" : 25,
"type" : "",
"position" : 3
} ]
}Ստանդարտ վերլուծիչը բաժանում է տողը բացատների վրա և տեղափոխում է ամեն ինչ փոքրատառ, russian վերլուծիչը՝ հեռացնում է աննշան բառերը, է փաստում փոքրատառ և թողնում է բառերի հիմքերը։
Կտեսնենք, թե որ Tokenizer, TokenFilters, CharFilters-ն է օգտագործում 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 բացակայում են */
}
}
}Կզարմանաք մեր վերլուծիչը, որը հիմնված է russian-ում, որը կտա html պիտակները։ Մենք կնվիրենք նրան default, քանի որ վերլուծիչի այս անունով կլինի օգտագործել Ջյայարդաս։
{
"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"
]
}
}
}Նախ դուրս կգան բոլոր html պիտակները, ապա այն կտնտվի token.circular_disable, ստացված տոկենները կվարկեն փոքրալում, զգացվում է ան նշանակալի բառեր և մնացած տոկեններից կմնա բառի հիմքը։
Ինդեքսի ստեղծում
Մեր վերևում նկարագրված է default անալիզատորը: Այն կիրառվելու է բոլոր տողային ոլորտներում: Մեր պոստը պարունակում է թեգերի զանգված, համապատասխանաբար, թեգերը նույնպես կարտահայտվեն անալիզատորով: Քանի որ մենք կատարում ենք պոստերի որոնում ըստ ճշգրիտ համապատասխանության թեգին, պետք է անջատենք անալիզը tags ոլորտի համար:
Ստեղծենք blog2 ինդեքս` անալիզատորով և թերթիկով, որտեղ անջատված է tags ոլորտի անալիզը:
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"
}
}
}
}
}'Նման 3 պոստեր կավելացնենք այս ինդեքսին (blog2): Ես կթողնեմ այս գործընթացը, քանի որ այն նման է փաստաթղթերի ավելացմանն blog ինդեքսում:
Լիալուսանկարային որոնում արտահայտությունների աջակցության հետ
Եկեք ծանոթանանք մեկ այլ տիպի запросներին:
# найдем документы, в которых встречается слово 'истории'
# 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"
]
}
}
}'Քանի որ մենք օգտագործում ենք անալիզատոր ռուսական ստեմինգով, այս запросը կտա բոլոր փաստաթղթերը, թեև դրանց մեջ միայն ‘история’ բառը կա:
Запросը կարող է պարունակել հատուկ խորհրդանշաններ, օրինակ:
""fried eggs" +(eggplant | potato) -frittata"Запросի սինտաքսը:
+ նշանավորում է AND գործողությունը
| նշանավորում է OR գործողությունը
- հակադարձում է մեկ նշան
" փաթաթում է մի քանի նշան՝ արտահայտություն՝ որոնման համար
* կրում է տերմինի վերջում՝ նախանշանի հարց
( և ) նշում են առաջնահերթությունը
~N բառի հետևում նշում է խմբավորմանը (մեղմություն)
~N արտահայտության հետևում նշում է հայտնի գումարը# найдем документы без слова 'щенки'
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
"query": {
"simple_query_string": {
"query": "-щенки",
"fields": [
"title^3",
"tags^2",
"content"
]
}
}
}'
# получим 2 поста про котиковՀղումներ
ՊՍ
Եթե նման հոդված-խոսքեր ձեզ հետաքրքիր են, ունեք նոր հոդվածների գաղափարներ կամ առաջարկություններ համագործակցության մասին, ուրախ կլինեմ հաղորդագրություն ստանալ անձամբ կամ էլ m.kuzmin+habr@darkleaf.ru:
Ընտանիք: habr.com
