Elasticsearch — un motore di ricerca con un'API REST JSON, che utilizza Lucene e scritto in Java. La descrizione di tutti i vantaggi di questo motore è disponibile su . Nel testo ci riferiremo a Elasticsearch come ES.
Motori simili sono utilizzati per ricerche complesse in un database di documenti. Ad esempio, ricerca tenendo conto della morfologia della lingua o ricerca in base alle coordinate geo.
In questo articolo parlerò delle basi di ES usando l'indicizzazione di post di un blog come esempio. Mostrerò come filtrare, ordinare e cercare documenti.
Per non dipendere dal sistema operativo, farò tutte le richieste a ES utilizzando CURL. Esiste anche un plugin per Google Chrome chiamato .
Nel testo ci sono collegamenti alla documentazione e ad altre fonti. Alla fine sono presenti collegamenti per un accesso rapido alla documentazione. Le definizioni dei termini sconosciuti possono essere lette nel .
Installazione di ES
Per questo avremo prima bisogno di Java. Gli sviluppatori devono installare versioni di Java più recenti di Java 8 update 20 o Java 7 update 55.
Il pacchetto di ES è disponibile su . Dopo aver estratto l'archivio, è necessario avviare bin/elasticsearch. Sono disponibili anche . C'è . .
Dopo l'installazione e l'avvio, verifichiamo il funzionamento:
# для удобства запомним адрес в переменную
#export ES_URL=$(docker-machine ip dev):9200
export ES_URL=localhost:9200
curl -X GET $ES_URLRiceveremo una risposta simile a questa:
{
"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"
}Indicizzazione
Aggiungiamo un post a ES:
# Добавим документ c id 1 типа post в индекс blog.
# ?pretty указывает, что вывод должен быть человеко-читаемым.
curl -XPUT "$ES_URL/blog/post/1?pretty" -d'
{
"title": "Веселые котята",
"content": "<p>Una storia divertente sui gattini<p>",
"tags": [
"gattini",
"storia divertente"
],
"published_at": "2014-09-12T20:44:42+00:00"
}'
risposta del server:
{
"_index" : "blog",
"_type" : "post",
"_id" : "1",
"_version" : 1,
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"created" : false
}
ES ha automaticamente creato blog e post. Si può fare un'analogia: l'indice è un database e il tipo è una tabella in questo database. Ogni tipo ha il proprio schema — , proprio come una tabella relazionale. Il mapping viene generato automaticamente durante l'indicizzazione del documento:
# Получим mapping всех типов индекса blog
curl -XGET "$ES_URL/blog/_mapping?pretty"Nella risposta del server ho aggiunto nei commenti i valori dei campi del documento indicizzato:
{
"blog" : {
"mappings" : {
"post" : {
"properties" : {
"content" : {
"type" : "string"
},
"published_at" : {
"type" : "date",
"format" : "strict_date_optional_time||epoch_millis"
},
"tags" : {
"type" : "string"
},
"title" : {
"type" : "string"
}
}
}
}
}
}<p>Una storia divertente sui gattini<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": ["gattini", "storia divertente"] */
"tags" : {
"type" : "string"
},
/* "title": "Gattini divertenti" */
"title" : {
"type" : "string"
}
}
}
}
}
}Va notato che ES non fa distinzioni tra un valore singolo e un array di valori. Ad esempio, il campo title contiene semplicemente il titolo, mentre il campo tags è un array di stringhe, anche se sono rappresentati nel mapping in modo identico.
Più tardi parleremo di mapping in modo più dettagliato.
Richieste
Estrazione del documento per 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>Una storia divertente sui gattini<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"
}
}Nella risposta sono apparsi nuovi tasti: _version e _source. In generale, tutte le chiavi che iniziano con _ sono relative ai servizi.
Chiave _version mostra la versione del documento. È necessaria per il meccanismo di ottimistic locking. Ad esempio, vogliamo modificare un documento che ha la versione 1. Inviamo il documento modificato e indichiamo che si tratta di una modifica del documento di versione 1. Se qualcun altro ha anche modificato il documento di versione 1 e ha inviato le modifiche prima di noi, allora ES non accetterà le nostre modifiche, poiché ha un documento di versione 2.
Chiave _source contiene il documento che abbiamo indicizzato. ES non utilizza questo valore per le operazioni di ricerca, poiché per la ricerca vengono utilizzati gli indici. Per risparmiare spazio, ES memorizza il documento originale compresso. Se abbiamo bisogno solo dell'id, e non dell'intero documento originale, possiamo disattivare la memorizzazione dell'originale.
Se non abbiamo bisogno di informazioni aggiuntive, possiamo ottenere solo il contenuto di _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>Una storia divertente sui gattini<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"
}
Possiamo anche selezionare solo alcuni campi:
# извлечем только поле title
curl -XGET "$ES_URL/blog/post/1?_source=title&pretty"{
"_index" : "blog",
"_type" : "post",
"_id" : "1",
"_version" : 1,
"found" : true,
"_source" : {
"title" : "Gattini divertenti"
}
}Indicizziamo altri post e facciamo richieste più complesse.
curl -XPUT "$ES_URL/blog/post/2" -d'
{
"title": "Cuccioli divertenti",
"content": "<p>Una storia divertente sui cuccioli<p>",
"tags": [
"cuccioli",
"storia divertente"
],
"published_at": "2014-08-12T20:44:42+00:00"
}'curl -XPUT "$ES_URL/blog/post/3" -d'
{
"title": "Come ho adottato un gattino",
"content": "<p>Una storia straziante su un povero gattino di strada<p>",
"tags": [
"gattini"
],
"published_at": "2014-07-21T20:44:42+00:00"
}'Ordinamento
# найдем последний пост по дате публикации и извлечем поля 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" : "Gattini divertenti",
"published_at" : "2014-09-12T20:44:42+00:00"
},
"sort" : [ 1410554682000 ]
} ]
}
}Abbiamo selezionato l'ultimo post. dimensione limita il numero di documenti restituiti. total mostra il numero totale di documenti che soddisfano la richiesta. sort nella risposta contiene un array di numeri interi, utilizzati per l'ordinamento. Quindi, la data è stata trasformata in un numero intero. Maggiori dettagli sull'ordinamento possono essere letti in .
Filtri e richieste
ES dalla versione 2 non fa distinzione tra filtri e richieste, al contrario .
Il contesto della richiesta si differenzia dal contesto del filtro in quanto la richiesta genera _score e non viene memorizzata nella cache. Che cos'è _score, lo mostrerò più tardi.
Filtraggio per data
Utilizziamo la richiesta nel contesto del filtro:
# получим посты, опубликованные 1ого сентября или позже
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
"filter": {
"range": {
"published_at": { "gte": "2014-09-01" }
}
}
}'Filtraggio per tag
Utilizziamo per cercare gli id dei documenti che contengono una parola specificata:
# найдем все документы, в поле 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" : "Gattini divertenti",
"tags" : [ "gattini", "storia divertente" ]
}
}, {
"_index" : "blog",
"_type" : "post",
"_id" : "3",
"_score" : 1.0,
"_source" : {
"title" : "Come ho adottato un gattino",
"tags" : [ "gattini" ]
}
} ]
}
}Ricerca full-text
Tre dei nostri documenti contengono nel campo content quanto segue:
<p>Una storia divertente sui gattini<p><p>Una storia divertente sui cuccioli<p><p>Una storia straziante su un povero gattino di strada<p>
Utilizziamo per cercare gli id dei documenti che contengono una parola specificata:
# 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
} ]
}
}Tuttavia, se cerchiamo «storie» nel campo contenuto, non troveremo nulla, poiché nell'indice ci sono solo parole originali, non le loro radici. Per eseguire una ricerca efficace, è necessario configurare l'analizzatore.
Campo _score mostra . Se la query viene eseguita nel contesto di filtro, il valore di _score sarà sempre 1, indicando una corrispondenza totale con il filtro.
Analizzatori
sono necessari per trasformare il testo originale in un insieme di token.
Gli analizzatori sono costituiti da uno e diversi opzionali Il Tokenizer può essere preceduto da diversi Il Tokenizer suddivide la stringa originale in token, ad esempio, in base a spazi e segni di punteggiatura. Il TokenFilter può modificare i token, rimuoverli o aggiungerne di nuovi, ad esempio mantenendo solo la radice della parola, rimuovendo le preposizioni, aggiungendo sinonimi. Il CharFilter modifica l'intera stringa originale, ad esempio, rimuovendo i tag html.
In ES ci sono diversi Ad esempio, l'analizzatore .
Utilizziamo e vediamo come gli analizzatori standard e russian trasformano la stringa «Ghirlande divertenti di gattini»:
# используем анализатор 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" : "ghirlande",
"start_offset" : 0,
"end_offset" : 8,
"type" : "",
"position" : 0
}, {
"token" : "divertenti",
"start_offset" : 9,
"end_offset" : 20,
"type" : "",
"position" : 1
}, {
"token" : "di",
"start_offset" : 21,
"end_offset" : 23,
"type" : "",
"position" : 2
}, {
"token" : "gattini",
"start_offset" : 24,
"end_offset" : 31,
"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" : "ghirland",
"start_offset" : 0,
"end_offset" : 8,
"type" : "",
"position" : 0
}, {
"token" : "divertent",
"start_offset" : 9,
"end_offset" : 20,
"type" : "",
"position" : 1
}, {
"token" : "gat",
"start_offset" : 24,
"end_offset" : 31,
"type" : "",
"position" : 3
} ]
}L'analizzatore standard ha suddiviso la stringa in base agli spazi e ha convertito tutto in minuscolo, mentre l'analizzatore russian ha rimosso le parole insignificanti, ha convertito in minuscolo e ha mantenuto la radice delle parole.
Vediamo quali Tokenizer, TokenFilters, CharFilters utilizza l'analizzatore 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 non presenti */
}
}
}Descriviamo il nostro analizzatore basato su russian, che rimuoverà i tag html. Lo chiameremo default, poiché l'analizzatore con questo nome sarà utilizzato per impostazione predefinita.
{
"filter": {
"ru_stop": {
"type": "stop",
"stopwords": "_russian_"
},
"ru_stemmer": {
"type": "stemmer",
"language": "russian"
}
},
"analyzer": {
"default": {
/* aggiungiamo rimozione dei tag html */
"char_filter": ["html_strip"],
"tokenizer": "standard",
"filter": [
"lowercase",
"ru_stop",
"ru_stemmer"
]
}
}
}Prima verranno rimossi tutti i tag html dalla stringa originale, poi verrà suddivisa in token dal tokenizer standard, i token ottenuti verranno convertiti in minuscolo, verranno rimossi i vocaboli insignificanti e rimarranno solo le radici delle parole.
Creazione dell'indice
Abbiamo descritto l'analizzatore default. Esso sarà applicato a tutti i campi di tipo stringa. Il nostro post contiene un array di tag, pertanto, i tag verranno elaborati dall'analizzatore. Poiché cerchiamo post per corrispondenza esatta del tag, è necessario disattivare l'analisi per il campo tags.
Creiamo l'indice blog2 con analizzatore e mapping, in cui è disattivata l'analisi del campo 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"
}
}
}
}
}'Aggiungiamo gli stessi 3 post a questo indice (blog2). Salterò questo processo, poiché è simile all'aggiunta di documenti all'indice blog.
Ricerca testuale completa con supporto per espressioni
Esploriamo un altro tipo di query:
# найдем документы, в которых встречается слово 'истории'
# 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"
]
}
}
}'Poiché utilizziamo un analizzatore con stemming russo, questa query restituirà tutti i documenti, anche se contengono solo la parola ‘storia’.
La query può contenere caratteri speciali, ad esempio:
""fried eggs" +(melanzana | patata) -frittata"Sintassi della query:
+ indica operazione AND
| indica operazione OR
- nega un singolo token
" racchiude un numero di token per indicare una frase da cercare
* alla fine di un termine indica una query di prefisso
( e ) indicano precedenza
~N dopo una parola indica la distanza di modifica (fuzziness)
~N dopo una frase indica l'ammontare di slop# найдем документы без слова 'щенки'
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
"query": {
"simple_query_string": {
"query": "-щенки",
"fields": [
"title^3",
"tags^2",
"content"
]
}
}
}'
# получим 2 поста про котиковLink
PS
Se sei interessato a articoli-corso simili, hai idee per nuovi articoli o suggerimenti per la collaborazione, sarei felice di ricevere un messaggio in privato o via mail a m.kuzmin+habr@darkleaf.ru.
Fonte: habr.com
