Skip to content

Instantly share code, notes, and snippets.

@reinaldocoelho
Last active November 22, 2022 15:27
Show Gist options
  • Save reinaldocoelho/7ec6ab7d01d34abc14a538141bf5ac5c to your computer and use it in GitHub Desktop.
Save reinaldocoelho/7ec6ab7d01d34abc14a538141bf5ac5c to your computer and use it in GitHub Desktop.
Documento de dicas de uso do Elastic Search

Ajuda com ElasticSearch

ElasticSearch 2

ElasticSearch

Elastic search é a API do servidor de pesquisa propriamente dito e depende das portas 9200 e 9300 onde a 9200 é a API HTTP para uso.

URL Padrão: http://localhost:9200

Kibana

URL Padrão: http://localhost:5601

Equiparação com banco relacional

No Relacional No ElasticSearch
Instância Indice/Index
Tabela Tipo/Type
Schema Mapeamento/Mapping
Registro/Tupla Documento/Objeto
Coluna Atributo

Importante: /index/type/id é um identificador único de um documento em um cluster ElasticSearch. Esta tripla é conhecida como Unique Resource Identifier (URI, identificador único de recurso).

Importante2: Incluir os Types na pesquisa foi depreciado em versões recentes e apresenta esse erro por exemplo na versão 7: #! Deprecation: [types removal] Specifying types in search requests is deprecated.

Shards

Shards (cacos) são estruturas de dados distribuidas do Elasticsearch, ele utiliza os chards para permitir que o banco escale, distribuindo os dados em pedaços sendo estes discos/máquinas diferentes.

A quantidade e distribuição de shards vão variar do uso do ElasticSearch e isso pode variar muito.

Alguns padrões estruturais dos Shards:

  1. Um shard não deve exceder 50GB de dados.

Usos e pesquisas

Listando todos os indices

GET _cat/indices ou GET _cat/indices?v

Vendo as configurações do indice

GET /index/_settings

Recuperando a quantidade de documentos no indice

GET indice/_count

Exists com ElasticSearch

Uma boa maneira de efetuar Exists com elastic search é efetuar a pesquisa usando protocolo HEAD, ele irá retornar a informação de existência de um objeto com determinado Id.

curl -XHEAD -v http://{servidor}:9200/{indice}/{tipo}/{id}

Se o item existir, será retornado status HTTP 200, caso contrario HTTP 404.

Exemplo pesquisa enContact (e-mail):

curl -XHEAD -v http://localhost:9200/ect_email_gsearch_2/_doc/114371

Desta forma o comando Elastic Search e SQL abaixo seriam equivalentes:

  • HEAD /index/type/id
  • select 1 from TYPE where id = ID;

Podemos também verificar a somente os campos que tem valor em algum valor no campo, utilizando o parâmetro exists, como no exemplo abaixo:

GET pessoas/_search
{
    "query": {
        "exists": {
            "field": "cidade"
        }
    }
}

Desta forma somente os documentos que tem valor no campo cidade seriam retornados.

Busca por Id com ElasticSearch

Para efetuar uma busca diretamente por um Id é simples, pode-se efetuar a seguinte chamada:

curl -XGET -v http://{servidor}:9200/{indice}/{tipo}/{id}

Desta forma, o comando Elastic Search e SQL abaixo seriam equivalentes:

  • GET /index/type/id
  • select * from TYPE where id = ID;

Caso o documento não seja encontrado será retornado o status HTTP 404 como resposta a pesquisa.

Busca por campo/atributo com ElasticSearch

Para efetuar uma busca em qualquer campo do documento é simples, pode-se efetuar a seguinte chamada:

curl -XGET -v http://{servidor}:9200/{indice}/_search?q={texto_pesquisado}

ou

curl -XGET -v http://{servidor}:9200/{indice}/_search?q=_all:{texto_pesquisado}

O Elastic Search ao indexar o documento cria um campo escondido chamado _all que contém uma junção de todas as palavras chaves do documento, e se nenhum campo é especificado para busca, o campo _all é utilizado.

Para efetuar aa busca apenas num campo específico também é simples, indique qual o campo antes do texto pesquisado:

curl -XGET -v http://{servidor}:9200/{indice}/_search?q={campo}:{texto_pesquisado}

E para filtros em campos diferentes:

curl -XGET -v http://{servidor}:9200/{indice}/_search?q=interesses:futebol&cidade:rio

Por padrão esta pesquisa é processada como OU ou seja, se atender uma das pesquisas retorna, para usar uma sintaxe de E pode-se:

curl -XGET -v http://{servidor}:9200/{indice}/_search?q=interesses:futebol+AND+cidade:rio

Buscas com aggregations (Group by)

Mais em: https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html

Efetuando contagens (count)

Equivalente ao SQL SELECT estado, count(*) FROM _doc WHERE formacao = 'fisica' GROUP BY estado

Temos a seguinte chamada:

GET pessoas/_search
{
    "query": {
        "match": { "formacao": "fisica" }
    },
    "aggs": {
        "pessoas formadas em fisica por estado": {
            "terms": {
                "field": "estado"
            }
        }
    }
}

Paginação e limitação de resultados

