Micronaut OpenSearch

Micronaut integration with OpenSearch

Version: 1.0.1-SNAPSHOT

1 Introduction

Micronaut OpenSearch simplifies integration with OpenSearch.

OpenSearch is the flexible, scalable, open-source way to build solutions for data-intensive applications.

OpenSearchClient

Once you have integrated Micronaut OpenSearch, you are able to inject a bean of type org.opensearch.client.opensearch.OpenSearchClient or org.opensearch.client.opensearch.OpenSearchAsyncClient.

package micronaut.example.service;

import java.util.Iterator;

import jakarta.inject.Singleton;
import micronaut.example.configuration.AppConfiguration;
import micronaut.example.exception.MovieServiceException;

import org.opensearch.client.opensearch.OpenSearchClient;
import org.opensearch.client.opensearch.core.IndexRequest;
import org.opensearch.client.opensearch.core.IndexResponse;
import org.opensearch.client.opensearch.core.SearchResponse;
import org.opensearch.client.opensearch.core.search.Hit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Singleton
class MovieServiceImpl implements MovieService {
    private static final Logger LOG = LoggerFactory.getLogger(MovieServiceImpl.class);
    private final AppConfiguration appConfiguration;
    private final OpenSearchClient client;

    public MovieServiceImpl(AppConfiguration appConfiguration, OpenSearchClient client) {
        this.appConfiguration = appConfiguration;
        this.client = client;
    }

    @Override
    public String saveMovie(Movie movie) {
        try {
            IndexRequest<Movie> indexRequest = createIndexRequest(movie);
            IndexResponse indexResponse = client.index(indexRequest);
            String id = indexResponse.id();
            LOG.info("Document for '{}' {} successfully in ES. The id is: {}", movie, indexResponse.result(), id);
            return id;
        } catch (Exception e) {
            String errorMessage = String.format("An exception occurred while indexing '%s'", movie);
            LOG.error(errorMessage);
            throw new MovieServiceException(errorMessage, e);
        }
    }

    private IndexRequest<Movie> createIndexRequest(Movie movie) {
        return new IndexRequest.Builder<Movie>()
            .index(appConfiguration.getMoviesIndexName())
            .document(movie)
            .build();
    }

    @Override
    public Movie searchMovies(String title) {
        try {
            SearchResponse<Movie> searchResponse = client.search(s ->
                    s.index(appConfiguration.getMoviesIndexName()).query(q ->
                            q.match(m ->
                                    m.field("title").query(fq -> fq.stringValue(title)))), Movie.class);
            LOG.info("Searching for '{}' took {} and found {}",
                    title,
                    searchResponse.took(),
                    searchResponse.hits().total().value());
            Iterator<Hit<Movie>> hits = searchResponse.hits().hits().iterator();
            if (hits.hasNext()) {
                return hits.next().source();
            }
            return null;
        } catch (Exception e) {
            String errorMessage = String.format("An exception occurred while searching for title '%s'", title);
            LOG.error(errorMessage);
            throw new MovieServiceException(errorMessage, e);
        }
    }
}
package micronaut.example.service

import micronaut.example.configuration.AppConfiguration
import micronaut.example.exception.MovieServiceException
import jakarta.inject.Singleton
import org.opensearch.client.opensearch.OpenSearchClient
import org.opensearch.client.opensearch.core.IndexRequest
import org.opensearch.client.opensearch.core.IndexResponse
import org.opensearch.client.opensearch.core.SearchResponse
import org.opensearch.client.opensearch.core.search.Hit
import org.slf4j.Logger
import org.slf4j.LoggerFactory

@Singleton
class MovieServiceImpl implements MovieService {

    private static final Logger LOG = LoggerFactory.getLogger(MovieServiceImpl.class)

    private final AppConfiguration appConfiguration

    private final OpenSearchClient client

    MovieServiceImpl(AppConfiguration appConfiguration, OpenSearchClient client) {
        this.appConfiguration = appConfiguration
        this.client = client
    }

