Elasticsearch — um mecanismo de busca com API REST JSON, que utiliza Lucene e é escrito em Java. A descrição de todas as vantagens deste mecanismo está disponível em . Daqui em diante, chamaremos Elasticsearch de ES.
Mecanismos como esse são usados em buscas complexas em bancos de documentos. Por exemplo, busca levando em conta a morfologia da língua ou busca por coordenadas geográficas.
Neste artigo, falarei sobre os fundamentos do ES usando o exemplo da indexação de posts de blog. Vou mostrar como filtrar, classificar e buscar documentos.
Para não depender do sistema operacional, farei todas as requisições ao ES usando CURL. Também há um plugin para o Google Chrome chamado .
O texto contém links para a documentação e outras fontes. No final, estão links para acesso rápido à documentação. As definições de termos desconhecidos podem ser lidas no .
Instalação do ES
Para isso, primeiro precisaremos do Java. Os desenvolvedores devem instalar versões do Java superiores a Java 8 update 20 ou Java 7 update 55.
O pacote do ES está disponível em . Após descompactar o arquivo, é necessário iniciar bin/elasticsearch. Também estão disponíveis . Há . .
Após a instalação e execução, vamos verificar se está funcionando:
# для удобства запомним адрес в переменную
#export ES_URL=$(docker-machine ip dev):9200
export ES_URL=localhost:9200
curl -X GET $ES_URLReceberemos uma resposta semelhante a esta:
{
"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"
}Indexação
Vamos adicionar um post ao ES:
# Добавим документ c id 1 типа post в индекс blog.
# ?pretty указывает, что вывод должен быть человеко-читаемым.
curl -XPUT "$ES_URL/blog/post/1?pretty" -d'
{
"title": "Веселые котята",
"content": "<p>Uma história engraçada sobre gatinhos<p>",
"tags": [
"gatinhos",
"história engraçada"
],
"published_at": "2014-09-12T20:44:42+00:00"
}'
resposta do servidor:
{
"_index" : "blog",
"_type" : "post",
"_id" : "1",
"_version" : 1,
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"created" : false
}
O ES criou automaticamente blog e post. Pode-se fazer uma analogia: o índice é como um banco de dados, e o tipo é como uma tabela nesse banco. Cada tipo tem seu próprio esquema — , assim como uma tabela relacional. O mapeamento é gerado automaticamente ao indexar um documento:
# Получим mapping всех типов индекса blog
curl -XGET "$ES_URL/blog/_mapping?pretty"Na resposta do servidor, adicionei em comentários os valores dos campos do documento indexado:
{
"blog" : {
"mappings" : {
"post" : {
"properties" : {
/* "content": "<p>Uma história engraçada sobre gatinhos<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"
}
}
}
}
}
}Vale destacar que o ES não faz distinção entre um único valor e um array de valores. Por exemplo, o campo title contém apenas um título, enquanto o campo tags é um array de strings, embora sejam apresentados no mapeamento de forma idêntica.
Mais tarde, falaremos sobre o mapeamento de forma mais detalhada.
Consultas
Extraindo um documento pelo seu 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>Uma hist\u00f3ria engra\u00e7ada sobre gatinhos<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"
}
}Na resposta, surgiram novas chaves: _version e _source. De fato, todas as chaves que começam com _ são relacionadas a serviços.
Chave _version mostra a versão do documento. Isso é necessário para o funcionamento do mecanismo de bloqueio otimista. Por exemplo, queremos alterar um documento que possui a versão 1. Enviamos o documento modificado e indicamos que se trata de uma edição do documento com a versão 1. Se outra pessoa também editou o documento com a versão 1 e enviou as alterações antes de nós, então o ES não aceitará nossas mudanças, pois ele armazena o documento com a versão 2.
Chave _source contém o documento que indexamos. O ES não usa esse valor para operações de busca, pois os índices são utilizados para busca. Para economizar espaço, o ES armazena o documento original de forma compactada. Se precisarmos apenas do id, e não do documento original completo, podemos desativar o armazenamento do original.
Se não precisarmos de informações adicionais, podemos obter apenas o conteúdo de _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>Uma hist\u00f3ria engra\u00e7ada sobre gatinhos<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"
}
Também é possível selecionar apenas determinados campos:
# извлечем только поле title
curl -XGET "$ES_URL/blog/post/1?_source=title&pretty"{
"_index" : "blog",
"_type" : "post",
"_id" : "1",
"_version" : 1,
"found" : true,
"_source" : {
"title" : "Gatinhos divertidos"
}
}Vamos indexar mais algumas postagens e realizar consultas mais complexas.
curl -XPUT "$ES_URL/blog/post/2" -d'
{
"title": "Cachorrinhos Divertidos",
"content": "<p>Uma história engraçada sobre cachorros<p>"
"tags": [
"cachorros",
"história engraçada"
],
"published_at": "2014-08-12T20:44:42+00:00"
}'curl -XPUT "$ES_URL/blog/post/3" -d'
{
"title": "Como eu ganhei um filhote de gato",
"content": "<p>Uma história comovente sobre um pobre filhote de gato da rua<p>",
"tags": [
"filhotes"
],
"published_at": "2014-07-21T20:44:42+00:00"
}'Classificação
# найдем последний пост по дате публикации и извлечем поля 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" : "Gatinhos divertidos",
"published_at" : "2014-09-12T20:44:42+00:00"
},
"sort" : [ 1410554682000 ]
} ]
}
}Selecionamos a última postagem. tamanho limita o número de documentos nos resultados. total mostra o número total de documentos que correspondem à solicitação. sort nos resultados contém um array de números inteiros, que são usados para a ordenação. Ou seja, a data foi convertida em um número inteiro. Mais informações sobre ordenação podem ser encontradas em .
Filtros e solicitações
ES a partir da versão 2 não distingue entre filtros e solicitações, em vez disso .
O contexto de uma solicitação difere do contexto de um filtro, pois a solicitação gera _score e não é armazenada em cache. O que é _score eu mostrarei mais tarde.
Filtragem por data
Usamos a solicitação no contexto do filtro:
# получим посты, опубликованные 1ого сентября или позже
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
"filter": {
"range": {
"published_at": { "gte": "2014-09-01" }
}
}
}'Filtragem por tags
Usaremos para buscar ids de documentos que contenham a palavra especificada:
# найдем все документы, в поле 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" : "Gatinhos Divertidos",
"tags" : [ "gatinhos", "história engraçada" ]
}
}, {
"_index" : "blog",
"_type" : "post",
"_id" : "3",
"_score" : 1.0,
"_source" : {
"title" : "Como eu adotei um gatinho",
"tags" : [ "gatinhos" ]
}
} ]
}
}Busca de texto completo
Nossos três documentos contêm o seguinte no campo content:
<p>Uma história engraçada sobre gatinhos<p><p>Uma história engraçada sobre cachorros<p><p>Uma história comovente sobre um pobre filhote de gato da rua<p>
Usaremos para buscar ids de documentos que contenham a palavra especificada:
# 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
} ]
}
}No entanto, se buscarmos "histórias" no campo de conteúdo, não encontraremos nada, pois o índice contém apenas as palavras originais e não suas raízes. Para realizar uma busca de qualidade, é necessário configurar o analisador.
Campo _score mostra . Se a consulta for executada no contexto de filtro, o valor de _score sempre será 1, o que significa total conformidade com o filtro.
Analisadores
são necessários para transformar o texto original em um conjunto de tokens.
Analisadores consistem em um e vários opcionais . O Tokenizer pode preceder vários . O Tokenizer divide a string original em tokens, por exemplo, por espaços e símbolos de pontuação. O TokenFilter pode modificar os tokens, removendo ou adicionando novos, por exemplo, mantendo apenas a raiz da palavra, removendo preposições, adicionando sinônimos. O CharFilter altera a string original completamente, por exemplo, cortando tags HTML.
No ES, existem vários . Por exemplo, o analisador .
Vamos utilizar e vamos ver como os analisadores standard e russian transformam a string "Histórias Divertidas sobre Gatinhos":
# используем анализатор 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" : "divertidas",
"start_offset" : 0,
"end_offset" : 9,
"type" : "",
"position" : 0
}, {
"token" : "histórias",
"start_offset" : 10,
"end_offset" : 18,
"type" : "",
"position" : 1
}, {
"token" : "sobre",
"start_offset" : 19,
"end_offset" : 24,
"type" : "",
"position" : 2
}, {
"token" : "gatinhos",
"start_offset" : 25,
"end_offset" : 33,
"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" : "divertido",
"start_offset" : 0,
"end_offset" : 7,
"type" : "",
"position" : 0
}, {
"token" : "história",
"start_offset" : 8,
"end_offset" : 15,
"type" : "",
"position" : 1
}, {
"token" : "gato",
"start_offset" : 20,
"end_offset" : 25,
"type" : "",
"position" : 3
} ]
}O analisador padrão dividiu a string por espaços e converteu tudo para minúsculas, o analisador russo removeu palavras irrelevantes, converteu para minúsculas e manteve a raiz das palavras.
Vamos ver quais Tokenizers, TokenFilters, CharFilters são utilizados pelo analisador russo:
{
"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 ausentes */
}
}
}Descreveremos nosso analisador baseado no russo, que irá remover as tags HTML. Vamos chamá-lo de default, pois um analisador com esse nome será usado por padrão.
{
"filter": {
"ru_stop": {
"type": "stop",
"stopwords": "_russian_"
},
"ru_stemmer": {
"type": "stemmer",
"language": "russian"
}
},
"analyzer": {
"default": {
/* adicionando remoção de tags HTML */
"char_filter": ["html_strip"],
"tokenizer": "standard",
"filter": [
"lowercase",
"ru_stop",
"ru_stemmer"
]
}
}
}Primeiro, todas as tags HTML serão removidas da string original, depois ela será dividida em tokens pelo tokenizer padrão, os tokens obtidos serão convertidos para minúsculas, palavras irrelevantes serão removidas e da lista restante de tokens, a raiz da palavra será mantida.
Criação do índice
Acima, descrevemos o analisador default. Ele será aplicado a todos os campos de string. Nossa postagem contém um array de tags, portanto, as tags também serão processadas pelo analisador. Como estamos buscando postagens por correspondência exata da tag, é necessário desativar a análise para o campo tags.
Vamos criar o índice blog2 com o analisador e mapeamento, onde a análise do campo tags está desativada:
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"
}
}
}
}
}'Vamos adicionar os mesmos 3 posts a este índice (blog2). Vou omitir este processo, pois é análogo à adição de documentos no índice blog.
Busca de texto completo com suporte a expressões
Vamos conhecer outro tipo de consultas:
# найдем документы, в которых встречается слово 'истории'
# 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"
]
}
}
}'Como estamos usando um analisador com stemming em russo, esta consulta retornará todos os documentos, mesmo que contenham apenas a palavra 'história'.
A consulta pode conter caracteres especiais, por exemplo:
""fried eggs" +(eggplant | potato) -frittata"Sintaxe da consulta:
+ significa operação AND
| significa operação OR
- nega um único token
" envolve um número de tokens para significar uma frase de busca
* no final de um termo significa uma consulta de prefixo
( e ) significam precedência
~N após uma palavra significa distância de edição (fuzziness)
~N após uma frase significa quantidade de slop# найдем документы без слова 'щенки'
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
"query": {
"simple_query_string": {
"query": "-щенки",
"fields": [
"title^3",
"tags^2",
"content"
]
}
}
}'
# получим 2 поста про котиковLinks
PS
Se você está interessado em artigos-aulas semelhantes, tem ideias para novos artigos ou sugestões de colaboração, ficarei feliz em receber uma mensagem no privado ou por e-mail m.kuzmin+habr@darkleaf.ru.
Fonte: habr.com
