Esse artigo é útil para estudantes e profissionais que tenham alguma experiência em Java e queiram dar os primeiros passos no desenvolvimento para Elasticsearch.
O artigo apresenta uma visão geral dos conceitos de mineração de texto, um resumo das ideias básicas contidas no framework, os primeiros passos para o desenvolvimento neste segmento e a criação de buscas avançadas.
Ao final, as ferramentas de filtros, agregações e sugestões também serão apresentadas, a fim de melhorar o resultado das pesquisas.
Big Data é um termo que engloba uma larga quantidade de conceitos, técnicas e ferramentas, e cujo foco é gerenciar grandes quantidades de dados. Visto como um novo paradigma computacional, o Big Data promete capturar, armazenar, analisar e compartilhar dados da ordem de petabytes gerados por aplicações nos mais distintos domínios.
Frequentemente associado ao Big Data, o objetivo do Elasticsearch é apoiar o desenvolvimento de aplicações centradas em texto, como redes sociais, sistemas de e-commerce, sites de notícias e canais de educação.
A grande vantagem do ES reside na sua arquitetura, projetada para ser escalável e para gerenciar grandes quantidades de dados de forma simples e eficiente.
A primeira versão foi lançada em 2010, pelo israelense Shay Banon. Desde então, muitas versões foram liberadas e empresas do calibre da Wikipedia, GitHub, Foursquare e Globo.com passaram a utilizar o framework.
O código do ES é desenvolvido em Java e está baseado principalmente em dois frameworks da Fundação Apache: o Lucene e o Hadoop.
O Lucene é utilizado como o motor de indexação e buscas em documentos desestruturados, e grande parte dos conceitos de programação deste é igualmente aplicável ao ES.
O Hadoop, por sua vez, é utilizado para escalar o sistema fazendo uso de jobs Map e Reduce, um modelo de programação paralela introduzido pelo Google. Como um dos seus diferenciais, o Elasticsearch possibilita que mesmo clientes não Java utilizem suas funcionalidades, via REST e JSON.
Com base nisso, este artigo tem como objetivo apresentar os conceitos básicos do ES e discutir sua API Java. Por motivos didáticos, o ES será apresentado através de uma comparação com um banco de dados relacional (ou BDR).
Sendo assim, serão analisados a partir de agora: os conceitos básicos e a arquitetura do ES; a instalação, a inserção e a busca de documentos utilizando comandos REST; e, finalmente, a API Java desta solução.
Conceitos básicos
O ES é uma ferramenta distribuída para mineração e tratamento de textos. Sua função principal é permitir que documentos desestruturados fossem armazenados e recuperados de forma simples e eficiente. A arquitetura do ES, que foi projetada para sempre trabalhar em cluster, suporta grandes quantidades de dados.
Elasticsearch é, de forma geral, uma versão distribuída do Lucene – framework para mineração e tratamento de texto desenvolvido pela Fundação Apache. Isto porque cada nó de um cluster ES contém tal framework para o gerenciamento das informações armazenadas.
Devido a essa relação, alguns dos conceitos básicos de ES são derivados do Lucene, a saber:
- Índice: define o endereço para acesso às informações guardadas no ES. De forma parecida com o esquema nos BDRs, necessitamos saber o nome e a localização na rede de um índice (por exemplo, localhost:9200/nome_indice) para conectar-se e manipular as estruturas de armazenamento do ES;
- Type: recurso usado para nomear conjuntos de documentos armazenados em um índice, podendo ser comparado ao conceito de tabela em BDRs, pois contém vários documentos que obedecem a uma mesma estrutura de campos;
- Documento: é um texto plano – isto é, não corresponde a formatos binários como .doc ou .pdf – organizado em campos delimitados por chaves e vírgulas, de acordo com o padrão JSON. Sua função é similar ao das linhas de tabelas, já que é sobre os documentos que as operações de manuseio de dados (inserção, recuperação, alteração e exclusão) são realizadas;
- Campo (field): é a unidade mínima de informação armazenada em um documento. Deve possuir um tipo, que pode ser padrão – por exemplo: string, integer/long, float/double, boolean, ou null – ou criado pelo desenvolvedor. O campo tem a mesma função de uma coluna no BDR;
- Mapeamento (mapping): define a estrutura de um documento, contendo campos e a maneira como cada um deve ser armazenado e recuperado. O mapeamento funciona como a definição de colunas nas tabelas em BDRs;
- Query DSL: é a linguagem de busca (para mais detalhes, veja a seção Links). Está para o Lucene como o SQL está para os BDRs;
- Score: valor numérico que representa quão bem um documento está relacionado a uma busca em Querydsql;
- Analisador: mecanismo para transformação de texto – por exemplo: a conversão de letras maiúsculas em minúsculas, o tratamento de espaços em branco – durante o armazenamento e recuperação de informações.
Para facilitar o entendimento, a Tabela 1 sumariza o relacionamento entre os conceitos do Lucene e os conceitos de BDRs apresentados nesta seção.
Lucene | Banco de dados relacional (BDR) |
Índice (Index) | Esquema |
Type | Tabela |
Documento (JSON) | Linha |
Campo (Field) | Coluna |
Mapeamento (Mapping) | Estrutura da tabela |
Query DSL | SQL |
Outros conceitos, igualmente importantes, foram adicionados pelo ES para estender o Lucene e permitir sua execução em cluster. Dentre eles, os principais são:
- Nó: um servidor – virtual ou físico – que contém certo número de shards e réplicas;
- Shard: um índice do Lucene que gerencia as informações armazenadas em um nó;
- Réplica: também um índice Lucene, porém gerenciado pelo ES como uma cópia completa de algum dos outros shards do cluster. A réplica contém os mesmos dados e é responsável pelas mesmas funções de um shard.
Ela pode ser utilizada em dois casos: para melhorar o desempenho das buscas e para garantir a disponibilidade do cluster. O primeiro caso permite que haja um balanceamento de carga entre os shards do ES, diminuindo o tempo de resposta, já que o processamento das buscas será dividido entre os shards e suas réplicas.
O segundo caso possibilita que o cluster continue funcionando mesmo ante uma falha do shard original. Assim, quando o shard do qual uma réplica foi criada não estiver mais disponível por causa de uma falha técnica ou um desastre, a réplica deverá assumir as funções do shard original, garantindo que o ES continue operando normalmente;
- Filter: oferece, em situações específicas – por exemplo, verificar se uma informação existe no índice sem que seja necessária sua recuperação completa do documento –, uma opção de melhor desempenho em relação às buscas com Query DSL;
- Aggregation: mecanismo para sumarizar dados em estatísticas relevantes, como a contagem, média, diferença de tempo, entre outras.
A Figura 1 descreve a arquitetura do ES. Como pode ser observado, cada nó de um cluster ES contém um ou mais shards e réplicas, gerenciados pelo Lucene.
E distribuídos em diversos shards, podem estar diferentes índices contendo documentos descritos em JSON. O ES ainda permite que os nós do cluster sejam configurados para armazenar ou não dados.
Os nós que não armazenam dados são responsáveis apenas pelas atividades de processamento das buscas. Com o propósito de trabalharem de forma auto gerenciada, os nós do cluster necessitam eleger um mestre (master), que será responsável por tarefas de administração, como a criação de um novo índice e adição de um novo nó.
Entretanto, o cliente não necessita saber o tipo do nó – ou seja, se contém dados, se é mestre, ou se apenas realiza busca – para enviar uma requisição HTTP/REST, já que todos os servidores conhecem a topologia do cluster, a localização dos documentos e podem redirecionar tais requisições para o servidor que contém as informações desejadas.
Em resumo, o ES adiciona funções de cluster ao Lucene, a fim de manipular informação textual e atender as necessidades do Big Data.
Entender essas funções é muito importante para o desenvolvedor, porém não é suficiente para a implementação de buscas de forma efetiva. Sendo assim, antes de continuar com exemplos práticos, se faz necessário conhecer um pouco sobre análise de texto e entender como o ES avalia, armazena e recupera informações.
Para facilitar, a análise de texto será explicada através de exemplos usando dois conceitos centrais do Lucene/ES: as listas invertidas e os tokens.
Como ilustrado na Figura 2, se documentos contendo um campo com as frases “O Elasticsearch é desenvolvido em Java” e “Elasticsearch é uma ferramenta BigData” fossem inseridos em um índice ES, diferentemente de um BDR, antes de serem armazenados esses documentos seriam analisados e divididos em pequenos pedaços de informação chamados tokens.
Nesse exemplo, a partir da quebra das frases nos espaços em branco, seriam gerados e armazenados em uma lista inversa os seguintes tokens: Elasticsearch, é, desenvolvido, em, Java, O, uma, ferramenta e BigData.
As listas inversas são utilizadas porque evitam a comparação textual da busca com todos os campos de todos os documentos de um índice – uma atividade que pode ser muito lenta, já que um índice pode conter muitos documentos.
Para ilustrar, se uma busca por documentos que contenham a palavra “Elasticsearch” fosse enviada ao índice da Figura 2, não seria necessária uma busca textual completa em cada um dos campos de texto dos dois documentos para verificar que ambos contêm a palavra procurada, pois através de uma simples pesquisa diretamente na lista invertida – muito mais rápida, já que é internamente implementada como uma tabela hash–, os dois documentos seriam recuperados.
O processo de criação de tokens pode ser bastante complexo, incluindo análises de texto, remoção de palavras indesejadas e criação de novos tokens derivados das palavras encontradas no documento original.
Tais transformações são realizadas por analisadores, como por exemplo, o analisador do tipo stemmer, que transforma as palavras em sua forma raiz. Na Figura 3 estão ilustradas as transformações das palavras trabalhador, trabalho e trabalhar para a forma raiz: trabalh.
O ES oferece um grande conjunto de ferramentas para transformação de texto, por exemplo: transformação do texto para caracteres ascii, modificação de palavras para letras maiúsculas e remoção de palavras muito longas, o que aumenta sua capacidade de busca, já que em uma busca que contenha uma dessas palavras (no exemplo, trabalhador, trabalho e trabalhar), internamente será a forma raiz (“trabalh”, neste caso) que será utilizada nas comparações.
Além disso, os analisadores podem ser combinados para obter tokens que sejam mais representativos para o domínio de negócio da aplicação que está sendo desenvolvida.
Por exemplo, em um site de e-commerce, as palavras usadas na busca de certo produto podem ser transformadas para sua forma raiz e em letras minúsculas, a fim de que mais resultados sejam retornados.
Para obter esse resultado, podemos combinar analisadores como whitespaces (que divide as palavras de acordo com os espaços em branco entre elas), lowercase (que transforma todas as letras de uma palavra em minúscula), stopwords (que remove palavras que tenham pouca relevância) e stemmer (que transforma a palavra na sua forma raiz), conforme ilustrado na Figura 4.
Em suma, internamente no ES, a seguinte sequência é executada:
- Uma inserção ou atualização de documento é recebida via PUT ou POST;
- Os analisadores são executados e cada documento é convertido em um ou mais tokens indexados;
- Os tokens são armazenados em uma lista com ponteiros para a versão completa do documento.
Instalação e comandos REST
Com os conceitos básicos já abordados, podemos iniciar um exemplo prático. Para tal, necessitamos do ES instalado em um servidor. Sendo assim, faça o download do arquivo contendo o framework diretamente no site (veja o endereço na seção Links), descompacte este arquivo no servidor, que deve possuir o JDK 1.6 instalado, e, na pasta bin, execute o comando bin/elasticsearch.
Caso tudo esteja funcionando bem, o Elasticsearch estará disponível no endereço https://localhost:9200 e poderemos enviar comandos para o servidor. Como já mencionado, a comunicação com o ES é baseada na tecnologia REST, cujas vantagens são o suporte de qualquer linguagem que possa enviar requisições HTTP, a facilidade de integração e a escalabilidade. No contexto do ES, cada comando REST possui um equivalente em BDR, como ilustrado na Tabela 2.
Operação | Elasticsearch | BDR |
Create (inserir) | POST ou PUT | INSERT |
Retrieve (recuperar) | GET | SELECT |
Update (atualizar) | PUT | UPDATE |
Delete (excluir) | DELETE | DELETE |
Dito isso, a partir de agora vamos ilustrar o seu uso através de um exemplo que simula a criação de uma biblioteca para artigos publicados na Java Magazine.
Para facilitar o desenvolvimento, os exemplos utilizarão o Sense (vide seção Links), um plugin para o Google Chrome que atua como um cliente enviando chamadas REST/HTTP.
O primeiro passo na utilização do ES é a criação de um índice no servidor. Conforme ilustrado na Figura 5, o comando PUT /javamagazine/ irá criar um índice chamado javamagazine.
Com o índice definido, o próximo passo é a criação de um type com o mapeamento dos campos, como demonstrado na Listagem 1. Nesse código, o type biblioteca é criado e nele, os campos autor, titulo, texto e assunto do artigo são definidos usando o tipo string.
PUT /javamagazine/biblioteca/_mapping
{
"biblioteca" : {
"properties" : {
"autor" : {"type" : "string" },
"titulo" : {"type" : "string" },
"texto" : {"type" : "string" },
"assunto" : {"type" : "string" }
}
}
}
O mapeamento define também como cada campo será analisado durante seu armazenamento e recuperação. Para isto, analisadores são definidos para cada campo ou o analisador padrão é utilizado, como no caso da Listagem 1, onde não definimos nenhum analisador.
Da mesma forma que no tópico anterior, o ES permite que analisadores sejam combinados para aumentar a capacidade de busca das aplicações.
Para tal, devemos adicionar um novo analisador nas configurações do índice, que estão disponíveis no endereço /javamagazine/_settings. Esse endereço não contém um arquivo, mas sim um documento JSON que especifica configurações válidas para todo o índice, como é o caso de analisadores e filtros.
A Listagem 2 mostra como criar um analisador chamado analisador_titulo, que combina o analisador whitespaces, do tipo tokenizer, com os analisadores trim e lowercase, do tipo filter.
É importante notar que antes de realizar a alteração das configurações o índice deve ser fechado (com o comando POST /javamagazine/_close), ficando assim indisponível para buscas, e depois da alteração deve ser reaberto (com o comando POST /javamagazine/_open), para que a alteração da configuração tenha efeito e para que os dados voltem a estar disponíveis.
POST /javamagazine/_close
PUT /javamagazine/_settings
{
"index": {
"analyzer": {
"analisador_texto": {
"type": "custom",
"tokenizer": "whitespace",
"filter": ["trim", "lowercase"]}
}
}
}
}
POST /javamagazine/_open
A fim de que o novo analisador seja utilizado, precisamos modificar o mapeamento de campos conforme a Listagem 3. Nesse exemplo, além do analisador_titulo, especificado na listagem anterior, são utilizados também os analisadores keyword e standard.
Os campos autor e assunto recebem o analisador keyword, que simplesmente transforma uma string em um token único e o campo texto recebe o analisador standard, que combina os analisadores lowercase e stopword.
Finalmente, o campo titulo recebe o analisador customizado analisador_titulo.
PUT /javamagazine/biblioteca/_mapping
{
"biblioteca" : {
"properties" : {
"autor" : {"type" : "string", "analyzer":"keyword" },
"titulo" : {"type" : "string","analyzer":"analisador_titulo"},
"texto" : {"type" : "string" ,"analyzer":"standard" },
"assunto" : {"type" : "string" }
}
}
}
Com o mapeamento definido, podemos utilizar o comando da Listagem 4 para inserir um novo artigo no índice criado para armazenar a biblioteca da Java Magazine. O valor “1” no comando POST /javamagazine/biblioteca/1 define o id do documento inserido.
Esse id pode conter qualquer valor alfanumérico e caso omitido será gerado automaticamente pelo ES. Após inserir o documento no índice, podemos recuperá-lo diretamente através do seu id.
Para tal, executamos o comando GET/javamagazine/biblioteca/1.
POST /javamagazine/biblioteca/1
{
"autor" : "Luiz",
"titulo" : "Sua primeira aplicação em Elasticsearch...",
"texto" : "O objetivo do Elasticsearch é apoiar …",
"assunto" : "Elasticsearch"
}
Como em bancos de dados relacionais, o ES também permite a atualização e exclusão de valores do índice. Na Listagem 5, o comando PUT atualiza o nome do autor para “Luiz Santana”.
Posteriormente, podemos utilizar o comando DELETE /javamagazine/biblioteca/1 para excluir esse registro.
PUT /javamagazine/biblioteca/1
{
"autor" : "Luiz Santana",
"titulo" : "Sua primeira aplicação em Elasticsearch...",
"texto" : "O objetivo do Elasticsearch é apoiar …",
"assunto" : "Elasticsearch"
}
Com o intuito de recuperar as informações manipuladas utilizando os conceitos apresentados até aqui, devemos utilizar o comando GET javamagazine/biblioteca/_search?q=assunto:elasticsearch.
Este comando busca no type biblioteca do índice javamagazine documentos que contenham a palavra Elasticsearch no campo assunto. O termo _search deve sempre ser utilizado para enviar ao servidor as consultas que são definidas usando Query DSL no campo q.
Essa busca irá retornar o documento JSON da Listagem 6, cujas propriedades mais importantes são:
- took: descreve o tempo total gasto na execução da busca;
- shards: conta quantos shards foram acessados para executar a busca;
- hits: representa a lista dos resultados;
- score: informa a relevância do resultado de acordo com o algoritmo de similaridade utilizado.
Esses parâmetros devem ser aproveitados pelo desenvolvedor para entender o comportamento do ES após uma busca. Um exemplo é o score, que indica quão significativo foi a busca em relação aos documentos presentes no índice.
O algoritmo padrão para o cálculo desse parâmetro é o TF/IDF, que se baseia simplesmente na semelhança entre os itens procurados na busca e os documentos do índice.
Modificando esse algoritmo podemos manipular o score de acordo com as necessidades do domínio de negócio para o qual a aplicação está sendo desenvolvida.
Por exemplo, na biblioteca para a Java Magazine que estamos desenvolvendo nesse artigo, podemos alterar o score para priorizar documentos que contenham mais páginas em lugar da simples similaridade textual.
Outra informação importante são os hits, que representam os documentos que possuem textos similares aos da busca enviada ao índice e estão, por padrão, ordenados do maior para o menor score.
Além disso, em relação ao array de hits, que o desenvolvedor deverá percorrer no momento de utilizar os dados retornados, podemos observar que cada hit indica o índice do qual foi recuperado (no caso do exemplo, javamagazine), seu tipo (no caso do exemplo, biblioteca), seu id, o score em relação a essa busca e, mais importante, o documento JSON contendo o resultado para os valores consultados.
{
"took": 2,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 1.4054651,
"hits": [
{
"_index": "javamagazine",
"_type": "biblioteca",
"_id": "1",
"_score": 1.4054651,
"_source": {
"autor": "Luiz Santana",
"titulo": "Sua primeira aplicação em Elasticsearch...",
"texto": "O objetivo do Elasticsearch é apoiar …",
"assunto": "Elasticsearch"
}
}
]
}
}
Java API para Elasticsearch
A API Java para ES permite realizar os mesmos comandos expostos em REST. Para isto, é indicado utilizar o Maven como gerenciador de pacotes, já que toda a documentação disponibilizada se baseia nesse padrão.
O ES está disponível no Maven Central (vide seção Links) e a Listagem 7 apresenta a dependência Maven que deve ser adicionada ao pom.xml do seu projeto.
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>1.3.2version>
</dependency>
Com o ambiente de desenvolvimento configurado, o primeiro passo na criação de uma aplicação é estabelecer a conexão com o cluster Elasticsearch, que pode estar em execução na própria máquina local ou remotamente.
Sendo assim, lembre-se que antes de executar a aplicação o servidor deve ser iniciado, como explicado no tópico anterior.
Existem duas formas de conectar-se ao cluster: criando sua aplicação como um nó, usada quando necessitamos estender as capacidades do cluster ES; ou como um cliente puro.
Neste caso, o cliente acessará o cluster remoto através de conexões REST/HTTP, de forma parecida aos BDRs comuns. A classe Connection, exposta na Listagem 8, apresenta as duas formas de conexão.
Em ambos os casos o ES deve estar disponível para conexão, isto é, necessitamos previamente instalar e configurar o cluster como apresentado na seção anterior.
A primeira forma de conexão é explicitada no método createNode(), onde utilizamos o objeto nodeBuilder para criar um nó que fará parte de um cluster denominado clusterJavaMagazine.
Caso o nome do cluster seja omitido, o default, elasticsearch, será utilizado. Apesar dessa facilidade, é importante que o nome seja definido para que nosso nó não se conecte a um cluster qualquer que casualmente esteja acessível – o que poderia causar um problema conhecido como split brain.
Outro ponto importante é que o novo nó não tente armazenar informações – por isso definimos .client(true) no método createNode() da Listagem 8 – e concentre seus esforços nas atividades relacionadas a buscas.
Apesar de estar disponível, a opção de criar um nó que armazene informações deve ser utilizada apenas em casos específicos, nos quais as funcionalidades do ES necessitem ser estendidas, ou seja, quando queremos, por exemplo, controlar como os dados são armazenados no índice, como o cluster é gerenciado ou como requisições são respondidas.
A outra forma de estabelecer a conexão com o ES é criar um cliente puro, como ilustrado no método createClient(). Essa é a maneira mais simples de utilizar a API.
O cliente criado não será um nó do cluster, ele apenas acessará as funcionalidades de um ES remoto. A partir de um socket que será conectado ao cluster denominado clusterJavaMagazine, configuramos o cliente através de um comando put settings encapsulado pela API no método ImmutableSettings.settingsBuilder().put().
Para realizar essa conexão (entre cliente e cluster), devemos definir o nome do cluster ao qual queremos conectar, seu endereço de rede (no exemplo, localhost) e a porta de conexão (no exemplo, usamos 9300, a padrão do ES).
Em ambos os casos, o cliente desenvolvido será do tipo da interface Client (org.elasticsearch.client.Client) e realizará todas as atividades de manipulação de dados na aplicação que estamos desenvolvendo, como veremos na sequência do artigo.
package example.javamagazine;
import org.elasticsearch.client.Client;
import org.elasticsearch.client.transport.TransportClient;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.InetSocketTransportAddress;
import org.elasticsearch.node.Node;
import static org.elasticsearch.node.NodeBuilder.*;
public class Connection {
private Client;
//conexão como nó do cluster
public Client createNode(){
Node = nodeBuilder().clusterName("clusterJavaMagazine").
client(true).node();
client = node.client();
return client;
}
//conexão como cliente
public Client createClient(){
Settings = ImmutableSettings.settingsBuilder().
put("cluster.name","clusterJavaMagazine").build();
TransportClient client = new TransportClient(settings);
client.addTransportAddress(new
InetSocketTransportAddress("localhost",9300));
return client;
}
}
Com a conexão estabelecida, podemos desenvolver métodos para manipular as informações no ES. Na Listagem 9 são apresentados, através dos métodos create(), retrieveAll(), update() e delete(), os seguintes comandos da interface org.elasticsearch.client.Client : prepareIndex(), para inserção; prepareSearch(), para busca; prepareUpdate(), para atualização; e prepareDelete(), para exclusão.
Da mesma maneira que no tópico anterior, devemos utilizar documentos JSON nos comandos de inserção e alteração. Por isso o ES oferece, através da sua API, a classe XContentBuilder, que facilita a criação de documentos JSON, evitando que o desenvolvedor tenha que escrever tais documentos a partir da concatenação de strings.
Os documentos JSON criados com essa classe são inseridos ou atualizados respectivamente pelos métodos prepareIndex() e prepareUpdate(), através da função setSource() presente em ambos, ou seja, devemos criar o documento utilizando o XContentBuilder e incluí-lo como parâmetro do método setSource().
Como ilustrado no método retrieveAll() da Listagem 9, o método prepareSearch() do objeto client é responsável por realizar uma consulta que retorna todos os documentos do índice. As informações são recuperadas como uma lista de SearchHit, sendo que cada elemento dessa lista representa um objeto contendo um documento JSON que pode ser acessado como um documento texto completo no método hit.getSource() ou através dos seus campos pelo método hit.getSource().get().
Este último método receberá como parâmetro o nome do campo que está sendo consultado, por exemplo: hit.getSource().get("titulo").
Finalmente, para excluir um documento, devemos utilizar prepareDelete(), como ilustrado no método delete() do nosso exemplo.
De forma bastante similar à seção anterior – onde excluímos valores do índice através de REST/HTTP apenas usando o id –, o método prepareDelete() exclui um documento de acordo com o id deste.
package example.javamagazine;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import java.io.IOException;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.Client;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.search.SearchHit;
public class BibliotecaDAO {
private Client client;
private final static String index = "javamagazine";
private final static String type = "biblioteca";
public BibliotecaDAO(){
Connection connection = new Connection();
client = connection.createClient();
}
//método para inclusão de documento no índice
public void create(String id, String autor,
String texto, String assunto) throws IOException {
XContentBuilder builder = jsonBuilder()
.startObject()
.field("autor", autor)
.field("titulo", texto)
.field("texto", texto)
.field("assunto",assunto)
.endObject();
client.prepareIndex(index, type, id)
.setSource(builder).execute()
.actionGet();
}
//método para alteração de documento no índice
public void update(String id, String autor,
String texto, String assunto) throws ElasticsearchException, Exception {
XContentBuilder builder = jsonBuilder()
.startObject()
.field("autor", autor)
.field("titulo", texto)
.field("texto", texto)
.field("assunto",assunto)
.endObject();
client.prepareUpdate(index, type, id)
.setSource(builder).execute()
.actionGet();
}
//método para recuperação de todos os documentos do índice
public void retrieveAll() {
SearchResponse response = client.prepareSearch(index)
.execute().actionGet();
for (SearchHit hit : response.getHits().getHits()) {
System.out.println("Id: " + hit.getId());
System.out.println("Título: " + hit.getSource().get("titulo"));
}
}
//método para exclusão de documento no índice
public void delete(String id) {
client.prepareDelete("javamagazine", "biblioteca", id);
}
}
Da mesma maneira que nos BDRs, normalmente o índice e o mapeamento do ES são definidos diretamente no servidor, de forma prévia ao desenvolvimento.
Entretanto, caso seja necessário, o ES oferece também uma API para administração e configuração do cluster. Essa API que permite, por exemplo, criar, alterar e excluir índices e mapeamentos, também está disponível na interface org.elasticsearch.client.Client, porém suas funções não são acessíveis através do objeto cliente (client) como nos exemplos anteriores, e sim através do método client.admin().
A Listagem 10 apresenta o código para criação programática do índice javamagazine e do seu mapeamento.
Para a criação do índice utilizamos o método prepareCreate(), ao qual nesse exemplo incluímos, a título de ilustração, a definição da configuração (setSettings()) para a alteração do número de shards para 1. Finalmente, usamos a classe XContentBuilder para criar um documento JSON definindo o tipo biblioteca e um mapeamento contendo os campos autor, titulo, texto e assunto, e posteriormente usamos o método preparePutMapping() para adicionar o tipo e o mapeamento ao índice javamagazine.
package example.javamagazine;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import java.io.IOException;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.client.Client;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.xcontent.XContentBuilder;
public class ClusterAdmin {
private Client client;
public void createIndex() throws ElasticsearchException, IOException {
client.admin().indices()
.prepareCreate("javamagazine")
.setSettings(
ImmutableSettings.settingsBuilder()
.put("number_of_shards", 1))
.execute().actionGet();
XContentBuilder builder = jsonBuilder().startObject()
.startObject("biblioteca")
.startObject("properties")
.field("autor", "string")
.field("titulo", "string")
.field("texto", "string")
.field("assunto", "string")
.endObject()
.endObject()
.endObject()
.endObject();
client.admin().indices()
.preparePutMapping("javamagazine")
.setType("biblioteca")
.setSource(builder)
.execute().actionGet();
}
}
Agora que já sabemos como criar e administrar um cluster, e realizar as funções básicas de manipulação de informações em um índice, podem aprofundar nossos conhecimentos e melhorar os resultados das buscas.
Buscas usando a API Java para ES
Busca é a atividade central do ES! Todas as outras operações, como inserção, atualização, mapeamento e administração do cluster, visam tornar essa atividade mais rápida e simples, e atender de forma eficiente as necessidades da aplicação independente do seu domínio.
Por esse motivo, o ES oferece dezenas de opções para recuperar documentos de um índice. No entanto, mesmo assim, as soluções mais utilizadas são: term e match.
A busca do tipo term pode ser comparada às consultas em bancos de dados relacionais, já que ela procura coincidências exatas entre os termos desejados e os valores do índice. A busca do tipo match, por outro lado, avalia as palavras da consulta enviada – utilizando os analisadores citados anteriormente – e retorna valores mesmo que a coincidência não seja exata.
Por exemplo, se existe um documento no índice contendo o termo “Java” (com a primeira letra maiúscula) e for realizada uma busca por “java”, o match poderá encontrar o documento, mas o term não vai encontrá-lo por conta da diferença entre letras maiúsculas e minúsculas.
A Listagem 11 mostra o uso desses dois tipos de busca. O método retrieveArtigosByAutor() procura artigos do índice usando term para retornar documentos que contenham no campo autor a palavra exata passada como parâmetro de busca, enquanto retrieveArtigosByTitulo() procura os artigos do índice usando match para retornar documentos que contenham no campo título palavras que sejam similares às palavras informadas para a busca.
O código Java para consulta de ambos os tipos – term ou match – têm a mesma estrutura: utilizando o objeto cliente, enviamos uma consulta ao índice através do método setQuery().
Essas consultas são definidas pelos métodos termQuery() e matchQuery() da classe QueryBuilders e, ao final da execução, retornarão como resultado uma lista de SearchHit, cuja manipulação foi omitida por ser igual à do exemplo da Listagem 9.
O método retrieveArtigos() realiza uma busca do tipo match, porém a estende aos campos titulo, texto e autor, ou seja, a mesma consulta vai considerar esses três campos na sua execução e por isso ela é chamada de multi match, sendo definida com o método QueryBuilders.multiMatchQuery().
Além disso, esse método ilustra o uso do SCAN, que permite paginar os resultados da busca a fim de limitar a quantidade de informação retornada. Nesse exemplo, os valores serão retornados em grupos de 100 por shard (tamanho este que é definido no método setSize()) até que todos os resultados sejam recuperados, isto é, quando nenhum SearchHit seja retornado.
Novamente, o código para manipulação da lista de SearchHit foi omitido por já ter sido apresentado.
public void retrieveArtigosByAutor(String autor){
SearchResponse response = client.prepareSearch(index)
.setTypes(type)
.setSearchType(SearchType.DFS_QUERY_THEN_FETCH)
.setQuery(QueryBuilders.termQuery("autor", autor))
.execute()
.actionGet();
for (SearchHit hit : response.getHits().getHits()) {
// Manusear resultados
}
}
public void retrieveArtigosByTitulo(String titulo){
SearchResponse response = client.prepareSearch(index)
.setTypes(type)
.setSearchType(SearchType.DFS_QUERY_THEN_FETCH)
.setQuery(QueryBuilders.matchQuery("titulo", titulo))
.execute()
.actionGet();
for (SearchHit hit : response.getHits().getHits()) {
// Manusear resultados
}
}
public void retrieveArtigos(String query) {
SearchResponse response;
while (true) {
response = client
.prepareSearch(index)
.setTypes(type)
.setSearchType(SearchType.SCAN)
.setQuery(
QueryBuilders.multiMatchQuery(query,
"titulo", "texto", "autor"))
.setSize(100)
.execute()
.actionGet();
for (SearchHit hit : response.getHits()) {
// Manusear resultados
}
// Condição de parada do while anterior
if (response.getHits().getHits().length == 0) {
break;
}
}
}
Adicionando filtros às buscas com a API
Os filtros atuam de forma muito parecida com as buscas, mas devem ser utilizados para situações em que as respostas são do tipo sim ou não (por exemplo, no caso da consulta sobre a existência de um valor no índice) e na pesquisa por termos exatos.
A vantagem do seu uso é o desempenho, pois os filtros não calculam o score dos documentos e seus resultados podem ser armazenados em cache.
Na API Java para ES, um filtro é criado utilizando a classe FilterBuilder, como ilustrado na Listagem 12. Neste exemplo, a função do filtro é limitar as respostas de uma pesquisa de acordo com um autor e um assunto específico.
Também usamos dois termFilters que filtram documentos de acordo com um assunto e um autor. Além disso, esses termFilters são implementados através de um boolFilter, que os combina de acordo com as regras da lógica booleana.
Em cada uma das regras definidas por boolFilter (must, should e mustNot) podem ser adicionados um ou mais filtros que serão analisados em conjunto. Tais regras são analisadas da seguinte forma: serão retornados os documentos que atendam aos filtros da regra must; não serão retornados os documentos que atendam aos filtros da regra mustNot; e os documentos que atendam aos filtros da regra should só serão retornados caso nenhuma regra must seja atendida e atendam a uma quantidade mínima de regras should (o valor mínimo padrão é 1).
Assim, o código da Listagem 12 irá retornar documentos que contenham exatamente o nome do autor, já que o termFilter para autor está na regra must, mas que não possuam o assunto passado para o método como parâmetro, já que o termFilter para assunto está na regra mustNot.
public void retrieveUsingFilter(String autor, String assunto){
SearchResponse response = client.prepareSearch(index).setTypes(type)
.setSearchType(SearchType.DFS_QUERY_THEN_FETCH)
.setPostFilter(
FilterBuilders.boolFilter()
.must(FilterBuilders.termFilter("autor", autor))
.mustNot(FilterBuilders.termFilter("assunto", assunto))
)
.execute()
.actionGet();
for (SearchHit hit : response.getHits()) {
// Manusear resultados
}
}
Adicionando agregadores às buscas com a API
Agregar estatísticas é parte essencial de uma ferramenta como o ES. Por exemplo, em um site de e-commerce, necessitamos saber quantos produtos existem no índice, o preço médio e o valor mínimo desses produtos.
No ES, as Aggregations, ou agregações, permitem calcular esse tipo de informações analíticas para sumarizar os dados a partir de um conjunto de documentos. Representantes dessa categoria de cálculo são: o valor máximo, o valor mínimo, a média e a soma.
O código da Listagem 11, por exemplo, pode ser modificado para retornar o número de artigos de acordo com o assunto, resultando no código da Listagem 13. Como ilustrado nesse código, o ES oferece a classe AggregationBuilders, com a qual podemos criar diferentes tipos de agregadores que são adicionados a uma busca pelo método addAggregation().
Nesse caso, que contabiliza a quantidade de artigos que um autor possui em relação a determinado assunto, é criado um agregador do tipo contador (AggregationBuilders.count()) para o campo assunto.
Após a execução dessa consulta, a resposta estará no método getAggregations(), que possui o resultado das agregações acessíveis por seu nome. No exemplo, usamos getAggregations().get("counter") para recuperar a quantidade, dividida por assuntos, de artigos presentes no índice.
public void countArtigosBySubject(String autor) {
SearchResponse response = client
.prepareSearch(index)
.setTypes(type)
.setSearchType(SearchType.DFS_QUERY_THEN_FETCH)
.setQuery(
QueryBuilders.matchQuery("autor", autor))
.addAggregation(
AggregationBuilders
.count("counter").field("assunto"))
.execute().actionGet();
System.out.println(response.getAggregations().get("counter"));
}
Adicionando sugestões às buscas com a API
Além de ser uma ferramenta simples e eficiente, o ES oferece alguns mecanismos interessantes para a criação de aplicações mais amigáveis aos usuários.
Um desses mecanismos é conhecido como suggestions, ou sugestões. As sugestões funcionam quando um usuário deseja realizar uma busca, mas não sabe como escrever as palavras dessa busca.
Por exemplo, digamos que o usuário quer buscar um artigo que contenha no título a palavra “Java Magazine”. Usando as sugestões, o usuário poderá digitar “Já” que o ES se encarregará de encontrar entre os documentos do índice possíveis complementos para as letras digitadas.
Como ilustrado na Listagem 14, podemos modificar os códigos anteriores a fim de permitir sugestões durante a busca por títulos de artigos presentes no índice javamagazine.
Para tal, a função addSuggestion() adiciona um objeto da classe TermSuggestionBuilder que terá a função de procurar no campo titulo dos documentos do índice as palavras passadas como parâmetro para a função do exemplo (retrieveWithSuggestions()). Podemos verificar na listagem que o construtor da classe TermSuggestionBuilder recebe como parâmetro um nome para essa sugestão – no exemplo, foi escolhido “sug”.
Os termos encontrados como sugestão estarão disponíveis em getSuggest(), que possui distintas entradas (entry) representando cada um dos campos para os quais foi criada uma sugestão – nesse exemplo possuímos apenas a sugestão “sug”.
Cada uma dessas entradas conterá, por sua vez, um conjunto de opções, que estão presentes em getOptions(). As opções são levantadas de acordo com os documentos presentes no índice, isto é, o ES faz uma consulta ao índice para verificar como seria possível completar a palavra baseando-se nos caracteres enviados pelo usuário.
No exemplo anterior, se o usuário procura por “Ja”, o ES poderia sugerir opções como “Java”, “Java Magazine”, “Java SE” ou “JavaScript”.
public void retrieveWithSuggestions(String query) {
SearchResponse response = client
.prepareSearch(index)
.setQuery(QueryBuilders.matchAllQuery())
.addSuggestion(
new TermSuggestionBuilder("sug")
.text(query).field("titulo")).execute()
.actionGet();
for (Entry<? extends Option> entry : response.getSuggest()
.getSuggestion("sug").getEntries()) {
System.out.println("Para o termo: " + entry.getText() + ". As opções são:");
for (Option option : entry.getOptions()) {
System.out.println("\t" + option.getText());
}
}
}
Em apenas cinco anos o Elasticsearch deixou de ser uma solução desconhecida para conquistar grandes players do mercado de Big Data. A maturidade dessa tecnologia pode ser demonstrada pela recente criação de uma empresa, também chamada Elasticsearch, com o objetivo de guiar o desenvolvimento, divulgar, dar suporte e construir ferramentas – por exemplo, para gerenciamento do cluster, análise de logs de execução, integração com BDRs e clientes – que auxiliem a criação de aplicações corporativas com alto nível de qualidade.
Em vista disso, espera-se que projetos de distintos domínios cada vez mais incluam o ES para gerenciar o armazenamento e a busca de suas informações textuais.
Do ponto de vista do desenvolvimento Java, é fundamental que, após entender os conceitos apresentados neste artigo, o leitor avance seu conhecimento sobre os mecanismos internos do ES, já que as possibilidades de combinação de mapeamento, analisadores e tipos de busca são enormes.
Para tirar proveito das capacidades desta solução como um todo é importante também conhecer bem o domínio dos dados que serão inseridos no ES e ter em mente que durante o processo de desenvolvimento a aplicação deve ser calibrada de acordo com suas peculiaridades.
Para manter-se atualizado, o leitor pode acompanhar, e eventualmente participar, do processo de desenvolvimento dos novos comandos e modificações no ES, que são previamente discutidos através da lista da comunidade de usuários (vide seção Links).
Outra possibilidade muito interessante é acessar o código do ES diretamente no GitHub, o que permite verificar detalhes da implementação que muitas vezes não estão explicados na documentação oficial.
Finalmente, muitos outros artigos ainda podem ser escritos sobre ES. A seguinte lista oferece ao leitor uma ideia dos conceitos que ainda podem ser explorados: criação de plugins que estendam as funcionalidades do ES (por exemplo, para conexão com BDRs, para análise de textos em português, para apresentação das informações armazenadas no índice); os mecanismos internos (como o processo de descoberta de servidores no cluster, o armazenamento de informações no shard e a recuperação do shard após uma falha); conceitos avançados de busca (por exemplo, ordenação, boosting, scripts para alteração do score); clientes para outras linguagens de programação (por exemplo, para PHP, Ruby, C#); tuning para melhoria de desempenho; monitoramento do cluster; e aplicações baseadas em informações geográficas.
- GitHubdo autor, com os códigos apresentados no artigo.
- GitHub do projeto Elasticsearch.
- Site da empresa Elasticsearch.
- Plugin Sense.
- ES no Maven Central.
- Query DSL.