1. Introducción
En el siguiente post veremos como conectarnos a una instancia de Elasticsearch a través de Spring Data. Para ello necesitaremos:
- Añadir dependencias Spring Data
- Configurar conexión
Si quieres ver como instalar Elasticsearch en local y saber un poco más ir a Primeros pasos en Elasticsearch
Os podéis descargar el código de ejemplo de mi GitHub aquí.
Tecnologías empleadas:
- Java 8
- Gradle 3.1
- Spring-Test 4.3.7.RELEASE
- SpringData-Elasticsearch 1.5.2.RELEASE
- Elasticsearch 2.4.0
2. Índices, tipos y documentos
El índice, tipo y documentos que vamos a utilizar son los que se han creado e insertado en el post Primeros pasos en Elasticsearch
- Índice: myindex
- Tipo: user
Documentos
{"name":"admin","surname":"admin","gender":"male","money":0,"roles":["ROLE_ADMIN"]}
{"name":"Jorge","surname":"Hernández Ramírez","gender":"male","money":1000,"roles":["ROLE_ADMIN"],"teams":[{"name":"UD.Las Palmas","sport":"Football"},{"name":"Real Madrid","sport":"Football"},{"name":"McLaren","sport":"F1"}]}
{"name":"Jose","gender":"male","surname":"Hernández Ramírez","money":2000,"roles":["ROLE_USER"],"teams":[{"name":"UD. Las Palmas","sport":"Football"},{"name":"Magnus Carlsen","sport":"Chess"}]}
{"name":"Raul","surname":"González Blanco","gender":"male","money":200000,"roles":["ROLE_USER"],"teams":[{"name":"Real Madrid","sport":"Football"},{"name":"Real Madrid","sport":"Basketball"}]}
{"name":"Constanza","surname":"Ramírez Rodríguez","gender":"female","money":500,"roles":["ROLE_USER"],"teams":[{"name":"UD. Las Palmas","sport":"Football"}]}
3. Ficheros
Dependencias
Añadimos la dependencia spring-boot-starter-data-elasticsearch
group 'com.jorgehernandezramirez.spring.springboot.springdata'
version '1.0-SNAPSHOT'
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.2.RELEASE")
}
}
apply plugin: 'java'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
apply plugin: 'maven'
sourceCompatibility = 1.8
springBoot {
mainClass = "com.jorgehernandezramirez.spring.springboot.https.Application"
}
repositories {
mavenCentral()
}
dependencies {
testCompile("org.springframework.boot:spring-boot-starter-test")
testCompile("org.springframework.boot:spring-boot-starter-data-elasticsearch")
}
Configuramos la conexión para conectarnos a la instancia de Elasticsearch. Además utilizamos la anotación @EnableElasticsearchRepositories para indicar el paquete donde se encuentran nuestros repositorios de spring data.
package com.jorgehernandezramirez.spring.springboot.springdata.elasticsearch.configuration;
import org.elasticsearch.client.Client;
import org.elasticsearch.client.transport.TransportClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.InetSocketTransportAddress;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
import java.net.InetAddress;
@Configuration
@EnableElasticsearchRepositories(basePackages = "com.jorgehernandezramirez.spring.springboot.springdata.elasticsearch.repository")
public class ElasticSearchConfiguration {
@Bean
public Client client() throws Exception {
Settings esSettings = Settings.settingsBuilder()
.put("cluster.name", "elasticsearch").build();
return TransportClient.builder()
.settings(esSettings)
.build()
.addTransportAddress(
new InetSocketTransportAddress(InetAddress.getByName("localhost"), 9300));
}
@Bean
public ElasticsearchOperations elasticsearchTemplate() throws Exception {
return new ElasticsearchTemplate(client());
}
}
Se muestra la entidad usuario.
- Mediante la anotación
@Documentestablecemos el índice y el tipo al que vamos a atacar - Utilizamos la expresión
type = FieldType.Nesteden la anotación@Fieldpara indicar que se trata de un objeto anidado.
package com.jorgehernandezramirez.spring.springboot.springdata.elasticsearch.entity;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import java.util.List;
@Document(indexName = "myindex", type = "user")
public class UserEntity {
@Id
private String id;
private String name;
private String gender;
private String surname;
private Integer money;
private List<String> roles;
@Field(type = FieldType.Nested)
private List<TeamEntity> teams;
public UserEntity(){
//Para Spring Data
}
public UserEntity(String id, String name, String gender, String surname, Integer money, List<String> roles, List<TeamEntity> teams) {
this.id = id;
this.name = name;
this.gender = gender;
this.surname = surname;
this.money = money;
this.roles = roles;
this.teams = teams;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public String getSurname() {
return surname;
}
public void setSurname(String surname) {
this.surname = surname;
}
public Integer getMoney() {
return money;
}
public void setMoney(Integer money) {
this.money = money;
}
public List<String> getRoles() {
return roles;
}
public void setRoles(List<String> roles) {
this.roles = roles;
}
public List<TeamEntity> getTeams() {
return teams;
}
public void setTeams(List<TeamEntity> teams) {
this.teams = teams;
}
@Override
public String toString() {
return "UserEntity{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", gender='" + gender + '\'' +
", surname='" + surname + '\'' +
", money=" + money +
", roles=" + roles +
", teams=" + teams +
'}';
}
}
package com.jorgehernandezramirez.spring.springboot.springdata.elasticsearch.entity;
public class TeamEntity {
private String name;
private String sport;
public TeamEntity(){
//Para Spring Data
}
public TeamEntity(String name, String sport) {
this.name = name;
this.sport = sport;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSport() {
return sport;
}
public void setSport(String sport) {
this.sport = sport;
}
@Override
public String toString() {
return "TeamEntity{" +
"name='" + name + '\'' +
", sport='" + sport + '\'' +
'}';
}
}
Se muestra el repositorio de usuarios. Podemos hacer uso de la anotación @Query para indicar la query que se va a ejecutar sobre Elasticsearch. Si no utilizamos esta anotación se compone la query con el nombre de los campos que aparecen en el nombre la función.
package com.jorgehernandezramirez.spring.springboot.springdata.elasticsearch.repository;
import com.jorgehernandezramirez.spring.springboot.springdata.elasticsearch.entity.UserEntity;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.annotations.Query;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import java.util.List;
public interface UserRepository extends ElasticsearchRepository<UserEntity, String> {
List<UserEntity> findByRoles(String role);
List<UserEntity> findBySurname(String name);
List<UserEntity> findBySurnameAndGender(String surname, String gender);
@Query("{\"query\":{\"match\":{\"name\":\"?0\"}}}")
List<UserEntity> findByName(String name);
}
Utilizamos una clase abstracta de apoyo para nuestros test. La idea es que en cada test que se ejecute creemos el índice myindex e insertemos los datos con los que vamos a trabajar.
package com.jorgehernandezramirez.spring.springboot.springdata.elasticsearch;
import com.jorgehernandezramirez.spring.springboot.springdata.elasticsearch.entity.TeamEntity;
import com.jorgehernandezramirez.spring.springboot.springdata.elasticsearch.entity.UserEntity;
import com.jorgehernandezramirez.spring.springboot.springdata.elasticsearch.repository.UserRepository;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
public abstract class AbstractElasticSearchTest {
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractElasticSearchTest.class);
@Autowired
protected ElasticsearchTemplate elasticsearchTemplate;
@Autowired
protected UserRepository userRepository;
@Test
public void shouldBeNotNullObjects(){
assertNotNull(userRepository);
assertNotNull(elasticsearchTemplate);
}
@Before
public void initialization(){
elasticsearchTemplate.deleteIndex(UserEntity.class);
elasticsearchTemplate.createIndex(UserEntity.class);
elasticsearchTemplate.putMapping(UserEntity.class);
elasticsearchTemplate.refresh(UserEntity.class);
insertMockUsers();
}
private void insertMockUsers() {
userRepository.save(new UserEntity("1", "Admin", "male", "Admin", 0, Arrays.asList("ROLE_ADMIN"), Collections.<TeamEntity>emptyList()));
userRepository.save(new UserEntity("2", "Jorge", "male", "Hernández Ramírez", 1000, Arrays.asList("ROLE_ADMIN"), Arrays.asList(new TeamEntity("UD. Las Palmas", "Football"), new TeamEntity("Real Madrid", "Football"), new TeamEntity("McLaren", "F1"))));
userRepository.save(new UserEntity("3", "Jose", "male", "Hernández Ramírez", 500, Arrays.asList("ROLE_USER"), Arrays.asList(new TeamEntity("UD. Las Palmas", "Football"), new TeamEntity("Magnus Carlen", "Chess"))));
userRepository.save(new UserEntity("4", "Raul", "male", "González Blanco", 10000000, Arrays.asList("ROLE_USER"), Arrays.asList(new TeamEntity("Real Madrid", "Football"), new TeamEntity("Real Madrid", "Basketball"))));
userRepository.save(new UserEntity("5", "Constanza", "female", "Ramírez Rodríguez", 700, Arrays.asList("ROLE_USER"), Arrays.asList(new TeamEntity("UD. Las Palmas", "Football"))));
}
}
Test de integración ElasticSearchTest
package com.jorgehernandezramirez.spring.springboot.springdata.elasticsearch;
import com.jorgehernandezramirez.spring.springboot.springdata.elasticsearch.configuration.ElasticSearchConfiguration;
import com.jorgehernandezramirez.spring.springboot.springdata.elasticsearch.entity.TeamEntity;
import com.jorgehernandezramirez.spring.springboot.springdata.elasticsearch.entity.UserEntity;
import com.jorgehernandezramirez.spring.springboot.springdata.elasticsearch.repository.UserRepository;
import com.jorgehernandezramirez.spring.springboot.springdata.elasticsearch.service.AggregationsResultsExtractor;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.StringTerms;
import org.elasticsearch.search.aggregations.metrics.sum.InternalSum;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.SearchQuery;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.AnnotationConfigContextLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.elasticsearch.action.search.SearchType.COUNT;
import static org.elasticsearch.index.query.QueryBuilders.*;
import static org.elasticsearch.search.aggregations.AggregationBuilders.sum;
import static org.elasticsearch.search.aggregations.AggregationBuilders.terms;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ElasticSearchConfiguration.class,loader=AnnotationConfigContextLoader.class)
public class ElasticSearchTest extends AbstractElasticSearchTest{
private static final Logger LOGGER = LoggerFactory.getLogger(ElasticSearchTest.class);
@Test
public void shouldReadAllDocumentsFromElasticsearch(){
final List<UserEntity> userEntities = getListUserEntity(userRepository.findAll());
assertEquals(5, userEntities.size());
}
@Test
public void shouldFindDocumentsBySurname(){
final List<UserEntity> userEntities = userRepository.findBySurname("Ramírez");
assertEquals(3, userEntities.size());
}
@Test
public void shouldFindDocumentsByName(){
final List<UserEntity> userEntities = userRepository.findByName("Jorge");
assertEquals(1, userEntities.size());
}
@Test
public void shouldFindDocumentsBySurnameAndGender(){
final List<UserEntity> userEntities = userRepository.findBySurnameAndGender("Ramírez", "female");
assertEquals(1, userEntities.size());
}
@Test
public void shouldFindDocumentsByRoles(){
final List<UserEntity> userEntities = userRepository.findByRoles("ROLE_ADMIN");
assertEquals(2, userEntities.size());
}
@Test
public void shouldFindDocumentsUsingNestedQuery(){
final QueryBuilder queryBuilder = nestedQuery("teams",
boolQuery().must(matchQuery("teams.name","Magnus Carlen"))
.must(matchQuery("teams.sport", "Chess")));
final List<UserEntity> persons = getUserListFromQueryBuilder(queryBuilder);
assertEquals(1, persons.size());
}
@Test
public void shouldNotFindDocumentsUsingNestedQuery(){
final QueryBuilder queryBuilder = nestedQuery("teams",
boolQuery().must(matchQuery("teams.name","Magnus Carlen"))
.must(matchQuery("teams.sport", "Football")));
final List<UserEntity> persons = getUserListFromQueryBuilder(queryBuilder);
assertEquals(0, persons.size());
}
private List<UserEntity> getUserListFromQueryBuilder(final QueryBuilder queryBuilder){
final SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(queryBuilder).build();
return userRepository.search(searchQuery).getContent();
}
private List<UserEntity> getListUserEntity(final Iterable<UserEntity> userEntityIterable){
final List<UserEntity> userEntities = new ArrayList<UserEntity>();
userEntityIterable.iterator().forEachRemaining(userEntities::add);
return userEntities;
}
}
Test donde hacemos uso de las agregaciones
package com.jorgehernandezramirez.spring.springboot.springdata.elasticsearch;
import com.jorgehernandezramirez.spring.springboot.springdata.elasticsearch.configuration.ElasticSearchConfiguration;
import com.jorgehernandezramirez.spring.springboot.springdata.elasticsearch.entity.UserEntity;
import com.jorgehernandezramirez.spring.springboot.springdata.elasticsearch.repository.UserRepository;
import com.jorgehernandezramirez.spring.springboot.springdata.elasticsearch.service.AggregationsResultsExtractor;
import org.elasticsearch.search.aggregations.AbstractAggregationBuilder;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.StringTerms;
import org.elasticsearch.search.aggregations.metrics.avg.InternalAvg;
import org.elasticsearch.search.aggregations.metrics.sum.InternalSum;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.SearchQuery;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.AnnotationConfigContextLoader;
import static org.elasticsearch.action.search.SearchType.COUNT;
import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery;
import static org.elasticsearch.search.aggregations.AggregationBuilders.avg;
import static org.elasticsearch.search.aggregations.AggregationBuilders.sum;
import static org.elasticsearch.search.aggregations.AggregationBuilders.terms;
import static org.junit.Assert.assertNotNull;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ElasticSearchConfiguration.class,loader=AnnotationConfigContextLoader.class)
public class AggregationElasticSearchTest extends AbstractElasticSearchTest {
private static final Logger LOGGER = LoggerFactory.getLogger(AggregationElasticSearchTest.class);
@Autowired
private ElasticsearchTemplate elasticsearchTemplate;
@Test
public void shouldPrintNumberOfDocumentsFromGender(){
final SearchQuery searchQuery = buildSearchQuery(terms("counter_gender").field("gender"));
final Aggregations aggregations = elasticsearchTemplate.query(searchQuery, new AggregationsResultsExtractor());
final StringTerms topTags = (StringTerms)aggregations.getAsMap().get("counter_gender");
topTags.getBuckets().forEach(bucket -> {
LOGGER.info("{}, {}", bucket.getKeyAsString(), bucket.getDocCount());
});
}
@Test
public void shouldPrintMoneySumOfAllDocumentes(){
final SearchQuery searchQuery = buildSearchQuery(sum("sum_money").field("money"));
final Aggregations aggregations = elasticsearchTemplate.query(searchQuery, new AggregationsResultsExtractor());
assertNotNull(aggregations);
assertNotNull(aggregations.asMap().get("sum_money"));
final InternalSum internalSum = (InternalSum)aggregations.getAsMap().get("sum_money");
LOGGER.info("{}", internalSum.getValue());
}
@Test
public void shouldPrintNumberOfDocumentsFromGenderAndSumAndAvgMoneyOfEveryone(){
final SearchQuery searchQuery = buildSearchQuery(terms("counter_gender").field("gender")
.subAggregation(sum("sum_money").field("money"))
.subAggregation(avg("avg_money").field("money")));
final Aggregations aggregations = elasticsearchTemplate.query(searchQuery, new AggregationsResultsExtractor());
final StringTerms counterBankIds = (StringTerms)aggregations.getAsMap().get("counter_gender");
counterBankIds.getBuckets().forEach(bucket -> {
LOGGER.info("{}, {}, {}, {}", bucket.getKeyAsString(), bucket.getDocCount(), ((InternalSum)bucket.getAggregations().getAsMap().get("sum_money")).getValue(),
((InternalAvg)bucket.getAggregations().getAsMap().get("avg_money")).getValue());
});
}
private SearchQuery buildSearchQuery(AbstractAggregationBuilder abstractAggregationBuilder){
return new NativeSearchQueryBuilder()
.withQuery(matchAllQuery())
.withIndices("myindex").withTypes("user")
.addAggregation(abstractAggregationBuilder)
.build();
}
}