    @Override
    String saveMovie(Movie movie) {
        try {
            IndexRequest<Movie> indexRequest = createIndexRequest(movie)

            IndexResponse indexResponse = client.index(indexRequest)
            String id = indexResponse.id()

            LOG.info("Document for '{}' {} successfully in ES. The id is: {}", movie, indexResponse.result(), id)
            return id
        } catch (Exception e) {
            String errorMessage = String.format("An exception occurred while indexing '%s'", movie)
            LOG.error(errorMessage)
            throw new MovieServiceException(errorMessage, e)
        }
    }

    private IndexRequest<Movie> createIndexRequest(Movie movie) {
        return new IndexRequest.Builder<Movie>()
            .index(appConfiguration.getMoviesIndexName())
            .document(movie)
            .build()
    }

    @Override
    Movie searchMovies(String title) {
        try {
            SearchResponse<Movie> searchResponse = client.search((s) ->
                s.index(appConfiguration.getMoviesIndexName())
                    .query(q -> q.match(m ->
                        m.field("title")
                         .query(fq -> fq.stringValue(title))
                    )), Movie.class
            )
            LOG.info("Searching for '{}' took {} and found {}", title, searchResponse.took(), searchResponse.hits().total().value())

            Iterator<Hit<Movie>> hits = searchResponse.hits().hits().iterator()
            if (hits.hasNext()) {
                return hits.next().source()
            }
            return null

        } catch (Exception e) {
            String errorMessage = String.format("An exception occurred while searching for title '%s'", title)
            LOG.error(errorMessage)
            throw new MovieServiceException(errorMessage, e)
        }
    }
}
package micronaut.example.service

import jakarta.inject.Singleton
import micronaut.example.configuration.AppConfiguration
import micronaut.example.exception.MovieServiceException
import org.opensearch.client.opensearch.OpenSearchClient
import org.opensearch.client.opensearch.core.IndexRequest
import org.opensearch.client.opensearch.core.IndexResponse
import org.opensearch.client.opensearch.core.SearchResponse
import org.opensearch.client.opensearch.core.search.Hit
import org.slf4j.Logger
import org.slf4j.LoggerFactory

@Singleton
class MovieServiceImpl(
    private val appConfiguration: AppConfiguration,
    private val client: OpenSearchClient
) : MovieService {

    override fun saveMovie(movie: Movie): String {
        try {
            val indexRequest: IndexRequest<Movie> = createIndexRequest(movie)
            val indexResponse: IndexResponse = client.index(indexRequest)
            val id: String = indexResponse.id()
            LOG.info("Document for '{}' {} successfully in ES. The id is: {}", movie, indexResponse.result(), id)
            return id
        } catch (e: Exception) {
            val errorMessage = String.format("An exception occurred while indexing '%s'", movie)
            LOG.error(errorMessage)
            throw MovieServiceException(errorMessage, e)
        }
    }

    private fun createIndexRequest(movie: Movie): IndexRequest<Movie> {
        return IndexRequest.Builder<Movie>()
            .index(appConfiguration.getMoviesIndexName())
            .document(movie)
            .build()
    }

    override fun searchMovies(title: String): Movie? {
        try {
            val searchResponse: SearchResponse<Movie> = client.search({ s ->
                    s.index(appConfiguration.getMoviesIndexName()).query { q ->
                            q.match { m ->
                                m.field("title").query { fq -> fq.stringValue(title) }
                            }}}, Movie::class.java)
            LOG.info(
                "Searching for '{}' took {} and found {}",
                title,
                searchResponse.took(),
                searchResponse.hits().total().value()
            )

            val hits: Iterator<Hit<Movie>> = searchResponse.hits().hits().iterator()
            if (hits.hasNext()) {
                return hits.next().source()
            }
            return null
        } catch (e: Exception) {
            val errorMessage = String.format("An exception occurred while searching for title '%s'", title)
            LOG.error(errorMessage)
            throw MovieServiceException(errorMessage, e)
        }
    }

    companion object {
        private val LOG: Logger = LoggerFactory.getLogger(MovieServiceImpl::class.java)
    }
}

2 OpenSearch Amazon

To use Micronaut OpenSearch and connect to Amazon OpenSearch Service add the following dependency:

implementation("io.micronaut.opensearch:micronaut-opensearch-amazon")
<dependency>
    <groupId>io.micronaut.opensearch</groupId>
    <artifactId>micronaut-opensearch-amazon</artifactId>
</dependency>