Para limitar a quantidade de itens retornados, é só indicar o filtro size com a quantidade desejada, exemplo:

curl -XGET -v http://{servidor}:9200/{indice}?q={texto_pesquisado}&size=10

Para efetuar paginação, você pode limitar a quantidade de registros e indicar o documento inicial, desta forma ele irá mostrar a página:

curl -XGET -v http://{servidor}:9200/{indice}?q={texto_pesquisado}&size=10&from=0

O filtro from indica a pagina de resultado onde será iniciada a pesquisa.

Nota: Apesar de possivel, não é uma boa abordagem de paginação pois todos os itens ainda seriam carregados em memória para filtrar.

Importante: Paginação deve ser usada apenas para pequenos volumes de dados, como alguns poucos milhares. Para volumes maiores, devemos utilizar a abordagem da API scroll (https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html).

Buscas limitando os campos de retorno

Podemos efetuar pesquisas também ajustando os campos a serem retornados, usando o parâmetro _source, como no exemplo abaixo:

GET indice/_search
{
    "_source": ["nome", "cidade", "documento"],
    "query": {
        "term": { "estado": "SP" }
    }
}

Neste caso filtramos somente aos usuários de SP retornando os campos nome, cidade e documento registrados na base. Um exemplo do resultado seria:

{
    "_index": "indice",
    "_type": "_doc",
    "_id": "xyz",
    "_score": 1.0,
    "_source": {
        "nome": "Reinaldo",
        "cidade": "Osasco",
        "documento": "123456"
    }
}

Ordenação de resultados (Sort)

Podemos efetuar no ElasticSearch também a ordenação dos dados no retorno utilizando o campo sort, como no exemplo:

GET indice/_search
{
    "_source": ["nome", "cidade", "documento"],
    "query": {
        "term": { "estado": "SP" }
    },
    "sort": [
        {
            "cidade.original": { "order": "asc" }
        }
    ]
}

Esta pesquisa seria equivalente ao SQL:

SELECT nome, cidade, documento FROM indice WHERE estado = 'SP' ORDER BY cidade.original/cidade asc

Nota: No caso acima ele ordena pelo campo cidade.original, provavelmente ele estará aplicando na ordenação o processamento do token, e isso pode alterar a ordem de retorno.

Entendendo os campos retornados numa busca

Vamos ao significados:

took: duração da consulta em milissegundos. time_out: valor booleano indicando se a consulta foi abortada por limite de tempo ou não. _shards.total: número de shards envolvidas na busca. _shards.successful: número de shards que não apresentaram falha durante a busca. _shards.failed: número de shards que apresentaram falha durante a busca. Note que o resultado pode não apresentar documentos presentes nas shards que falharam. hits.total: quantidade de documentos encontrados. hits.max_score: valor máximo de score entre os documentos encontrados. O valor 1.0 significa que o valor da busca foi encontrado totalmente. hits.hits: os documentos encontrados.

Gravações com DELETE/PUT/POST

DELETE /index/type/id

Uma requisição DELETE /index/type/id é Equivalente ao comando:

delete from TYPE where id = ID;

Remove o documento do tipo caso ele exista, ou NOT FOUND 404, caso contrário.

PUT /index/type/id

Aqui temos uma pequena ressalva em relação aos comandos SQL. A rigor, o comando PUT significa coloque o documento sob o localizador /index/type/id. Repare que id, assim como nos comandos anteriores é obrigatório. Poderíamos pensar então que o comando PUT é uma espécie de:

insert into TYPE (id, atributo1, atributo2) values (ID, valor1, valor2);

A sintaxe seria:

PUT /catalogo/pessoas/

{
    "nome": "João Silva",
    "interesses": [
        "futebol",
        "música",
        "literatura"
    ],
    "cidade": "São Paulo",
    "formação": "Letras",
    "estado": "SP",
    "país": "Brasil"
}

Contudo, também podemos utilizar o comando PUT para substituir um documento. Neste caso, o comando PUT funciona como uma espécie de:

update TYPE set atributo1 = valor1, atributo2 = valor2 where id = ID;

A sintaxe seria:

PUT /catalogo/pessoas/1/_update
{
    "doc": {
        "nome": "João Pedro"
    }
}

Importante: PUT não permite efetuar uma atualização parcial do registro, somente do registro completo.

POST /index/type

Aqui temos mais uma pequena ressalva em relação aos comandos SQL. A rigor, o comando POST significa crie um documento sob /index/type. Repare que, diferentemente do comando PUT e dos outros comandos, id não é obrigatório.

Poderíamos pensar então que o comando POST é uma espécie de insert que utiliza sequences ou um algum tipo de auto incremento. Contudo, também podemos utilizar o comando POST para atualizar um documento existente assim como o comando PUT. A diferença é que POST nos permite atualização parcial de documentos. Por exemplo:

POST /catalogo/pessoas/1/_update
{
    "doc": {
        "nome": "João Pedro"
    }
}

Note que, neste caso, o uso de "doc" é obrigatório.

Importante: Uma vez que um documento é criado em uma instância do ElasticSearch, este documento torna-se imutável. No caso de uma atualização a um documento existente, por exemplo como fizemos com o método POST, uma nova versão do documento é criada. Se repararmos bem nas respostas recebidas, notaremos os atributos *_created e _version. A resposta para a criação de um novo documento possui _created = true e _version = 1. A resposta para atualizações possui _created = false e _version será a versão anterior do documento acrescida de 1.

