Elasticsearch — pretraživački motor s json rest api-jem, koji koristi Lucene i napisan je na Javi. Opis svih prednosti ovog motora dostupan je na . Dalje u tekstu nazivamo Elasticsearch kao ES.
Slični motori se koriste za složeno pretraživanje u bazi dokumenata. Na primjer, pretraživanje uzimajući u obzir morfologiju jezika ili pretraživanje prema geo koordinatama.
U ovom članku ću govoriti o osnovama ES-a koristeći primjer indeksiranja blog objava. Pokazat ću kako filtrirati, sortirati i pretraživati dokumente.
Da ne zavisim od operativnog sustava, sve zahtjeve prema ES-u izradit ću pomoću CURL-a. Također postoji plugin za google chrome pod nazivom .
U tekstu su postavljene veze na dokumentaciju i druge izvore. Na kraju su postavljene veze za brzi pristup dokumentaciji. Definicije nepoznatih termina možete pročitati u .
Instalacija ES-a
Za to će nam prvo trebati Java. Razvijatelji moraju instalirati verzije Java novije od Java 8 update 20 ili Java 7 update 55.
Distribucija ES-a dostupna je na . Nakon raspakiranja arhive potrebno je pokrenuti bin/elasticsearch. Također su dostupni . Postoji . .
Nakon instalacije i pokretanja provjerit ćemo funkcionalnost:
# для удобства запомним адрес в переменную
#export ES_URL=$(docker-machine ip dev):9200
export ES_URL=localhost:9200
curl -X GET $ES_URLPrimit ćemo otprilike ovakav 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 objavu u ES:
# Добавим документ c id 1 типа post в индекс blog.
# ?pretty указывает, что вывод должен быть человеко-читаемым.
curl -XPUT "$ES_URL/blog/post/1?pretty" -d'
{
"title": "Веселые котята",
"content": "<p>Smiješna priča o mačićima<p>",
"tags": [
"mačići",
"smiješna priča"
],
"published_at": "2014-09-12T20:44:42+00:00"
}'
odgovor servera:
{
"_index" : "blog",
"_type" : "post",
"_id" : "1",
"_version" : 1,
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"created" : false
}
ES je automatski stvorio blog i post. Može se napraviti uvjetna analogija: indeks — to je baza podataka, a tip — tablica u toj Bazi podataka. Svaki tip ima svoju shemu — , kao što i relacijska tablica. Mapping se generira automatski prilikom indeksiranja dokumenta:
# Получим mapping всех типов индекса blog
curl -XGET "$ES_URL/blog/_mapping?pretty"U odgovoru servera dodao sam u komentarima vrijednosti polja proindeksiranog dokumenta:
{
"blog" : {
"mappings" : {
"post" : {
"properties" : {
/* "content": "<p>Smiješna priča o mačićima<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"
}
}
}
}
}
}Valja napomenuti da ES ne pravi razliku između pojedinačne vrijednosti i niza vrijednosti. Na primjer, polje title sadrži samo naslov, dok polje tags — predstavlja niz stringova, iako su prikazani u mappig-u isto.
Kasnije ćemo govoriti o mapiranju više detaljno.
Upiti
Izvlačenje dokumenta prema njegovom id-u:
# извлечем документ с 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>Smije\u0161na pri\u010da o ma\u010di\u0107ima<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"
}
}U odgovoru su se pojavili novi ključevi: _version i _source. Uglavnom, svi ključevi koji počinju s _ se odnose na sistemske.
Ključ _version pokazuje verziju dokumenta. Potreban je za rad mehanizma optimističkih zaključavanja. Na primjer, ako želimo promijeniti dokument koji ima verziju 1, šaljemo izmijenjeni dokument i navodimo da je to ispravka dokumenta s verzijom 1. Ako je netko drugi također uređivao dokument s verzijom 1 i poslao promjene prije nas, ES neće prihvatiti naše promjene jer pohranjuje dokument s verzijom 2.
Ključ _source sadrži dokument koji smo indeksirali. ES ne koristi ovu vrijednost za pretraživačke operacije, jer za pretraživanje koristi indekse. Radi uštede prostora, ES pohranjuje komprimirani izvorni dokument. Ako nam je potrebna samo id oznaka, a ne cijeli izvorni dokument, pohranu izvora možemo onemogućiti.
Ako nam nije potrebna 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>Smije\u0161na pri\u010da o ma\u010di\u0107ima<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đer 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" : "Veseli mačići"
}
}Hajde da indeksiramo još nekoliko postova i izvršimo složenije upite.
curl -XPUT "$ES_URL/blog/post/2" -d'
{
"title": "Veseli psići",
"content": "<p>Smiješna priča o psićima<p>",
"tags": [
"psići",
"smiješna priča"
],
"published_at": "2014-08-12T20:44:42+00:00"
}'curl -XPUT "$ES_URL/blog/post/3" -d'
{
"title": "Kako sam dobio mačića",
"content": "<p>Potresna priča o jadnom mačiću s ulice<p>",
"tags": [
"mačići"
],
"published_at": "2014-07-21T20:44:42+00:00"
}'Sortiranje
# найдем последний пост по дате публикации и извлечем поля 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" : "Veseli mačići",
"published_at" : "2014-09-12T20:44:42+00:00"
},
"sort" : [ 1410554682000 ]
} ]
}
}Odabrali smo posljednji post. size ograničava broj dokumenata u rezultatu. ukupno pokazuje ukupan broj dokumenata koji odgovaraju upitu. sort u rezultatu sadrži niz cijelih brojeva prema kojima se vrši sortiranje. To jest, datum se pretvorio u cjelobrojnu vrijednost. Više o sortiranju možete pročitati u .
Filtri i upiti
ES od verzije 2 ne razlikuje filtre i upite, umjesto toga .
Kontekst upita se razlikuje od konteksta filtra jer upit generira _score i ne kešira se. Što je _score pokazat ću kasnije.
Filtriranje po datumu
Koristimo upit u kontekstu filtra:
# получим посты, опубликованные 1ого сентября или позже
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
"filter": {
"range": {
"published_at": { "gte": "2014-09-01" }
}
}
}'Filtriranje po oznakama
Koristimo za pretraživanje id dokumenata koji sadrže zadanu riječ:
# найдем все документы, в поле 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čići",
"tags" : [ "mačići", "smiješna priča" ]
}
}, {
"_index" : "blog",
"_type" : "post",
"_id" : "3",
"_score" : 1.0,
"_source" : {
"title" : "Kako sam dobio mačića",
"tags" : [ "mačići" ]
}
} ]
}
}Potpuna pretraga
Tri naša dokumenta sadrže u polju content sljedeće:
<p>Smiješna priča o mačićima<p><p>Smiješna priča o psićima<p><p>Potresna priča o jadnom mačiću s ulice<p>
Koristimo za pretraživanje id dokumenata koji sadrže zadanu riječ:
# 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 «priče» u polju content, nećemo pronaći ništa, jer u indeksu se nalaze samo izvorne riječi, a ne njihovi korijeni. Da bismo omogućili kvalitetnu pretragu, potrebno je konfigurirati analize.
Polje _score prikazuje . Ako se upit izvršava u filter kontekstu, vrijednost _score će uvijek biti 1, što znači potpuno usklađivanje s filtrima.
Analizatori
su potrebni za pretvaranje izvorne tekstualne informacije u skup tokena.
Analizatori se sastoje od jednog i nekoliko opcionalnih . Tokenizer može prethoditi nekoliko . Tokenizer razdvaja izvorni niz u tokene, na primjer, prema razmacima i znakovima interpunkcije. TokenFilter može mijenjati tokene, uklanjati ili dodavati nove, na primjer, ostavljati samo korijen riječi, uklanjati prijedloge, dodavati sinonime. CharFilter mijenja cijeli izvorni niz, na primjer, izrezuje HTML oznake.
U ES postoji nekoliko . Na primjer, analizator .
Iskoristimo i pogledajmo kako analizatori standard i russian pretvaraju niz «Vesele priče o mačićima»:
# используем анализатор 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" : "priče",
"start_offset" : 8,
"end_offset" : 15,
"type" : "",
"position" : 1
}, {
"token" : "o",
"start_offset" : 16,
"end_offset" : 19,
"type" : "",
"position" : 2
}, {
"token" : "mačićima",
"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" : "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 razdijelio je niz po razmacima i sve prenio u malo slovo, analizator russian - uklonio je beznačajne riječi, prenio u malo slovo i ostavio osnovu riječi.
Pogledajmo koji 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",
"filter": [
"lowercase",
"russian_stop",
"russian_keywords",
"russian_stemmer"
]
}
}
}Opisat ćemo svoj analizator temeljen na russian, koji će ukloniti html oznake. Nazvat ćemo ga default, jer će se analizator s tim imenom koristiti prema zadanim postavkama.
{
"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"
]
}
}
}Prvo će se iz izvornog niza ukloniti sve html oznake, zatim će ga razdijeliti na tokene tokenizer standard, dobiveni tokeni će preći u malo slovo, uklonit će se beznačajne riječi i od preostalih tokena ostati će osnova riječi.
Stvaranje indeksa
Gore smo opisali default analizator. On će se primjenjivati na svim poljima s nizovima. Naš post sadrži niz oznaka, stoga će i oznake biti obrađene analizatorom. Budući da tražimo postove prema točnom podudaranju oznake, potrebno je onemogućiti analizu za polje tags.
Stvaramo indeks blog2 s analizatorom i mapiranjem, u kojem je isključena analiza polja tags:
curl -XPOST "$ES_URL/blog2" -d'
{
"settings": {
"analysis": {
"filter": {
"hr_stop": {
"type": "stop",
"stopwords": "_hrvatski_"
},
"hr_stemmer": {
"type": "stemmer",
"language": "hrvatski"
}
},
"analyzer": {
"default": {
"char_filter": [
"html_strip"
],
"tokenizer": "standard",
"filter": [
"lowercase",
"hr_stop",
"hr_stemmer"
]
}
}
}
},
"mappings": {
"post": {
"properties": {
"content": {
"type": "string"
},
"published_at": {
"type": "date"
},
"tags": {
"type": "string",
"index": "not_analyzed"
},
"title": {
"type": "string"
}
}
}
}
}'Dodajmo iste 3 posta u ovu indeksu (blog2). Preskočit ću ovaj proces jer je sličan dodavanju dokumenata u indeks blog.
Puni tekstualni pretraživač sa podrškom za izraze
Upoznajmo se s još jednim tipom 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 analizator s hrvatskim stemenjem, ovaj upit će vratiti sve dokumente, iako se u njima pojavljuje samo riječ ‘povijest’.
Upit može sadržavati posebne znakove, na primjer:
""pržena jaja" +(patlidžan | krumpir) -frittata"Sintaksa upita:
+ označava AND operaciju
| označava OR operaciju
- negira jedan token
" okružuje niz tokena kako bi označio frazu za pretraživanje
* na kraju termina označava pretraživanje prefiksa
( i ) označavaju prioritet
~N nakon riječi označava udaljenost od pregleda (neizvjesnost)
~N nakon fraze označava količinu slopa# найдем документы без слова 'щенки'
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
"query": {
"simple_query_string": {
"query": "-щенки",
"fields": [
"title^3",
"tags^2",
"content"
]
}
}
}'
# получим 2 поста про котиковLinkovi
PS
Ako vas zanimaju slični članci-lekcije, imate ideje za nove članke ili prijedloge za suradnju, bit ću sretan ako me kontaktirate u privatnoj poruci ili na email m.kuzmin+habr@darkleaf.ru.
Izvor: habr.com
