Tanto Java como Elasticsearch son elementos populares dentro de los stacks de tecnología comunes que utilizan las empresas. Java es un lenguaje de programación que se lanzó en 1996. Java es propiedad de Oracle y todavía está en desarrollo activo.
Elasticsearch es una tecnología joven en comparación con Java; solo se lanzó en 2010 (lo que la hace 14 años más joven que Java). Está ganando popularidad rápidamente y ahora se utiliza en muchas empresas como motor de búsqueda.
Al ver lo populares que son ambos, muchas personas y empresas quieren conectar Java con Elasticsearch para desarrollar su propio motor de búsqueda. En este artículo, quiero enseñarle cómo conectar Java Spring Boot 2 con Elasticsearch. Aprenderemos a crear una API que llamará a Elasticsearch para producir resultados.
Conectando Java con Elasticsearch
Lo primero que debemos hacer es conectar nuestro proyecto Spring Boot con Elasticsearch. La forma más fácil de hacer esto es usar la biblioteca cliente proporcionada por Elasticsearch, que podemos agregar a nuestro administrador de paquetes (como Maven o Gradle).
Para este artículo, usaremos una biblioteca spring-data-elasticsearch proporcionada por Spring Data, que también incluye la biblioteca de cliente de alto nivel de Elasticsearch.
Comencemos por crear nuestro proyecto Spring Boot con Spring Initialzr. Configuraré mi proyecto para que sea como la imagen a continuación, ya que vamos a utilizar un cliente de alto nivel. Luego, podemos usar una biblioteca conveniente proporcionada por Spring, Spring Data Elasticsearch:
Al generar un proyecto con este conveniente formulario ya tenemos todas las dependencias preconfiguradas en un proyecto de maven y lo único que queda es abrirlo con nuestro IDE favorito (IntelliJ) y ponernos manos a la obra.
Creando el bean del cliente Elasticsearch
Hay dos métodos para inicializar el bean: puede utilizar los beans definidos en la biblioteca Spring Data Elasticsearch o puede crear su propio bean.
La opción más sencilla es utilizar el bean configurado por Spring Data Elasticsearch.
Por ejemplo, puede agregar estas propiedades en su application.properties:
spring.elasticsearch.rest.uris=localhost:9200
spring.elasticsearch.rest.connection-timeout=1s
spring.elasticsearch.rest.read-timeout=1m
spring.elasticsearch.rest.password=
spring.elasticsearch.rest.username=
El segundo método consiste en crear su propio bean. Puede configurar los ajustes creando el bean RestHighLevelClient. Si el bean existe, Spring Data lo usará como configuración.
@Configuration
@RequiredArgsConstructor
public class ElasticsearchConfiguration extends AbstractElasticsearchConfiguration {
private final ElasticsearchProperties elasticsearchProperties;
@Override
@Bean
public RestHighLevelClient elasticsearchClient() {
final ClientConfiguration clientConfiguration = ClientConfiguration.builder()
.connectedTo(elasticsearchProperties.getHostAndPort())
.withConnectTimeout(elasticsearchProperties.getConnectTimeout())
.withSocketTimeout(elasticsearchProperties.getSocketTimeout())
.build();
return RestClients.create(clientConfiguration).rest();
}
}
Debemos respetar los nombres en ingles de esta clase o de lo contrario nuestra configuración no tomará efecto.
Probando la conexión de nuestra aplicación Spring Boot a Elasticsearch
Su aplicación Spring Boot y Elasticsearch deberían estar conectados ahora que ha configurado el bean. Ya que vamos a probar la conexión, asegúrese de que su Elasticsearch esté en funcionamiento.
Para probarlo, podemos crear un bean que creará un índice en Elasticsearch en DemoApplication.java. La clase se vería así:
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Bean
public boolean createTestIndex(RestHighLevelClient restHighLevelClient) throws Exception {
try {
DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest("hello-world");
restHighLevelClient.indices().delete(deleteIndexRequest, RequestOptions.DEFAULT); // 1
} catch (Exception ignored) { }
CreateIndexRequest createIndexRequest = new CreateIndexRequest("hello-world");
createIndexRequest.settings(
Settings.builder().put("index.number_of_shards", 1)
.put("index.number_of_replicas", 0));
restHighLevelClient.indices().create(createIndexRequest, RequestOptions.DEFAULT); // 2
return true;
}
}
Bien, en ese código llamamos a Elasticsearch dos veces con RestHighLevelClient, que aprenderemos más adelante en este artículo. La primera llamada es eliminar el índice “hello-world” si ya existe. Usamos un try / catch eso, porque si el índice, no existe elasticsearch arrojará un error, fallando el proceso de inicio de nuestra aplicación, por eso simplemente ignoramos el error ya que esta bien que no exista, adicionalmente podriamos agregar algunos logs.
La segunda llamada es crear un índice. Como solo estoy ejecutando un Elasticsearch de un solo nodo, configuré los fragmentos (shards) en 1 y las réplicas en 0.
Si todo salió bien, entonces debería ver los índices cuando revise su Elasticsearch. Para comprobarlo, vaya a http://localhost:9200/_cat/indices?v
, y podrá ver la lista de índices en su Elasticsearch.
Otras formas de conectarse
Les recomiendo que use la biblioteca spring-data-elasticsearch si desea conectarse a Elasticsearch con Java. Pero si no puede usar esa biblioteca, existe otra forma de conectar sus aplicaciones a Elasticsearch.
Cliente High Level
Como sabemos por la sección anterior, la biblioteca spring-data-elasticsearch que usamos también incluye el cliente de alto nivel de Elasticsearch. Si ya ha importado spring-data-elasticsearch, entonces ya puede usar el cliente de alto nivel de Elasticsearch.
Si lo desea, también es posible utilizar la biblioteca de High Level Client directamente sin la dependencia de Spring Data. Solo necesita agregar esta dependencia en su administrador de dependencias:
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>8.0.0</version>
</dependency>
También usaremos este cliente en nuestros ejemplos porque la funcionalidad en el cliente de alto nivel es más completa que la de spring-data-elasticsearch.
Para obtener más información, puede leer la documentación de Elasticsearch.
Cliente Low Level
Te resultará más difícil con esta biblioteca, pero puedes personalizarla más. Para usarlo, puede agregar la siguiente dependencia:
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>8.0.0</version>
</dependency>
Para obtener más información, puede leer la documentación de Elasticsearch sobre esto.
Conexión REST
La última forma de conectarse a Elasticsearch es mediante una llamada REST. Dado que Elasticsearch usa la API REST para conectarse a su cliente, básicamente puede usar una llamada REST para conectar sus aplicaciones a Elasticsearch. Puede usar OkHttp, Feign o su cliente web para conectar sus aplicaciones con Elasticsearch.
Tampoco recomiendo este método porque es una molestia. Dado que Elasticsearch ya proporciona bibliotecas cliente, es mejor usarlas en su lugar. Utilice este método solo si no tiene otra forma de conectarse.
Usando Spring Data Elasticsearch
Primero, aprendamos a usar spring-data-elasticsearch
en nuestro proyecto Spring. spring-data-elasticsearch
es muy fácil de usar y una biblioteca de alto nivel que podemos usar para acceder a Elasticsearch.
Creando una entidad y configurando nuestro índice
Una vez que haya terminado de conectar sus aplicaciones con Elasticsearch, es hora de crear una entidad. Con Spring Data, podemos agregar metadatos a nuestra entidad, que serán leídos por el bean de repositorio que creamos. De esta manera, el código será mucho más limpio y más rápido de desarrollar, ya que no necesitaremos crear ninguna lógica de mapeo en nuestro nivel de servicio.
Creemos una entidad llamada Producto
:
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Document(indexName = "product", shards = 1, replicas = 0, refreshInterval = "5s", createIndex = true)
public class Producto {
@Id
private String id;
@Field(type = FieldType.Text)
private String nombre;
@Field(type = FieldType.Keyword)
private Categoria categoria;
@Field(type = FieldType.Long)
private double precio;
public enum Categoria {
ROPA,
ELECTRONICOS,
JUEGOS;
}
}
Así que déjame explicarte lo que está sucediendo en el bloque de código anterior. Primero, no explicaré sobre @Data
, @AllArgsConstructor
, @NoArgsConstructor
y @Builder
. Son anotaciones de la biblioteca de Lombok para constructor, captador, definidor, constructor y otras cosas.
Ahora, hablemos de la primera anotación de datos de primavera en la entidad, @Document
. La anotación @Document
muestra que la clase es una entidad que contiene metadatos de la configuración del índice Elasticsearch. Para usar el repositorio de Spring Data, que aprenderemos más adelante, la anotación @Document
es obligatoria.
La única anotación obligatoria en @Document
es indexName
. Debe quedar bastante claro por el nombre, debemos completarlo con el nombre de índice que queremos usar para la entidad. En este artículo, usaremos el mismo nombre que la entidad, producto.
El segundo parámetro de @Document
del que hablar es el parámetro createIndex
. Si configura el createIndex
como verdadero, sus aplicaciones crearán un índice automáticamente cuando inicie las aplicaciones si el índice aún no existe.
Los parámetros shards
, replicas
y refreshInterval
determinan la configuración del índice cuando se crea el índice. Si cambia el valor de esos parámetros después de que el índice ya está creado, la configuración no se aplicará. Por lo tanto, los parámetros solo se utilizarán al crear el índice por primera vez.
Si desea utilizar una ID personalizada en Elasticsearch, puede utilizar las anotaciones @Id
. Si usa anotaciones @Id
, Spring Data le dirá a Elasticsearch que almacene la ID en el documento y la fuente del documento.
El tipo @Field
determinará la asignación de campo del campo. Al igual que los fragmentos, las réplicas y refreshInterval, el tipo @Field
solo afectará a Elasticsearch al crear el índice por primera vez. Si agrega un nuevo campo o cambia los tipos cuando el índice ya está creado, no hará nada.
Ahora que configuramos la entidad, ¡probemos la creación automática de índices por parte de Spring Data! Cuando configuramos el createIndex como verdadero, Spring Data verificará si el índice existe en Elasticsearch. Si no existe, Spring Data creará el índice con la configuración que creamos en la entidad.
Comencemos nuestra aplicación. Después de que se esté ejecutando, verifiquemos la configuración y veamos si es correcta:
curl --request GET \
--url http://localhost:9200/producto/_settings
El resultado es:
{
"producto": {
"settings": {
"index": {
"routing": {
"allocation": {
"include": {
"_tier_preference": "data_content"
}
}
},
"refresh_interval": "5s",
"number_of_shards": "1",
"provided_name": "producto",
"creation_date": "1607959499342",
"store": {
"type": "fs"
},
"number_of_replicas": "0",
"uuid": "iuoO8lE6QyWVSoECxa0I8w",
"version": {
"created": "7100099"
}
}
}
}
}
¡Todo es como lo configuramos! El intervalo de actualización se establece en 5 s, el numero de shards es 1 y el numero de replicas es 0.
Ahora, revisemos las asignaciones:
curl --request GET \
--url http://localhost:9200/producto/_mappings
Resulta en:
{
"producto": {
"mappings": {
"properties": {
"categoria": {
"type": "keyword"
},
"nombre": {
"type": "text"
},
"precio": {
"type": "long"
}
}
}
}
}
Las asignaciones también son las que esperábamos. Es lo mismo que configuramos en la clase de entidad.
CRUD básico con la interfaz de repositorio de Spring Data
Una vez que hemos creado la entidad, tenemos todo lo que necesitamos para crear una interfaz de repositorio en Spring Boot. Creemos un repositorio llamado ProductoRepository
.
Cuando esté creando una interfaz, asegúrese de extender ElasticsearchRepository
. En este caso, el objeto T
es su entidad y el tipo de objeto U
que desea usar para el ID de datos. En nuestro caso, usaremos la entidad Producto
que creamos anteriormente como T
y String
como U
.
public interface ProductRepository extends ElasticsearchRepository<Product, String> {
}
Ahora que la interfaz de su repositorio está lista, no es necesario que se ocupe de la implementación porque Spring se encarga de ella. Ahora, puede llamar a todas las funciones de las clases a las que se extiende su repositorio.
Para ver ejemplos de CRUD, puede consultar el siguiente código:
@Service
@RequiredArgsConstructor
public class SpringDataProductServiceImpl implements SpringDataProductService {
private final ProductoRepository productoRepository;
public Product createProducto(Producto producto) {
return productoRepository.save(producto);
}
public Optional<Producto> getProducto(String id) {
return productoRepository.findById(id);
}
public void deleteProducto(String id) {
productoRepository.deleteById(id);
}
public Iterable<Producto> insertBulk(List<Producto> productos) {
return productoRepository.saveAll(productos);
}
}
En los bloques de código anteriores, creamos una clase de servicio llamada SpringDataProductServiceImpl
, que está conectada automáticamente al ProductRepository
que creamos antes.
Hay cuatro funciones CRUD básicas en él. El primero es createProduct
, que, como su nombre lo indica, creará un nuevo producto en el índice de productos. El segundo, getProduct
, obtiene el producto que indexamos por su ID. La función deleteProduct
se puede utilizar para eliminar el producto en el índice por ID. La función insertBulk
le permitirá insertar varios productos en Elasticsearch.
¡Todo está hecho! No escribiré sobre las pruebas de API en este artículo porque quiero centrarme en cómo nuestras aplicaciones pueden interactuar con Elasticsearch. Pero si quieres probar la API, dejé un enlace de GitHub al final del artículo para que puedas clonar y probar este proyecto.
Métodos de consulta personalizados en Spring Data
En la sección anterior, solo aprovechamos el uso de los métodos básicos que ya están definidos en las otras clases. Pero también podemos crear métodos de consulta personalizados para usar.
Lo que es muy conveniente sobre Spring Data es que puede crear un método en la interfaz del repositorio y no necesita codificar ninguna implementación. La biblioteca Spring Data leerá el repositorio y creará automáticamente las implementaciones para él.
Intentemos buscar productos por el campo de nombre
:
Sí, eso es todo lo que necesita hacer para crear una función en la interfaz del repositorio de Spring Data.
También puede definir una consulta personalizada con la anotación @Query
e insertar una consulta JSON en los parámetros.
public interface ProductRepository extends ElasticsearchRepository<Producto, String> {
List<Producto> findAllByNombre(String nombre);
@Query("{\"match\":{\"nombre\":\"?0\"}}")
List<Producto> findAllByNombreUsingAnnotations(String nombre);
}
Ambos métodos que hemos creado hacen lo mismo: use la consulta de coincidencia con el nombre
como parámetro. Si lo prueba, obtendrá los mismos resultados.
Es importante respetar el nombre de los atributos en la firma de los métodos, funciona un poco como magia semántica. Si quisiéramos obtener por precio los métodos deberían llamarse findAllByPrecio
y findAllByPrecioUsingAnnotations
respectivamente.
Usando ElasticsearchRestTemplate
Si desea realizar una consulta más avanzada, como agregaciones, resaltados o sugerencias, puede usar ElasticsearchsearchRestTemplate
proporcionada por la biblioteca Spring Data. Al usarlo, puede crear su propia consulta, haciéndola tan compleja como desee.
Por ejemplo, creemos una función para hacer una consulta de coincidencia con el campo de nombre
como antes:
public List<Producto> getProductosByName(String nombre) {
Query query = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.matchQuery("nombre", nombre))
.build();
SearchHits<Producto> searchHits = elasticsearchRestTemplate.search(query, Producto.class);
return searchHits.get().map(SearchHit::getContent).collect(Collectors.toList());
}
Debería notar que el código anterior es más complejo que el que definimos en ElasticserchRepository. Se recomienda utilizar el repositorio de Spring Data si puede. Pero para una consulta más avanzada como agregación, resaltado o sugerencias, debe usar ElasticsearchRestTemplate.
Por ejemplo, escriba un fragmento de código que agregue un término:
public Map<String, Long> aggregateTerm(String term) {
Query query = new NativeSearchQueryBuilder()
.addAggregation(new TermsAggregationBuilder(term).field(term).size(10))
.build();
SearchHits<Producto> searchHits = elasticsearchRestTemplate.search(query, Producto.class);
Map<String, Long> result = new HashMap<>();
searchHits.getAggregations().asList().forEach(aggregation -> {
((Terms) aggregation).getBuckets()
.forEach(bucket -> result.put(bucket.getKeyAsString(), bucket.getDocCount()));
});
return result;
}
Elasticsearch RestHighLevelClient
Si no está usando Spring o su versión de Spring no es compatible con spring-data-elasticsearch
, puede usar una biblioteca Java desarrollada por Elasticsearch, RestHighLevelClient
.
RestHighLevelClient
es una biblioteca que puede usar para hacer cosas básicas como CRUD o administrar su Elasticsearch. Aunque el nombre implica que es un nivel alto, en realidad es un nivel más bajo en comparación con spring-data-elasticsearch
.
La ventaja de esta biblioteca sobre Spring Data es que también puede administrar su Elasticsearch con ella. Proporciona una configuración de índice y Elasticsearch, que puede usar con más flexibilidad en comparación con Spring Data. También tiene una funcionalidad más completa para interactuar con Elasticsearch.
La desventaja de esta biblioteca sobre Spring Data es que esta biblioteca es de un nivel más bajo, lo que significa que debe codificar más.
CRUD con RestHighLevelClient
Veamos cómo podemos crear una función simple con la biblioteca para poder compararla con los métodos anteriores que hemos usado:
@Service
@RequiredArgsConstructor
@Slf4j
public class HighLevelClientProductServiceImpl implements HighLevelClientProductService {
private final RestHighLevelClient restHighLevelClient;
private final ObjectMapper objectMapper;
public Product createProducto(Producto producto) {
IndexRequest indexRequest = new IndexRequest("producto");
indexRequest.id(producto.getId());
indexRequest.source(producto);
try {
IndexResponse indexResponse = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
if (indexResponse.status() == RestStatus.ACCEPTED) {
return product;
}
throw new RuntimeException("Wrong status: " + indexResponse.status());
} catch (Exception e) {
log.error("Error indexando, producto: {}", producto, e);
return null;
}
}
}
Como puede ver, ahora es más complicado y más difícil de implementar. Ahora, debe manejar la excepción y también convertir el resultado JSON a su entidad. Se recomienda usar Spring Data en su lugar para operaciones CRUD básicas porque RestHighLevelClient
es más complicado.