Mapping (Schema)

O mapping do Elastic Search é equivalente a estrutura de dados de uma tabela no SQL, ou seja, ver o mapping do indice é como executar o comando desc TABELA num banco MariaDB por exemplo.

No caso para ver o mapeamento no ElasticSearch se usa:

curl -XGET -v http://{servidor}:9200/{indice}/_mapping

Ao se enviar um documento Json, o ElasticSearch irá tentar inferir os tipois inicialmente.

Dica Para ver o Json resultante formatado bunitinho, usar &pretty ao final, por exemplo

curl -XGET -v http://{servidor}:9200/{indice}/_mapping?pretty

Dica 2 Os seguintes tipos são suportados no ElasticSearch: https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html.

Analyzers (Analisadores)

Os analysers mais comuns são:

  • Espaço em branco (whitespace) - Basicamente sempre que encontra um espaço efetua quebra e cria um token.
  • Simples (simple) - Este analisador reduz o texto removendo tudo que não forem palavras (numeros, virgula, acentos, parenteses e etc), e transforma todo texto final em caixa baixa.
  • Padrão (standard) - Altera os textos para caixa baixa, mas não remove números, este é o padrão do ElasticSearch se nenhum analyser for definido.
  • Idioma (ex. portuguese, english) - Aqui ele efetua tratamentos específicos da linguagem como retirar Plural/Singular, tratar acentuações, sinonimos e etc.

Um exemplo simples de como aplicar um analyser num texto simples, seria o processo abaixo, onde imagine que temos a seguinte frase:

Eu nasci a 10 mil (sim, 10 mil) anos atrás

E efetuamos a seguinte pesquisa:

GET /_analyze?analyzer=standard&text=Eu+nasci+a+10+mil+(sim,+10+mil)+anos+atrás

Analise gerada para esta frase na chamada acima:

{
  "tokens": [
    {
      "token": "eu",
      "start_offset": 0,
      "end_offset": 2,
      "type": "<ALPHANUM>",
      "position": 0
    },
    {
      "token": "nasci",
      "start_offset": 3,
      "end_offset": 8,
      "type": "<ALPHANUM>",
      "position": 1
    },
    {
      "token": "",
      "start_offset": 9,
      "end_offset": 10,
      "type": "<ALPHANUM>",
      "position": 2
    },
    {
      "token": "10",
      "start_offset": 11,
      "end_offset": 13,
      "type": "<NUM>",
      "position": 3
    },
    {
      "token": "mil",
      "start_offset": 14,
      "end_offset": 17,
      "type": "<ALPHANUM>",
      "position": 4
    },
    {
      "token": "sim",
      "start_offset": 19,
      "end_offset": 22,
      "type": "<ALPHANUM>",
      "position": 5
    },
    {
      "token": "10",
      "start_offset": 24,
      "end_offset": 26,
      "type": "<NUM>",
      "position": 6
    },
    {
      "token": "mil",
      "start_offset": 27,
      "end_offset": 30,
      "type": "<ALPHANUM>",
      "position": 7
    },
    {
      "token": "anos",
      "start_offset": 32,
      "end_offset": 36,
      "type": "<ALPHANUM>",
      "position": 8
    },
    {
      "token": "atrás",
      "start_offset": 37,
      "end_offset": 42,
      "type": "<ALPHANUM>",
      "position": 9
    }
  ]
}

Mais sobre analysers em: https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-analyzers.html Mais sobre Índice invertidos em: https://www.elastic.co/guide/en/elasticsearch/guide/current/inverted-index.html Mais sobre tratamento de sinônimos: https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-synonym-tokenfilter.html Mais sobre Fuzzy match: https://www.elastic.co/guide/en/elasticsearch/guide/current/fuzzy-matching.html

Efetuando contagens (_count)

Para efetuar contagem de registros, existe uma api _count que pode ser usada como abaixo.

GET /indice/_count

Inserções em lote (Bulk)

Para efetuar a importação de registros em lote, utilizamos a api _bulk, passando em seguida a lista de registros a serem inseridos no ElasticSearch.

POST /pessoas/_bulk
{"create": {}}
{"nome": "Kamille Carlete Matielo", "cidade": "Senhora da Gloria", "formação": "Economia", "estado": "MG", "país": "Brasil", "interesses": ["volleyball","basquete","matemática","física"] }
{"create": {}}
{"nome": "Kandy Horita", "cidade": "Vila Dirce", "formação": "Artes Cênicas", "estado": "SP", "país": "Brasil", "interesses": ["society","volei","voleibol"] }
{"create": {}}
{"nome": "Karen Ayumi Miyamoto", "cidade": "Dores do Rio Preto", "formação": "História", "estado": "ES", "país": "Brasil", "interesses": ["volei","voleibol"] }
{"create": {}}
{"nome": "Karen Batista Caixeta", "cidade": "Porto Novo", "formação": "Física", "estado": "SP", "país": "Brasil", "interesses": ["futebol","society","volei","voleibol"] }

