CRUDing dati NoSQL con Quarkus, Parte Due: Elasticsearch

In Parte 1 di questa serie, abbiamo esaminato MongoDB, una delle basi di dati document-oriented NoSQL più affidabili e robuste. Qui nella Parte 2, esploreremo un altro NoSQL database quasi inevitabile: Elasticsearch.

Oltre ad essere una base di dati distribuita open-source popolare e potente, Elasticsearch è prima di tutto un motore di ricerca e analisi. È costruito sulla cima di Apache Lucene, la più celebre libreria di motori di ricerca in Java, e può eseguire operazioni di ricerca e analisi in tempo reale su dati strutturati e non strutturati. È progettato per gestire efficientemente grandi quantità di dati.

Ancora una volta, dobbiamo precisare che questo breve post non è affatto un tutorial su Elasticsearch. Di conseguenza, il lettore è fortemente incoraggiato a utilizzare ampiamente la documentazione ufficiale, nonché l’eccellente libro, “Elasticsearch in Action” di Madhusudhan Konda (Manning, 2023) per imparare di più sull’architettura e le operazioni del prodotto. Qui, stiamo solo reimplementando lo stesso caso d’uso come prima, ma questa volta utilizzando Elasticsearch invece di MongoDB.

Allora, eccoci qua!

Il Modello del Dominio

Il diagramma qui sotto mostra il nostro modello del dominio *customer-order-product*:

Questo diagramma è lo stesso presentato nella Parte 1. Come MongoDB, anche Elasticsearch è un deposito di dati documentali e, come tale, si aspetta che i documenti siano presentati in JSON. L’unica differenza è che per gestire i suoi dati, Elasticsearch ha bisogno di farli indicizzare.

Esistono diversi modi in cui i dati possono essere indicizzati in un deposito di dati Elasticsearch; per esempio, convogliarli da un database relazionale, estrarli da un filesystem, trasmetterli in streaming da una fonte in tempo reale, ecc. Ma qualunque sia il metodo di ingestione, alla fine si riduce a chiamare l’API RESTful di Elasticsearch tramite un client dedicato. Ci sono due categorie di tali client dedicati:

  1. client basati su REST come curl, Postman, moduli HTTP per Java, JavaScript, Node.js, ecc.
  2. SDK delle lingue di programmazione (Software Development Kit): Elasticsearch fornisce SDK per tutte le lingue di programmazione più utilizzate, inclusi ma non limitati a Java, Python, ecc.

Indicizzare un nuovo documento con Elasticsearch significa crearlo utilizzando una richiesta POST contro un endpoint RESTful speciale chiamato _doc. Per esempio, la seguente richiesta creerà un nuovo indice Elasticsearch e archivierà una nuova istanza cliente in esso.

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"
        }
      ]
    }

Eseguire la richiesta sopra con curl o la console Kibana (come vedremo più tardi) produrrà il seguente risultato:

Plain Text

 

    {
      "_index": "customers",
      "_id": "ZEQsJI4BbwDzNcFB0ubC",
      "_version": 1,
      "result": "created",
      "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
      },
      "_seq_no": 1,
      "_primary_term": 1
    }

Questa è la risposta standard di Elasticsearch a una richiesta POST. Conferma aver creato l’indice chiamato customers, avente un nuovo documento customer, identificato da un ID generato automaticamente (in questo caso, ZEQsJI4BbwDzNcFB0ubC). 

Altri parametri interessanti appaiono qui, come _version e specialmente _shards. Senza entrare troppo nei dettagli, Elasticsearch crea indici come raccolte logiche di documenti. Proprio come conservare documenti cartacei in un armadio, Elasticsearch tiene i documenti in un indice. Ogni indice è composto da shards, che sono istanze fisiche di Apache Lucene, il motore dietro le quinte responsabile del recupero dei dati dalla memoria o del loro deposito. Possono essere sia primari, che archiviano i documenti, sia repliche, che, come suggerisce il nome, archiviano copie degli shards primari. Per ulteriori dettagli, consultate la documentazione di Elasticsearch – per ora, dobbiamo notare che il nostro indice chiamato customers è composto da due shards: uno dei quali, ovviamente, è primario.

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.

Per tornare al nostro diagramma del modello del dominio, come potete vedere, il documento centrale è Order, archiviato in una raccolta dedicata chiamata Orders. Un Order è un aggregato di documenti OrderItem, ciascuno dei quali punta al proprio Product associato. Un documento Order fa anche riferimento al Customer che lo ha effettuato. In Java, questo è implementato come segue:

Java

 

    public class Customer
    {
      private Long id;
      private String firstName, lastName;
      private InternetAddress email;
      private Set<Address> addresses;
      ...
    }

Il codice sopra mostra un frammento della classe Customer. Questo è un semplice POJO (Plain Old Java Object) che ha proprietà come l’ID del cliente, il nome e il cognome, l’indirizzo email e un set di indirizzi postali.

Analizziamo ora il documento Order.

Java

 

    public class Order
    {
      private Long id;
      private String customerId;
      private Address shippingAddress;
      private Address billingAddress;
      private Set<String> orderItemSet = new HashSet<>()
      ...
    }

Qui si possono notare alcune differenze rispetto alla versione MongoDB. Infatti, con MongoDB, stavamo utilizzando un riferimento all’istanza del cliente associata a questo ordine. Questa nozione di riferimento non esiste con Elasticsearch, quindi stiamo utilizzando questo ID di documento per creare un’associazione tra l’ordine e il cliente che l’ha effettuato. Lo stesso vale per la proprietà orderItemSet che crea un’associazione tra l’ordine e i suoi articoli.

Il resto del nostro modello di dominio è piuttosto simile e basato sulle stesse idee di normalizzazione. Ad esempio, il documento OrderItem:

Java

 

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

Qui, dobbiamo associare il prodotto che costituisce l’oggetto dell’attuale articolo dell’ordine. Ultimo ma non meno importante, abbiamo il documento Product:

Java

 

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

I Repository dei Dati

Quarkus Panache semplifica notevolmente il processo di persistenza dei dati supportando sia il pattern active record che il pattern repository. Nella Parte 1, abbiamo utilizzato l’estensione Quarkus Panache per MongoDB per implementare i nostri repository di dati, ma non esiste ancora un’estensione equivalente di Quarkus Panache per Elasticsearch. Di conseguenza, aspettando una possibile futura estensione di Quarkus per Elasticsearch, qui dobbiamo implementare manualmente i nostri repository di dati utilizzando il client dedicato di Elasticsearch.

Elasticsearch è scritto in Java e, di conseguenza, non sorprende che offra supporto nativo per l’invocazione dell’API di Elasticsearch utilizzando la libreria client Java. Questa libreria è basata sul design pattern di builder di API fluenti e fornisce sia modelli di elaborazione sincrona che asincrona. Richiede almeno Java 8.

Allora, come appariranno i nostri repository di dati basati sul builder di API fluenti? Di seguito è riportato un estratto dalla classe CustomerServiceImpl che funge da repository di dati per il documento 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();
      }
      ...

Come possiamo vedere, la nostra implementazione del repository di dati deve essere un bean CDI con una portata applicativa. Il client Java di Elasticsearch viene semplicemente iniettato, grazie all’estensione quarkus-elasticsearch-java-client di Quarkus. In questo modo, evitiamo molti fronzoli che avremmo dovuto utilizzare altrimenti. L’unica cosa di cui abbiamo bisogno per essere in grado di iniettare il client è dichiarare la seguente proprietà:

Properties files

 

quarkus.elasticsearch.hosts = elasticsearch:9200

Qui, elasticsearch è il nome DNS (Domain Name Server) che associamo al server di database Elasticsearch nel file docker-compose.yaml. 9200 è il numero di porta TCP utilizzato dal server per ascoltare le connessioni.

Il metodo doIndex() qui sopra crea un nuovo indice chiamato customers se non esiste già e ivi indice (salva) un nuovo documento che rappresenta un’istanza della classe Customer. Il processo di indicizzazione viene eseguito in base a una richiesta di IndexRequest che accetta come argomenti il nome dell’indice e il corpo del documento. Per quanto riguarda l’ID del documento, viene generato automaticamente e restituito al chiamante per ulteriori referenze.

Il seguente metodo permette di recuperare il cliente identificato dall’ID fornito come argomento:

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;
      }
      ...

Il principio è lo stesso: utilizzando questo fluent API builder pattern, costruiamo un’istanza di GetRequest in modo simile a come abbiamo fatto con la IndexRequest, e la eseguiamo contro il client Java di Elasticsearch. Gli altri endpoint del nostro repository di dati, che ci permettono di eseguire operazioni di ricerca complete o di aggiornare e eliminare i clienti, sono progettati allo stesso modo.

Prenditi del tempo per guardare il codice per capire come funzionano le cose.

La REST API

La nostra interfaccia API REST per MongoDB è stata semplice da implementare, grazie all’estensione quarkus-mongodb-rest-data-panache, in cui il processore di annotazioni ha generato automaticamente tutti gli endpoint richiesti. Con Elasticsearch, non beneficiamo ancora dello stesso comfort e, pertanto, dobbiamo implementarlo manualmente. Non è un grande problema, poiché possiamo iniettare i repository di dati precedenti, come mostrato di seguito:

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();
      }
    }

Questa è l’implementazione dell’API REST del cliente. Le altre associate a ordini, articoli d’ordine e prodotti sono simili.

Ora vediamo come eseguire e testare l’intero sistema.

Eseguire e Testare i Nostri Microservices

Ora che abbiamo esaminato i dettagli della nostra implementazione, vediamo come eseguirla e testarla. Abbiamo scelto di farlo utilizzando lo strumento docker-compose. Ecco il file associato 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:

Questo file istruisce lo strumento docker-compose a eseguire tre servizi:

  • 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

Ora, puoi verificare che tutti i processi richiesti siano in esecuzione:

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
    $

Per confermare che il server Elasticsearch è disponibile e in grado di eseguire query, puoi connetterti a Kibana all’indirizzo http://localhost:601. Dopo aver scrollato verso il basso la pagina e aver selezionato Dev Tools nel menu delle preferenze, puoi eseguire query come mostrato di seguito:

Per testare i microservices, procedi come segue:

1. Clona il repository GitHub associato:

Shell

 

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

2. Vai al progetto:

Shell

 

$ cd docstore

3. Esegui il checkout della branch corretta:

Shell

 

$ git checkout elastic-search

4. Compila:

Shell

 

$ mvn clean install

5. Esegui i test di integrazione:

Shell

 

$ mvn -DskipTests=false failsafe:integration-test

Questo ultimo comando eseguirà i 17 test di integrazione forniti, che dovrebbero tutti avere esito positivo. Puoi anche utilizzare l’interfaccia Swagger UI per scopi di test aprendo il tuo browser preferito all’indirizzo http://localhost:8080/q:swagger-ui. Per testare le endpoint, puoi utilizzare i payload nei file JSON situati nella directory src/resources/data del progetto docstore-api.

Divertiti!

Source:
https://dzone.com/articles/cruding-nosql-data-with-quarkus-part-two-elasticse