Micronaut OpenSearch

Micronaut integration with OpenSearch

Version: 1.7.0-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 Default value

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 Default value

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 Default value

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 Default value

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: