Elasticsearch — iskalni mehanizem z json rest api, ki uporablja Lucene in je napisan v Javi. Opišemo vse prednosti tega mehanizma na . Kasneje v besedilu bomo Elasticsearch imenovali ES.
Taki mehanizmi se uporabljajo pri zahtevnem iskanju po bazi dokumentov. Na primer, iskanje ob upoštevanju morfologije jezika ali iskanje po geo koordinatah.
V tem članku bom predstavil osnove ES na primeru indeksiranja objav bloga. Prikazal bom, kako filtrirati, razvrstiti in iskati dokumente.
Da ne bi bili odvisni od operacijskega sistema, bom vse poizvedbe k ES izvajal s pomočjo CURL. Obstaja tudi razširitev za Google Chrome z imenom .
V besedilu so postavljene povezave na dokumentacijo in druge vire. Na koncu so vključene povezave za hiter dostop do dokumentacije. Opredelitve neznanih izrazov lahko preberete v .
Namestitev ES
Za to potrebujemo najprej Javo. Razvijalci namestitev različic Jave, novejših od Java 8 update 20 ali Java 7 update 55.
Distribucija ES je na voljo na . Po razpakiranju arhiva je potrebno zagnati bin/elasticsearch. Prav tako so na voljo . Na voljo je . .
Po namestitvi in zagonu preverimo delovanje:
# для удобства запомним адрес в переменную
#export ES_URL=$(docker-machine ip dev):9200
export ES_URL=localhost:9200
curl -X GET $ES_URLPrejeli bomo približno takšen odgovor:
{
"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"
}Indeksiranje
Dodajmo objavo v ES:
# Добавим документ c id 1 типа post в индекс blog.
# ?pretty указывает, что вывод должен быть человеко-читаемым.
curl -XPUT "$ES_URL/blog/post/1?pretty" -d'
{
"title": "Веселые котята",
"content": "<p>Smešna zgodba o mačkih<p>",
"tags": [
"mački",
"smešna zgodba"
],
"published_at": "2014-09-12T20:44:42+00:00"
}'
odgovor strežnika:
{
"_index" : "blog",
"_type" : "post",
"_id" : "1",
"_version" : 1,
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"created" : false
}
ES je samodejno ustvaril blog in post. Lahko naredimo analogijo: indeks je baza podatkov, tip pa je tabela v tej bazi. Vsak tip ima svojo shemo — , prav tako kot relacijska tabela. Mapping se samodejno generira pri indeksiranju dokumenta:
# Получим mapping всех типов индекса blog
curl -XGET "$ES_URL/blog/_mapping?pretty"V odgovoru strežnika sem dodal v komentarjih vrednosti polj indeksiranega dokumenta:
{
"blog" : {
"mappings" : {
"post" : {
"properties" : {
/* "content": "<p>Smešna zgodba o mačkih<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"
}
}
}
}
}
}Pomembno je poudariti, da ES ne razlikuje med enojno vrednostjo in poljem vrednosti. Na primer, polje title vsebuje zgolj naslov, polje tags pa je seznam nizov, čeprav sta v mappingu predstavljena enako.
Kasneje se bomo bolj podrobno pogovorili o mappingu.
Poizvedbe
Pridobitev dokumenta po njegovem 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>Sme\u0161na zgodba o ma\u010dkih<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 odgovoru so se pojavili novi ključi: _version in _source. Na splošno vsi ključi, ki se začnejo z _ pripada službenim.
Opcija _version prikazuje verziju dokumenta. Potreban je za rad mehanizma optimističkih zaključavanja. Na primer, ako želimo da promenimo dokument koji ima verziju 1, šaljemo izmenjeni dokument i navodimo da je to izmena dokumenta sa verzijom 1. Ako je neko drugi takođe uređivao dokument sa verzijom 1 i poslao izmene pre nas, ES neće prihvatiti naše izmene, jer čuva dokument sa verzijom 2.
Opcija _source sadrži onaj dokument koji smo indeksirali. ES ne koristi ovu vrednost za pretraživačke operacije, jer se za pretraživanje koriste indeksi. U cilju štednje prostora, ES čuva komprimovani izvorni dokument. Ako nam je potreban samo id, a ne ceo izvorni dokument, možemo isključiti čuvanje izvora.
Ako nam ne treba dodatna informacija, možemo dobiti samo sadržaj _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>Sme\u0161na zgodba o ma\u010dkih<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"
}
Takođe možemo odabrati samo određena polja:
# извлечем только поле title
curl -XGET "$ES_URL/blog/post/1?_source=title&pretty"{
"_index" : "blog",
"_type" : "post",
"_id" : "1",
"_version" : 1,
"found" : true,
"_source" : {
"title" : "Vesele mačke"
}
}Hajde da indeksiramo još nekoliko postova i izvršimo složenije upite.
curl -XPUT "$ES_URL/blog/post/2" -d'
{
"title": "Veseli psi",
"content": "<p>Smešna zgodba o psih<p>"
"tags": [
"psi",
"smešna zgodba"
],
"published_at": "2014-08-12T20:44:42+00:00"
}'curl -XPUT "$ES_URL/blog/post/3" -d'
{
"title": "Kako je prišel moj maček",
"content": "<p>Srce parajoča zgodba o ubogem mučku z ulice<p>",
"tags": [
"mački"
],
"published_at": "2014-07-21T20:44:42+00:00"
}'Razvrščanje
# найдем последний пост по дате публикации и извлечем поля 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" : "Vesele mačke",
"published_at" : "2014-09-12T20:44:42+00:00"
},
"sort" : [ 1410554682000 ]
} ]
}
}Odabrali smo poslednji post. size ograničava broj dokumenata u rezultatu. total prikazuje ukupni broj dokumenata koji odgovaraju upitu. sort u rezultatu sadrži niz celih brojeva, po kojima se vrši sortiranje. To jest, datum je preveden u celo broj. Više o sortiranju možete pročitati u .
Filtri i upiti
ES od verzije 2 ne razlikuje filtre i upite, umesto toga .
Kontekst upita se razlikuje od konteksta filtra po tome što upit generiše _score i ne kešira se. Šta je _score ću prikazati kasnije.
Filtriranje po datumu
Koristimo upit u kontekstu filter:
# получим посты, опубликованные 1ого сентября или позже
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
"filter": {
"range": {
"published_at": { "gte": "2014-09-01" }
}
}
}'Filtriranje po tagovima
Uporabljamo za pretragu id dokumenata koji sadrže zadatu reč:
# найдем все документы, в поле 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" : "Veseli mačkoti",
"tags" : [ "mačkoti", "smešna istorija" ]
}
}, {
"_index" : "blog",
"_type" : "post",
"_id" : "3",
"_score" : 1.0,
"_source" : {
"title" : "Kako sam dobio mačića",
"tags" : [ "mačkoti" ]
}
} ]
}
}Potpuno pretraživanje
Tri naša dokumenta sadrže u polju content sledeće:
<p>Smešna zgodba o mačkih<p><p>Smešna zgodba o psih<p><p>Srce parajoča zgodba o ubogem mučku z ulice<p>
Uporabljamo za pretragu id dokumenata koji sadrže zadatu reč:
# 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
} ]
}
}Međutim, ako tražimo „historije“ u polju content, nećemo ništa pronaći, jer indeks sadrži samo originalne reči, a ne njihova osnova. Da bismo omogućili kvalitetno pretraživanje, potrebno je postaviti analizator.
Field _score prikazuje . Ako se zahtjev izvršava u filter kontekstu, vrednost _score će uvek biti 1, što označava potpuno usklađivanje sa filtrom.
Analizatori
su potrebni za pretvaranje izvornog teksta u skup tokena.
Analizatori se sastoje od jednog i nekoliko opcionalnih . Tokenizer može prethoditi nekoliko . Tokenizer razdvaja izvorni niz na tokene, na primer, po razmacima i interpunkcijskim simbolima. TokenFilter može menjati tokene, brisati ili dodavati nove, na primer, ostavljajući samo osnovu reči, uklanjajući predloge, dodajući sinonime. CharFilter menja ceo izvorni niz, na primer, uklanja HTML oznake.
U ES postoji nekoliko . Na primer, analizator .
Iskoristimo i pogledajmo kako analizatori standard i russian pretvaraju niz „Veseli istorije o mačkama“:
# используем анализатор 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" : "veseli",
"start_offset" : 0,
"end_offset" : 7,
"type" : "",
"position" : 0
}, {
"token" : "istorije",
"start_offset" : 8,
"end_offset" : 15,
"type" : "",
"position" : 1
}, {
"token" : "o",
"start_offset" : 16,
"end_offset" : 17,
"type" : "",
"position" : 2
}, {
"token" : "mačkama",
"start_offset" : 18,
"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" : "vesel",
"start_offset" : 0,
"end_offset" : 7,
"type" : "",
"position" : 0
}, {
"token" : "istor",
"start_offset" : 8,
"end_offset" : 15,
"type" : "",
"position" : 1
}, {
"token" : "kot",
"start_offset" : 20,
"end_offset" : 25,
"type" : "",
"position" : 3
} ]
}Standardni analizator je podelio string na reči i sve preveo u malo slovo, analizator russian je odstranio nebitne reči, preveo u malo slovo i sačuvao osnovu reči.
Pogledajmo koje Tokenizer, TokenFilters, CharFilters koristi analizator 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 nedostaju */
}
}
}Opisujemo svoj analizator na osnovu russian, koji će ukloniti html tagove. Nazvaćemo ga default, jer će se analizator sa tim imenom koristiti po defaultu.
{
"filter": {
"ru_stop": {
"type": "stop",
"stopwords": "_russian_"
},
"ru_stemmer": {
"type": "stemmer",
"language": "russian"
}
},
"analyzer": {
"default": {
/* dodajemo uklanjanje html tagova */
"char_filter": ["html_strip"],
"tokenizer": "standard",
"filter": [
"lowercase",
"ru_stop",
"ru_stemmer"
]
}
}
}Prvo će iz izvorne stringa biti uklonjeni svi html tagovi, zatim će ga razdvojiti na tokene tokenizer standard, dobijeni tokeni će preći u mala slova, biće uklonjene nebitne reči i od preostalih tokena će ostati osnova reči.
Kreiranje indeksa
Gore smo opisali default analizator. On će se primeniti na sva string polja. Naš post sadrži niz tagova, tako da će i tagovi biti obrađeni analizatorom. Pošto tražimo postove po tačnom podudaranju taga, neophodno je isključiti analizu za polje tags.
Kreirajmo indeks blog2 sa analizatorom i mapiranjem, u kojem je isključena analiza polja 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"
}
}
}
}
}'Dodajmo iste 3 objave u ovaj indeks (blog2). Preskočiću ovaj proces, jer je sličan dodavanju dokumenata u indeks blog.
Potpuno pretraživanje sa podrškom za izraze
Upoznajmo još jedan tip upita:
# найдем документы, в которых встречается слово 'истории'
# 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"
]
}
}
}'Budući da koristimo analizer s ruskim stemingom, ovaj upit će vratiti sve dokumente, iako se u njima pojavljuje samo reč 'istorija'.
Upit može sadržavati specijalne simbole, na primer:
""pržena jaja" +(patlidžan | krompir) -frittata"Sintaksa upita:
+ označava AND operaciju
| označava OR operaciju
- negira jedan token
" obavija više tokena da označi frazu za pretragu
* na kraju termina označava pretragu prefiksa
( i ) označavaju prioritet
~N nakon reči označava udaljenost uredbe (nepreciznost)
~N nakon fraze označava količinu nerednosti# найдем документы без слова 'щенки'
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
"query": {
"simple_query_string": {
"query": "-щенки",
"fields": [
"title^3",
"tags^2",
"content"
]
}
}
}'
# получим 2 поста про котиковPovezave
PS
Ako vas zanimaju slični članci, imate ideje za nove članke ili predloge za saradnju, biću srećan da primim poruku u ličnoj ili na e-mail m.kuzmin+habr@darkleaf.ru.
Izvor: habr.com
