Elasticsearch: búsqueda geoespacial usando ES|QL

Autor: De Elastic Craig Taverner

Elasticsearch ha tenido poderosas capacidades de análisis y búsqueda geoespacial durante años , pero su API es muy diferente de lo que los usuarios típicos de SIG están acostumbrados. El año pasado, agregamos el lenguaje de consulta ES|QL , un lenguaje de consulta de canalización que es tan simple como SQL, si no más simple. Es particularmente adecuado para los casos de uso de búsqueda, seguridad y observabilidad en los que Elastic se destaca. También agregamos soporte para búsqueda y análisis geoespacial en ES|QL, haciéndolo aún más fácil de usar, especialmente para usuarios provenientes de las comunidades SQL o GIS .

Elasticsearch 8.12 y 8.13 brindan soporte básico para tipos geoespaciales en ES|QL. Esta funcionalidad se ha mejorado enormemente con la incorporación de capacidades de búsqueda geoespacial en 8.14. Más importante aún, este soporte está diseñado para estar estrechamente alineado con el estándar de acceso simple a funciones (OGC) del Open Geospatial Consortium ( OGC ) utilizado por otras bases de datos espaciales como PostGIS, lo que facilita el uso para los expertos en SIG familiarizados con estos estándares.

En esta publicación de blog, le mostraremos cómo realizar búsquedas geoespaciales usando ES|QL y cómo se compara con sus equivalentes SQL y DSL de consulta. También le mostraremos cómo realizar uniones espaciales usando ES|QL y visualizaremos los resultados en Kibana Maps. Tenga en cuenta que todas las funciones descritas aquí se encuentran en estado de "vista previa técnica" y nos encantaría escuchar sus comentarios sobre cómo mejorar estas funciones.

preparar datos

Podemos descargar los datos utilizados en este tutorial desde la siguiente ubicación:

git clone https://github.com/liu-xiao-guo/esql

El documento que utilizamos es  esql/airport_city_boundaries.csv en main · liu-xiao-guo/esql · GitHub

Luego abrimos Kibana:

Arriba, configuramos el nombre del índice en aeropuerto_ciudad_límites.

Modificamos las asignaciones anteriores de la siguiente manera:

      "properties": {
        "abbrev": {
          "type": "keyword"
        },
        "airport": {
          "type": "text"
        },
        "city": {
          "type": "keyword"
        },
        "city_boundary": {
          "type": "geo_shape"
        },
        "city_location": {
          "type": "geo_point"
        },
        "region": {
          "type": "text"
        }
      }
    }

Como se muestra arriba, hemos escrito con éxito 769 documentos.

Buscar datos geoespaciales

Comencemos con una consulta de ejemplo:

FROM airport_city_boundaries
| WHERE ST_INTERSECTS(
      city_boundary,
      "POLYGON((109.4 18.1, 109.6 18.1, 109.6 18.3, 109.4 18.3, 109.4 18.1))"::geo_shape
  )
| KEEP abbrev, airport, region, city, city_location

Esto buscará cualquier polígono límite de la ciudad que intersecte el polígono de búsqueda rectangular que rodea el Aeropuerto Internacional Sanya Phoenix (SYX).

En el conjunto de datos de muestra de aeropuertos, ciudades y límites de ciudades, esta búsqueda encuentra polígonos que se cruzan y devuelve los campos obligatorios de los documentos coincidentes:

abreviatura aeropuerto región ciudad Ubicación de la ciudad
SÍX Sanya Phoenix Int'l Distrito de Tianya colócalo PUNTO(109.5036 18.2533)

¡Es fácil! Ahora compare esto con la consulta DSL clásica de Elasticsearch para la misma consulta:

GET /airport_city_boundaries/_search
{
  "_source": ["abbrev", "airport", "region", "city", "city_location"],
  "query": {
    "geo_shape": {
      "city_boundary": {
        "shape": {
          "type": "polygon",
          "coordinates" : [[
            [109.4, 18.1],
            [109.6, 18.1],
            [109.6, 18.3],
            [109.4, 18.3],
            [109.4, 18.1]
          ]]
        }
      }
    }
  }
}