E para inserir informando um Id pode-se usar:

POST star_wars/_bulk
{ "index" : { "_id" : "4" } }
{ "titulo" : "Solo: Uma História Star Wars" }
{ "delete" : { "_id" : "4" } }
{ "index" : { "_id" : "4" } }
{ "titulo" : "Rogue One: Uma História Star Wars" }
{ "delete" : { "_id" : "4" } }
{ "index" : { "_id" : "4" } }
{ "titulo" : "Episode IV - A New Hope" }
{ "update" : {"_id" : "4"} }
{ "doc" : {"titulo" : "Episódio IV - Uma nova esperança"} }
{ "index" : { "_id" : "5" } }
{ "titulo" : "Episódio V - O Império Contra-Ataca" }
{ "index" : { "_id" : "6" } }
{ "titulo" : "Episódio VI - O Retorno de Jedi" }
{ "index" : { "_id" : "1" } }
{ "titulo" : "Episódio I - A Ameaça Fantasma" }
{ "index" : { "_id" : "2" } }
{ "titulo" : "Episódio II - Ataque dos Clones" }
{ "index" : { "_id" : "3" } }
{ "titulo" : "A Guerra dos Clones" }
{ "index" : { "_id" : "3" } }
{ "titulo" : "Episódio III - A Vingança dos Sith" }
{ "index" : { "_id" : "7" } }
{ "titulo" : "Episódio VII - O Despertar da Força" }
{ "index" : { "_id" : "8" } }
{ "titulo" : "Episódio VIII - Os Últimos Jedi" }
{ "index" : { "_id" : "9" } }
{ "titulo" : "Episódio IX - The Rise of Skywalker" }

Shards e Réplicas

Shard é um termo que passou a ser muito utilizado quando pensamos em particionamento horizontal de dados. Para colocar de modo mais claro, pense que temos um volume muito grande de dados, tão grande, que é inviável em armazená-lo em um único local. Nosso instinto de bom engenheiro de software nos diz que deveríamos quebrar este grande volume de dados em blocos menores, já que fica mais fácil de operar com blocos menores durante operações como cópia, remoção ou relocação. Cada pedacinho resultante desta quebra é o que chamamos de shard.

Shards são de altíssima importância quando pensamos em escalabilidade horizontal. Podemos quebrar grandes blocos de dados em pedaços menores e distribuí-los entre diferentes máquinas em um cluster. Dado o número de shards, podemos usar uma função de espalhamento (hashing) para definir em qual shard um determinado documento deve ser armazenado. Isso é exatamente o que o ElasticSearch faz (bem, o processo na prática é um pouco mais complexo, mas a ideia é a mesma). A escalabilidade horizontal para o volume de dados que foi comentada no capítulo anterior é implementada com o uso de shards.

Como temos diversas shards, podemos criar cópias, chamadas de réplicas, de uma mesma shard e armazená-las em outras máquinas. Isso funciona como uma espécie de backup dos dados, com uma pequena ressalva: réplicas são atualizadas constantemente e podem ser utilizadas para leitura durante a execução de consultas. Existem dois tipos de shards no ElasticSeach:

Shard primária (primary shards): é a shard onde as operações de escrita como criação, atualização ou remoção de um documento acontece primeiro. Shard réplica (replica shard): é a shard que, uma vez que a operação de escrita tenha sido concluída com sucesso na sua respectiva shard primária, recebe a mesma operação para que ela seja replicada. A operação só será confirmada para o cliente quando todas as réplicas confirmarem a replicação. Logo, quando recebemos o HTTP OK para uma operação de escrita, sabemos que a informação esta segura em todas as réplicas.

Importante: O número de *shards é definido no momento da criação do índice e não pode ser alterado. O número de réplicas também é definido no momento da criação do índice, porém pode ser alterado com o passar do tempo. É muito importante escolher bem o número de shards durante a criação do índice. A escolha deste número depende do volume de informações que queremos armazenar nas shards. Em geral, queremos shards com alguns gigabytes.

Ajuda com ElasticSearch 5

O Elasticsearch 5 é o sucessor do Elasticsearch 2 mesmo que o versionamento possa indicar que há várias outras versões, isso não é a verdade. Na documentação abaixo podemos verificar isto:

https://www.elastic.co/guide/en/elasticsearch/reference/current/setup-upgrade.html

Detalhes alterados da versão 2 para 5

  • A partir do ElasticSearch 5, em vez usarmos a palavra terms, usamos keywords
  • Antes da versão 5, o ElasticSerch inferia propriedades decimais como double, a partir desta versão, passou a considerar o tipo float.

Saúde do Cluster

Para verificarmos a saúde do Cluster, podemos usar o comando abaixo:

GET /_cat/health?v

Retirando o alerta de replicas

Quando subimos o Kibana e executarmos o comando GET /_cat/health?v consultamos a saúde do nosso cluster. Contudo, sem qualquer outra configuração, ele estará yellow e isso não permitirá executarmos outras operações.

Como estamos em ambiente de desenvolvimento, podemos indicar que o número de réplicas é 0. Isso fará com que nosso cluster deixe de ser yellow. Aliás, essa mudança é válida apenas para ambiente de desenvolvimento.

