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();
    }
}