El propósito de ambas consultas es bastante claro, pero las consultas ES|QL son muy similares a SQL. La misma consulta en PostGIS se ve así:

SELECT abbrev, airport, region, city, city_location
FROM airport_city_boundaries
WHERE ST_INTERSECTS(
    city_boundary,
    'SRID=4326;POLYGON((109.4 18.1, 109.6 18.1, 109.6 18.3, 109.4 18.3, 109.4 18.1))'::geometry
);

Repasemos el ejemplo de ES|QL. Bastante parecido, ¿verdad?

FROM airport_city_boundaries
| WHERE ST_INTERSECTS(
      city_boundary,
      "POLYGON((109.4 18.1, 109.6 18.1, 109.6 18.3, 109.4 18.3, 109.4 18.1))"::geo_shape
  )
| KEEP abbrev, airport, region, city, city_location

Descubrimos que los usuarios existentes de la API de Elasticsearch consideran que ES|QL es más fácil de usar. Ahora esperamos que los usuarios de SQL existentes (particularmente los usuarios de SQL espacial) encuentren que ES|QL es muy similar a lo que están acostumbrados.

¿Por qué no utilizar SQL?

¿Qué pasa con Elasticsearch SQL? Ha existido por un tiempo y tiene algunas capacidades geoespaciales. Sin embargo, Elasticsearch SQL está escrito como un contenedor sobre la API de consulta sin formato, lo que significa que solo se admiten consultas que se pueden convertir a la API sin formato. ES|QL no tiene esta limitación. Como pila completamente nueva, permite muchas optimizaciones que no son posibles en SQL. Nuestros puntos de referencia muestran que ES|QL suele ser más rápido que la API de consulta , ¡especialmente en agregaciones!

Diferencias con SQL

Obviamente, según el ejemplo anterior, ES|QL es algo similar a SQL, pero existen algunas diferencias importantes. Por ejemplo, ES|QL es un lenguaje de consulta de canalización que comienza con un comando fuente como FROM y luego vincula todos los comandos posteriores con el carácter de canalización |. Esto facilita comprender cómo cada comando recibe una tabla de datos y realiza alguna operación en esa tabla, como filtrar con WHERE, agregar una columna con EVAL o realizar una agregación con STATS. En lugar de comenzar con SELECT y definir las columnas de salida finales , puede haber uno o más comandos KEEP, el último de los cuales especifica la salida final. Esta estructura simplifica el razonamiento sobre las consultas.

Centrándonos en el comando WHERE en el ejemplo anterior, podemos ver que es muy similar al ejemplo de PostGIS:

ES|QL

WHERE ST_INTERSECTS(
    city_boundary,
    "POLYGON((109.4 18.1, 109.6 18.1, 109.6 18.3, 109.4 18.3, 109.4 18.1))"::geo_shape
)

PostGIS

WHERE ST_INTERSECTS(
    city_boundary,
    'SRID=4326;POLYGON((109.4 18.1, 109.6 18.1, 109.6 18.3, 109.4 18.3, 109.4 18.1))'::geometry
)

Además de la diferencia en los caracteres de comillas de cadena, la mayor diferencia es cómo convertimos la cadena a un tipo de espacio. En PostGIS usamos el sufijo ::geometry y en ES|QL usamos el sufijo ::geo_shape. Esto se debe a que ES|QL se ejecuta en Elasticsearch y el operador de conversión de tipos:: se puede utilizar para convertir una cadena a cualquier tipo de ES|QL compatible, en este caso geo_shape. Además, los tipos geo_shape y geo_point en Elasticsearch implican un sistema de coordenadas espaciales llamado WGS84, comúnmente representado por el número SRID 4326. En PostGIS, esto debe indicarse explícitamente, por lo que la cadena WKT tiene el prefijo SRID=4326;. Si elimina el prefijo, el SRID se establecerá en 0, que se parece más a los tipos de Elasticsearch cartesian_point y cartesian_shape, que no están vinculados a ningún sistema de coordenadas específico.