Para alterar o Kibana para aceitar o servidor sem réplicas, execute:

PUT /.kibana/_settings
{
    "index": {
        "number_of_replicas": 0
    }
}

Lembrete: Isso não deve ser utilizado em ambiente produtivo, pois sem réplicas em caso de conflito ou inconsistência o servidor irá perder dados.

Buscas

  • Filters são buscas binárias, elas resultam em atende/não atende o filtro, neste caso a score/relevância não é importante e tem valores cacheados (barato e eficiente).
  • Queries são buscas que atendem score/relevância do resultado, estas pesquisas não são cacheadas.
    • Exemplo de query: GET /produtos/v1/_search?q=digital, o campo _score indica a similaridade do retorno com a pesquisa.

Desvendando Queries

Queries DSL

Mais em: https://www.elastic.co/guide/en/elasticsearch/reference/5.2/query-dsl-bool-query.html

Match, Should, Must, Must_Not

Para efetuar uma query ElasticSearch equivalente a um comando Select * from indice

GET /indice/_search
{
  "query": {
    "match_all": {}
  }
}

Para efetuar uma query de pesquisa simples equivalente ao comando Select * from indice where categoria = 'calçados'

GET /indice/_search
{
    "query": {
        "match": {
            "categoria": "calçados"
        }
    }
}

ou da lista de tags

GET /indice/_search
{
    "query": {
        "match": {
            "tags": "impresso"
        }
    }
}

Para uma pesquisa de AND equivalente a select * from indice where tags = 'impresso' and nome = 'scala':

GET /indice/_search
{
    "query": {
        "bool": {
            "must": [
                {"match": {"tags": "impresso" }},
                {"match": {"nome": "scala" }}
            ]
        }
    }
}

Nas queries ao colocar um texto maior, ele é sempre interpretado como OR na quebra das palavras, entretanto, podemos alterar a forma utilizando o parâmetro operator, como abaixo:

GET /produtos/v1/_search
{
  "query" : {
    "match": {
      "nome": {
        "query" : "big data futebol brasileiro",
        "operator": "and"
      }
    }
  }
}
GET /produtos/v1/_search
{
  "query" : {
    "match": {
      "nome": {
        "query" : "brasileiro futebol",
        "operator": "or"
      }
    }
  }
}

Podemos também incluir um filtro que indica uma exigência para aprimorar a query como por exemplo, indicar quantos % dos elementos devem ser encontrados obrigatoriamente para o documento ser considerado no retorno. Por exemplo:

GET /produtos/v1/_search
{
  "query" : {
    "match": {
      "nome": {
        "query" : "big data futebol brasileiro",
        "minimum_should_match": "50%"
      }
    }
  }
}

Usando o operador minimum_should_match acima, pelo menos 2 palavras devem constar no texto, independentemente da ordem, para que ele seja um resultado válido.

  • Match: É a exigência de que o campo e valor escolhidos existam.
  • bool: Indica que resultado da query (no caso o resultado do filtro executado mesmo), precisa ser um booleano (ou encontra ou não), sem aproximação. E o Bool deve ser colocado sempre que for usado MUST ou MUST_NOT.
  • Should É o equivalente a incluir um OR nas opções que estiverem inclusas nele.
  • Must: É o equivalente ao AND do SQL e quantos itens estiverem dentro do array MUST são obrigatórios. Por padrão as queries do Elastic rodam como OR.
  • Must_not É para negar ou realmente não conter na pesquisa o que for indicado nele. Como um AND NOT mesmo.
GET /indice/_search
{
    "query": {
        "bool": {
            "must": {"match": {"categoria": "livro" }},
            "should": [
                {"match": {"tags": "imutabilidade" }},
                {"match": {"tags": "larga escala"}}
            ],
            "must_not": {"match": {"nome": "scala"}}
        }
    }
}

Dica: Como pode ser visto, todos esses itens de filtro podem ter um ou vários objetos de validação.

Filters

Filtros normalmente são utilizados com a intenção de delimitar a pesquisa, por exemplo Produtos com valor menor que 100 reais ou que A data esteja no período de X e Y.

Term - Buscas exatas nos campos

O term sempre busca exatamente o mesmo string definido na busca, sem aplica nenhum analyzer antes. O match já use os mesmos analyzers desse campo. Importante: No final isso significa que se você tem o valor física gravado e processado para o token fisic, ao efetuar a busca usando term e passando o valor de filtro fisica, o banco não irá retornar nenhuma informação, porque ele está procurando no campo que o token foi processado para o valor fisic.

Por esse motivo guardamos o campo original no index. Usando o term junto com campo original fará algo que estamos acostumado no mundo SQL. Por exemplo, a query abaixo procura pela categoria livro que na verdade se chama de Livros:

GET /produtos/v1/_search
{
  "query" : {
        "match" : {"categoria": "livro" }
    }
}

Ao executar a pesquisa em cima entramos 3 documentos, mas se testarmos com term encontramos nada:

GET /produtos/v1/_search
{
  "query" : {
        "term" : {"categoria": "livro" }
    }
}

Nem adianta colocar Livros como valor:

GET /produtos/v1/_search
{
  "query" : {
        "term" : {"categoria": "Livros" }
    }
}

Mas se usamos o campo original (campo que deixamos com o valor sem processamento do token) encontramos os 3 documentos:

GET /produtos/v1/_search
{
  "query" : {
        "term" : {"categoria.original": "Livros" }
    }
}

Dica: A diferença entre o match e o term é que o match usa os analyzers definidas e o term não aplica nenhum analyzer (exact match). Dica2: Você pode usar term no singular ou plural onde term aplica um filtro e terms aplica um array de filtros.

Range

Mais em: https://www.elastic.co/guide/en/elasticsearch/reference/5.2/query-dsl-range-query.html

Efetuar uma pesquisa entre dados usamos a propriedade range. Por exemplo para termos a mesma pesquisa do SQL Select * from produtos where preco >= 20 and <= 50 usamos:

GET /produtos/v1/_search
{
  "query" : {
    "range": {
      "preco": {
        "lte": 50,
        "gte": 20
      }
    }
  }
}

Nota: A clausula filter faz parte da Bool Query do Elasticsearch 5. Em outras palavras, o filter, como must, must_not e should, sempre deve ser embrulhado pelo elemento bool. O interessante é que podemos combinar o filter com as outras clausulas. Isso é algo muito comum no dia a dia. Filtramos os documentos para aplicar depois uma query. Exemplo:

GET /produtos/v1/_search
{
  "query" : {
    "bool": {
      "must": {
        "match": {"tags": "esportes"}},
      "filter": {
        "range": {"preco": {"lte": 100}}
      }
    }
  }
}
GET /produtos/v1/_search
{
    "query" : {
        "bool": {
            "must": { "match": {"tags": "esportes"}},
            "filter": {
                "bool": {
                    "must": [
                        {"range": {"preco": {"lte": 100}}},
                        {"term": {"categoria.original": "Livros"}}
                    ]
                }
            }
        }
    }
}

Dica: Você sabia que o Elasticsearch possui uma funcionalidade interessante chamada Date Math que nos ajuda a fazer contas com datas para buscas que envolvem ranges. Mais em: https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#date-math Dica2: Você sabia que podemos combinar a Query DSL com Agregações? A Query DSL vai além do que vimos e permite que combinemos filtros com agregações em uma mesma requisição. Mais em: https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html

Boost - Ajustando peso para a pesquisa

Podemos aplicar pesos usando o parâmetro boost onde quando colocado um valor mais alto, os dados pesquisados ali devem aparecer primeiro nos resultados, como no exemplo:

GET /produtos/v1/_search
{
  "query" : {
    "bool": {
      "should": [
        {
          "match": {
            "nome": {
              "query" : "big data",
              "operator": "and",
              "boost": "2"
            }
          }
        },
        {
          "match": {
            "nome": {
              "query" : "brasileiro futebol"
            }
          }
        }
      ]
    }
  }
}

Se quisermos efetuar o contrário e fazer com que parte da pesquisa tenha menos peso, sendo apresentada somente ao final, podemos colocar um valor mais baixo no parâmetro boost, como no exemplo:

GET /produtos/v1/_search
{
  "query" : {
    "bool": {
      "should": [
        {
          "match": {
            "nome": {
              "query" : "big data",
              "operator": "and",
              "boost": "0.2"
            }
          }
        },
        {
          "match": {
            "nome": {
              "query" : "brasileiro futebol"
            }
          }
        }
      ]
    }
  }
}

Nota: Aplicar um boost de valor 0.8 por exemplo dá 20% de relevância ao filtro.

Prefix, match_phrase_prefix - Busca parcial

Com a tag prefix podemos efetuar pesquisas que começam com um termo como no caso do exemplo abaixo que encontraria qualquer tag amarelo, amante, etc... Não se recomenda utilizar prefixos muito pequenos ou usar em excesso pois onera a performance da pesquisa.

GET /produtos/v1/_search
{
  "query" : {
    "prefix": {
      "tags": "ama"
    }
  }
}

Com a tag match_phrase_prefix você pode efetuar a busca parcial dentro de uma frase, o que pode ser muito interessante para implementar uma lógica de sugestão, onde o usuário vai digitando e vamos retornando os primeiros X itens para apresentação:

GET /produtos/v1/_search
{
  "query" : {
    "match_phrase_prefix": {
      "nome": "big d"
    }
  }
}
N-Grams - Performance em pesquisas parciais

Os N-Grams são uma técnica de pesquisa que quebra uma palavra em blocos que permitem pesquisa, por exemplo a palavra cavalo, pode ser quebrada em locos de tamanho 2 ficando para pesquisa os termos ca, va, lo.

Para entender melhor como o N-Grams funciona, utilize a API _analyse para entender como serão quebradas as palavras abaixo.

GET /_analyze
{
  "tokenizer": {
    "type": "ngram",
    "max_gram": 10
  },
  "text": "Big Data"
}
GET /_analyze
{
  "tokenizer": {
    "type": "ngram",
    "min_gram": 2,
    "max_gram": 10
  },
  "text": "Big Data"
}
GET /_analyze
{
  "tokenizer": {
    "type": "edge_ngram"
  },
  "text": "Big Data"
}
GET /_analyze
{
  "tokenizer": {
    "type": "edge_ngram",
    "max_gram": 10
  },
  "text": "Big Data"
}
GET /_analyze
{
  "tokenizer": {
    "type": "edge_ngram",
    "min_gram": 2,
    "max_gram": 10
  },
  "text": "Big Data"
}

