Em Parte 1 desta série, analisamos o MongoDB, uma das bases de dados NoSQL orientadas a documentos mais confiáveis e robustas. Aqui na Parte 2, vamos examinar outra base de dados NoSQL inevitável: Elasticsearch.
Mais do que apenas uma base de dados distribuída NoSQL popular e poderosa, o Elasticsearch é antes de mais nada um motor de busca e análise. É construído sobre o Apache Lucene, a biblioteca de busca Java mais famosa, e é capaz de realizar operações de busca e análise em tempo real em dados estruturados e não estruturados. Ele é projetado para lidar eficientemente com grandes volumes de dados.
No entanto, precisamos declarar novamente que este post curto não é de forma alguma um tutorial de Elasticsearch. Consequentemente, o leitor é fortemente aconselhado a usar amplamente a documentação oficial, bem como o excelente livro, “Elasticsearch in Action” de Madhusudhan Konda (Manning, 2023) para aprender mais sobre a arquitetura e operações do produto. Aqui, estamos apenas reimplantando o mesmo caso de uso como antes, mas dessa vez, usando Elasticsearch em vez de MongoDB.
Então, vamos lá!
O Modelo de Domínio
Diagrama abaixo mostra nosso modelo de domínio *customer-order-product*:
Este diagrama é o mesmo que o apresentado na Parte 1. Assim como MongoDB, Elasticsearch também é um armazenamento de dados em documentos e, como tal, espera que os documentos sejam apresentados em JSON. A única diferença é que, para lidar com seus dados, o Elasticsearch precisa indexá-los.
Existem várias maneiras de dados serem indexados em um armazenamento de dados Elasticsearch; por exemplo, redirecionando-os de um banco de dados relacional, extraí-los de um sistema de arquivos, transmiti-los de uma fonte em tempo real, etc. Mas independentemente do método de ingestão, ele finalmente consiste em invocar a API RESTful do Elasticsearch por meio de um cliente dedicado. Existem duas categorias de tais clientes dedicados:
- clientes baseados em REST como
curl
,Postman
, módulos HTTP para Java, JavaScript, Node.js, etc. - SDKs de linguagens de programação (Software Development Kit): o Elasticsearch fornece SDKs para todas as linguagens de programação mais utilizadas, incluindo, mas não se limitando a Java, Python, etc.
Indexar um novo documento com Elasticsearch significa criá-lo usando uma solicitação POST
contra um endpoint RESTful especial chamado _doc
. Por exemplo, a seguinte solicitação criará um novo índice Elasticsearch e armazenará uma nova instância de cliente nele.
POST customers/_doc/
{
"id": 10,
"firstName": "John",
"lastName": "Doe",
"email": {
"address": "[email protected]",
"personal": "John Doe",
"encodedPersonal": "John Doe",
"type": "personal",
"simple": true,
"group": true
},
"addresses": [
{
"street": "75, rue Véronique Coulon",
"city": "Coste",
"country": "France"
},
{
"street": "Wulfweg 827",
"city": "Bautzen",
"country": "Germany"
}
]
}
Executar a solicitação acima usando curl
ou o console Kibana (como veremos mais tarde) produzirá o seguinte resultado:
{
"_index": "customers",
"_id": "ZEQsJI4BbwDzNcFB0ubC",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 1,
"_primary_term": 1
}
Esta é a resposta padrão do Elasticsearch para uma solicitação POST
. Confirma que criou o índice chamado customers
, tendo um novo documento customer
, identificado por um ID gerado automaticamente (neste caso, ZEQsJI4BbwDzNcFB0ubC
).
Outros parâmetros interessantes aparecem aqui, como _version
e especialmente _shards
. Sem entrar em muitos detalhes, o Elasticsearch cria índices como coleções lógicas de documentos. Assim como manter documentos em papel em um arquivo, o Elasticsearch mantém documentos em um índice. Cada índice é composto por shards, que são instâncias físicas do Apache Lucene, o motor por trás que é responsável por colocar os dados no armazenamento ou retirá-los. Podem ser primárias, armazenando documentos, ou replicadas, armazenando, como o nome sugere, cópias dos shards primários. Mais sobre isso na documentação do Elasticsearch – por ora, precisamos notar que nosso índice chamado customers
é composto por dois shards: um deles, obviamente, é primário.
A final notice: the POST
request above doesn’t mention the ID value as it is automatically generated. While this is probably the most common use case, we could have provided our own ID value. In each case, the HTTP request to be used isn’t POST
anymore, but PUT
.
Para voltar ao nosso diagrama de modelo de domínio, como você pode ver, seu documento central é Order
, armazenado em uma coleção dedicada chamada Orders
. Uma Order
é um agregado de documentos OrderItem
, cada um dos quais aponta para seu Product
associado. Um documento Order
também faz referência ao Customer
que a fez. Em Java, isso é implementado da seguinte forma:
public class Customer
{
private Long id;
private String firstName, lastName;
private InternetAddress email;
private Set<Address> addresses;
...
}
O código acima mostra um fragmento da classe Customer
. Esta é uma simples POJO (Plain Old Java Object) que possui propriedades como o ID do cliente, nome e sobrenome, endereço de e-mail e um conjunto de endereços postais.
Vamos agora olhar para o documento Order
.
public class Order
{
private Long id;
private String customerId;
private Address shippingAddress;
private Address billingAddress;
private Set<String> orderItemSet = new HashSet<>()
...
}
Aqui você pode notar algumas diferenças em comparação com a versão MongoDB. Na verdade, com MongoDB, estávamos usando uma referência à instância do cliente associada a este pedido. Esta noção de referência não existe com Elasticsearch, e portanto, estamos usando este ID de documento para criar uma associação entre o pedido e o cliente que o realizou. O mesmo se aplica à propriedade orderItemSet
, que cria uma associação entre o pedido e seus itens.
O restante do nosso modelo de domínio é bastante semelhante e baseado nas mesmas ideias de normalização. Por exemplo, o documento OrderItem
:
public class OrderItem
{
private String id;
private String productId;
private BigDecimal price;
private int amount;
...
}
Aqui, precisamos associar o produto que constitui o objeto do item de pedido atual. Por último, mas não menos importante, temos o documento Product
:
public class Product
{
private String id;
private String name, description;
private BigDecimal price;
private Map<String, String> attributes = new HashMap<>();
...
}
O Data Repositories
Quarkus Panache极大地简化了数据持久化过程,支持活动记录和仓库设计模式。在第一部分中,我们使用Quarkus Panache扩展来为MongoDB实现数据仓库,但目前还没有相应的Quarkus Panache扩展用于Elasticsearch。因此,在等待可能未来的Quarkus Elasticsearch扩展的同时,我们必须手动使用Elasticsearch专用客户端来实现我们的数据仓库。
Elasticsearch是用Java编写的,因此,它提供原生支持,通过Java客户端库调用Elasticsearch API并不令人惊讶。这个库基于流式API构建器设计模式,并提供了同步和异步处理模型。它至少需要Java 8。
那么,基于流式API构建器的数据仓库看起来是什么样的呢?下面是CustomerServiceImpl
类的一个摘录,该类作为Customer
文档的数据仓库。
@ApplicationScoped
public class CustomerServiceImpl implements CustomerService
{
private static final String INDEX = "customers";
@Inject
ElasticsearchClient client;
@Override
public String doIndex(Customer customer) throws IOException
{
return client.index(IndexRequest.of(ir -> ir.index(INDEX).document(customer))).id();
}
...
正如我们所看到的,我们的数据仓库实现必须是一个应用程序作用域的CDI bean。Elasticsearch Java客户端只是通过Quarkus扩展quarkus-elasticsearch-java-client
注入的。这种方式避免了我们否则必须使用的大量复杂配置。我们需要做的唯一事情是声明以下属性才能注入客户端:
quarkus.elasticsearch.hosts = elasticsearch:9200
Aqui, elasticsearch
é o nome DNS (Domain Name Server) que associamos ao servidor de banco de dados Elastic search no arquivo docker-compose.yaml
. 9200
é o número da porta TCP utilizada pelo servidor para ouvir conexões.
O método doIndex()
acima cria um novo índice chamado customers
se ele não existir e.indexa (armazena) nele um novo documento representando uma instância da classe Customer
. O processo de indexação é realizado com base em um IndexRequest
que aceita como argumentos de entrada o nome do índice e o corpo do documento. Quanto ao ID do documento, ele é gerado automaticamente e retornado ao chamador para referência futura.
O método a seguir permite recuperar o cliente identificado pelo ID fornecido como argumento de entrada:
...
@Override
public Customer getCustomer(String id) throws IOException
{
GetResponse<Customer> getResponse = client.get(GetRequest.of(gr -> gr.index(INDEX).id(id)), Customer.class);
return getResponse.found() ? getResponse.source() : null;
}
...
O princípio é o mesmo: usando este padrão de construtor de API fluente, construímos uma instância de GetRequest
de maneira semelhante ao que fizemos com o IndexRequest
, e a executamos contra o cliente Java do Elasticsearch. Os outros endpoints do nosso repositório de dados, que permitem realizar operações de busca completa ou atualizar e excluir clientes, são projetados da mesma forma.
Por favor, reserve um tempo para olhar o código para entender como as coisas estão funcionando.
A API REST
Nosso interface de API REST para MongoDB foi simples de implementar, graças à extensão quarkus-mongodb-rest-data-panache
, na qual o processador de anotações gerou automaticamente todos os endpoints necessários. Com Elasticsearch, ainda não desfrutamos do mesmo conforto e, portanto, precisamos implementá-lo manualmente. Isso não é um grande problema, pois podemos injetar os repositórios de dados anteriores, como mostrado abaixo:
@Path("customers")
@Produces(APPLICATION_JSON)
@Consumes(APPLICATION_JSON)
public class CustomerResourceImpl implements CustomerResource
{
@Inject
CustomerService customerService;
@Override
public Response createCustomer(Customer customer, @Context UriInfo uriInfo) throws IOException
{
return Response.accepted(customerService.doIndex(customer)).build();
}
@Override
public Response findCustomerById(String id) throws IOException
{
return Response.ok().entity(customerService.getCustomer(id)).build();
}
@Override
public Response updateCustomer(Customer customer) throws IOException
{
customerService.modifyCustomer(customer);
return Response.noContent().build();
}
@Override
public Response deleteCustomerById(String id) throws IOException
{
customerService.removeCustomerById(id);
return Response.noContent().build();
}
}
Esta é a implementação da API REST do cliente. As outras associadas a pedidos, itens de pedido e produtos são semelhantes.
Vamos agora ver como executar e testar tudo isso.
Executando e Testando Nossos Microservices
Agora que analisamos os detalhes da nossa implementação, vamos ver como executá-la e testá-la. Optamos por fazer isso em nome do utilitário docker-compose
. Aqui está o arquivo associado docker-compose.yml
:
version: "3.7"
services:
elasticsearch:
image: elasticsearch:8.12.2
environment:
node.name: node1
cluster.name: elasticsearch
discovery.type: single-node
bootstrap.memory_lock: "true"
xpack.security.enabled: "false"
path.repo: /usr/share/elasticsearch/backups
ES_JAVA_OPTS: -Xms512m -Xmx512m
hostname: elasticsearch
container_name: elasticsearch
ports:
- "9200:9200"
- "9300:9300"
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- node1-data:/usr/share/elasticsearch/data
networks:
- elasticsearch
kibana:
image: docker.elastic.co/kibana/kibana:8.6.2
hostname: kibana
container_name: kibana
environment:
- elasticsearch.url=http://elasticsearch:9200
- csp.strict=false
ulimits:
memlock:
soft: -1
hard: -1
ports:
- 5601:5601
networks:
- elasticsearch
depends_on:
- elasticsearch
links:
- elasticsearch:elasticsearch
docstore:
image: quarkus-nosql-tests/docstore-elasticsearch:1.0-SNAPSHOT
depends_on:
- elasticsearch
- kibana
hostname: docstore
container_name: docstore
links:
- elasticsearch:elasticsearch
- kibana:kibana
ports:
- "8080:8080"
- "5005:5005"
networks:
- elasticsearch
environment:
JAVA_DEBUG: "true"
JAVA_APP_DIR: /home/jboss
JAVA_APP_JAR: quarkus-run.jar
volumes:
node1-data:
driver: local
networks:
elasticsearch:
Este arquivo instrui o utilitário docker-compose
a executar três serviços:
- A service named
elasticsearch
running the Elasticsearch 8.6.2 database - A service named
kibana
running the multipurpose web console providing different options such as executing queries, creating aggregations, and developing dashboards and graphs - A service named
docstore
running our Quarkus microservice
Agora, você pode verificar se todos os processos necessários estão em execução:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
005ab8ebf6c0 quarkus-nosql-tests/docstore-elasticsearch:1.0-SNAPSHOT "/opt/jboss/containe…" 3 days ago Up 3 days 0.0.0.0:5005->5005/tcp, :::5005->5005/tcp, 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp, 8443/tcp docstore
9678c0a04307 docker.elastic.co/kibana/kibana:8.6.2 "/bin/tini -- /usr/l…" 3 days ago Up 3 days 0.0.0.0:5601->5601/tcp, :::5601->5601/tcp kibana
805eba38ff6c elasticsearch:8.12.2 "/bin/tini -- /usr/l…" 3 days ago Up 3 days 0.0.0.0:9200->9200/tcp, :::9200->9200/tcp, 0.0.0.0:9300->9300/tcp, :::9300->9300/tcp elasticsearch
$
Para confirmar que o servidor Elasticsearch está disponível e capaz de executar consultas, você pode se conectar ao Kibana em http://localhost:601. Após rolar a página para baixo e selecionar Dev Tools
no menu de preferências, você pode executar consultas como mostrado abaixo:
Para testar os microservices, proceda da seguinte forma:
1. Clone o repositório GitHub associado:
$ git clone https://github.com/nicolasduminil/docstore.git
2. Vá para o projeto:
$ cd docstore
3. Faça checkout da branch correta:
$ git checkout elastic-search
4. Construa:
$ mvn clean install
5. Execute os testes de integração:
$ mvn -DskipTests=false failsafe:integration-test
Este último comando executará os 17 testes de integração fornecidos, que devem todos ter sucesso. Você também pode usar a interface do Swagger UI para fins de teste, abrindo seu navegador preferido em http://localhost:8080/q:swagger-ui. Em seguida, para testar endpoints, você pode usar o payload nos arquivos JSON localizados no diretório src/resources/data
do projeto docstore-api
.
Aproveite!
Source:
https://dzone.com/articles/cruding-nosql-data-with-quarkus-part-two-elasticse