CRUDing NoSQL-Daten mit Quarkus, Teil Zwei: Elasticsearch

In Teil 1 dieser Reihe haben wir uns mit MongoDB beschäftigt, einer der zuverlässigsten und robustesten dokumentorientierten NoSQL-Datenbanken. Hier in Teil 2 werden wir uns eine weitere unvermeidliche NoSQL-Datenbank ansehen: Elasticsearch.

Mehr als nur eine beliebte und leistungsstarke quelloffene verteilte NoSQL-Datenbank ist Elasticsearch vor allem eine Such- und Analyse-Engine. Es basiert auf Apache Lucene, der bekanntesten Suchmaschinen-Java-Bibliothek, und kann Echtzeit-Suche und Analyseoperationen an strukturierten und unstrukturierten Daten durchführen. Es ist darauf ausgelegt, große Mengen an Daten effizient zu verarbeiten.

Wir müssen erneut darauf hinweisen, dass dieser kurze Beitrag keinesfalls ein Elasticsearch-Tutorial ist. Der Leser wird daher eindringlich darauf hingewiesen, die offizielle Dokumentation sowie das hervorragende Buch „Elasticsearch in Action“ von Madhusudhan Konda (Manning, 2023) zu nutzen, um mehr über die Architektur und Operationen des Produkts zu erfahren. Hier implementieren wir einfach denselben Use Case wie previously, aber diesmal verwenden wir Elasticsearch anstelle von MongoDB.

Na, dann los!

Das Domänenmodell

Das nachstehende Diagramm zeigt unser *customer-order-product* Domänenmodell:

Dieses Diagramm ist dasselbe wie das in Teil 1 vorgestellte. Wie MongoDB ist auch Elasticsearch ein Dokumentendaten Speicher und erwartet daher, dass Dokumente in JSON-Notation präsentiert werden. Der einzige Unterschied ist, dass Elasticsearch zur Verwaltung seiner Daten diese indiziert haben muss.

Es gibt mehrere Möglichkeiten, wie Daten in einem Elasticsearch-Daten Speicher indiziert werden können; zum Beispiel durch das Umleiten von Daten aus einer relationalen Datenbank, das Extrahieren aus einem Dateisystem, das Streamen von Echtzeitquellen usw. Aber egal, welche Methode der Datenimport ist, es besteht letztendlich darin, die Elasticsearch-RESTful-API über einen dedizierten Client aufzurufen. Es gibt zwei Kategorien solcher dedizierter Clients:

  1. REST-basierte Clients wie curl, Postman, HTTP-Module für Java, JavaScript, Node.js usw.
  2. Programmiersprachen-SDKs (Software Development Kit): Elasticsearch bietet SDKs für alle am häufigsten verwendeten Programmiersprachen, einschließlich aber nicht beschränkt auf Java, Python usw.

Das Indizieren eines neuen Dokuments mit Elasticsearch bedeutet, es über eine POST-Anfrage gegen einen speziellen RESTful-API-Endpunkt namens _doc zu erstellen. Zum Beispiel wird die folgende Anfrage einen neuen Elasticsearch-Index erstellen und eine neue Kundeninstanz darin speichern.

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

Die Ausführung der obigen Anfrage mit curl oder der Kibana-Konsole (wie wir später sehen werden) wird das folgende Ergebnis erzeugen:

Plain Text

 

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

Dies ist die Standardantwort von Elasticsearch auf eine POST-Anfrage. Sie bestätigt die Erstellung des Indexnamens customers, einen neuen customer-Dokument, das durch eine automatisch generierte ID identifiziert wird (in diesem Fall ZEQsJI4BbwDzNcFB0ubC).

Hier erscheinen andere interessante Parameter wie _version und insbesondere _shards. Ohne zu viel ins Detail zu gehen, erstellt Elasticsearch Indizes als logische Sammlungen von Dokumenten. Genau wie das Aufbewahren von Papierdokumenten in einem Aktenordner, speichert Elasticsearch Dokumente in einem Index. Jeder Index besteht aus Shards, die physische Instanzen von Apache Lucene sind, der Motor im Hintergrund, verantwortlich für das Speichern und Abrufen der Daten. Sie können entweder primär sein, Dokumente speichern, oder Replikate, die, wie der Name vermuten lässt, Kopien der primären Shards speichern. Mehr dazu in der Elasticsearch-Dokumentation – für jetzt müssen wir bemerken, dass unser Index namens customers aus zwei Shards besteht: davon ist natürlich einer primär.

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.

Um zu unserem Domänenmodell-Diagramm zurückzukehren, wie Sie sehen können, ist sein zentrales Dokument Order, das in einer dedizierten Sammlung namens Orders gespeichert ist. Eine Order ist ein Aggregat von OrderItem-Dokumenten, jedes davon verweist auf sein assoziiertes Product. Ein Order-Dokument verweist auch auf den Customer, der sie aufgegeben hat. In Java wird dies wie folgt implementiert:

Java

 

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

Der obige Code zeigt einen Ausschnitt der Customer-Klasse. Dies ist ein einfaches POJO (Plain Old Java Object) mit Eigenschaften wie der Kunden-ID, Vor- und Nachname, E-Mail-Adresse und einer Reihe von Postanschriften.

Lassen Sie uns nun das Order-Dokument betrachten.

Java

 

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

Bei diesem können Sie einige Unterschiede im Vergleich zur MongoDB-Version bemerken. Tatsächlich haben wir bei MongoDB eine Referenz zur Kundeninstanz verwendet, die mit dieser Bestellung verbunden ist. Diese Vorstellung von Referenz existiert nicht bei Elasticsearch, und daher verwenden wir diese Dokument-ID, um eine Beziehung zwischen der Bestellung und dem Kunden, der sie aufgegeben hat, herzustellen. Das gilt ebenfalls für die Eigenschaft orderItemSet, die eine Beziehung zwischen der Bestellung und ihren Artikeln herstellt.

Der Rest unseres Domänenmodells ist fairly ähnlich und basiert auf den gleichen Normalisierungsideen. Zum Beispiel das OrderItem-Dokument:

Java

 

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

Hier müssen wir das Produkt associieren, das Gegenstand des aktuellen Bestellpostens ist. Letztlich haben wir das Product-Dokument:

Java

 

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

Die Daten-Speicherorte

Quarkus Panache stark vereinfacht den Datenpersistenz-Prozess durch die Unterstützung sowohl des Active Record– als auch des Repository-Designmusters. In Teil 1 haben wir die Quarkus Panache-Erweiterung für MongoDB verwendet, um unsere Daten Repositories zu implementieren, aber es gibt noch keine äquivalente Quarkus Panache-Erweiterung für Elasticsearch. correspondierend, müssen wir hier unsere Daten Repositories manuell mit dem dedizierten Elasticsearch-Client implementieren, während wir auf eine mögliche zukünftige Quarkus-Erweiterung für Elasticsearch warten.

Elasticsearch ist in Java geschrieben und daher ist es keine Überraschung, dass es nativen Support für den Aufruf der Elasticsearch-API über die Java-Client-Bibliothek bietet. Diese Bibliothek basiert auf dem fluent API Builder-Designmuster und bietet sowohl synchrone als auch asynchrone Verarbeitungsmodule. Sie erfordert mindestens Java 8.

Wie sehenalso unsere auf fluent API Builder basierenden Daten Repositories aus? Below ist ein Auszug aus der CustomerServiceImpl-Klasse, die als Daten Repository für das Customer-Dokument dient.

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

Wie wir sehen können, muss unsere Daten Repository-Implementierung ein CDI-Bean mit Anwendungsbereich sein. Der Elasticsearch Java-Client wird einfach injiziert, dank der quarkus-elasticsearch-java-client-Quarkus-Erweiterung. Auf diese Weise umgeht man viele überflüssige Verzierungen, die wir ansonsten verwenden mussten. Das einzige, was wir benötigen, um den Client injizieren zu können, ist die Deklaration der folgenden Eigenschaft:

Properties files

 

quarkus.elasticsearch.hosts = elasticsearch:9200

Hier ist elasticsearch der DNS-(Domain Name Server)-Name, den wir mit dem Elastic Search-Datenbankserver im docker-compose.yaml-Datei verknüpfen. 9200 ist die TCP-Portnummer, die der Server verwendet, um Verbindungen zu empfangen.

Die Methode doIndex() oben erstellt einen neuen Index namens customers, falls dieser nicht existiert, und indiziert (speichert) darin ein neues Dokument, das eine Instanz der Klasse Customer darstellt. Der Indizierungsprozess wird basierend auf einer IndexRequest durchgeführt, die als Eingabeargumente den Indexnamen und den Dokumentenkörper akzeptiert. Was die Dokument-ID angeht, wird diese automatisch generiert und dem Aufrufer für weitere Referenz zurückgegeben.

Die folgende Methode ermöglicht es, den Kunden zu abrufen, der durch die als Eingabeargument übergebene ID identifiziert wird:

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

Das Prinzip ist das gleiche: Mit diesem fluent API-Builder-Muster erstellen wir eine GetRequest-Instanz ähnlich wie bei der IndexRequest, und wir führen sie gegen den Elasticsearch Java-Client aus. Die anderen Endpunkte unseres DatenSpeichers, die es uns ermöglichen, vollständige Suchoperationen durchzuführen oder Kunden zu aktualisieren und zu löschen, sind auf die gleiche Weise gestaltet.

Bitte nehmen Sie sich die Zeit, den Code anzusehen, um zu verstehen, wie Dinge funktionieren.

Die REST-API

Unsere MongoDB REST API-Schnittstelle war einfach zu implementieren, dank der quarkus-mongodb-rest-data-panache-Erweiterung, bei der der Annotation Processor automatisch alle erforderlichen Endpunkte generierte. Bei Elasticsearch profitieren wir noch nicht vom gleichen Komfort und müssen daher manuell implementieren. Das ist kein großes Problem, da wir die vorherigen Datenrepräsentationen injizieren können, wie unten gezeigt:

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

Dies ist die Implementierung der REST API des Kunden. Die anderen in Verbindung mit Bestellungen, Bestellpositionen und Produkten sind ähnlich.

Sehen wir uns nun an, wie man das ganze Ding ausführt und testet.

Running and Testing Our Microservices

Nun, da wir uns die Details unserer Implementierung angesehen haben, schauen wir uns an, wie man sie ausführt und testet. Wir haben uns entschieden, dies im Namen des docker-compose-Dienstprogramms zu tun. Hier ist die zugehörige docker-compose.yml-Datei:

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:

Diese Datei weist das docker-compose-Dienstprogramm an, drei Dienste auszuführen:

  • 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

Jetzt können Sie überprüfen, ob alle erforderlichen Prozesse laufen:

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
    $

Um zu bestätigen, dass der Elasticsearch-Server verfügbar ist und Abfragen ausführen kann, können Sie sich mit Kibana unter http://localhost:601 verbinden. Nach dem Blättern nach unten auf der Seite und der Auswahl von Dev Tools im Einstellungsmenü können Sie Abfragen wie unten gezeigt ausführen:

Um die Microservices zu testen, gehen Sie wie folgt vor:

1. Klonen Sie das zugehörige GitHub-Repository:

Shell

 

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

2. Gehen Sie zum Projekt:

Shell

 

$ cd docstore

3. Wechseln Sie zur richtigen Zweigstelle:

Shell

 

$ git checkout elastic-search

4. Kompilieren:

Shell

 

$ mvn clean install

5. Führen Sie die Integrations tests aus:

Shell

 

$ mvn -DskipTests=false failsafe:integration-test

Dieser letzte Befehl führt die 17 bereitgestellten Integrations tests aus, die alle erfolgreich abgeschlossen werden sollten. Sie können auch die Swagger UI-Oberfläche zu Testzwecken verwenden, indem Sie Ihren bevorzugten Browser auf http://localhost:8080/q:swagger-ui aufrufen. Anschließend können Sie die in den JSON-Dateien im Verzeichnis src/resources/data des docstore-api-Projekts befindlichen Daten als Payload für die Endpunkt-Tests verwenden.

Genießen Sie!

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