Blog

Pesquisa geoespacial do Elasticsearch com ES|QL

Pesquisa geoespacial na linguagem de consulta Elasticsearch (ES|QL). O Elasticsearch possui recursos poderosos de busca geoespacial, que agora estão chegando ao ES|QL para uma facilidade de uso drasticamente aprimorada e familiaridade com o OGC.

O Elasticsearch possui recursos poderosos de busca e análise geoespacial há muitos anos, mas a API era bastante diferente daquilo a que os usuários típicos de SIG estavam acostumados. No último ano , adicionamos a linguagem de consulta ES|QL, uma linguagem de consulta encadeada tão fácil, ou até mais fácil, que o SQL. É particularmente adequado para os casos de uso de busca, segurança e observabilidade nos quais o Elastic se destaca. Também estamos adicionando suporte para pesquisa e análise geoespacial no ES|QL, tornando-o muito mais fácil de usar, especialmente para usuários vindos das comunidades SQL ou GIS .

O Elasticsearch 8.12 e 8.13 trouxeram suporte básico para tipos geoespaciais ao ES|QL. Isso foi significativamente aprimorado com a adição de recursos de busca geoespacial na versão 8.14. Mais importante ainda, esse suporte foi projetado para estar em estrita conformidade com o padrão Simple Feature Access do Open Geospatial Consortium (OGC), usado por outros bancos de dados espaciais como o PostGIS, tornando-o muito mais fácil de usar para especialistas em SIG familiarizados com esses padrões.

Neste blog, mostraremos como usar o ES|QL para realizar buscas geoespaciais e como ele se compara aos seus equivalentes em SQL e Query DSL. Também mostraremos como usar o ES|QL para realizar junções espaciais e como visualizar os resultados no Kibana Maps. Note que todos os recursos descritos aqui estão em "prévia técnica" e gostaríamos muito de receber seu feedback sobre como podemos melhorá-los.

Pesquisa de dados geoespaciais

Vamos começar com um exemplo de consulta:

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

Esta função realiza uma busca por quaisquer polígonos de limites urbanos que se intersectem com um polígono de busca retangular ao redor do Aeroporto Internacional de Sanya Phoenix (SYX).

Pesquisa Geoespacial ESQL

Em um conjunto de dados de exemplo contendo aeroportos, cidades e limites urbanos, esta busca encontra o polígono de interseção e retorna os campos desejados do documento correspondente:

abreviar

aeroporto

região

cidade

localização da cidade

SYX

Sanya Phoenix Int'l

天涯区

Sanya

PONTO(109,5036 18,2533)

Isso foi fácil! Agora compare isso com a DSL de consulta clássica do Elasticsearch para a mesma 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]
          ]]
        }
      }
    }
  }
}

Ambas as consultas são razoavelmente claras em sua intenção, mas a consulta ES|QL se assemelha bastante ao SQL. A mesma consulta no PostGIS se parece com isto:

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

Relembre o exemplo em ES|QL. São muito parecidos, não é?

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

Constatamos que os usuários existentes da API Elasticsearch consideram o ES|QL muito mais fácil de usar. Agora, esperamos que os usuários atuais de SQL, especialmente os usuários de SQL Espacial, achem o ES|QL muito familiar ao que já estão acostumados a ver.

Por que não usar SQL?

E quanto ao Elasticsearch SQL? Já existe há algum tempo e possui algumas funcionalidades geoespaciais. No entanto, o Elasticsearch SQL foi escrito como um wrapper sobre a API de consulta original, o que significa que apenas as consultas que podiam ser transpiladas para a API original eram suportadas. ES|QL não possui essa limitação. Por ser uma pilha de tecnologias completamente nova, permite muitas otimizações que não eram possíveis em SQL. Nossos testes de desempenho mostram que o ES|QL é frequentemente mais rápido que a API de consulta, principalmente em operações de agregação!

referência de interseção de polígonos

Diferenças em relação ao SQL

Claramente, pelo exemplo anterior, o ES|QL é de certa forma semelhante ao SQL, mas existem algumas diferenças importantes. Por exemplo, ES|QL é uma linguagem de consulta encadeada, começando com um comando de origem como FROM e, em seguida, encadeando todos os comandos subsequentes com o caractere pipe |. Isso torna muito fácil entender como cada comando recebe uma tabela de dados e realiza alguma ação nessa tabela, como filtrar com WHERE, adicionar colunas com EVAL ou realizar agregações com STATS. Em vez de começar com SELECT para definir as colunas de saída finais, pode haver um ou mais comandos KEEP , com o último especificando os resultados de saída finais. Essa estrutura simplifica o raciocínio sobre a consulta.

Analisando o comando WHERE no exemplo acima, podemos ver que ele é bastante semelhante ao exemplo do 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
)

Além da diferença nos caracteres de aspas da string, a maior diferença está em como convertemos a string para um tipo espacial. No PostGIS, usamos o sufixo ::geometry , enquanto no ES|QL, usamos o sufixo ::geo_shape . Isso ocorre porque o ES|QL é executado dentro do Elasticsearch e o operador de conversão de tipo :: pode ser usado para converter uma string em qualquer um dos tipos ES|QL suportados, neste caso, um geo_shape. Além disso, os tipos geo_shape e geo_point no Elasticsearch implicam o sistema de coordenadas espaciais conhecido como WGS84, mais comumente referido usando o número SRID 4326. No PostGIS, isso precisa ser explícito, daí o uso do prefixo SRID=4326; na string WKT. Se esse prefixo for removido, o SRID será definido como 0, que é mais parecido com os tipos do Elasticsearch cartesian_point e cartesian_shape, que não estão vinculados a nenhum sistema de coordenadas específico.

Tanto o ES|QL quanto o PostGIS também fornecem sintaxe para funções de conversão de tipo:

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
    )
)

Funções OGC

O Elasticsearch 8.14 introduz as seguintes quatro funções de pesquisa espacial OGC:

ES|QL

PostGIS

Descrição

ST_INTERSETOS

ST_Interseções

Retorna verdadeiro se duas geometrias se intersectam e falso caso contrário.

ST_DISJOINT

ST_Disjunto

Retorna verdadeiro se as duas geometrias não se intersectarem e falso caso contrário. O inverso de ST_INTERSETOS.

ST_CONTÉM

ST_Contém

Retorna verdadeiro se uma geometria contém outra, e falso caso contrário.

ST_DENTRO

ST_Dentro

Retorna verdadeiro se uma geometria estiver dentro de outra, e falso caso contrário. O inverso de ST_CONTAINS.

Essas funções se comportam de maneira semelhante às suas contrapartes no PostGIS e são usadas da mesma forma. Por exemplo, ST_INTERSECTS retorna verdadeiro se duas geometrias se intersectam e falso caso contrário. Se você seguir os links de documentação na tabela acima, poderá notar que todos os exemplos ES|QL estão dentro de uma cláusula WHERE após uma cláusula FROM , enquanto todos os exemplos PostGIS estão usando geometrias literais. Na verdade, ambas as plataformas suportam o uso das funções em qualquer parte da consulta onde façam sentido.

O primeiro exemplo na documentação do PostGIS para ST_INTERSECTS é:

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

O equivalente em ES|QL seria:

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

Observe que não especificamos o SRID no exemplo do PostGIS. Isso ocorre porque no PostGIS, ao usar o tipo geometry , todos os cálculos são feitos em um sistema de coordenadas planas e, portanto, se ambas as geometrias tiverem o mesmo SRID, não importa qual seja o SRID. No Elasticsearch, isso também é verdade para a maioria das funções, no entanto, existem exceções onde geo_shape e geo_point usam cálculos esféricos, como veremos no próximo blog sobre pesquisa de distância espacial.

Versatilidade ES|QL

Então, vimos exemplos acima de uso de funções espaciais em cláusulas WHERE e em comandos ROW . Em que outro lugar fariam sentido? Um local muito útil é no comando EVAL . Este comando permite avaliar uma expressão e retornar o resultado. Por exemplo, vamos determinar se os centroides de todos os aeroportos agrupados por seus nomes de país estão dentro de um limite que delimita o 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

Os resultados são os esperados: o centroide dos aeroportos do Reino Unido está dentro das fronteiras do Reino Unido, e não dentro das fronteiras da Islândia, e vice-versa.

centroide

Contagem

no Reino Unido

na Islândia

dentro do Reino Unido

dentro da Islândia

PONTO (-21,946634463965893 64.13187285885215)

1

falso

verdadeiro

falso

verdadeiro

PONTO (-2,597342072712148 54,33551226578214)

17

verdadeiro

falso

verdadeiro

falso

PONTO (0,04453958108176276 23,74658354606057)

873

falso

falso

falso

falso

Na verdade, essas funções podem ser usadas em qualquer parte da consulta onde sua assinatura faça sentido. Todas elas recebem dois argumentos, que podem ser um objeto espacial literal ou um campo de um tipo espacial, e todas retornam um valor booleano. Uma consideração importante é que o sistema de referência de coordenadas (SRC) das geometrias deve coincidir, caso contrário, será retornado um erro. Isso significa que você não pode misturar os tipos geo_shape e cartesian_shape na mesma chamada de função. Você pode, no entanto, misturar os tipos geo_point e geo_shape , já que o tipo geo_point é um caso especial do tipo geo_shape e ambos compartilham o mesmo sistema de referência de coordenadas. A documentação de cada uma das funções definidas acima lista as combinações de tipos suportadas.

Além disso, qualquer um dos argumentos pode ser um literal espacial ou um campo, em qualquer ordem. Você pode até especificar dois campos, dois literais, um campo e um literal, ou um literal e um campo. O único requisito é que os tipos sejam compatíveis. Por exemplo, esta consulta compara dois campos no mesmo í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

A consulta basicamente pergunta se a localização da cidade está dentro dos limites da cidade, o que geralmente deve ser verdade, mas sempre há exceções:

cardinalidade

Contagem

na cidade

alguns

29

falso

muitos

740

verdadeiro

Uma questão muito mais interessante seria saber se a localização do aeroporto está dentro dos limites da cidade que ele serve. No entanto, a localização do aeroporto reside em um índice diferente daquele que contém os limites da cidade. Isso requer um método para consultar e correlacionar dados de forma eficaz a partir desses dois índices separados.

Junções espaciais

ES|QL não suporta comandos JOIN , mas você pode obter um caso especial de junção usando o comando ENRICH , que se comporta de forma semelhante a uma 'junção esquerda' em SQL. Este comando funciona de forma semelhante a uma 'junção à esquerda' em SQL, permitindo enriquecer os resultados de um índice com dados de outro índice com base em uma relação espacial entre os dois conjuntos de dados.

Por exemplo, vamos enriquecer os resultados de uma tabela de aeroportos com informações adicionais sobre a cidade que eles atendem, encontrando o limite da cidade que contém a localização do aeroporto e, em seguida, realizar algumas análises estatísticas dos 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

Isso retorna as 5 principais regiões com o maior número de aeroportos, juntamente com o centroide de todos os aeroportos que possuem regiões correspondentes e o intervalo de comprimento da representação WKT dos limites das cidades dentro dessas regiões:

centroide

Contagem

min_wkt

max_wkt

região

PONTO (-32,56093470960719 32,598117914802714)

90

207

207

nulo

PONTO (-73,94515332765877 40,70366442203522)

9

438

438

Cidade de Nova York

PONTO (-83.10398317873478 42.300230911932886)

9

473

473

Detroit

PONTO (-156.3020245861262 20,176383580081165)

5

307

803

Havaí

PONTO (-73,88902732171118 45.57078813901171)

4

837

837

Montreal

Então, o que realmente aconteceu aqui? Onde ocorreu o suposto JOIN ? O ponto crucial da questão reside no comando ENRICH :

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

Este comando instrui o Elasticsearch a enriquecer os resultados recuperados do índice airports e a realizar uma junção intersects entre o campo city_location do índice original e o campo city_boundary do índice airport_city_boundaries , que usamos em alguns exemplos anteriores. Mas algumas dessas informações não estão claramente visíveis nesta consulta. O que vemos é o nome de uma política de enriquecimento city_boundaries e a informação em falta está encapsulada na definição dessa política.

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

Aqui podemos ver que ele executará uma consulta geo_match (intersects é o padrão), o campo para comparar é city_boundary e os enrich_fields são os campos que queremos adicionar ao documento original. Um desses campos, o region foi na verdade usado como chave de agrupamento para o comando STATS , algo que não poderíamos ter feito sem essa capacidade de 'junção à esquerda'. Para obter mais informações sobre políticas de enriquecimento, consulte a documentação de enriquecimento. Ao ler esses documentos, você notará que eles descrevem o uso de índices de enriquecimento para enriquecer dados no momento da indexação, configurando pipelines de ingestão. Isso não é necessário para ES|QL, pois o comando ENRICH funciona no momento da consulta. Basta preparar o índice de enriquecimento com os dados necessários e a política de enriquecimento e, em seguida, usar o comando ENRICH em suas consultas ES|QL.

Você também pode notar que a região mais comumente encontrada foi null. O que isso poderia implicar? Lembre-se de que comparei este comando a um 'left join' em SQL, o que significa que se nenhum limite de cidade correspondente for encontrado para um aeroporto, o aeroporto ainda será retornado, mas com valores null para os campos do índice airport_city_boundaries . Descobriu-se que havia 89 aeroportos que não encontraram nenhum city_boundary correspondente e um aeroporto com uma correspondência onde o campo region era null. Isso levou a uma contagem de 90 aeroportos sem nenhum region nos resultados. Outro detalhe interessante é a necessidade do comando MV_EXPAND . Isso é necessário porque o comando ENRICH pode retornar vários resultados para cada linha de entrada e MV_EXPAND ajuda a separar esses resultados em várias linhas, uma para cada resultado. Isso também esclarece por que "Havaí" mostra resultados diferentes min_wkt e max_wkt : havia várias regiões com o mesmo nome, mas limites diferentes.

Mapas do Kibana

O Kibana adicionou suporte para Spatial ES|QL no aplicativo Maps. Isso significa que agora você pode usar o ES|QL para pesquisar dados geoespaciais no Elasticsearch e visualizar os resultados em um mapa.

Camadas do Kibana ES|QL

Existe uma nova opção de camada no menu "Adicionar camadas", chamada "ES|QL". Assim como todos os recursos geoespaciais descritos até agora, este está em "prévia técnica". Selecionar esta opção permite adicionar uma camada ao mapa com base nos resultados de uma consulta ES|QL. Por exemplo, você poderia adicionar uma camada ao mapa que mostrasse todos os aeroportos do mundo.

Kibana ES|QL - Aeroportos

Ou você poderia adicionar uma camada que mostre os polígonos do índice airport_city_boundaries , ou ainda melhor, que tal aquela consulta complexa ENRICH acima que gera estatísticas de quantos aeroportos existem em cada região?

Kibana ES|QL - Estatísticas da região

O que vem a seguir?

Você deve ter notado que em dois dos exemplos acima incluímos mais uma função espacial ST_CENTROID_AGG. Esta é uma função de agregação usada no comando STATS e a primeira de muitas funcionalidades de análise espacial que planejamos adicionar ao ES|QL. Vamos publicar um artigo sobre isso quando tivermos mais informações para mostrar!

Antes disso, gostaríamos de falar mais sobre um recurso particularmente interessante no qual trabalhamos: a capacidade de realizar buscas por distância espacial, um dos recursos de busca espacial mais utilizados do Elasticsearch. Você consegue imaginar como seria a sintaxe para buscas por distância? Talvez semelhante a uma função OGC? Fique ligado(a) no próximo post desta série para descobrir!

Alerta de spoiler: o Elasticsearch 8.15 acaba de ser lançado e inclui pesquisa por distância espacial com ES|QL!

Conteúdo relacionado

Avaliação da relevância de busca - parte 1: o benchmark BEIR

Thanos Papaoikonomou

Detecção de plágio por IA: usando o Elasticsearch

Priscilla Parodi

Treinamento de modelos LTR no Elasticsearch com listas de julgamento baseadas em dados de comportamento do usuário.

Alexander Dávila

Busca multimodal de picos de montanhas com Elasticsearch e SigLIP-2

Navneet Kumar

Busca lexical e semântica com Elasticsearch

Priscilla Parodi

Pronto para criar buscas de última geração?

Uma pesquisa suficientemente avançada não se consegue apenas com o esforço de uma só pessoa. O Elasticsearch é impulsionado por cientistas de dados, especialistas em operações de aprendizado de máquina, engenheiros e muitos outros que são tão apaixonados por buscas quanto você. Vamos nos conectar e trabalhar juntos para construir a experiência de busca mágica que lhe trará os resultados desejados.

Experimente você mesmo(a)