You can configure the connection with:

🔗
Table 1. Configuration Properties for AmazonOpenSearchConfigurationProperties
Property Type Description

micronaut.opensearch.aws.enabled

boolean

Whether Amazon OpenSearch Service integration is enabled. Default value true

micronaut.opensearch.aws.endpoint

java.lang.String

Sets the OpenSearch endpoint, without https:// For example: search-…​us-east-1.aoss.amazonaws.com.

micronaut.opensearch.aws.signing-region

software.amazon.awssdk.regions.Region

The region to use for signing requests. For example: us-east-1.

Moreover, you can create a BeanCreatedEventListener for a bean of type org.opensearch.client.transport.aws.AwsSdk2TransportOptions.Builder, to configure the connection according to your use case.

import io.micronaut.context.event.BeanCreatedEvent;
import io.micronaut.context.event.BeanCreatedEventListener;
import jakarta.inject.Singleton;
import org.opensearch.client.transport.aws.AwsSdk2TransportOptions;

@Singleton
class AwsSdk2TransportOptionsBeanCreatedEventListener implements BeanCreatedEventListener<AwsSdk2TransportOptions.Builder> {
    @Override
    public AwsSdk2TransportOptions.Builder onCreated(BeanCreatedEvent<AwsSdk2TransportOptions.Builder> event) {
        AwsSdk2TransportOptions.Builder builder = event.getBean();
        // Modify the builder here
        return builder;
    }
}

3 OpenSearch using Apache HttpClient 5 Transport

To use Micronaut OpenSearch and connect using Apache HttpClient 5 transport, add the following dependencies:

implementation("io.micronaut.opensearch:micronaut-opensearch-httpclient5")
<dependency>
    <groupId>io.micronaut.opensearch</groupId>
    <artifactId>micronaut-opensearch-httpclient5</artifactId>
</dependency>

You can configure the connection with:

🔗
Table 1. Configuration Properties for HttpClient5OpenSearchConfigurationProperties
Property Type Description

micronaut.opensearch.httpclient5.http-hosts

org.apache.hc.core5.http.HttpHost

One or more hosts that client will connect to.

micronaut.opensearch.httpclient5.enabled

boolean

If OpenSearch integration is enabled. Default value true

You can create a BeanCreatedEventListener for a bean of type org.opensearch.client.transport.httpclient5.ApacheHttpClient5TransportBuilder to configure the connection according to your use case.

4 OpenSearch RestClient

To use Micronaut OpenSearch and connect with RestClient-based transport add the following dependencies:

implementation("io.micronaut.opensearch:micronaut-opensearch-restclient")
<dependency>
    <groupId>io.micronaut.opensearch</groupId>
    <artifactId>micronaut-opensearch-restclient</artifactId>
</dependency>

implementation("io.micronaut:micronaut-jackson-databind")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-jackson-databind</artifactId>
</dependency>

You can configure the connection with:

🔗
Table 1. Configuration Properties for RestClientOpenSearchConfigurationProperties
Property Type Description

micronaut.opensearch.rest-client.http-hosts

org.apache.http.HttpHost

One or more hosts that client will connect to.

micronaut.opensearch.rest-client.node-selector

org.opensearch.client.NodeSelector

The {@link NodeSelector} to be used, in case of multiple nodes.

micronaut.opensearch.rest-client.default-headers

org.apache.http.Header

The defaults {@link Header} to sent with each request.

micronaut.opensearch.rest-client.enabled

boolean

If OpenSearch integration is enabled. Default value true

You can create BeanCreatedEventListeners for a beans of type org.apache.http.client.config.RequestConfig.Builder, org.apache.http.impl.nio.client.HttpAsyncClientBuilder, and org.opensearch.client.RestClientBuilder to configure the connection according to your use case.

5 Health

After you add the management dependency, the Health Endpoint exposes a health indicator for OpenSearch.

You can disable it with:

🔗
Table 1. Configuration Properties for OpenSearchHealthIndicatorConfigurationProperties
Property Type Description

endpoints.health.opensearch.enabled

boolean

Whether OpenSearch Health Indicator is enabled. Default value true

6 Release History

For this project, you can find a list of releases (with release notes) here:

7 Repository

You can find the source code of this project in this repository: