Noções básicas do Elasticsearch

Elasticsearch é um mecanismo de busca com API json rest, usando Lucene e escrito em Java. Uma descrição de todas as vantagens deste motor está disponível em o site oficial. A seguir nos referiremos ao Elasticsearch como ES.

Mecanismos semelhantes são usados ​​para pesquisas complexas em um banco de dados de documentos. Por exemplo, pesquise tendo em conta a morfologia da língua ou pesquise por coordenadas geográficas.

Neste artigo falarei sobre os fundamentos do ES usando o exemplo de indexação de postagens de blog. Mostrarei como filtrar, classificar e pesquisar documentos.

Para não depender do sistema operacional, farei todas as requisições ao ES utilizando CURL. Existe também um plugin para o Google Chrome chamado sentido.

O texto contém links para documentação e outras fontes. No final há links para acesso rápido à documentação. Definições de termos desconhecidos podem ser encontradas em glossários.

Instalação

Para fazer isso, primeiro precisamos de Java. Desenvolvedores Recomendar 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 site do desenvolvedor. Depois de descompactar o arquivo você precisa executar bin/elasticsearch. Também disponível pacotes para apt e yum. Existe imagem oficial do docker. Mais sobre instalação.

Após a instalação e lançamento, vamos verificar a funcionalidade:

# для удобства запомним адрес в переменную
#export ES_URL=$(docker-machine ip dev):9200
export ES_URL=localhost:9200

curl -X GET $ES_URL

Receberemos algo assim:

{
  "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 índice blog e тип publicar. Podemos fazer uma analogia condicional: um índice é um banco de dados e um tipo é uma tabela nesse banco de dados. Cada tipo tem seu próprio esquema - mapeamento, assim como uma tabela relacional. O mapeamento é gerado automaticamente quando o 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"
          }
        }
      }
    }
  }
}

É importante notar que ES não diferencia entre um único valor e uma matriz de valores. Por exemplo, o campo de título contém simplesmente um título e o campo de tags contém uma matriz de strings, embora sejam representadas da mesma maneira no mapeamento.
Falaremos mais sobre mapeamento posteriormente.

pedidos

Recuperando 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 и _source. Em geral, todas as chaves começando com _ são classificados como oficiais.

Ключ _version mostra a versão do documento. É necessário para que o mecanismo de bloqueio otimista funcione. Por exemplo, queremos alterar um documento que possui a versão 1. Enviamos o documento alterado e indicamos que se trata de uma edição de um documento com a versão 1. Se outra pessoa também editou um documento com a versão 1 e enviou as alterações antes de nós, então ES não aceitará nossas alterações, pois ele armazena o documento com a versão 2.

Ключ _source contém o documento que indexamos. ES não usa esse valor para operações de pesquisa porque Índices são usados ​​para pesquisa. Para economizar espaço, o ES armazena um documento de origem compactado. Se precisarmos apenas do ID, e não de todo o documento de origem, podemos desabilitar o armazenamento de origem.

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" : "Веселые котята",
  "content" : "<p>Смешная история про котят<p>",
  "tags" : [ "котята", "смешная история" ],
  "published_at" : "2014-09-12T20:44:42+00:00"
}

Você também pode selecionar apenas alguns 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 o último post. size limita o número de documentos a emitir. total mostra o número total de documentos correspondentes à solicitação. sort na saída contém uma matriz de números inteiros pelos quais a classificação é executada. Aqueles. a data foi convertida para um número inteiro. Mais informações sobre classificação podem ser encontradas em documentação.

Filtros e consultas

ES desde a versão 2 não faz distinção entre filtros e consultas, em vez disso o conceito de contextos é introduzido.
Um contexto de consulta difere de um contexto de filtro porque a consulta gera um _score e não é armazenada em cache. Vou mostrar o que é _score mais tarde.

Filtrar por data

Usamos a solicitação alcance no contexto do 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 consulta de termo para procurar IDs de documentos contendo 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

Três de nossos documentos contêm o seguinte no campo de conteúdo:

  • <p>Смешная история про котят<p>
  • <p>Смешная история про щенков<p>
  • <p>Душераздирающая история про бедного котенка с улицы<p>

Nós usamos consulta de correspondência para procurar IDs de documentos contendo 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
    } ]
  }
}

Porém, se procurarmos 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 fazer uma pesquisa de alta qualidade, é necessário configurar o analisador.

Campo _score mostra relevância. Se a solicitação for executada em um contexto de filtro, o valor _score será sempre igual a 1, o que significa uma correspondência completa com o filtro.

Analisadores

Analisadores são necessários para converter o texto fonte em um conjunto de tokens.
Os analisadores consistem em um Tokenizador e vários opcionais Filtros de token. O tokenizer pode ser precedido por vários Filtros de caracteres. Os tokenizadores dividem a string de origem em tokens, como espaços e caracteres de pontuação. TokenFilter pode alterar tokens, excluir ou adicionar novos, por exemplo, deixar apenas o radical da palavra, remover preposições, adicionar sinônimos. CharFilter - altera toda a string de origem, por exemplo, corta tags html.

ES tem vários analisadores padrão. Por exemplo, um analisador russo.

Vamos usar api e vamos ver como os analisadores 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 dividiu a string em espaços e converteu tudo para minúsculas, o analisador russo removeu palavras sem importância, converteu para minúsculas e deixou o radical das palavras.

Vamos ver quais Tokenizer, TokenFilters, CharFilters o analisador russo usa:

{
  "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 analisador baseado em russo, que cortará tags HTML. Vamos chamá-lo de padrão, porque um analisador com este nome será usado por 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, depois o padrão do tokenizador irá dividi-la em tokens, os tokens resultantes serão movidos para letras minúsculas, palavras insignificantes serão removidas e os tokens restantes permanecerão como o radical da palavra.

Criando um índice

Acima descrevemos o analisador padrão. Isso se aplicará a todos os campos de string. Nossa postagem contém uma série de tags, portanto as tags também serão processadas pelo analisador. Porque Estamos procurando postagens por correspondência exata com uma tag, então precisamos desabilitar a análise do campo tags.

Vamos criar um índice blog2 com analisador e mapeamento, no qual a análise do campo tags está desabilitada:

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 esse processo porque... é semelhante a adicionar documentos ao índice do blog.

Pesquisa de texto completo com suporte a expressões

Vejamos outro tipo de solicitação:

# найдем документы, в которых встречается слово 'истории'
# 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"
      ]
    }
  }
}'

Porque Estamos usando um analisador com derivação russa, então esta solicitação retornará todos os documentos, embora contenham apenas a palavra 'histórico'.

A solicitação pode conter caracteres especiais, por exemplo:

""fried eggs" +(eggplant | potato) -frittata"

Sintaxe da solicitação:

+ 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ê estiver interessado em artigos-aulas semelhantes, tiver ideias para novos artigos ou tiver propostas de cooperação, terei o maior prazer em receber uma mensagem em mensagem pessoal ou por e-mail [email protegido].

Fonte: habr.com