La interacción programática con Elasticsearch desde aplicaciones Java es fundamental para construir sistemas que aprovechen su potente capacidad de búsqueda y análisis. Este artículo detalla cómo utilizar el cliente Java de Elasticsearch para realizar operaciones comunes como la gestión de índices, la manipulación de documentos y la ejecución de consultas complejas, incluyendo paginación y resaltado de resultados.
Configuración Inicial del Proyecto Java
Para comenzar, es necesario configurar un proyecto Maven y añadir las dependencias del cliente de transporte de Elasticsearch. Este cliente permite la comunicación directa con el clúster de Elasticsearch a través del puerto de transporte.
Dependencias Maven
Asegúrese de incluir las siguientes dependencias en su archivo pom.xml. Este ejemplo utiliza la versión 5.6.8 de Elasticsearch, por lo que las dependencias deben ser acordes.
<dependencies>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>5.6.8</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>transport</artifactId>
<version>5.6.8</version>
</dependency>
<!-- Dependencias de logging -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
<version>2.9.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.24</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.21</version>
</dependency>
<!-- JUnit para pruebas -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
Gestión de Índices en Elasticsearch
1. Creación de un Índice
Para crear un nuevo índice en Elasticsearch, se necesita una instancia de TransportClient configurada con los detalles del clúster. La configuración básica incluye el nombre del clúster y las direcciones de los nodos.
package com.ejemplo.es;
import org.elasticsearch.client.transport.TransportClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.InetSocketTransportAddress;
import org.elasticsearch.transport.client.PreBuiltTransportClient;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class ElasticsearchClientOperations {
private TransportClient esCliente;
@Before
public void inicializarCliente() throws UnknownHostException {
// Configuración del cliente: nombre del clúster
Settings configuracion = Settings.builder()
.put("cluster.name", "my-elasticsearch") // Reemplace con el nombre de su clúster
.build();
// Creación del cliente de transporte
esCliente = new PreBuiltTransportClient(configuracion);
// Añadir nodos del clúster
esCliente.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("127.0.0.1"), 9301));
esCliente.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("127.0.0.1"), 9302));
esCliente.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("127.0.0.1"), 9303));
}
@After
public void cerrarCliente() {
if (esCliente != null) {
esCliente.close();
}
}
@Test
public void crearNuevoIndice() {
// Ejecutar la creación del índice "mi_indice_documentos"
esCliente.admin().indices().prepareCreate("mi_indice_documentos").get();
System.out.println("Índice 'mi_indice_documentos' creado exitosamente.");
}
}
2. Definición de Mapeos (Mappings)
Los mapeos definen cómo se almacenan y se indexan los campos en un índice. Esto incluye el tipo de dato, si se almacena, y el analizador a utilizar para campos de texto. Se pueden definir utilizando un objeto XContentBuilder para construir la estructura JSON.
package com.ejemplo.es;
import org.elasticsearch.client.transport.TransportClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.InetSocketTransportAddress;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.transport.client.PreBuiltTransportClient;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class ElasticsearchClientOperations {
// ... (inicializarCliente y cerrarCliente como antes) ...
@Test
public void establecerMapeoParaDocumento() throws Exception {
// Construcción del JSON de mapeo usando XContentBuilder
XContentBuilder mapeoBuilder = XContentFactory.jsonBuilder()
.startObject()
.startObject("entrada_blog") // Tipo de documento
.startObject("properties")
.startObject("id")
.field("type", "long")
.field("store", true)
.endObject()
.startObject("titulo")
.field("type", "text")
.field("analyzer", "standard")
.field("store", true)
.endObject()
.startObject("contenido")
.field("type", "text")
.field("analyzer", "standard")
.field("store", true)
.endObject()
.startObject("fechaCreacion")
.field("type", "date")
.field("format", "yyyy-MM-dd HH:mm:ss")
.field("store", true)
.endObject()
.endObject()
.endObject()
.endObject();
// Enviar el mapeo al índice "mi_indice_documentos" y tipo "entrada_blog"
esCliente.admin().indices().preparePutMapping("mi_indice_documentos")
.setType("entrada_blog")
.setSource(mapeoBuilder)
.get();
System.out.println("Mapeo para 'entrada_blog' establecido exitosamente.");
}
}
Manipulación de Documentos
1. Añadir Documentos Usando XContentBuilder
Los documentos se pueden añadir al índice especificando el índice, el tipo y un ID opcional. El contenido del documento se construye como JSON.
package com.ejemplo.es;
import org.elasticsearch.client.transport.TransportClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.InetSocketTransportAddress;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.transport.client.PreBuiltTransportClient;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Date;
public class ElasticsearchClientOperations {
// ... (inicializarCliente y cerrarCliente como antes) ...
@Test
public void agregarDocumentoConBuilder() throws Exception {
XContentBuilder documentoBuilder = XContentFactory.jsonBuilder()
.startObject()
.field("id", 1L)
.field("titulo", "Elasticsearch: Un Servidor de Búsqueda Basado en Lucene")
.field("contenido", "Es un motor de búsqueda de texto completo distribuido y multitenant con interfaz web RESTful.")
.field("fechaCreacion", "2023-01-15 10:30:00")
.endObject();
esCliente.prepareIndex("mi_indice_documentos", "entrada_blog", "1")
.setSource(documentoBuilder)
.get();
System.out.println("Documento 1 añadido exitosamente.");
}
}
2. Añadir Documentos desde un Objeto POJO (Usando Jackson)
Para una integración más limpia con aplicaciones Java, puede convertir objetos POJO (Plain Old Java Objects) a JSON y luego indexarlos. Esto requiere la librería Jackson.
Primero, cree la clase POJO EntradaBlog:
package com.ejemplo.es;
import java.util.Date;
public class EntradaBlog {
private Long id;
private String titulo;
private String contenido;
private String fechaCreacion; // Usar String para formato específico o Date para manejo automático
// Constructor vacío
public EntradaBlog() {}
// Constructor con campos
public EntradaBlog(Long id, String titulo, String contenido, String fechaCreacion) {
this.id = id;
this.titulo = titulo;
this.contenido = contenido;
this.fechaCreacion = fechaCreacion;
}
// Getters y Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTitulo() { return titulo; }
public void setTitulo(String titulo) { this.titulo = titulo; }
public String getContenido() { return contenido; }
public void setContenido(String contenido) { this.contenido = contenido; }
public String getFechaCreacion() { return fechaCreacion; }
public void setFechaCreacion(String fechaCreacion) { this.fechaCreacion = fechaCreacion; }
@Override
public String toString() {
return "EntradaBlog{id=" + id + ", titulo='" + titulo + "'}";
}
}
Añada las dependencias de Jackson a su pom.xml:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.8.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId&
<artifactId>jackson-annotations</artifactId>
<version>2.8.1</version>
</dependency>
Y luego, el método para añadir el documento:
package com.ejemplo.es;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.elasticsearch.client.transport.TransportClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.junit.Test;
import java.util.Date;
public class ElasticsearchClientOperations {
// ... (inicializarCliente y cerrarCliente como antes) ...
@Test
public void agregarDocumentoDesdePojo() throws Exception {
EntradaBlog miEntrada = new EntradaBlog(
2L,
"La Búsqueda es un Placer",
"Nuestra solución de búsqueda debe ser rápida, con configuración cero y gratuita.",
"2023-01-16 11:00:00"
);
// Convertir el objeto POJO a String JSON
String jsonDocumento = new ObjectMapper().writeValueAsString(miEntrada);
System.out.println("JSON del documento: " + jsonDocumento);
// Indexar el documento en Elasticsearch
esCliente.prepareIndex("mi_indice_documentos", "entrada_blog", "2")
.setSource(jsonDocumento, XContentType.JSON)
.get();
System.out.println("Documento 2 añadido exitosamente desde POJO.");
}
}
Búsqueda de Documentos en Elasticsearch
El cliente Java ofrece una API rica para construir y ejecutar consultas. Aquí se muestran varios tipos de consultas.
Método Auxiliar para Impresión de Resultados
Para evitar la repetición de código, crearemos un método auxiliar que procesa e imprime los resultados de cualquier consulta.
package com.ejemplo.es;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.transport.TransportClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.InetSocketTransportAddress;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.transport.client.PreBuiltTransportClient;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Map;
public class DocumentSearchOperations {
private TransportClient esCliente;
@Before
public void inicializarCliente() throws UnknownHostException {
Settings configuracion = Settings.builder().put("cluster.name", "my-elasticsearch").build();
esCliente = new PreBuiltTransportClient(configuracion);
esCliente.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("127.0.0.1"), 9301));
esCliente.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("127.0.0.1"), 9302));
esCliente.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("127.0.0.1"), 9303));
}
@After
public void cerrarCliente() {
if (esCliente != null) {
esCliente.close();
}
}
private void procesarResultadosBusqueda(SearchResponse respuestaBusqueda) {
SearchHits hits = respuestaBusqueda.getHits();
System.out.println("Total de documentos encontrados: " + hits.getTotalHits());
for (SearchHit hit : hits.getHits()) {
System.out.println("--- Documento encontrado ---");
System.out.println("ID: " + hit.getId());
System.out.println("Fuente JSON: " + hit.getSourceAsString());
// Acceder a campos específicos
Map<String, Object> sourceMap = hit.getSource();
System.out.println("Título: " + sourceMap.get("titulo"));
System.out.println("Contenido: " + sourceMap.get("contenido"));
}
}
// ... métodos de prueba para búsquedas ...
}
1. Búsqueda por ID de Documento
Permite recuperar documentos específicos utilizando uno o varios IDs.
package com.ejemplo.es;
import org.elasticsearch.index.query.QueryBuilders;
import org.junit.Test;
public class DocumentSearchOperations {
// ... (inicializarCliente, cerrarCliente, procesarResultadosBusqueda como antes) ...
@Test
public void buscarPorIds() {
QueryBuilder consultaIds = QueryBuilders.idsQuery().addIds("1", "2");
SearchResponse respuesta = esCliente.prepareSearch("mi_indice_documentos")
.setTypes("entrada_blog")
.setQuery(consultaIds)
.get();
procesarResultadosBusqueda(respuesta);
}
}
2. Búsqueda por Término (Term Query)
Busca documentos que contienen un término exacto en un campo específico. No realiza análisis de texto.
package com.ejemplo.es;
import org.elasticsearch.index.query.QueryBuilders;
import org.junit.Test;
public class DocumentSearchOperations {
// ... (inicializarCliente, cerrarCliente, procesarResultadosBusqueda como antes) ...
@Test
public void buscarPorTerminoExacto() {
QueryBuilder consultaTermino = QueryBuilders.termQuery("titulo", "servidor"); // Busca el término "servidor" en el campo "titulo"
SearchResponse respuesta = esCliente.prepareSearch("mi_indice_documentos")
.setTypes("entrada_blog")
.setQuery(consultaTermino)
.get();
procesarResultadosBusqueda(respuesta);
}
}
3. Búsqueda por Cadena de Consulta (Query String Query)
Una consulta más flexible que permite analizar la cadena de entrada y buscar en múltiples campos o en un campo por defecto.
package com.ejemplo.es;
import org.elasticsearch.index.query.QueryBuilders;
import org.junit.Test;
public class DocumentSearchOperations {
// ... (inicializarCliente, cerrarCliente, procesarResultadosBusqueda como antes) ...
@Test
public void buscarPorCadenaDeConsulta() {
// Busca "búsqueda" en el campo "titulo"
QueryBuilder consultaCadena = QueryBuilders.queryStringQuery("búsqueda").defaultField("titulo");
SearchResponse respuesta = esCliente.prepareSearch("mi_indice_documentos")
.setTypes("entrada_blog")
.setQuery(consultaCadena)
.get();
procesarResultadosBusqueda(respuesta);
}
}
Paginación de Resultados de Búsqueda
Para gestionar grandes conjuntos de resultados, es crucial implementar paginación. Se utilizan los parámetros from (offset) y size (número de resultados por página).
package com.ejemplo.es;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.elasticsearch.client.transport.TransportClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.InetSocketTransportAddress;
import org.elasticsearch.common.xcontent.XContentType;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class BulkDocumentInsertion { // Clase separada para insertar muchos docs
private TransportClient esCliente;
@Before
public void inicializarCliente() throws UnknownHostException {
Settings configuracion = Settings.builder().put("cluster.name", "my-elasticsearch").build();
esCliente = new PreBuiltTransportClient(configuracion);
esCliente.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("127.0.0.1"), 9301));
esCliente.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("127.0.0.1"), 9302));
esCliente.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("127.0.0.1"), 9303));
}
@After
public void cerrarCliente() {
if (esCliente != null) {
esCliente.close();
}
}
@Test
public void insertarMultiplesDocumentos() throws Exception {
ObjectMapper mapper = new ObjectMapper();
for (int i = 3; i <= 100; i++) {
EntradaBlog entrada = new EntradaBlog(
(long) i,
i + " Artículo sobre la tecnología de búsqueda",
i + " Detalles sobre cómo funcionan los motores de búsqueda modernos.",
"2023-01-20 09:00:00"
);
String jsonDoc = mapper.writeValueAsString(entrada);
esCliente.prepareIndex("mi_indice_documentos", "entrada_blog", String.valueOf(i))
.setSource(jsonDoc, XContentType.JSON)
.get();
}
System.out.println("Se han insertado 98 documentos adicionales.");
}
}
Ahora, una prueba para la paginación:
package com.ejemplo.es;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.junit.Test;
public class DocumentSearchOperations {
// ... (inicializarCliente, cerrarCliente, procesarResultadosBusqueda como antes) ...
@Test
public void buscarConPaginacion() {
QueryBuilder consultaPaginada = QueryBuilders.matchAllQuery(); // Consulta que trae todos los documentos
SearchResponse respuesta = esCliente.prepareSearch("mi_indice_documentos")
.setTypes("entrada_blog")
.setQuery(consultaPaginada)
.setFrom(10) // Inicia en el documento 11 (índice 10)
.setSize(5) // Trae 5 documentos por página
.get();
procesarResultadosBusqueda(respuesta);
}
}
Resaltado de Resultados de Búsqueda
El resaltado (highlighting) permite que los términos de búsqueda aparezcan marcados dentro de los fragmentos de texto devueltos, facilitando al usuario la identificación de la relevancia de los resultados.
package com.ejemplo.es;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.junit.Test;
import java.util.Map;
public class DocumentSearchOperations {
// ... (inicializarCliente y cerrarCliente como antes) ...
private void procesarResultadosConResaltado(SearchResponse respuestaBusqueda, String campoResaltado) {
SearchHits hits = respuestaBusqueda.getHits();
System.out.println("Total de documentos encontrados: " + hits.getTotalHits());
for (SearchHit hit : hits.getHits()) {
System.out.println("--- Documento encontrado (ID: " + hit.getId() + ") ---");
System.out.println("Fuente JSON: " + hit.getSourceAsString());
// Acceder a campos específicos
Map<String, Object> sourceMap = hit.getSource();
System.out.println("Título original: " + sourceMap.get("titulo"));
System.out.println("Contenido original: " + sourceMap.get("contenido"));
// Imprimir resultados resaltados
System.out.println("***** Fragmentos Resaltados *****");
Map<String, HighlightField> camposResaltados = hit.getHighlightFields();
if (camposResaltados.containsKey(campoResaltado)) {
HighlightField campoHighlight = camposResaltados.get(campoResaltado);
for (Text fragmento : campoHighlight.getFragments()) {
System.out.println("Fragmento: " + fragmento);
}
} else {
System.out.println("No hay resaltado para el campo '" + campoResaltado + "'.");
}
}
}
@Test
public void buscarConResaltado() {
String campoParaResaltar = "titulo";
QueryBuilder consultaResaltado = QueryBuilders.queryStringQuery("búsqueda").defaultField(campoParaResaltar);
// Configuración del resaltado
HighlightBuilder constructorResaltado = new HighlightBuilder();
constructorResaltado.field(campoParaResaltar); // Campo a resaltar
constructorResaltado.preTags("<em style=\"color:red;\">"); // Etiqueta HTML de apertura
constructorResaltado.postTags("</em>"); // Etiqueta HTML de cierre
SearchResponse respuesta = esCliente.prepareSearch("mi_indice_documentos")
.setTypes("entrada_blog")
.setQuery(consultaResaltado)
.highlighter(constructorResaltado) // Aplicar configuración de resaltado
.setFrom(0)
.setSize(10)
.get();
procesarResultadosConResaltado(respuesta, campoParaResaltar);
}
}