Spring Boot Elasticsearch Java API Client Demo Application

Nachdem ich vor ein paar Tagen mal wieder eine aktuelle Version von Elasticsearch installiert habe, wollte ich natürlich auch etwas herumprobieren und mal die neuen Features ausprobieren. Als ich dann aber mal auf die Schnelle eine alte Spring Boot Bootstrap Applikation aus der Versenkung geholt habe ist mir als erstes aufgefallen, dass der „Java High Level REST Client“ seit Version 7.15.0 „Deprecated“ ist.

Java High Level REST Client Deprecated in 7.15.0.
The High Level REST Client is deprecated in favour of the Java API Client.

https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high.html

Alle Versionen des „Java REST Client (deprecated)“ kann man noch unter https://www.elastic.co/guide/en/elasticsearch/client/java-rest/index.html finden.

OK, also mal sehen, was der neue Java API Client so kann und ob es auch so einfach ist, das neue API zu versenden.

Die Doku zu dem neuen Elasticsearch Java API Client kann unter https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/index.html gefunden werden. Die besonderen Features sind:

  • Stark typisierte Anfragen und Antworten für alle Elasticsearch-APIs.
  • Blockierende und asynchrone Versionen aller APIs.
  • Verwendung von fluent builders und funktionalen Patterns, um kurzen und dennoch lesbaren Code zu schreiben, wenn komplexe verschachtelte Strukturen erstellt werden. (Ich sage nur Lamda und Streams)
  • Nahtlose Integration von Anwendungsklassen durch Verwendung von Objektmappern wie Jackson oder einer anderen JSON-B Implementierung.
  • Delegierung der Verarbeitung an einen HTTP-Client wie den Java Low Level REST Client, der sich um alle Belange auf Transportebene kümmert (und noch nicht „Deprecated“ ist 🙂 ):
    • HTTP-Verbindungspooling
    • Wiederholungen
    • Knotenermittlung
    • etc.

Um den Elasticsearch Java API Client in einem Spring Boot Gradle Projekt zu verwenden, kann man einfach folgende Abhängigkeiten in die build.gradle eintragen.

    implementation 'co.elastic.clients:elasticsearch-java:8.5.0'
    implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.3'

Bei einem Maven Projekt sieht das dann wie folgt aus:

    <dependency>
      <groupId>co.elastic.clients</groupId>
      <artifactId>elasticsearch-java</artifactId>
      <version>8.5.0</version>
    </dependency>

    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.12.3</version>
    </dependency>

Die Kommunikation zwischen der Applikation und Elasticsearch erfolgt über HTTP/REST. Da HTTP ein verbindungsloses Protokoll ist besteht keine permanente Verbindung zwischen der Anwendung und der Datenbank. Daher ist auch kein Connection-Pool oder etwas ähnliches benötigt wird.

Das HTTP-Protokoll basiert auf einem Anfrage/Antwort-Paradigma. Ein Client baut eine Verbindung zu einem Server auf und sendet eine Anfrage an den Server. Der Server antwortet mit einer Statuszeile, die die Protokollversion der Nachricht und einen Erfolgs- oder Fehlercode, gefolgt von einer Nachricht.

https://www.rfc-editor.org/rfc/rfc1945

Der Java-API-Client besteht aus den folgenden drei Hauptkomponenten:

  • API-Client-Klassen.
    bietet stark typisierte Datenstrukturen und Methoden für Elasticsearch-APIs.
    Da die Elasticsearch-API sehr umfangreich ist, ist sie in Gruppen strukturiert, die jeweils ihre eigene Client-Klasse haben. Die Kernfunktionen von Elasticsearch sind in der Klasse ElasticsearchClient implementiert.
  • Ein JSON-Objekt-Mapper.
    Dieser bildet Ihre Anwendungsklassen auf JSON ab und integriert sie nahtlos in den API-Client.
  • Eine Transportschicht-Implementierung.
    Hier findet die gesamte Bearbeitung von HTTP-Anfragen statt.

So werden die drei Komponenten erstellt und miteinander verdrahtet:
Quelle; https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/connecting.html
In dem Beispielprojekt findet sich der Code in der „ElasticsearchJavaClient.java“ Klasse wieder.

// Create the low-level client
RestClient restClient = RestClient.builder(new HttpHost("localhost", 9200)).build();

// Create the transport with a Jackson mapper
ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());

// And create the API client
ElasticsearchClient client = new ElasticsearchClient(transport);

Der so erstellte ElasticsearchClient kann dazu verwendet werden, um alle möglichen Anfragen an die Elasticsearch Datenbank abzusetzen. Dazu gehören z.B. einfache Suchanfragen, aber auch das Speichern und Löschen von Daten ist mit dem ElasticsearchClient möglich.

Das Erstellen von Entitäten ist sehr einfach. Es handelt sich dabei nur um eine einfache Bean. Dank Project Lombok kann man sich sogar den einfachen Konstruktor und die Getter/Setter sparen 🙂

@Data
@NoArgsConstructor
public class Product {
    private Long id;
    private String name;
    private Double price;
    private String description;
    private Long stocks;
}

Das Speichern eines Produktes in der Datenbank erfolgt über die IndexRequest Klasse mit der Dokumente angelegt und aktualisiert werden können. Z.B. wie in dem folgenden Beispiel:

    public Result saveProduct(Product product)
            throws IOException
    {
        IndexRequest<Product> request = IndexRequest.of(i->
                i.index(index).id(String.valueOf(product.getId())).document(product));
        IndexResponse response = client.index(request);
        Result result = response.result();
        return result;
    }

Result ist hierbei ein Enum, das folgende Werte enthalten kann:

public enum Result implements JsonEnum {
	Created("created"),
	Updated("updated"),
	Deleted("deleted"),
	NotFound("not_found"),
	NoOp("noop");
	....
}

Mit einem SearchRequest kann man Suchanfragen starten, die dann mit einem SearchResponse Objekt beantwortet werden.

        SearchResponse<Product> response = client.search(s -> s
                        .index(index)
                        .query(q -> q.match(t -> getQuery(t, product))), Product.class);

Zum Löschen verwendet man einen DeleteRequest der mit einem DeleteResponse Objekt beantwortet wird, dass wie schon zuvor ein Result Enum enthält, dass hier im Erfolgsfall „Deleted“ ist.

Neben den hier kurz angerissenen Request/Response Paaren gibt es noch eine Reihe anderer Request Typen, die ich hier aber nur kurz auflisten. Details dazu kann man unter https://artifacts.elastic.co/javadoc/co/elastic/clients/elasticsearch-java/8.5.0/co/elastic/clients/elasticsearch/core/package-summary.html nachlesen.

BulkRequestAllows to perform multiple index/update/delete operations in a single request.
ClearScrollRequestExplicitly clears the search context for a scroll.
ClosePointInTimeRequestClose a point in time
CountRequestReturns number of documents matching a query.
CreateRequest<TDocument>Creates a new document in the index.
DeleteByQueryRequestDeletes documents matching the provided query.
DeleteByQueryRethrottleRequestChanges the number of requests per second for a particular Delete By Query operation.
DeleteRequestRemoves a document from the index.
DeleteScriptRequestDeletes a script.
ExistsRequestReturns information about whether a document exists in an index.
ExistsSourceRequestReturns information about whether a document source exists in an index.
ExplainRequestReturns information about why a specific matches (or doesn’t match) a query.
FieldCapsRequestThe field capabilities API returns the information about the capabilities of fields among multiple indices.
GetRequestReturns a document.
GetScriptContextRequestReturns all script contexts.
GetScriptLanguagesRequestReturns available script types, languages and contexts
GetScriptRequestReturns a script.
GetSourceRequestReturns the source of a document.
IndexRequest<TDocument>Creates or updates a document in an index. 
InfoRequestReturns basic information about the cluster.
KnnSearchRequestPerforms a kNN search.
MgetRequestAllows to get multiple documents in one request.
MsearchRequestAllows to execute several search operations in one request.
MsearchTemplateRequestRuns multiple templated searches with a single request.
MtermvectorsRequestReturns multiple termvectors in one request.
OpenPointInTimeRequestA search request by default executes against the most recent visible data of the target indices, which is called point in time.
PingRequestReturns whether the cluster is running.
PutScriptRequestCreates or updates a script.
RankEvalRequestEnables you to evaluate the quality of ranked search results over a set of typical search queries.
ReindexRequestAllows to copy documents from one index to another, optionally filtering the source documents by a query, changing the destination index settings, or fetching the documents from a remote cluster.
ReindexRethrottleRequestChanges the number of requests per second for a particular Reindex operation.
RenderSearchTemplateRequestAllows to use the Mustache language to pre-render a search definition.
ScriptsPainlessExecuteRequestAllows an arbitrary script to be executed and a result to be returned
ScrollRequestAllows to retrieve a large numbers of results from a single search request.
SearchRequestReturns results matching a query.
SearchShardsRequestReturns information about the indices and shards that a search request would be executed against.
SearchTemplateRequestAllows to use the Mustache language to pre-render a search definition.
TermsEnumRequestThe terms enum API can be used to discover terms in the index that begin with the provided string.
TermvectorsRequest<TDocument>Returns information and statistics about terms in the fields of a particular document.
UpdateByQueryRequestPerforms an update on every document in the index without changing the source, for example to pick up a mapping change.
UpdateByQueryRethrottleRequestChanges the number of requests per second for a particular Update By Query operation.
UpdateRequest<TDocument,?TPartialDocument>Updates a document with a script or partial document.
Elasticsearch Request Typen

Alles in allem ein sehr umfangreiches API das leider an vielen Ecken und Enden noch ein wenig dürftig dokumentiert ist und leider auch nicht immer selbsterklärend ist. Für meine Experimente habe ich ein kleines Beispielprojekt angelegt, dass man unter https://github.com/msoftware/Elasticsearch-Java-API-Client-Demo-Application finden kann. Es enthält neben dem reinen Elasticsearch JavaClient auch ein REST-Interface, mit dem Objekte vom Type Produkt angelegt, geändert, gelöscht und abgefragt werden können.