Elasticsearch é um mecanismo de busca com uma API REST em JSON, que utiliza Lucene e é escrito em Java. Uma descrição de todos os benefícios deste mecanismo está disponível em [link para a descrição]. A partir de agora, nos referiremos ao Elasticsearch como ES.
Esses mecanismos são usados para buscas complexas em bancos de dados de documentos, como buscas baseadas na morfologia da linguagem ou buscas por coordenadas geográficas.
Neste artigo, abordarei os conceitos básicos do Elasticsearch usando o exemplo da indexação de posts de blog. Mostrarei como filtrar, classificar e pesquisar documentos.
Para ser independente do sistema operacional, farei todas as minhas requisições ao Elasticsearch usando o CURL. Existe também um plugin para o Google Chrome chamado .
Ao longo do texto, são fornecidos links para documentação e outras fontes. Links de acesso rápido à documentação encontram-se no final. Definições de termos desconhecidos podem ser encontradas em .
Instalando o ES
Para isso, primeiro precisamos de Java. Desenvolvedores Instale versões do Java mais recentes que o Java 8 atualização 20 ou o Java 7 atualização 55.
A distribuição ES está disponível em Após descompactar o arquivo, você precisa executar bin/elasticsearchTambém disponível . Existe . .
Após a instalação e inicialização, vamos verificar a funcionalidade:
# для удобства запомним адрес в переменную
#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 uma postagem ao 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"
}'
Resposta do servidor:
{
"_index" : "blog",
"_type" : "post",
"_id" : "1",
"_version" : 1,
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"created" : false
}
ES criado automaticamente blog e postagem. Uma analogia aproximada pode ser feita: um índice é um banco de dados e um tipo é uma tabela dentro desse banco de dados. Cada tipo tem seu próprio esquema— Assim como em uma tabela relacional, o mapeamento é gerado automaticamente quando um documento é indexado.
# Получим mapping всех типов индекса blog
curl -XGET "$ES_URL/blog/_mapping?pretty"Na resposta do servidor, adicionei os valores dos campos do documento indexado nos comentários:
{
"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"
}
}
}
}
}
}Vale ressaltar que o Elasticsearch não distingue entre um único valor e um array de valores. Por exemplo, o campo `title` contém apenas um título, enquanto o campo `tags` contém um array de strings, mesmo que sejam representados de forma idêntica no mapeamento.
Falaremos sobre mapeamento com mais detalhes posteriormente.
pedidos
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" : "Веселые котята",
"content" : "<p>Смешная история про котят<p>",
"tags" : [ "котята", "смешная история" ],
"published_at" : "2014-09-12T20:44:42+00:00"
}
}Novas chaves apareceram na resposta: _version и _sourceEm geral, todas as teclas que começam com _ são classificados como itens de serviço.
Ключ _version Mostra a versão do documento. Isso é necessário para que o mecanismo de bloqueio otimista funcione. Por exemplo, queremos alterar um documento com a versão 1. Enviamos o documento modificado e indicamos que esta é uma revisã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, o Elasticsearch não aceitará nossas alterações, pois ele armazena o documento com a versão 2.
Ключ _source Contém o documento que indexamos. O Elasticsearch não usa esse valor para operações de busca, pois os índices são usados para buscar. Para economizar espaço, o Elasticsearch armazena uma versão compactada do documento original. Se precisarmos apenas do ID e não do documento original completo, podemos desativar o armazenamento do original.
Se não precisarmos de nenhuma informação adicional, podemos obter apenas o conteúdo de _source:
curl -XGET "$ES_URL/blog/post/1/_source?pretty"{
"title" : "Веселые котята",
"content" : "<p>Смешная история про котят<p>",
"tags" : [ "котята", "смешная история" ],
"published_at" : "2014-09-12T20:44:42+00:00"
}
Você também pode 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" : "Веселые котята"
}
}Vamos indexar mais algumas postagens e executar consultas mais complexas.
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 ]
} ]
}
}Escolhemos a última publicação. size limita o número de documentos emitidos. total Mostra o número total de documentos que correspondem à consulta. sort A saída contém uma matriz de números inteiros pela qual a ordenação é realizada. Ou seja, a data foi convertida em um número inteiro. Você pode ler mais sobre ordenação em [link para a documentação]. .
Filtros e consultas
O Elasticsearch, desde a versão 2, não distingue entre filtros e consultas. .
O contexto da consulta difere do contexto do filtro, pois a consulta gera uma pontuação (_score) e não é armazenada em cache. Explicarei o que é _score mais tarde.
Filtrar por data
Usando a consulta No contexto de filtro:
# получим посты, опубликованные 1ого сентября или позже
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
"filter": {
"range": {
"published_at": { "gte": "2014-09-01" }
}
}
}'Filtrar por tags
Nós usamos Para pesquisar IDs de documentos que contenham uma determinada palavra:
# найдем все документы, в поле 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" : [ "котята" ]
}
} ]
}
}Pesquisa de texto completo
Nossos três documentos contêm o seguinte no campo de conteúdo:
<p>Смешная история про котят<p><p>Смешная история про щенков<p><p>Душераздирающая история про бедного котенка с улицы<p>
Nós usamos Para pesquisar IDs de documentos que contenham uma determinada palavra:
# 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 pesquisarmos por "histórias" no campo de conteúdo, não encontraremos nada, pois o índice contém apenas as palavras originais, não seus radicais. Para realizar uma pesquisa de qualidade, precisamos configurar o analisador.
Campo _score mostra Se a consulta for executada em um contexto de filtro, o valor de _score será sempre 1, o que significa que o filtro correspondeu totalmente.
Analisadores
são necessários para transformar o texto de origem em um conjunto de tokens.
Os analisadores consistem em um e várias opcionais O tokenizador pode preceder vários Os tokenizadores dividem uma string de origem em tokens, por exemplo, por espaços e caracteres de pontuação. Os filtros de token podem modificar tokens, removê-los ou adicionar novos, por exemplo, deixando apenas o radical da palavra, removendo preposições ou adicionando sinônimos. Os filtros de caractere modificam a string de origem completamente, por exemplo, removendo tags HTML.
Existem vários em ES Por exemplo, um analisador .
Vamos usar E vejamos como os analisadores sintáticos padrão e russo transformam a string "Histórias engraçadas 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" : "веселые",
"start_offset" : 0,
"end_offset" : 7,
"type" : "<ALPHANUM>",
"position" : 0
}, {
"token" : "истории",
"start_offset" : 8,
"end_offset" : 15,
"type" : "<ALPHANUM>",
"position" : 1
}, {
"token" : "про",
"start_offset" : 16,
"end_offset" : 19,
"type" : "<ALPHANUM>",
"position" : 2
}, {
"token" : "котят",
"start_offset" : 20,
"end_offset" : 25,
"type" : "<ALPHANUM>",
"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" : "<ALPHANUM>",
"position" : 0
}, {
"token" : "истор",
"start_offset" : 8,
"end_offset" : 15,
"type" : "<ALPHANUM>",
"position" : 1
}, {
"token" : "кот",
"start_offset" : 20,
"end_offset" : 25,
"type" : "<ALPHANUM>",
"position" : 3
} ]
}O analisador padrão dividia a string nos espaços e convertia tudo para minúsculas, enquanto o analisador russo removia palavras irrelevantes, convertia-as para minúsculas e mantinha os radicais das palavras.
Vejamos quais Tokenizer, TokenFilters e CharFilters o analisador russo utiliza:
{
"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 отсутствуют */
}
}
}Vamos descrever nosso próprio analisador baseado em russo que removerá as tags HTML. Chamaremos de padrão, pois este será o analisador padrão.
{
"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"
]
}
}
}Primeiro, todas as tags HTML serão removidas da string de origem. Em seguida, ela será dividida em tokens pelo analisador léxico padrão. Os tokens resultantes serão convertidos para minúsculas, palavras irrelevantes serão removidas e os tokens restantes formarão a raiz da palavra.
Criando um índice
Acima, descrevemos o analisador padrão. Ele será aplicado a todos os campos de texto. Nossa postagem contém uma matriz de tags, portanto, as tags também serão processadas pelo analisador. Como estamos procurando postagens que correspondam exatamente à tag, precisamos desativar a análise para o campo "tags".
Vamos criar um índice blog2 com um analisador e um mapeamento, no qual a análise do campo de 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 as mesmas três postagens a este índice (blog2). Vou pular este processo, pois é semelhante a adicionar documentos ao índice do blog.
Pesquisa de texto completo com suporte a expressões
Vamos nos familiarizar com outro tipo de consulta:
# найдем документы, в которых встречается слово 'истории'
# 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 lematização em russo, essa 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:
+ signifies AND operation
| signifies OR operation
- negates a single token
" wraps a number of tokens to signify a phrase for searching
* at the end of a term signifies a prefix query
( and ) signify precedence
~N after a word signifies edit distance (fuzziness)
~N after a phrase signifies slop amount# найдем документы без слова 'щенки'
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
"query": {
"simple_query_string": {
"query": "-щенки",
"fields": [
"title^3",
"tags^2",
"content"
]
}
}
}'
# получим 2 поста про котиковreferências
PS
Se você tiver interesse em artigos tutoriais semelhantes, ideias para novos artigos ou sugestões de colaboração, ficarei feliz em receber sua mensagem privada ou um e-mail para m.kuzmin+habr@darkleaf.ru.
Fonte: habr.com
