CRUDing Dados NoSQL com Quarkus, Parte Dois: Elasticsearch

Em Parte 1 desta série, analisamos o MongoDB, um dos bancos de dados NoSQL orientados a documentos mais confiáveis e robustos. Aqui na Parte 2, vamos examinar outro banco de dados NoSQL quite inevitável: Elasticsearch.

Mais do que apenas um popular e poderoso banco de dados distribuído open-source NoSQL, o Elasticsearch é antes de tudo 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. É projetado para lidar eficientemente com grandes volumes de dados.

No entanto, precisamos declarar novamente que este breve post 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 anteriormente, mas dessa vez, usando Elasticsearch em vez de MongoDB.

Então, vamos lá!

O Modelo de Domínio

O diagrama abaixo mostra nosso modelo de domínio *customer-order-product*:

Este diagrama é o mesmo 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 manipular 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índo-os de um sistema de arquivos, transmitindo-os de uma fonte em tempo real, etc. Mas qualquer que seja o 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:

  1. clientes baseados em REST como curl, Postman, módulos HTTP para Java, JavaScript, Node.js, etc.
  2. SDKs de linguagens de programação (Software Development Kit): 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.

Plain Text

 

    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:

Plain Text

 

    {
      "_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, com 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 responsável por colocar os dados na storage ou tirá-los dela. Podem ser primárias, armazenando documentos, ou réplicas, armazenando, como o nome sugere, cópias dos shards primários. Mais sobre isso na documentação do Elasticsearch – por enquanto, precisamos notar que nosso índice chamado customers é composto por dois shards: um deles, é claro, é 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, o 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 referencia o Customer que a placed. Em Java, isso é implementado da seguinte forma:

Java

 

    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.

Java

 

    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 fez. 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:

Java

 

    public class OrderItem
    {
      private String id;
      private String productId;
      private BigDecimal price;
      private int amount;
      ...
    }

Aqui, precisamos associar o produto que compõe o objeto do item de pedido atual. Por último, mas não menos importante, temos o documento Product:

Java

 

    public class Product
    {
      private String id;
      private String name, description;
      private BigDecimal price;
      private Map<String, String> attributes = new HashMap<>();
      ...
    }

O Repositórios de Dados

Quarkus Panache极大地简化了数据持久化过程,同时支持活动记录仓库设计模式。在第一部分中,我们使用了Quarkus Panache扩展来为MongoDB实现我们的数据仓库,但目前还没有针对Elasticsearch的等效Quarkus Panache扩展。因此,在等待可能未来的Quarkus Elasticsearch扩展的同时,我们必须手动使用Elasticsearch专用客户端来实现我们的数据仓库。

Elasticsearch是用Java编写的,因此它提供了使用Java客户端库调用Elasticsearch API的本地支持并不奇怪。这个库基于流式API构建器设计模式,并提供了同步和异步处理模型。它至少需要Java 8。

那么,基于流式API构建器的数据仓库看起来是什么样子呢?下面是CustomerServiceImpl类的一个片段,该类作为Customer文档的数据仓库。

Java

 

    @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注入。这种方式避免了我们否则必须使用的大量复杂特性。我们需要能够注入客户端的唯一事情是声明以下属性:

Properties files

 

quarkus.elasticsearch.hosts = elasticsearch:9200

Aqui, elasticsearch é o nome DNS (Servidor de Nomes de Domínio) que associamos ao servidor de banco de dados Elasticsearch 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 aceitando 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:

Java

 

      ...
      @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 à que fizemos com o IndexRequest, e a executamos contra o cliente Java do Elasticsearch. Os outros endpoints do nosso repositório de dados, permitindo a execução de operações de busca completa ou a atualização e exclusão de clientes, são projetados da mesma maneira.

Por favor, reserve um tempo para olhar o código e entender como as coisas funcionam.

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:

Java

 

    @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 pedidos e produtos são semelhantes.

Vamos ver agora como executar e testar todo o sistema.

Executando e Testando Nossos Microservices

Agora que olhamos os detalhes da nossa implementação, vamos ver como executá-la e testá-la. Escolhemos fazê-lo por meio do utilitário docker-compose. Aqui está o arquivo associado docker-compose.yml:

YAML

 

    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:

Shell

 

    $ 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 scrolls na página e ao 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:

Shell

 

$ git clone https://github.com/nicolasduminil/docstore.git

2. Vá para o projeto:

Shell

 

$ cd docstore

3. Faça checkout do branch correto:

Shell

 

$ git checkout elastic-search

4. Construa:

Shell

 

$ mvn clean install

5. Execute os testes de integração:

Shell

 

$ 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. Então, 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