Data

29 nov, 2016

Desmistificando o ElasticSearch: Mapping & Analyzers

Publicidade

Na era da informação, vemos cada vez mais uma quantidade expressiva de conteúdos espalhados pela Internet. Diversos sites, aplicativos e ferramentas em geral oferecem em abundância os mais variados tipos de informações, porém seus motores de busca nem sempre são eficazes. Muitas vezes, apesar da qualidade do conteúdo, localizar a informação pode ser muito difícil pelo formato da busca e pelas limitações na utilização de bancos relacionais, causando uma experiência ruim ao usuário, incluindo o fato de que, em grandes volumes de dados, o tempo de processamento pode ser alto. Para tornar a busca do seu conteúdo fácil, com diversos recursos para informações que são aparentemente subjetivas e extremamente rápidas, podemos utilizar o ElasticSearch.

O ElasticSearch é um servidor de buscas distribuído desenvolvido em Java, baseado no Apache Lucene e possui código aberto para toda a comunidade. Ele é utilizado principalmente para buscas full-text, possuindo recursos muitos interessantes. A comunicação com o serviço é feita através do protocolo HTTP utilizando Json, sendo ele uma API Rest. Sua documentação é bem completa e na Internet é possível achar bastante conteúdo de alta qualidade sobre o assunto.

Dentre alguns de seus recursos, além de diversas formas de buscas full-text e outros tipos de dados, é possível citar agregações – highlighting, autocomplete, geo-localização, entre outros. A utilização de todas essas funcionalidades é simples, mas em alguns casos podemos ter problemas relacionados à estrutura que criamos no momento de indexar nossos documentos. Neste artigo, abordaremos alguns conceitos para que fique mais claro como funciona a indexação, incluindo o mapeamento e os analisadores.

Mapping: É o processo de definição de como um documento e seus campos são armazenados e indexados. Nesta etapa, podemos definir os tipos de dados para cada campo (string, integer, date, geolocation etc.). Para campos do tipo string, é possível definir o tipo de indexação e, caso seja necessário, qual analisador será utilizado. Para os diversos tipos de dados, os índices têm um comportamento comum, exceto para string, que será o nosso foco.

É importante ressaltar que, após criado, o mapeamento não pode ser alterado e por isso devemos ter muita atenção ao criar o esquema de nossos documentos. Se for necessário alterar o esquema, é preciso reindexar todos os documentos.

Para strings, pode-se definir o tipo de indexação e o analisador que será usado. Os tipos de indexação podem ser:

  • analyzed – O dado é tratado e quebrado em diversos índices a partir do analisador definido;
  • not_analyzed – O dado não é analisado, e o índice possui exatamente o valor recebido;
  • no – O dado não é analisado, nem indexado;

Analyzers: Os analisadores são responsáveis por tratar os dados e gerar os índices associados a eles. O texto recebido é quebrado em diversos tokens, seja por espaços em branco ou caracteres especiais, e cada token é analisado e normalizado. Apenas campos definidos no seu mapeamento com indexação do tipo analyzed passam por esse processo. Os analisadores também atuam quando é efetuada uma query em cima desses dados, usando sempre o analisador definido, para que os dados enviados para a consulta também sejam normalizados. Existem diversos tipos de analisadores, para diversas necessidades, idiomas específicos e com possibilidade de customização.

Vamos supor que estamos indexando a informação: “Apenas um teste”. Se o campo for not_analyzed, o seu índice será exatamente seu valor. Mas se o campo for analyzed, seu índice seria composto pelos seguintes tokens: “apenas”, “um”, “teste”.

Agora que compreendemos esse conceito, podemos começar a nos questionar sobre algum dos impactos – por que usar e como usar índices analisáveis e não analisáveis. Um campo não analisável é usado normalmente para expressões exatas, únicas. Podemos ter como exemplos possíveis enumeradores, unique identifier (esse tipo deve ser usado como string, pois não existe nativo), siglas etc. Se executado uma query full-text em cima desse dado, ele não seria encontrado mesmo que a palavra-chave fosse exatamente o valor que está armazenado, porque o texto buscado seria quebrado em diversos tokens e o índice do dado armazenado é o valor completo, logo, não teríamos um “match”. O inverso também não funcionaria. Executar uma query por valor exato em um campo analisável não traria resultados, uma vez que a palavra-chave seria o valor completo, buscado nos tokens processados, e também não retornariam resultados. Logo, fica claro que devemos pensar e planejar bem ao definir o que são dados exatos e subjetivos na hora de mapear nossos campos, e usar as queries corretas ao executar uma consulta.

Existe um grande problema quando falamos de dados analisáveis e, em certas situações, quando precisaríamos que essa informação pudesse ser utilizada também com seu valor exato, dois exemplos clássicos ocorrem com ordenação e agregações.

Vamos supor que temos os seguintes dados armazenados em nosso servidor:

01

Se fosse solicitado para ordenar por esse campo, a ordenação seria executada no conjunto dos tokens referentes a cada valor, e o primeiro valor seria para ordenar. Para o valor “Apenas um teste”, uma ordenação crescente se basearia no token “apenas”, enquanto uma ordenação decrescente se basearia no token “um”, causando na verdade uma desordenação dos dados.

02

Se fôssemos agregar todos os valores distintos desse campo, também teríamos problemas. Em vez de obtermos os três valores distintos (“Apenas um teste”, “Estou aqui ainda”, “Boa tarde”), iríamos obter todos os tokens separadamente. O resultado seria o conjunto: “apenas”, “um”, “teste”, “estou”, “aqui”, “ainda”, “boa”, “tarde”.

Para campos que não serão ordenados e nem agregados como conteúdos muito extensos, isso não seria um problema. Mas para campos curtos e analisáveis, como título de um artigo, nome de pessoas, nome de bairros etc., cujas possíveis ordenações e agregações sejam necessárias, seria bem complicado lidar com isso. A melhor maneira para contornar esse problema é ter o mesmo campo indexado das duas formas, sendo um analisável para o uso de buscas full-text e outro não analisável para ordenações e agregações. Para implementar essa estratégia, não é necessário criar dois campos com nomes diferentes. Dentro do mapeamento de um campo, é possível declarar subcampos (multifields) e especificar como eles serão indexados. Por convenção, usamos um subcampo chamado raw não analisável dentro do campo analisável, permitindo que essas operações possam ser executadas. Um campo title teria também um subcampo title.raw.

Um mapeamento já existente não pode ser modificado, mas novos campos podem ser adicionados a qualquer momento. Podemos adotar o uso de templates para evitar esse mesmo problema em campos adicionados dinamicamente, na indexação de novos campos, mas isso é um assunto para um próximo artigo.