Tanto ES|QL como PostGIS proporcionan la sintaxis de la función de conversión de tipos:

ES|QL

WHERE ST_INTERSECTS(
    city_boundary,
    TO_GEOSHAPE("POLYGON((109.4 18.1, 109.6 18.1, 109.6 18.3, 109.4 18.3, 109.4 18.1))")
)

PostGIS

WHERE ST_INTERSECTS(
    city_boundary,
    ST_SetSRID(
      ST_GeomFromText('POLYGON((109.4 18.1, 109.6 18.1, 109.6 18.3, 109.4 18.3, 109.4 18.1))'),
      4326
    )
)

funciones OGC

Elasticsearch 8.14 presenta las siguientes cuatro funciones de búsqueda espacial OGC:

ES|QL PostGIS Descripción
INTERSECCIONES ST Intersecciones ST Devuelve verdadero si dos geometrías se cruzan; falso en caso contrario.
ST_DISJUNTO ST_Disjunto Devuelve verdadero si las dos geometrías no se cruzan; falso en caso contrario. Lo opuesto a ST_INTERSECTS.
ST_CONTIENE ST_Contiene Devuelve verdadero si una geometría contiene otra geometría; falso en caso contrario.
ST_DENTRO ST_Dentro Devuelve verdadero si una geometría está dentro de otra geometría; falso en caso contrario. Operación inversa de ST_CONTAINS.

Estas funciones se comportan de manera similar a sus contrapartes de PostGIS y se pueden usar de la misma manera. Por ejemplo, ST_INTERSECTS devuelve verdadero si dos geometrías se cruzan, falso en caso contrario. Si hace clic en los enlaces de documentación en la tabla anterior, puede notar que todos los ejemplos de ES|QL están en la cláusula WHERE después de la cláusula FROM, mientras que todos los ejemplos de PostGIS usan geometrías literales. De hecho, ambas plataformas admiten el uso de estas funciones en cualquier parte de una consulta.

El primer ejemplo de ST_INTERSECTS en la documentación de PostGIS es:

SELECT ST_Intersects(
    'POINT(0 0)'::geometry,
    'LINESTRING ( 2 0, 0 2 )'::geometry
);

La versión equivalente en ES|QL es:

ROW ST_INTERSECTS(
    "POINT(0 0)"::geo_point,
    "LINESTRING ( 2 0, 0 2 )"::geo_shape
)

Tenga en cuenta que no especificamos un SRID en el ejemplo de PostGIS. Esto se debe a que cuando se utilizan tipos de geometría en PostGIS, todos los cálculos se realizan en un sistema de coordenadas planas, por lo que si dos geometrías tienen el mismo SRID, no importa cuál sea el SRID. En Elasticsearch, esto también es cierto para la mayoría de las funciones; sin embargo, hay excepciones donde geo_shape y geo_point usan cálculos esféricos, como veremos en el próximo blog sobre búsquedas de distancia espacial.

ES|Versatilidad QL

Entonces, hemos visto el ejemplo anterior del uso de funciones espaciales en la cláusula WHERE y el comando FILA. ¿Dónde más pueden ser útiles? Un lugar muy útil es el comando EVAL. Este comando le permite evaluar una expresión y devolver el resultado. Por ejemplo, determinemos si los centroides de todos los aeropuertos agrupados por nombre de país están dentro de los límites que describen el país:

FROM airports
| EVAL in_uk = ST_INTERSECTS(location, TO_GEOSHAPE("POLYGON((1.2305 60.8449, -1.582 61.6899, -10.7227 58.4017, -7.1191 55.3291, -7.9102 54.2139, -5.4492 54.0078, -5.2734 52.3756, -7.8223 49.6676, -5.0977 49.2678, 0.9668 50.5134, 2.5488 52.1065, 2.6367 54.0078, -0.9668 56.4625, 1.2305 60.8449))"))
| EVAL in_iceland = ST_INTERSECTS(location, TO_GEOSHAPE("POLYGON ((-25.4883 65.5312, -23.4668 66.7746, -18.4131 67.4749, -13.0957 66.2669, -12.3926 64.4159, -20.1270 62.7346, -24.7852 63.3718, -25.4883 65.5312))"))
| EVAL within_uk = ST_WITHIN(location, TO_GEOSHAPE("POLYGON((1.2305 60.8449, -1.582 61.6899, -10.7227 58.4017, -7.1191 55.3291, -7.9102 54.2139, -5.4492 54.0078, -5.2734 52.3756, -7.8223 49.6676, -5.0977 49.2678, 0.9668 50.5134, 2.5488 52.1065, 2.6367 54.0078, -0.9668 56.4625, 1.2305 60.8449))"))
| EVAL within_iceland = ST_WITHIN(location, TO_GEOSHAPE("POLYGON ((-25.4883 65.5312, -23.4668 66.7746, -18.4131 67.4749, -13.0957 66.2669, -12.3926 64.4159, -20.1270 62.7346, -24.7852 63.3718, -25.4883 65.5312))"))
| STATS centroid = ST_CENTROID_AGG(location), count=COUNT() BY in_uk, in_iceland, within_uk, within_iceland
| SORT count ASC

Los resultados son los esperados, con los centroides de los aeropuertos del Reino Unido dentro de las fronteras del Reino Unido en lugar de las fronteras de Islandia y viceversa:

centroide contar en_reino unido en islandia dentro del reino unido dentro de islandia
PUNTO (-21,946634463965893 64,13187285885215) 1 FALSO verdadero FALSO verdadero
PUNTO (-2,597342072712148 54,33551226578214) 17 verdadero FALSO verdadero FALSO
PUNTO (0,04453958108176276 23,74658354606057) 873 FALSO FALSO FALSO FALSO

De hecho, estas funciones se pueden utilizar en cualquier parte de la consulta, siempre que su firma tenga sentido. Ambos aceptan dos argumentos, ya sea un objeto espacial literal o un campo de tipo espacial, y ambos devuelven un valor booleano. Una consideración importante es que el sistema de referencia de coordenadas (CRS) de la geometría debe coincidir; de lo contrario, se devolverá un error. Esto significa que no puede mezclar los tipos geo_shape y cartesian_shape en la misma llamada de función. Sin embargo, puede mezclar los tipos geo_point y geo_shape porque el tipo geo_point es un caso especial del tipo geo_shape y ambos comparten el mismo sistema de referencia de coordenadas. La documentación para cada función definida anteriormente enumera las combinaciones de tipos admitidas.

Además, cualquiera de los parámetros puede ser un espacio literal o un campo, en cualquier orden. Incluso puedes especificar dos campos, dos textos, un campo y un texto, o un texto y un campo. El único requisito es la compatibilidad de tipos. Por ejemplo, esta consulta compara dos campos en el mismo índice:

FROM airport_city_boundaries
| EVAL in_city = ST_INTERSECTS(city_location, city_boundary)
| STATS count=COUNT(*) BY in_city
| SORT count ASC
| EVAL cardinality = CASE(count < 10, "very few", count < 100, "few", "many")
| KEEP cardinality, count, in_city

Básicamente, la consulta pregunta si la ubicación de la ciudad está dentro de los límites de la ciudad, lo que generalmente debería ser correcto, pero siempre hay excepciones:

cardinalidad contar en_ciudad
pocos 29 FALSO
muchos 740 verdadero

Una pregunta más interesante es si la ubicación del aeropuerto está dentro de los límites de la ciudad a la que llega el aeropuerto. Sin embargo, la ubicación del aeropuerto se encuentra en un índice diferente al que contiene los límites de la ciudad. Esto requiere una forma de consultar y correlacionar datos de manera eficiente en estos dos índices independientes.

uniones espaciales

ES|QL no admite el comando JOIN, pero puede usar el comando ENRICH para implementar un caso especial de combinaciones, que se comporta como una "unión izquierda" en SQL. Este comando funciona como una "unión izquierda" en SQL, lo que le permite enriquecer los resultados de un índice con datos de otro índice según la relación espacial entre los dos conjuntos de datos.

Por ejemplo, enriquezcamos los resultados de la tabla de aeropuertos buscando límites de ciudades que contengan ubicaciones de aeropuertos con información adicional sobre las ciudades atendidas por el aeropuerto y luego ejecutemos algunas estadísticas sobre los resultados:

FROM airports
| ENRICH city_boundaries ON city_location WITH airport, region, city_boundary
| MV_EXPAND city_boundary
| EVAL boundary_wkt_length = LENGTH(TO_STRING(city_boundary))
| STATS centroid = ST_CENTROID_AGG(location), count = COUNT(city_location), min_wkt = MIN(boundary_wkt_length), max_wkt = MAX(boundary_wkt_length) BY region
| SORT count DESC
| LIMIT 5

Esto devuelve las 5 regiones principales con la mayor cantidad de aeropuertos, junto con los centroides de todos los aeropuertos con regiones coincidentes y el rango de longitudes de las representaciones WKT de los límites de las ciudades dentro de esas regiones:

centroide contar mi_wkt trabajo máximo región
PUNTO (-32,56093470960719 32,598117914802714) 90 207 207 nulo
PUNTO (-73,94515332765877 40,70366442203522) 9 438 438 Ciudad de Nueva York
PUNTO (-83,10398317873478 42,300230911932886) 9 473 473 Detroit
PUNTO (-156,3020245861262 20,176383580081165) 5 307 803 Hawai
PUNTO (-73.88902732171118 45.57078813901171) 4 837 837 Montréal

Entonces, ¿qué está pasando aquí exactamente? ¿Dónde ocurre el llamado JOIN? La clave de la consulta reside en el comando ENRICH:

FROM airports
| ENRICH city_boundaries ON city_location WITH airport, region, city_boundary

Este comando indica a Elasticsearch que enriquezca los resultados recuperados del índice de aeropuertos y realice una unión de intersección entre el campo ubicación_ciudad del índice original y el campo límite_ciudad del índice límites_ciudad_aeropuerto, que hemos utilizado en varios ejemplos anteriores. Pero parte de esta información no es claramente visible en esta consulta. Lo que vemos es el nombre de la política enriquecida city_boundaries y la información que falta está encapsulada en la definición de la política.

{
  "geo_match": {
    "indices": "airport_city_boundaries",
    "match_field": "city_boundary",
    "enrich_fields": ["city", "airport", "region", "city_boundary"]
  }
}

Aquí podemos ver que realizará una consulta geo_match (se cruza de forma predeterminada), los campos a coincidir son city_boundary y enrich_fields son los campos que queremos agregar al documento original. Uno de los campos, la región, en realidad se utiliza como clave de agrupación para el comando ESTADÍSTICAS, lo cual no podríamos hacer sin esta característica de "unión izquierda". Para obtener más información sobre estrategias de enriquecimiento, consulte  la documentación de enriquecimiento . A medida que lea estos documentos, notará que describen el uso de índices enriquecidos para enriquecer los datos en el momento del índice mediante la configuración de la canalización de ingesta. Esto no es necesario para ES|QL porque el comando ENRICH funciona en el momento de la consulta. Basta con preparar el índice enriquecido con los datos necesarios y la estrategia de enriquecimiento, y luego usar el comando ENRICH en la consulta ES|QL.

También puedes notar que el campo más común es nulo. ¿qué significa eso? Recuerde que comparé este comando con una "unión izquierda" en SQL, lo que significa que si no se encuentran límites de ciudad coincidentes para un aeropuerto, el aeropuerto aún se devuelve, pero el valor del campo en el índice aeropuerto_ciudad_límites es nulo. Se descubrió que 89 aeropuertos no tenían límites de ciudad coincidentes y un aeropuerto tenía un campo de región coincidente nulo. Esto resultó en 90 aeropuertos sin regiones en los resultados. Otro detalle interesante es la necesidad del comando MV_EXPAND. Esto es necesario porque el comando ENRICH puede devolver varios resultados para cada fila de entrada y MV_EXPAND ayuda a dividir estos resultados en varias filas, una para cada resultado. Esto también explica por qué "Hawái" muestra diferentes resultados min_wkt y max_wkt: hay varias regiones con el mismo nombre pero con límites diferentes.

mapa de kibana

Kibana agrega soporte para Spatial ES|QL en la aplicación de mapas. Esto significa que ahora puede usar ES|QL para buscar datos geoespaciales en Elasticsearch y visualizar los resultados en un mapa.

Hay una nueva opción de capa en el menú Agregar capa llamada "ES|QL". Como todas las características geoespaciales descritas hasta ahora, esta opción se encuentra en estado de "vista previa técnica". Seleccionar esta opción le permite agregar una capa al mapa según los resultados de una consulta ES|QL. Por ejemplo, podrías agregar una capa a tu mapa que muestre todos los aeropuertos del mundo.

O podría agregar una capa que muestre los polígonos en el índice aeropuerto_ciudad_límites, o mejor aún, ¿cómo genera la compleja consulta ENRICH anterior estadísticas sobre cuántos aeropuertos hay en cada región?

¿Qué sigue?

Es posible que hayas notado que en los dos ejemplos anteriores, incluimos otra función espacial ST_CENTROID_AGG. Esta es la función agregada utilizada en el comando STATS y es la primera de muchas capacidades de análisis espacial que planeamos agregar a ES|QL. Cuando tengamos más que mostrar, ¡lo publicaremos en un blog!

Antes de llegar a eso, queremos contarle un poco más sobre una característica particularmente interesante que hemos desarrollado: la capacidad de realizar búsquedas de distancia espacial, una de las características de búsqueda espacial más utilizadas en Elasticsearch. ¿Te imaginas cómo sería la sintaxis de una búsqueda a distancia? ¿Quizás algo así como la función OGC? ¡Estén atentos al próximo blog de esta serie para obtener más información!

Alerta de spoiler: Elasticsearch 8.15 acaba de ser lanzado e incluye búsquedas de distancia espacial usando ES|QL.

 

¿Listo para probarlo tú mismo? Comienza tu prueba gratuita .
¿Quieres obtener la certificación Elastic? ¡Descubra cuándo comienza la próxima capacitación para ingenieros de Elasticsearch !

 

Temas relacionados: Búsqueda geoespacial con Elasticsearch ES|QL — Search Labs

Google: La transición a Rust ha reducido significativamente las vulnerabilidades de Android. Se lanza PostgreSQL 17. Huawei anuncia que UBMC, el antiguo reproductor de música clásico , Winamp, se lanza oficialmente en 2024.2.3. ¿Se ha convertido en una marca registrada de Oracle? Open Source Daily | PostgreSQL 17; Cómo las empresas chinas de IA eluden la prohibición de chips de EE. UU. ¿Quién puede saciar la sed de los desarrolladores de IA? La startup "Zhihuijun" tiene AimRT de código abierto, un marco de desarrollo en tiempo de ejecución para el campo de la robótica moderna, Tcl/Tk 9.0, lanzó Meta y lanzó el modelo de IA multimodal Llama 3.2.
{{o.nombre}}
{{m.nombre}}

Supongo que te gusta

Origin my.oschina.net/u/3343882/blog/16010045
Recomendado
Clasificación