Abaixo segue todo um exemplo de como utilizar o N-Grams para fazer um indice de autocomplete por exemplo.

Vamos primeiro criar o analisador/analyser no indice de nome indice_ngramas:

PUT /indice_ngramas
{
  "settings": {
    "index": {
      "number_of_shards": 1,
      "number_of_replicas": 0
    },
    "analysis" : {
      "filter": {
        "filtro_autocomplete": {
          "type": "edge_ngram",
          "min_gram": 2,
          "max_gram": 20
        }
      },
      "analyzer": {
        "autocomplete" : {
          "type": "custom",
          "tokenizer": "standard",
          "filter" : [
            "lowercase",
            "filtro_autocomplete"
          ]
        }
      }
    }
  }
}

Agora vamos analisar o comportamento deste analisador em algumas situações:

GET /indice_ngramas/_analyze?analyzer=autocomplete&text=tecnicamente GET /indice_ngramas/_analyze?analyzer=autocomplete&text=politecnica GET /indice_ngramas/_analyze?analyzer=autocomplete&text=Big Data GET /indice_ngramas/_analyze?analyzer=autocomplete&text=História do Futebol

Destacando os termos encontrados (highlight)

Uma feature muito legal que pode ser aplicada numa busca também é o highlight, que irá incluir a tag de destaque <em> em todas as palavras do retorno que casaram com a pesquisa. Nota: Uma coisa importante é colocar a tag highlight aplicada a um campo que também tenha sido filtrado, caso contrario, não irá apresentar o destaque.

Segue um exemplo de uso:

Criando o indice:

PUT /produtos_autocomplete
{
  "settings": {
    "index": {
      "number_of_shards": 3,
      "number_of_replicas": 0
    },
    "analysis" : {
      "filter": {
        "filtro_autocomplete": {
          "type": "edge_ngram",
          "min_gram": 2,
          "max_gram": 20
        }
      },
      "analyzer": {
        "autocomplete" : {
          "type": "custom",
          "tokenizer": "standard",
          "filter" : [
            "lowercase",
            "filtro_autocomplete"
          ]
        }
      }
    }
  },
  "mappings": {
    "v1": {
      "_all": {
        "type": "text",
        "analyzer": "portuguese"
      },
      "properties": {
        "nome": {
          "fields": {
            "original": {
              "type": "keyword"
            },
            "autocomplete": {
              "type": "text",
              "analyzer": "autocomplete"
            }
          },
          "type": "text",
          "analyzer": "portuguese"
        },
        "categoria": {
          "fields": {
            "original": {
              "type": "keyword"
            }
          },
          "type": "text",
          "analyzer": "portuguese"
        },
        "subcategoria": {
          "fields": {
            "original": {
              "type": "keyword"
            }
          },
          "type": "text",
          "analyzer": "portuguese"
        },
        "tags": {
          "fields": {
            "original": {
              "type": "keyword"
            }
          },
          "type": "string",
          "index": "analyzed",
          "analyzer": "portuguese"
        },
        "fornecedor": {
          "fields": {
            "original": {
              "type": "keyword",
              "index": "not_analyzed"
            }
          },
          "type": "text"
        },
        "preco": {
          "type": "float"
        }
      }
    }
  }
}

Incluindo dados:

POST /produtos_autocomplete/v1/
{
  "nome": "Big Data rápido e fácil",
  "categoria": "Livros",
  "subcategoria": "Tecnologia",
  "tags": ["impresso", "digital", "larga escala", "computação"],
  "fornecedor": "Casa do Código",
  "preco": 29.90
}

POST /produtos_autocomplete/v1/
{
  "nome": "Guia rápido e fácil para big data",
  "categoria": "Livros",
  "subcategoria": "Tecnologia",
  "tags": ["impresso", "digital", "larga escala", "computação"],
  "fornecedor": "Casa do Código",
  "preco": 219.90
}

POST /produtos_autocomplete/v1/
{
  "nome": "Big Data: Técnicas e tecnologias para extração de valor dos dados",
  "categoria": "Livros",
  "subcategoria": "Tecnologia",
  "tags": ["impresso", "digital", "larga escala", "computação"],
  "fornecedor": "Casa do Código",
  "preco": 49.90
}

POST /produtos_autocomplete/v1/
{
  "nome": "Think Big: Guia para sua micro empresa",
  "categoria": "Livros",
  "subcategoria": "Tecnologia",
  "tags": ["impresso", "digital", "larga escala", "computação"],
  "fornecedor": "Casa do Código",
  "preco": 49.90
}

POST /produtos_autocomplete/v1/
{
  "nome": "A história do futebol brasileiro",
  "categoria": "Livros",
  "subcategoria": "Esportes",
  "tags": ["impresso", "digital", "esportes", "futebol"],
  "fornecedor": "Casa do Código",
  "preco": 59.90
}

