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
@Document
establecemos el índice y el tipo al que vamos a atacar - Utilizamos la expresión
type = FieldType.Nested
en la anotación@Field
para 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(); } }