Spring Boot, ElasticSearch y búsqueda en polígonos geográficos

En este otro artículo hablábamos de ElasticSearch, su fácil integración con Spring Boot a partir de Spring Data ElasticSearch e hicimos un ejemplo de un CRUD. Hoy vamos a comentar otra características interesante de ES, y es su capacidad para el manejo de datos geográficos.

En particular vamos a tratar sobre puntos y polígonos geográficos y veremos como determinar dada una lista de localizaciones las que pertenecen a un polígono o área geográfica.

GeoInfo: Creando nuestra API de localizaciones y áreas geográficas

Como anuncia el título, crearemos un API de ejemplo para explicar en la práctica como llevar a cabo la tarea de buscar localizaciones en una zona geográfica usando ES y Spring Boot.

Entidades del dominio

Nuestra API será muy sencilla, tendrá dos entidades del dominio que se convertirán en dos indices de ES, cada una contendrá información particular.

La entidad Area contendrá un nombre para la región que encierra y una lista de puntos geográficos (un polígono cerrado).

import org.elasticsearch.common.geo.GeoPoint;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import java.util.List;
@Document(indexName = "regions")
public class Area {
    @Id
    private String id;
    private String name;
    private List<GeoPoint> region;
    // setter & getters
}

Importante es el tipo de datos de los elementos de la lista “GeoPoint” del paquete org.elasticsearch.common.geo.GeoPoint.

La entidad Place por su parte posee información de los lugares y su posición geográfica – un punto en coordenadas del mismo tipo GeoPoint -.

import org.elasticsearch.common.geo.GeoPoint;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.GeoPointField;
@Document(indexName = "places")
public class Place {
    @Id
    private String id;
    private String name;
    @GeoPointField
    private GeoPoint position;
   // setter & getters
}

Repositorios

Para cada entidad vamos a manejar un repositorio.

public interface PlaceRepository extends ElasticsearchRepository<Place, String> {
}
public interface AreaRepository extends ElasticsearchRepository<Area, String> {
}

Servicios

En este ejemplo ofrecemos un API para crear áreas (Area) y lugares (Place) de forma sencilla, crearemos un Service para cada uno para luego inyectarlo a los controladores (Resource) y poder realizar las operaciones sobre los repositorios.

Vamos a mostrar acá solo el Service de Place (PlaceService) que es donde está la lógica de buscar los Place que están en un Area o región.

public interface PlaceService {
    Place save(Place place);
    List<Place> list();
    List<Place> getPlacesInArea(String areaId);
}
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import static org.elasticsearch.index.query.QueryBuilders.geoPolygonQuery;
import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery;
@Service
public class PlaceServiceImpl implements PlaceService {
    @Autowired
    private PlaceRepository placeRepository;
    @Autowired
    AreaRepository areaRepository;
    @Override
    public Place save(Place place) {
      return placeRepository.save(place);
    }
    @Override
    public List<Place> list() {
        return StreamSupport
                .stream( this.placeRepository.findAll().spliterator(), false)
                .collect(Collectors.toList());
    }
    @Override
    public List<Place> getPlacesInArea(String areaId) {
       Optional<Area> areaOptional = this.areaRepository.findById(areaId);
        if (areaOptional.isPresent()) {
            QueryBuilder qb = geoPolygonQuery("position", areaOptional.get().getRegion());
            BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery().must(matchAllQuery()).filter(qb);
            return  StreamSupport
                    .stream( this.placeRepository.search(boolQueryBuilder).spliterator(), false)
                    .collect(Collectors.toList());
        }
         else
             return null;
    }
}

Es relevante en este servicio la implementación de getPlacesInArea que es donde está lo fundamental.

Los pasos son:

1 – Se buscá el área de la que se quieren obtener los lugares (Place) que hay dentro. (Esto se hace usando el repositorio de Area y su findById)

2 – Si existe el área se procede a buscar todos los puntos que hay dentro.

2.1 – Se crea un QueryBuilder que permite encontrar la position (nombre del atributo de Place que posee el GeoPoint del lugar) dentro del polígono del área, fijarse que se le pasa al método que crea la QueryBuilder (geoPolygonQuery) la lista de puntos que definen la region en el área.

2.2 – Se crea una BooleanQuery de ES en la que se obtienen todos los lugares y se filtran por los que cumplen con la Query del paso 2.1

2.3 – Por último se aprovecha el método search que provee Spring Boot Data ElasticSearch que permite pasar una QueryBuilder construida manualmente como lo que acabamos de hacer, esto nos devolverá un Iterator de Places con el resultado de la consulta. Lo demás es Java para convertir el Iterator a una List.

3 – Si no hay área se devuelve nulo o una excepción o el valor que se estime, por simplicidad se devuelve nulo en el ejemplo.

Controladores

Se han creado dos controladores (Resources) para manejar ofrecer un API que permita crear y listar Areas y Lugares. No se colocarán acá pero pueden ser vistos en el ejemplo.

Open API y configuración de ES

Complementan este ejemplo un API con soporte Swagger que permite realizar desde swagger-ui las operaciones de crear, listar y obtener lugares por identificador de área y la configuración para conectarse a ES.

Versiones

En este ejemplo se han usado las siguientes versiones de componentes de software.

  • ElasticSearch, con versión 7.4 (la última estable a fecha)
  • Driver de ES para Java en igual versión.
  • Spring Boot 2.2

Puede acceder a este ejemplo en GitHub. Si te ha sido útil puedes invitarme un café 🙂