Ejemplos de cliente java con Elasticsearch

De ChuWiki


Para acceder desde Java a Elasticsearch hay varias opciones. Puesto que Elasticsearch ofrece una API REST, podemos ir desde un cliente REST cualquiera en Java hasta usar lo más sofisitcado que nos ofrece Elasticsearc: la clase Java ElasticsearchClient.

Aquí veremos ejemplos básicos de inserción, consulta, modificación y borrado de documentos en Elasticsearch usando esta clase. Tienes todo el código en elastic-example en github.

Dependencias maven[editar]

Lo primero es añadir las dependencias necesarias en nuestro proyecto Java. Si usamos Maven podrían ser las siguientes

    <dependency>
      <groupId>co.elastic.clients</groupId>
      <artifactId>elasticsearch-java</artifactId>
      <version>8.12.0</version>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.16.1</version>
      <scope>runtime</scope>
    </dependency>

Tenemos elasticsearch-java que es el conector de Java con Elasticsearch y donde está la clase ElasticsearchClient. Es necesario además algún JsonProvier, pero solo en ejecución. Así que debemos añadir la dependencia de jackson-databind, que es el que espera elastic-seasrch-java.

Conexión[editar]

El siguiente paso es establecer la conexión con Elasticsearch. Según esté configurado hay varias opciones. Aquí vamos con una en concreto. Elasticsearch usa HTTPS con un certificado firmado por un certificado de confianza generado también por el propio Elasticsearch. Y además necesitamos usuario y password para el acceso.

El código puede ser el siguiente.

        // URL de Elasticsearch
        String elasticsearchUrl = "https://localhost:9200";

        try {
            // Copia del certificado que usa Elasticsearch para crear sus certificados. Está en
            // la instalación de Elasticsearch, carpeta /usr/share/elasticsearch/config/certs/http_ca.crt
            File certFile = new File("src/main/files/http_ca.crt");
            SSLContext sslContext = TransportUtils
                    .sslContextFromHttpCaCrt(certFile);

            // Credenciales para acceso a Elasticsearch. Usuario y Password.
            BasicCredentialsProvider credentialsProvider =  new BasicCredentialsProvider();
            credentialsProvider.setCredentials(AuthScope.ANY,
                    new UsernamePasswordCredentials("elastic","prWv9i_2iKn193lA0gS2"));

            // Creación del cliente REST necesario para uso de la API de Elasticsearch
            RestClient restClient = RestClient.builder(HttpHost.create(elasticsearchUrl))
                    .setHttpClientConfigCallback(hc ->
                            hc.setDefaultCredentialsProvider(credentialsProvider)
                                    .setSSLContext(sslContext))
                    .build();

            transport = new RestClientTransport(restClient, new JacksonJsonpMapper());

            // Elasticsearch Cliente. Es la instancia que usaremos para hablar con Elasticsearch.
            elasticsearchClient = new ElasticsearchClient(transport);
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(-1);
        }

Explicamos los puntos principales.

elasticsearchUrl tiene la URL en la que está escuchando Elasticsearch, por https.

El certificado de confianza que ha generado Elasticsearch es el fichero http_ca.crt. Lo hemos extraído del servidor de Elasticsearch y hemos hecho una copia en una carpeta de nuestro proyecto, en concreto src/main/files/http_ca.crt. Dentro de Elasticsearch, este certificado está en la carpeta elasticsearch/config/certs/http_ca.crt de donde lo tengamos instalado. La imagen docker de Elasticsearch, por ejemplo, lo ubica en /usr/share/elasticsearch/config/certs/http_ca.crt.

Con el certificado creamos el SSLContext

Con BasicCredentialsProvider creamos una instancia con el usuario y password de acceso.

Con RestClient.builder creamos un RestClient al que pasamos tanto el SSLContext como el BasicCredentialsProvider.

Necesitamos ahora crear un RestClientTransport en el que pasamos el RestClient recién creado y un mapper de JSON, de forma que Elasticsearch sea capaz de convertir nuestras clases de datos a JSON y viceversa. La clase JacksonJsonpMapper es propia de Elasticsearch, pero es la que usa por debajo el JSONProvider que comentamos en el primer punto y por el que es necesario que tengamos, como runtime, una dependencia de la librería jackson-databind

Y finalmente, con todo esto, ya podemos crear nuestra instancia de ElasticsearchClient, que es la que usaremos para todas las operaciones con Elasticsearch desde Java.

Crear un índice[editar]

El siguiente paso es crear un índice. En Elasticsearch un índice es donde se guardan todos los documentos de un determinado tipo. Sería el equivalente a una tabla en una base de datos SQL o una colección en una base de datos MongoDB.

        String PRODUCTS = "products";

        try {
            CreateIndexRequest cir = new CreateIndexRequest.Builder()
                    .index(PRODUCTS)
                    .build();

            elasticsearchClient.indices().create(cir);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ElasticsearchException e){
            e.printStackTrace();
        }

PRODUCTS es una constante String con el nombre de nuestro índice. "products" en nuestro ejemplo. Puede ser el que queramos.

CreateIndexRequest es la petición para crear un índice. Usamos su Builder para obtener una instancia indicando que queremos crear un índice PRODUCTS.

Finalmente, con elasticsearchClient.indices().create() lanzamos al petición.

Hacer una inserción[editar]

Vamos ahora a insertar un documento en ese índice. Será nuestra clase Product.java

public record Product(String id, String name, Integer value) {
}

El código para la inserción puede ser el siguiente

        Product product = new Product("bk-1", "City bike", 123);

        try {
            IndexRequest ir = new IndexRequest.Builder<Product>()
                    .index(PRODUCTS)
                    .id(product.id())
                    .document(product)
                    .build();
            IndexResponse response = elasticsearchClient.index(ir);
        } catch (IOException e) {
            e.printStackTrace();
        }

Hacemos una instancia de Product con los tres valores id, name y value.

Usando el Builder correspondiente, creamos una instancia de IndexRequest que es la petición para meter un documento en el índice. Indicamos que el índice es PRODUCTS, que usamos como id clave el campo product.id() y que el documento a insertar es la instancia de Product.

Finalmente, con la llamada elasticsearchClient.index() lanzamos la petición. Obtenemos el resultado de cómo ha ido en IndexResponse.

Hacer inserción múltiple (bulk)[editar]

Insertar documentos de uno en uno no suele ser eficiente. Cuando se quieren hacer muchas operaciones en base de datos suele haber mecanismos para hacerlas por bloques. Suelen llamarse operaciones "batch" o "bulk". En el caso de Elasticsearch tenemos BulkRequest. Para hacer varias inserciones el código sería el sigiuente

        List<Product> productList = new ArrayList<>();
        productList.add(new Product("1","Uno", 1));
        productList.add(new Product("2","Dos", 2));
        productList.add(new Product("3","Tres", 3));
        productList.add(new Product("4","Cuatro", 1));  // El 1 está a posta para que haya dos documentos con el valor igual.

        BulkRequest.Builder bulkRequest = new BulkRequest.Builder();

        productList.forEach(product -> {
            IndexOperation io = new IndexOperation.Builder<Product>().index(PRODUCTS).document(product).id(product.id()).build();
            BulkOperation bo = (BulkOperation) new BulkOperation.Builder().index(io).build();
            bulkRequest.operations(bo);
        });

        try {
            BulkResponse bulkResponse = elasticsearchClient.bulk(bulkRequest.build());
            if (bulkResponse.errors()){
                System.out.println("Algo va mal en bulk insert");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

Primero creamos una lista de Product a insertar y una instancia de BulkRequest.

En un bucle para cada Product. Puesto que queremos hacer inserciones, creamos una IndexOperation por cada producto. Indicamos que el índice es PRODUCTS, el documento product que queremos insertar y cual es su clave primaria product.id().

Esta IndexOperation la metemos en una BulkOperation. Y esta a su vez la añadimos al BulkRequest.

Terminado el bucle, lanzamos BulkRequest con elasticsearchClient.bulk(). Obtenemos el resultado en BulkResponse donde podemos ver si ha ahbido algún error.

Consultar todos los documentos de un índice[editar]

El siguiente código muestra una consulta de todos los documentos de un índice.

        try {
            SearchRequest sr = new SearchRequest.Builder()
                    .index(PRODUCTS)
                    .build();

            SearchResponse<Product> products = elasticsearchClient.search(sr, Product.class);

            products.hits().hits().forEach(product -> System.out.println(product));
        } catch (IOException e) {
            e.printStackTrace();
        }

Creamos una SerachRequest pasando el índice que queremos consultar PRODUCTS y no ponemos ninguna condición.

Lanzamos SearchRequest con elasticsearchClient.search(). Se pasa de parámetro la SearchRequst y la clase Java que esperamos como resultado de la consulta.

El resultado se obtiene en un SearchResponse. La llamada al método hits() nos devuelve unas estadísticas del resultado de la consulta. La segunda llamada nos devueve los Product en sí. Hacemos un bucle para ir sacando por pantalla cada uno de ellos.

Consultar por id[editar]

El siguiente código hace una consulta de un documento por su id.

        try {
            GetRequest gr = new GetRequest.Builder()
                    .index(PRODUCTS)
                    .id("1")
                    .build();

            GetResponse<Product> productGetResponse = elasticsearchClient.get(gr, Product.class);

            System.out.println(productGetResponse.source());
        } catch (IOException e) {
            e.printStackTrace();
        }

Creamos una instancia de GetRequest indicando el índice a consultar PRODUCTS y el id del documento que queremos obtener "1".

Una llamada a elasticsearchClient.get() lanza la consulta. Dos parámetros, el GetRequest y la clase Java que esperamos obtener como resultado de la consulta.

El product resultado se obtiene en GetResponse.source()

Consultar con criterios de query[editar]

El siguiente código hace una consulta por el campo "value".

        try {
            Query q = QueryBuilders
                    .match()
                    .field("value")
                    .query(1)
                    .build()
                    ._toQuery();

            SearchRequest sr = new SearchRequest.Builder()
                    .index(PRODUCTS)
                    .query(q)
                    .build();

            SearchResponse<Product> search = elasticsearchClient.search(sr, Product.class);

            search.hits().hits().forEach(product -> System.out.println(product.source()));
        } catch (IOException e) {
            e.printStackTrace();
        }

Usando QueryBuilders construimos una consulta match() que sirve para buscar por un campo.

Con field() indicamos qué campo queremos buscar, con query() el valor que queramos que tenga ese campo. Con build()._toQuery() construimos la Query con los datos que le hemos pasado.

Ahora construimos un SearchRequest() para indicar que queremos buscar en el índice PRODUCTS y paar la query() con la condición que acabamos de construir.

Finalmente, con elasticsearchClient.search() lanzamos la consulta. Como segundo parámetro, decimos que estamos esperando un resultado de la clase Product.

En SearchResponse tenemos el resultado de la consulta. hits() nos dará una lista de todos los Product resultado de la consulta. Basta hacer un bucle para ir sacándolos por pantalla.

Modificar un documento[editar]

El siguiente código modifica un documento.

        Product updatedProduct = new Product(null, "One", null);
        try {
            UpdateRequest ur = new UpdateRequest.Builder<Product, Product>()
                    .index(PRODUCTS)
                    .id("1")
                    .doc(updatedProduct)
                    .build();

            UpdateResponse<Product> update = elasticsearchClient.update(ur, Product.class);
        } catch (IOException e) {
            e.printStackTrace();
        }

Creamos un UpdateRequest usando el Builder. Builder es un tipo genérico que está parametrizado por dos tipos. En nuestro ejemplo, en ambos hemos puesto <Product>. El primero es el tipo de documento que está en la base de datos, es decir, nuestro Product. El segundo es el tipo que contiene los cambios que queremos realizar. Usamos también Product, aunque podría ser otra clase que contenga sólo los campos que queramos actualizar.

En la línea anterior a la creación de UpdateRequest, hemos creado un Product rellenando solo los campos que queremos cambiar, el resto los hemos dejado a null.

Volviendo a la creación de UpdateRequest, indicamos también con index() que el documento que queremos cambiar es del índice PRODUCTS, que el id() del documento que queremos cambiar es "1" y con doc() pasamos el Product con los campos que queremos cambiar.

Una vez construido build() el UpdateRequest, lo lanzamos con elasticsearchClient.update(). El resultado del update podríamos analizarlo en el UpdateResponse que nos devuelve el método.

Tenemos también la clase UpdateByQueryRequest si quisiéramos modificar todos los documentos que cumplen una determinada condición. En vez de el identificador del documento id(), pondríamos la query query() con las condiciones que deben cumplir los documentos que queramos modificar.

Borrar un documento[editar]

El siguiente código borra un documento.

        try {
            DeleteRequest dr = new DeleteRequest.Builder()
                    .index(PRODUCTS)
                    .id("1")
                    .build();

            DeleteResponse delete = elasticsearchClient.delete(dr);
        } catch (IOException e) {
            e.printStackTrace();
        }

Creamos un DeleteRequest utilizando el Builder. Indicamos que el índice ``index()`` donde está el documento es PRODUCTS, que el identificador id() del documento es "1" y construimos build() la instancia.

Con elasticsearchClient.delete() lanzamos la petición. El resultado de la operación se puede analizar en DeleteResponse.

Tenemos también DeleteByQuery si queremos borrar documentos que cumplan una determinada condición. En esta clase, vez de id() del documento, podemos pasar query() con la condición.

Borrar un índice[editar]