Introducing Java API Client (1/2)

Elasticsearch is written in Java language and as you can expect there’s a native support for invoking Elasticsearch APIs using its Java Client library. It is built using fluent API builder patterns and has support for both synchronous and asynchronous (blocking and nonblocking) API invocations. It does require Java 8 as a minimum requirement, so make sure your applications are at least on Java 8.

This is part 1 of 2 article min-series:

  1. Introducing Java API client: Creating an index
  2. Search and index using Java API client

Elasticsearch released a new forward compatible Java client — called Java API Client — from version 7.17. The Java API client is a modern client that follows a decent pattern of feature clients with strongly typed requests and responses. The earlier incarnation of the client, the Java High Level REST Client attracted fair amount of criticism as well as become a headache to maintain and manage for elastic folks due to its inherent nature of how it was designed by having a dependency on the shared common code with Elasticsearch server. It was hence being deprecated in favour of Java API client.

Elastic folks recognised the need for a modern client — a client independent of the server’s code base, with its client APIs code generated based on the server’s API schema as well as providing a “feature client” pattern (we will see this in action shortly). This led to creating a Java API client. It is the next generation lightweight client whose code is pretty much (99%!) generated embracing fluent API builder pattern and auto marshaling and unmarshalling of Java Objects to JSON and vice versa.

Let’s see it in action.

Maven/Gradle project setup

The ElasticsearchClient related classes are provided in a jar artifact which can be downloaded as part of the dependencies in our project. For convenience, I’ve created a maven based project and it is made available here on GitHub: https://github.com/madhusudhankonda/elasticsearch-clients

There are usually two dependencies that are required to bring in the related classes, which are declared in the pom file, as the listing below shows:

<dependencies>
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>8.5.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.7</version>
</dependency>
...
</dependencies>

I am using the latest and greatest library versions at the time of writing this section: the 8.5.3 version of the elasticsearch-java client artifact and the 2.12.7 version of Jackson core library. You may need to upgrade these libraries as per your need.

Should you use Gradle, add the following artifacts as dependencies in your grable build file:

dependencies {
implementation 'co.elastic.clients:elasticsearch-java:8.5.3'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.7'
}

Once you have the project setup, the next step is to initialize the client and get it to work for us. This is described in the next couple of sections.

Client Initialization

Now let’s start looking at the initialization of the client and how we can put it to work. The client class is co.elastic.clients.elasticsearch.ElasticsearchClient which is initialized by providing the transport object (co.elastic.clients.transport.ElasticsearchTransport) to its constructor. This transport object in turn needs the restclient and JSON mapper objects. Let’s see these through individual steps:

Step 1:

The first step is to create a RestClient object that encapsulates Apache’s HttpHost pointing to the URL of the Elasticsearch server, as shown below:

# Step 1: create the RestClient
RestClient restClient = RestClient.builder(
new HttpHost("localhost", 9200)).build();

We see that the RestClient’s builder is created that accepts our Elasticsearch’s endpoint exposed on localhost at 9200 port.

Step 2:

Once the restClient object is created, the next step is to create the transport object, shown in the following code snippet. The transport object is to be constructed with the previously instantiated restClient instance as well as a JSON mapper (we are using Jackson JSON mapper in this instance):

# Step 2: Construct the ElasticsearchTransport object

# Create a new JassonJsonpMapper object
JacksonJsonpMapper jsonMapper = new JacksonJsonpMapper();

# Create the transport object
ElasticsearchTransport elasticsearchTransport =
new RestClientTransport(restClient, jsonMapper);

The ElasticsearchTransport object is created with the restClient and JSON mapper objects. As you can see, we are passing the restClient and jsonMapper (which was already instantiated before creating the transport object).

Step 3:

Now that we have the transport object created, the final step is to bring the ElasticsearchClient to life. The code in the following block, shows how:

# Step 3: Construct the ElasticsearchTransport object
ElasticsearchClient elasticsearchClient =
new ElasticsearchClient(elasticsearchTransport);

That’s pretty much it — you got a client at hand, it’s time to use it to interact with Elasticsearch. Let’s jump into action for creating an index for flights using this Java API Client.

Creating an Index

All indices related operations form a namespace called “indices”, hence classes and a client related to operations on indices are present in co.elastic.clients.elasticsearch.indices package. As expected, the client supporting the “indices” operations is the ElasticearchIndicesClient. To obtain this client, one must ask the main Java API client (the ElasticsearchClient) to provide the instance of the indices client. This is shown in the following snippet:

ElasticsearchIndicesClient elasticsearchIndicesClient =
this.elasticsearchClient.indices();

As you can sense, calling the indices() function on the elasticsearchClient instance returns us the ElasticsearchIndicesClient. Once we have the respective client, like ElasticsearchIndicesClient, we can invoke create() method to create an index. The create() method expects a Request object — a CreateIndexRequest object to be precise. This pattern leads to our next request/response pattern.

All methods on the client are expected to be passed in with a request object. As you can expect there are a lot of request classes. Each of these request objects are instantiated using the builder pattern.

Take an example of “creating an index” use case. This requires a CreateIndexRequest to be instantiated with arguments that are required to create an index. The following code snippet creates the CreateIndexRequest using builder:

CreateIndexRequest createIndexRequest =
new CreateIndexRequest.Builder().index("flights").build();

The index() method accepts a string as the name of the index. Once we have the index created, we can then invoke the create method on the indices client by passing this request. This is shown in the following snippet:

CreateIndexResponse createIndexResponse =
elasticsearchIndicesClient.create(createIndexRequest);

This call will invoke the create method on the elasticsearchIndicesClient — which in turn sends a query to Elasticsearch to get the index created.

The result of this invocation is captured in the response object: CreateIndexResponse in this case. Again, any invocation will return a response — it follows the same pattern — for example: the CreateIndexRequest’s response would be CreateIndexResponse object. The response object would have all the required information about the newly created index.

The full method is given in the listing c.4 below — but the source code for the whole class along with the project is available on my GitHub repository here: https://github.com/madhusudhankonda/elasticsearch-clients

/**
* Method to create an index using bog-standard ElasticsearchIndicesClient
*
* @param indexName Name of the index
* @throws IOException Exception thrown
*/
public void createIndexUsingClient(String indexName) throws IOException {
ElasticsearchIndicesClient elasticsearchIndicesClient =
this.elasticsearchClient.indices();
CreateIndexRequest createIndexRequest =
new CreateIndexRequest.Builder()
.index(indexName).build();

CreateIndexResponse createIndexResponse =
elasticsearchIndicesClient.create(createIndexRequest);
System.out.println("Index created successfully: "+createIndexResponse);
}

Now, rather than instantiating the ElasticsearchIndicesClient separately, as shown in the above code snippet, we can instead use a builder as shown in the following listing:

// Method to create the index using Builder pattern
public void createIndexUsingBuilder(String indexName) throws IOException {

CreateIndexResponse createIndexResponse = this.elasticsearchClient
.indices().create(new CreateIndexRequest.Builder()
.index(indexName)
.build());

System.out.println("Index created successfully using Builder"+createIndexResponse);
}

We did not create the ElasticsearchIndicesClient in this method, instead we passed in the request object (as a builder) to the create() method (the indices() method will fetch the ElasticsearchIndicesClient on which the create method gets called behind the scenes).

We can still go further — we can use a Lambda function to concise this code even more. The following listing will show you how we can shorten the code by using a lambda function:

// A method to create an index using Lambda expression
public void createIndexUsingLambda(String indexName) throws IOException {

CreateIndexResponse createIndexResponse =
this.elasticsearchClient.indices().create(
request -> request.index(indexName)
);

System.out.println("Index created using Lambda"+createIndexResponse);
}

In the next article, we will put this client to work by indexing a flight document into our index — stay tuned

This is part 1 of 2 article mini-series:

  1. Introducing Java API client: Creating an index
  2. Search and index using Java API client

Me @ Medium || LinkedIn || Twitter || GitHub