POST /produtos_autocomplete/v1/
{
  "nome": "A história do Brasil",
  "categoria": "Livros",
  "subcategoria": "Escolar",
  "tags": ["impresso", "digital", "escola", "história"],
  "fornecedor": "Casa do Código",
  "preco": 34.90
}

POST /produtos_autocomplete/v1/
{
  "nome": "Contos dos melhores escritores do Brasil",
  "categoria": "Livros",
  "subcategoria": "Literatura",
  "tags": ["impresso", "literatura", "nacional"],
  "fornecedor": "Casa do Código",
  "preco": 57.25
}

POST /produtos_autocomplete/v1/
{
  "nome": "Bola de futsal",
  "categoria": "Esportes",
  "subcategoria": "Futebol",
  "tags": ["futebol", "amador", "quadra"],
  "fornecedor": "Irmãos Silveira Esportes",
  "preco": 29.90
}

POST /produtos_autocomplete/v1/
{
  "nome": "Camiseta Seleção Brasileira",
  "categoria": "Roupas",
  "subcategoria": "Esportes",
  "tags": ["futebol", "amador", "seleção", "amarelinha"],
  "fornecedor": "Irmãos Silveira Esportes",
  "preco": 129.90
}

POST /produtos_autocomplete/v1/
{
  "nome": "Chuteira de Futebol",
  "categoria": "Calçados",
  "subcategoria": "Esportes",
  "tags": ["futebol", "amador", "campo", "amarelinha"],
  "fornecedor": "Irmãos Silveira Esportes",
  "preco": 129.90
}

Efetuando o filtro com destaque:

// fazer passo a passo
// 1- simular busca
GET /produtos_autocomplete/_search
{
  "query" : {
    "match" : {
      "nome.autocomplete": "data bi"
    }
  }
}

// 2- highlight dos termos
GET /produtos_autocomplete/_search
{
  "query" : {
    "match" : {
      "nome.autocomplete": "data bi"
    }
  },
  "highlight" : {
    "fields" : {
        "nome.autocomplete" : {}
    }
  }
}

// 3- includir apenas o fornecedor no resultado, alem do highlight
GET /produtos_autocomplete/_search
{
  "_source": ["fornecedor"],
  "query" : {
    "match" : {
      "nome.autocomplete": "data bi"
    }
  },
  "highlight" : {
    "fields" : {
        "nome.autocomplete" : {}
    }
  }
}
Como validar uma query

Você sabia que podemos validar uma query ou mesmo entender como ela será executada? Este é um recurso muito útil para saber se uma consulta vai ser muito custosa para o cluster e se pode ser melhorada. Veja a documentação oficial:

https://www.elastic.co/guide/en/elasticsearch/reference/current/search-validate.html

Detalhes internos

Debaixo do capô, uma pesquisa como esta abaixo é quebrada e processada em diversas pesquisas tipo term e isso causa a diferença dos pesos referentes aos resultados de pesquisa.

GET /produtos/v1/_search
{
  "query" : {
    "match": {
      "nome": "big data futebol brasileiro"
    }
  }
}

Esta seria a pesquisa executada após o parse:

GET /produtos/v1/_search
{
  "query" : {
    "bool": {
      "should": [
        {"term": {"nome": "big"}},
        {"term": {"nome": "brasileir"}},
        {"term": {"nome": "futebol"}},
        {"term": {"nome": "data"}}
      ]
    }
  }
}

Ou seja Mais matches ganham um score maior.

Em detalhes, a documentação do Elasticsearch destaca 3 itens para o calculo:

  • Term frequency
    • Quantas vezes o termo procurado se encontra dento do campo? Um termo encontrado mais vezes é mais relevante no cálculo.
  • Field-length norm
    • Qual é o tamanho do field? Matchs em fields mais longos terão um score menor, pois por exemplo em um campo com 3 palavras, cada uma dessas palavras é importante para descrever o campo, pois ele é bem curto. Agora em um campo com 100 palavras, é provável que uma única tenha menos importância perante todas as outras. Também é levado em consideração o tamanho médio dos campos de todos os documentos, por exemplo se todos os campos do seu documento tem 80 palavras em média, e você está fazendo a busca em um campo com tamanho de 5 palavras, é mais provável que este campo seja mais importante, logo esta relação entre tamanho do campo buscado e tamanho dos campos em gerais também é considerada.
  • Inverse document frequency
    • Quantas vezes o termo procurado pode ser encontrado em todos os documentos buscados? Um termo que aparece mais vez nos documentos é menos importante para o calculo do que um termo que apareceu exclusivamente em um documento.

Dica: Elasticsearch nos permite usar o mesmo termo para buscar em vários campos sem que precisemos copiar e colar a consulta para cada campo. Esta funcionalidade é conhecida como busca cross campo. Mais em https://www.elastic.co/guide/en/elasticsearch/guide/master/_cross_fields_entity_search.html Dica2: Código fonte do curso de Buscas avançadas, que inclui agregação e outros: https://github.com/caiocmsouza/AluraElasticSearch/archive/master.zip

Tasks de Longa duração

Para acompanhar uma task de longa duração você pode executa o seguinte comando:

GET /_tasks/<TASK_ID>

Uma task-id tem um formato parecido com "oZwgByqsScKr40iImvwGFw:221121220"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment