Engenharia

Uma introdução prática ao Elasticsearch

Nota do editor (3 de agosto de 2021): este post usa recursos obsoletos. Consulte a documentação de mapeamento de regiões customizadas com geocodificação reversa para obter as instruções atuais.

Por que escrevi este post?

Recentemente tive o prazer de ministrar uma aula de mestrado na Universidade da Coruña, no curso de Recuperação de Informações e Web Semântica. O objetivo dessa aula foi fornecer uma visão geral do Elasticsearch aos alunos para que eles pudessem começar a usar o Elasticsearch nas tarefas do curso; os participantes variavam desde pessoas já familiarizadas com o Lucene até quem estava tendo o primeiro contato com os conceitos de Recuperação de Informações. Por ser uma aula no fim do dia (começava às 19h30), um dos desafios era prender a atenção dos alunos (ou, em outras palavras, evitar que dormissem!). Existem duas abordagens básicas para prender a atenção ao ensinar: levar chocolate (o que eu me esqueci de fazer) e tornar a aula o mais prática possível.

E é isso que o post de hoje traz: vamos ver a parte prática dessa mesma aula. O objetivo não é aprender cada comando ou solicitação no Elasticsearch (para isso, temos a documentação); a ideia é que você experimente a parte legal de usar o Elasticsearch sem conhecimento prévio em um tutorial guiado de 30 a 60 minutos. Basta copiar e colar cada solicitação para ver os resultados e tentar descobrir a solução para as questões propostas.

Quem se beneficiará com este post?

Mostrarei os recursos básicos do Elastic para apresentar alguns dos conceitos principais, ocasionalmente introduzindo conceitos mais técnicos ou complexos, e vincularei a documentação para referência futura (mas lembre-se: para referência adicional; você pode simplesmente continuar com os exemplos e deixar a documentação para depois). Se você nunca usou o Elasticsearch antes e quer ver como ele funciona (e também comandar a ação), este post é para você. Se você já tem experiência com o Elasticsearch, dê uma olhada no conjunto de dados que usaremos: quando um amigo perguntar o que você pode fazer com o Elasticsearch, será mais fácil explicar com buscas nas peças de Shakespeare!

O que vamos e não vamos cobrir?

Vamos começar adicionando alguns documentos, fazendo buscas e removendo-os. Depois disso, usaremos um conjunto de dados de Shakespeare para fornecer mais informações sobre buscas e agregações. Este é um post prático, do tipo “quero começar a vê-lo funcionando agora mesmo”.

Observe que não abordaremos nada relacionado à configuração ou às práticas recomendadas em implantações de produção: portanto, use estas informações para ter uma amostra do que o Elasticsearch oferece, um ponto de partida para ver como ele pode atender às suas necessidades.

Configuração

Primeiramente, você precisa do Elasticsearch. Siga as instruções da documentação para baixar a versão mais recente, instalá-la e iniciá-la. Basicamente, você precisa de uma versão recente do Java, baixar e instalar o Elasticsearch para seu sistema operacional e, por fim, iniciá-lo com os valores padrão — bin/elasticsearch. Nesta lição, usaremos a versão mais recente disponível no momento, 5.5.0.

Em seguida, você precisa se comunicar com o Elasticsearch: isso é feito emitindo solicitações HTTP para a REST API. O Elastic é iniciado por padrão na porta 9200. Para acessá-lo, você pode usar a ferramenta que melhor se adapta ao seu conhecimento: existem ferramentas de linha de comando (como curl para Linux), plugins REST para Chrome ou Firefox, ou você pode simplesmente instalar o [Kibana](https:// www.elastic.co/pt/kibana) e usar o plugin do console. Cada solicitação consiste em um verbo HTTP (GET, POST, PUT…), um endpoint de URL e um corpo opcional — na maioria dos casos, o corpo é um objeto JSON.

Como exemplo, e para confirmar que o Elasticsearch foi iniciado, vamos fazer um GET no URL de base para acessar o endpoint básico (nenhum corpo é necessário):

GET localhost:9200

A resposta deve se parecer com a exibida abaixo. Como não configuramos nada, o nome da nossa instância será uma string aleatória de sete letras:

{
    "name": "t9mGYu5",
    "cluster_name": "elasticsearch",
    "cluster_uuid": "xq-6d4QpSDa-kiNE4Ph-Cg",
    "version": {
        "number": "5.5.0",
        "build_hash": "260387d",
        "build_date": "2017-06-30T23:16:05.735Z",
        "build_snapshot": false,
        "lucene_version": "6.6.0"
    },
    "tagline": "You Know, for Search"
}

Alguns exemplos básicos

Já temos uma instância limpa do Elasticsearch inicializada e em execução. A primeira coisa que vamos fazer é adicionar documentos e recuperá-los. Os documentos no Elasticsearch são representados no formato JSON. Além disso, os documentos são adicionados a índices e têm um tipo. Estamos adicionando agora ao índice chamado “accounts” um documento do tipo “person” com o id 1; como o índice ainda não existe, o Elasticsearch o criará automaticamente.

POST localhost:9200/accounts/person/1 
{
    "name" : "John",
    "lastname" : "Doe",
    "job_description" : "Systems administrator and Linux specialit"
}

The response will return information about the document creation:

{
    "_index": "accounts",
    "_type": "person",
    "_id": "1",
    "_version": 1,
    "result": "created",
    "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
    },
    "created": true
}

Agora que o documento existe, podemos recuperá-lo:

GET localhost:9200/accounts/person/1 

O resultado conterá metadados e também o documento completo (mostrado no campo _source):

{
    "_index": "accounts",
    "_type": "person",
    "_id": "1",
    "_version": 1,
    "found": true,
    "_source": {
        "name": "John",
        "lastname": "Doe",
        "job_description": "Systems administrator and Linux specialit"
    }
}

O leitor atento já percebeu que cometemos um erro de digitação na descrição do cargo (specialit); vamos corrigi-lo atualizando o documento (_update):

POST localhost:9200/accounts/person/1/_update
{
      "doc":{
          "job_description" : "Systems administrator and Linux specialist"
       }
}

Depois que a operação for bem-sucedida, o documento será alterado. Vamos recuperá-lo novamente e verificar a resposta:

{
    "_index": "accounts",
    "_type": "person",
    "_id": "1",
    "_version": 2,
    "found": true,
    "_source": {
        "name": "John",
        "lastname": "Doe",
        "job_description": "Systems administrator and Linux specialist"
    }
}

Para preparar as próximas operações, vamos adicionar mais um documento com id 2:

POST localhost:9200/accounts/person/2
{
    "name" : "John",
    "lastname" : "Smith",
    "job_description" : "Systems administrator"
}

Até agora, recuperamos documentos por id, mas não fizemos buscas. Ao consultar usando a REST API, podemos passar a consulta no corpo da solicitação ou diretamente no URL com uma sintaxe específica. Nesta seção, faremos buscas diretamente no URL no formato /_search?q=something:

GET localhost:9200/_search?q=john

Esta busca retornará ambos os documentos, pois ambos incluem john:

{
    "took": 58,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "failed": 0
    },
    "hits": {
        "total": 2,
        "max_score": 0.2876821,
        "hits": [
            {
                "_index": "accounts",
                "_type": "person",
                "_id": "2",
                "_score": 0.2876821,
                "_source": {
                    "name": "John",
                    "lastname": "Smith",
                    "job_description": "Systems administrator"
                }
            },
            {
                "_index": "accounts",
                "_type": "person",
                "_id": "1",
                "_score": 0.28582606,
                "_source": {
                    "name": "John",
                    "lastname": "Doe",
                    "job_description": "Systems administrator and Linux specialist"
                }
            }
        ]
    }
}

Nesse resultado, podemos ver os documentos correspondentes e também alguns metadados, como o número total de resultados da consulta. Vamos continuar fazendo mais buscas. Antes de executar as buscas, tente descobrir por conta própria quais documentos serão recuperados (a resposta vem após o comando):

GET localhost:9200/_search?q=smith

Essa busca retornará apenas o último documento que adicionamos, o único que contém smith.

GET localhost:9200/_search?q=job_description:john

Essa busca não retornará nenhum documento. Nesse caso, estamos restringindo a busca apenas ao campo job_description, que não contém o termo. Como um exercício, tente fazer:

  • uma busca nesse campo que retorne apenas o documento com id 1
  • uma busca nesse campo que retorne ambos os documentos
  • uma busca nesse campo que retorne apenas o documento com id 2; você vai precisar de uma dica: o parâmetro “q” usa a mesma sintaxe da [string de consulta](https://www.elastic.co/guide/en/ elasticsearch/reference/5.5/query-dsl-query-string-query.html#query-string-syntax).

Este último exemplo traz uma pergunta relacionada: podemos fazer buscas em campos específicos; é possível buscar apenas dentro de um índice específico? A resposta é sim: podemos especificar o índice e digitar o URL. Tente isto:

GET localhost:9200/accounts/person/_search?q=job_description:linux

Além de buscar em um índice, podemos buscar em vários índices ao mesmo tempo fornecendo uma lista separada por vírgulas de nomes de índices, e o mesmo pode ser feito para tipos. Existem mais opções: informações sobre elas podem ser encontradas em Multi-Index, Multi-type (Múltiplos índices, múltiplos tipos). Como exercício, adicione documentos a um segundo índice (diferente) e faça buscas em ambos os índices simultaneamente.

Para fechar esta seção, excluiremos um documento e depois todo o índice. Depois de excluir o documento, tente recuperá-lo ou encontrá-lo nas buscas.

DELETE localhost:9200/accounts/person/1

A resposta será a confirmação:

{
    "found": true,
    "_index": "accounts",
    "_type": "person",
    "_id": "1",
    "_version": 3,
    "result": "deleted",
    "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
    }
}

Por fim, podemos excluir o índice completo.

DELETE localhost:9200/accounts

E este é o fim da primeira seção. Vamos resumir o que fizemos:

  1. Adicionamos um documento. Implicitamente, um índice foi criado (o índice não existia anteriormente).
  2. Recuperamos o documento.
  3. Atualizamos o documento para corrigir um erro de digitação e verificamos que ele foi corrigido.
  4. Adicionamos um segundo documento.
  5. Buscamos, incluindo buscas que usavam implicitamente todos os campos e uma busca concentrada apenas em um campo.
  6. Propusemos vários exercícios de busca.
  7. Explicamos os fundamentos da busca em vários índices e tipos simultaneamente.
  8. Propusemos uma busca em vários índices simultaneamente.
  9. Excluímos um documento.
  10. Excluímos um índice inteiro.

Para obter mais informações sobre os tópicos desta seção, consulte estes links:

Experimento com dados mais interessantes

Até agora, fizemos experimentos com alguns dados fictícios. Nesta seção, exploraremos as peças de Shakespeare. O primeiro passo é baixar o arquivo shakespeare.json, disponível em Kibana: Loading Sample Data (Carregamento de dados de amostra). O Elasticsearch oferece uma Bulk API que permite adicionar, excluir, atualizar e criar operações em massa, ou seja, muitas de uma vez. Esse arquivo contém dados prontos para serem ingeridos usando essa API, preparados para serem indexados em um índice chamado Shakespeare contendo documentos do tipo “act', “scene” e “line”. O corpo das solicitações para a Bulk API consiste em 1 objeto JSON por linha; para operações de adição, como as do arquivo, há 1 objeto JSON indicando metadados sobre a operação de adição e um segundo objeto JSON na próxima linha contendo o documento a ser adicionado:

{"index":{"_index":"shakespeare","_type":"act","_id":0}}
{"line_id":1,"play_name":"Henry IV","speech_number":"","line_number":"","speaker":"","text_entry":"ACT I"}

Não vamos nos aprofundar na Bulk API: caso haja interesse, consulte a documentação da Bulk API.

Vamos colocar todos esses dados no Elasticsearch. Como o corpo desta solicitação é bastante grande (mais de 200 mil linhas), é recomendável fazer isso por meio de uma ferramenta que permita carregar o corpo de uma solicitação de um arquivo — por exemplo, usando curl:

curl -XPOST "localhost:9200/shakespeare/_bulk?pretty" --data-binary @shakespeare.json

Uma vez carregados os dados, podemos começar a fazer algumas buscas. Na seção anterior, fizemos as buscas passando a consulta no URL. Nesta seção, apresentaremos o Query DSL, que especifica um formato JSON a ser usado no corpo das solicitações de busca para definir as consultas. Dependendo do tipo de operação, as consultas podem ser emitidas usando os verbos GET e POST. Vamos começar com o mais simples: obter todos os documentos. Para fazer isso, especificamos no corpo uma chave query e, para o valor, a consulta match_all.

GET localhost:9200/shakespeare/_search
{
    "query": {
            "match_all": {}
    }
}

O resultado mostrará 10 documentos; segue uma saída parcial:

{
    "took": 7,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "failed": 0
    },
    "hits": {
        "total": 111393,
        "max_score": 1,
        "hits": [
              ...          
            {
                "_index": "shakespeare",
                "_type": "line",
                "_id": "19",
                "_score": 1,
                "_source": {
                    "line_id": 20,
                    "play_name": "Henry IV",
                    "speech_number": 1,
                    "line_number": "1.1.17",
                    "speaker": "KING HENRY IV",
                    "text_entry": "The edge of war, like an ill-sheathed knife,"
                }
            },
            ...          

O formato das buscas é bastante simples. Há muitos tipos diferentes de buscas disponíveis: a Elastic oferece buscas diretas (“Buscar este termo”, “Buscar elementos neste intervalo” etc.) e consultas compostas (“a AND b”, “a OR b” etc). A referência completa pode ser encontrada na documentação do Query DSL; apresentaremos apenas alguns exemplos aqui para mostrar como podemos usá-los.

POST localhost:9200/shakespeare/scene/_search/
{
    "query":{
        "match" : {
            "play_name" : "Antony"
        }
    }
}

Na consulta anterior, procuramos todas as cenas (consulte o URL) nas quais o nome da peça contém Antony. Podemos refinar essa busca e selecionar também as cenas em que Demetrius é quem fala:

POST localhost:9200/shakespeare/scene/_search/
{
    "query":{
        "bool": {
            "must" : [
                {
                    "match" : {
                        "play_name" : "Antony"
                    }
                },
                {
                    "match" : {
                        "speaker" : "Demetrius"
                    }
                }
            ]
        }
    }
}

Como primeiro exercício, modifique a consulta anterior para que a busca retorne não apenas cenas em que o falante é Demetrius, mas também cenas em que o falante é Antony; como dica, verifique a cláusula booliana should. Como segundo exercício, resta explorar as diferentes opções que podem ser utilizadas no [corpo da solicitação](https://www.elastic.co/guide/en/elasticsearch/reference/5.5/search-request -body.html) quando fazemos uma busca — por exemplo, selecionando de qual posição nos resultados queremos iniciar e quantos resultados queremos recuperar para fazer a paginação.

Até agora, fizemos algumas consultas usando o Query DSL. E se, além de recuperar os conteúdos que procuramos, também pudermos fazer algumas análises? É aqui que as agregações entram em jogo. As agregações nos permitem obter uma visão mais profunda dos dados: por exemplo, quantas peças diferentes existem no nosso conjunto de dados atual? Quantas cenas há por obra, em média? Quais são as obras com mais cenas?

Antes de pular para os exemplos práticos, vamos dar um passo para trás até o ponto em que criamos o índice de Shakespeare, pois continuar sem um pouco de teoria seria um desperdício. No Elastic, podemos criar índices definindo quais são os tipos de dados para os diferentes campos que eles podem ter: campos numéricos, campos de palavras-chave, campos de texto… há muitos tipos de dados. Os tipos de dados que um índice pode ter são definidos por meio dos mapeamentos. Neste caso, não criamos nenhum índice antes de indexar os documentos, então o Elastic decidiu qual era o tipo de cada campo (ele criou o mapeamento do índice). O tipo text foi selecionado para os campos textuais: esse tipo é analisado, que é o que nos permitiu encontrar play_name Antony and Cleopatra simplesmente buscando Antony. Por padrão, não podemos fazer agregações nos campos analisados. Como vamos mostrar agregações se os campos não são válidos para fazê-las? Quando o Elastic decidiu o tipo de cada campo, também adicionou uma versão não analisada dos campos de texto (chamada keyword) para o caso de querermos fazer agregações/classificações/scripts: podemos apenas usar play_name.keyword nas agregações. Como exercício, resta saber como inspecionar os mapeamentos atuais.

Depois desta lição relativamente breve e teórica, vamos voltar ao teclado e às agregações! Podemos começar a inspecionar nossos dados verificando quantas peças diferentes temos:

GET localhost:9200/shakespeare/_search
{
    "size":0,
    "aggs" : {
        "Total plays" : {
            "cardinality" : {
                "field" : "play_name.keyword"
            }
        }
    }
}

Observe que, como não estamos interessados nos documentos, decidimos apenas mostrar 0 resultados. Além disso, como queremos explorar todo o índice, não temos uma seção de consulta: as agregações serão calculadas usando todos os documentos que atendem à consulta, cujo padrão é match_all, neste caso. Por fim, decidimos usar uma agregação cardinality que nos permitirá saber quantos valores únicos temos para o campo play_name.

{
    "took": 67,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "failed": 0
    },
    "hits": {
        "total": 111393,
        "max_score": 0,
        "hits": []
    },
    "aggregations": {
        "Total plays": {
            "value": 36
        }
    }
}

Agora, vamos listar as peças que aparecem com mais frequência no nosso conjunto de dados:

GET localhost:9200/shakespeare/_search
{
    "size":0,
    "aggs" : {
        "Popular plays" : {
            "terms" : {
                "field" : "play_name.keyword"
            }
        }
    }
}

Sendo o resultado:

{
    "took": 35,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "failed": 0
    },
    "hits": {
        "total": 111393,
        "max_score": 0,
        "hits": []
    },
    "aggregations": {
        "Popular plays": {
            "doc_count_error_upper_bound": 2763,
            "sum_other_doc_count": 73249,
            "buckets": [
                {
                    "key": "Hamlet",
                    "doc_count": 4244
                },
                {
                    "key": "Coriolanus",
                    "doc_count": 3992
                },
                {
                    "key": "Cymbeline",
                    "doc_count": 3958
                },
                {
                    "key": "Richard III",
                    "doc_count": 3941
                },
                {
                    "key": "Antony and Cleopatra",
                    "doc_count": 3862
                },
                {
                    "key": "King Lear",
                    "doc_count": 3766
                },
                {
                    "key": "Othello",
                    "doc_count": 3762
                },
                {
                    "key": "Troilus and Cressida",
                    "doc_count": 3711
                },
                {
                    "key": "A Winters Tale",
                    "doc_count": 3489
                },
                {
                    "key": "Henry VIII",
                    "doc_count": 3419
                }
            ]
        }
    }
}

Podemos ver os 10 valores mais populares de play_name. Cabe ao leitor consultar a documentação para descobrir como mostrar mais ou menos valores na agregação.

Se você chegou até aqui, certamente pode descobrir o próximo passo: combinar agregações. Poderíamos nos interessar em saber quantas cenas, atos e falas temos no índice; mas também, poderíamos estar interessados no mesmo valor por peça. Podemos fazer isso aninhando agregações dentro de agregações:

GET localhost:9200/shakespeare/_search
{
    "size":0,
    "aggs" : {
        "Total plays" : {
            "terms" : {
                "field" : "play_name.keyword"
            },
            "aggs" : {
                "Per type" : {
                    "terms" : {
                        "field" : "_type"
                     }
                }
            }
        }
    }
}

E uma parte da resposta:

    "aggregations": {
        "Total plays": {
            "doc_count_error_upper_bound": 2763,
            "sum_other_doc_count": 73249,
            "buckets": [
                {
                    "key": "Hamlet",
                    "doc_count": 4244,
                    "Per type": {
                        "doc_count_error_upper_bound": 0,
                        "sum_other_doc_count": 0,
                        "buckets": [
                            {
                                "key": "line",
                                "doc_count": 4219
                            },
                            {
                                "key": "scene",
                                "doc_count": 20
                            },
                            {
                                "key": "act",
                                "doc_count": 5
                            }
                        ]
                    }
                },
                ...

Existem várias agregações diferentes no Elasticsearch: agregações usando o resultado de agregações, agregações de métricas como cardinality, agregações de buckets como terms… Dê uma olhada na lista e decida qual agregação se encaixará em um caso de uso específico que você já tenha em mente! Talvez uma agregação de Significant terms para encontrar o incomumente comum?

E este é o fim da segunda seção. Vamos resumir o que fizemos:

  1. Usamos a Bulk API para adicionar peças de Shakespeare.
  2. Fizemos buscas simples, inspecionando o formato genérico para fazer consultas via Query DSL.
  3. Fizemos uma busca de folha, procurando um texto em um campo.
  4. Fizemos uma busca composta, combinando duas buscas de texto.
  5. Propusemos adicionar uma segunda busca composta.
  6. Propusemos testar diferentes opções no corpo da solicitação.
  7. Apresentamos o conceito de agregações, juntamente com uma breve revisão de mapeamentos e tipos de campos.
  8. Calculamos quantas peças temos no nosso conjunto de dados.
  9. Recuperamos quais eram as peças que apareciam com mais frequência no conjunto de dados.
  10. Combinamos várias agregações para ver quantos atos, cenas e falas cada uma das 10 peças mais frequentes teve.
  11. Propusemos explorar mais algumas das agregações no Elastic.

Alguns conselhos extras

Durante esses exercícios, exploramos um pouco o conceito de tipos no Elasticsearch. No final, um tipo é apenas um campo extra interno: vale ressaltar que, a partir da versão 6, só permitiremos a criação de índices com um único tipo e, a partir da versão 7, espera-se que os tipos sejam removidos. Mais informações podem ser encontradas neste post do blog.

Conclusão

Neste post, usamos alguns exemplos para apresentar uma pequena amostra do que o Elasticsearch pode fazer.

Há muito (muito!) mais no Elasticsearch e no Elastic Stack para explorar do que o que foi mostrado neste breve artigo. Um item que vale a pena mencionar antes de terminarmos é a relevância. O Elasticsearch não responde apenas a Este documento atende aos meus requisitos de busca?, mas também a O quanto este documento atende aos meus requisitos de busca?, oferecendo primeiro os resultados de busca mais relevantes para a consulta. A documentação é extensa e está cheia de exemplos.

Antes de implementar qualquer novo recurso customizado, é aconselhável verificar a documentação para ver se já o implementamos, facilitando o aproveitamento para o seu projeto. É bem possível que um recurso ou ideia que você considere útil já esteja disponível, pois nosso roadmap de desenvolvimento é fortemente influenciado pelo que nossos usuários e desenvolvedores nos dizem que querem!

Se você precisa de autenticação/controle de acesso/criptografia/auditoria, esses recursos já estão disponíveis no Security. Se você precisa monitorar o cluster, isso está disponível no Monitoring. Se você precisa criar campos sob demanda nos seus resultados, isso já é possível por meio dos [campos de script](https://www.elastic.co/guide/en/elasticsearch/reference/5.5/search-request-script-fields. html). Se você precisa criar alertas por email/Slack/Hipchat/etc., isso está disponível via Alerting. Se você precisa visualizar os dados e as agregações em gráficos, nós já oferecemos um ambiente rico para fazer isso: o Kibana. Se você precisa indexar dados de bancos de dados, arquivos de log, filas de gerenciamento ou praticamente qualquer fonte imaginável, isso é oferecido via Logstash e Beats. O que você precisar… verifique se já pode ter!