Elasticsearch: pesquisa geoespacial usando ES|QL

Autor: De Elastic Craig Taverner

O Elasticsearch possui poderosos recursos de pesquisa e análise geoespacial há anos , mas sua API é muito diferente daquela com a qual os usuários típicos de GIS estão acostumados. No ano passado, adicionamos a linguagem de consulta ES|QL , uma linguagem de consulta de pipeline que é tão simples quanto SQL, se não mais simples. É particularmente adequado para os casos de uso de pesquisa, segurança e observabilidade nos quais a Elastic se destaca. Também adicionamos suporte para pesquisa e análise geoespacial no ES|QL, tornando-o ainda mais fácil de usar, especialmente para usuários provenientes das comunidades SQL ou GIS .

Elasticsearch 8.12 e 8.13 fornecem suporte básico para tipos geoespaciais em ES|QL. Esta funcionalidade foi bastante aprimorada com a adição de recursos de pesquisa geoespacial na versão 8.14. Mais importante ainda, este suporte foi projetado para estar intimamente alinhado com o padrão Simple Feature Access (OGC) do Open Geospatial Consortium ( OGC ) usado por outros bancos de dados espaciais, como PostGIS, tornando mais fácil para especialistas em GIS familiarizados com esses padrões.

Nesta postagem do blog, mostraremos como realizar pesquisas geoespaciais usando ES|QL e como ele se compara aos seus equivalentes SQL e DSL de consulta. Também mostraremos como realizar junções espaciais usando ES|QL e visualizar os resultados no Kibana Maps. Observe que todos os recursos descritos aqui estão no status de "visualização técnica" e adoraríamos ouvir seus comentários sobre como melhorar esses recursos.

Preparar dados

Podemos baixar os dados usados ​​neste tutorial no seguinte local:

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

O documento que usamos é  esql/airport_city_boundaries.csv em main · liu-xiao-guo/esql · GitHub

Em seguida, abrimos o Kibana:

Acima, definimos o nome do índice como aeroporto_cidade_limites.

Modificamos os mapeamentos acima da seguinte forma:

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

Conforme mostrado acima, escrevemos com sucesso 769 documentos.

Pesquisar 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

Isso procurará qualquer polígono de limite de cidade que cruze o polígono de pesquisa retangular ao redor do Aeroporto Internacional de Sanya Phoenix (SYX).

No conjunto de dados de amostra de aeroportos, cidades e limites de cidades, esta pesquisa encontra polígonos que se cruzam e retorna os campos obrigatórios dos documentos correspondentes:

abreviar aeroporto região cidade cidade_localização
SYX Internacional de Sanya Phoenix Distrito de Tianya Coloque-o PONTO(109.5036 18.2533)

É 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]
          ]]
        }
      }
    }
  }
}

O propósito de ambas as consultas é bastante claro, mas as consultas ES|QL são muito semelhantes 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
);

Vamos revisar o exemplo ES|QL. Muito parecido, certo?

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

Descobrimos que os usuários existentes da API Elasticsearch consideram o ES|QL mais fácil de usar. Esperamos agora que os usuários SQL existentes (particularmente usuários de SQL Espacial) considerem o ES|QL muito semelhante ao que estão acostumados.

Por que não usar SQL?

E quanto ao Elasticsearch SQL? Já existe há algum tempo e possui alguns recursos geoespaciais. No entanto, o Elasticsearch SQL é escrito como um wrapper sobre a API de consulta bruta, o que significa que apenas as consultas que podem ser convertidas na API bruta são suportadas. ES|QL não tem essa limitação. Por ser uma pilha completamente nova, permite muitas otimizações que não são possíveis em SQL. Nossos benchmarks mostram que o ES|QL costuma ser mais rápido que a API de consulta , especialmente em agregações!

Diferenças do SQL

Obviamente, do exemplo anterior, ES|QL é um pouco semelhante ao SQL, mas existem algumas diferenças importantes. Por exemplo, ES|QL é uma linguagem de consulta de pipeline que começa com um comando de origem como FROM e depois vincula todos os comandos subsequentes com o caractere de barra vertical | Isso facilita a compreensão de como cada comando recebe uma tabela de dados e realiza alguma operação nessa tabela, como filtrar com WHERE, adicionar uma coluna com EVAL ou realizar uma agregação com STATS. Em vez de começar com SELECT e definir as colunas de saída final , pode haver um ou mais comandos KEEP, o último dos quais especifica a saída final. Essa estrutura simplifica o raciocínio sobre consultas.

Focando no comando WHERE do exemplo acima, podemos ver que ele é muito semelhante ao exemplo do PostGIS:

PT|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 é como convertemos a string em um tipo de espaço. No PostGIS usamos o sufixo ::geometry, e no ES|QL usamos o sufixo ::geo_shape. Isso ocorre porque o ES|QL é executado no Elasticsearch, e o operador de conversão de tipo:: pode ser usado para converter uma string em qualquer tipo ES|QL compatível, neste caso geo_shape. Além disso, os tipos geo_shape e geo_point no Elasticsearch implicam um sistema de coordenadas espaciais chamado WGS84, comumente representado pelo número SRID 4326. No PostGIS, isso precisa ser declarado explicitamente, então a string WKT é prefixada com SRID=4326;. Se você remover o prefixo, o SRID será definido como 0, o 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 ES|QL quanto PostGIS fornecem sintaxe de função de conversão de tipo:

PT|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 apresenta as seguintes quatro funções de pesquisa espacial OGC:

PT|QL PostGIS Descrição
ST_INTERCEITA ST_Intersecta Retorna verdadeiro se duas geometrias se cruzarem, caso contrário, retorna falso.
ST_DISJUNTA ST_Disjuntor Retorna verdadeiro se as duas geometrias não se cruzarem, caso contrário, retorna falso. O oposto de ST_INTERSECTS.
ST_CONTÉM ST_Contém Retorna verdadeiro se uma geometria contém outra geometria, caso contrário, retorna falso.
ST_DENTRO ST_Dentro Retorna verdadeiro se uma geometria estiver dentro de outra geometria, caso contrário, retorna falso. Operação inversa de ST_CONTAINS.

Essas funções se comportam de maneira semelhante às suas contrapartes PostGIS e podem ser usadas da mesma maneira. Por exemplo, ST_INTERSECTS retorna verdadeiro se duas geometrias se cruzam, falso caso contrário. Se você clicar nos links de documentação na tabela acima, poderá notar que todos os exemplos ES|QL estão na cláusula WHERE após a cláusula FROM, enquanto todos os exemplos PostGIS usam geometrias literais. Na verdade, ambas as plataformas suportam o uso destas funções em qualquer parte de uma consulta.

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

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

A versão equivalente em ES|QL é:

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

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

Versatilidade ES|QL

Portanto, vimos o exemplo acima de uso de funções espaciais na cláusula WHERE e no comando ROW. Onde mais eles podem ser úteis? Um local muito útil é o 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 nome de país estão dentro dos limites que delimitam 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, com os centroides dos aeroportos do Reino Unido situados dentro das fronteiras do Reino Unido e não nas fronteiras da Islândia e vice-versa:

centróide contar no_reino unido na_islândia dentro do reino unido dentro_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 utilizadas em qualquer parte da consulta, desde que sua assinatura faça sentido. Ambos aceitam dois argumentos, um objeto espacial literal ou um campo do tipo espacial, e ambos retornam um valor booleano. Uma consideração importante é que o sistema de referência de coordenadas (CRS) da geometria deve corresponder, caso contrário um erro será retornado. Isso significa que você não pode misturar os tipos geo_shape e cartesian_shape na mesma chamada de função. No entanto, você pode misturar os tipos geo_point e geo_shape porque 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 para cada função definida acima lista as combinações de tipos suportadas.

Além disso, qualquer parâmetro pode ser um literal de espaço ou um campo, em qualquer ordem. Você pode até especificar dois campos, dois textos, um campo e um texto ou um texto e um campo. O único requisito é a compatibilidade de tipo. 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 deveria estar correto, mas sempre há exceções:

cardinalidade contar na_cidade
alguns 29 falso
muitos 740 verdadeiro

Uma questão mais interessante é se a localização do aeroporto está dentro dos limites da cidade servida pelo aeroporto. No entanto, a localização do aeroporto está num índice diferente daquele que contém os limites da cidade. Isso requer uma maneira eficiente de consultar e correlacionar dados nesses dois índices independentes.

junções espaciais

ES|QL não suporta o comando JOIN, mas você pode usar o comando ENRICH para implementar um caso especial de junções, que se comporta como uma "junção esquerda" em SQL. Este comando funciona como uma "junção esquerda" em SQL, permitindo enriquecer os resultados de um índice com dados de outro índice com base no relacionamento espacial entre os dois conjuntos de dados.

Por exemplo, vamos enriquecer os resultados da tabela de aeroportos encontrando os limites das cidades que contêm localizações de aeroportos com informações adicionais sobre as cidades atendidas pelo aeroporto e, em seguida, executar algumas estatísticas sobre os 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 mais aeroportos, juntamente com os centróides de todos os aeroportos com regiões correspondentes, e o intervalo de comprimentos das representações WKT dos limites das cidades dentro dessas regiões:

centróide contar meu_wkt Máx. 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 Montréal

Então, o que exatamente está acontecendo aqui? Onde acontece o chamado JOIN? A chave para a consulta está 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 de aeroportos e realizar uma junção de interseção entre o campo city_location do índice original e o campo city_boundary do índice airport_city_boundaries, que usamos em vários exemplos anteriores. Mas algumas dessas informações não são claramente visíveis nesta consulta. O que vemos é o nome da política enriquecida city_boundaries, e as informações que faltam estão encapsuladas na definição da política.

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

Aqui podemos ver que ele realizará uma consulta geo_match (cruza por padrão), os campos a serem correspondidos são city_boundary e rich_fields são os campos que queremos adicionar ao documento original. Um dos campos, a região, é na verdade usado como chave de agrupamento para o comando STATS, o que não poderíamos fazer sem esse recurso de "junção à esquerda". Para obter mais informações sobre estratégias de enriquecimento, consulte  a documentação de enriquecimento . Ao ler estes documentos, você notará que eles descrevem o uso de índices ricos para enriquecer os dados no momento do índice, configurando o pipeline de ingestão. Isso não é necessário para ES|QL porque o comando ENRICH funciona no momento da consulta. Basta preparar o índice rico com os dados necessários e a estratégia de enriquecimento e, em seguida, utilizar o comando ENRICH na consulta ES|QL.

Você também pode notar que o campo mais comum é nulo. o que isso significa? Lembre-se de que comparei esse comando a uma "junção à esquerda" no SQL, o que significa que se nenhum limite de cidade correspondente for encontrado para um aeroporto, o aeroporto ainda será retornado, mas o valor do campo no índice airport_city_boundaries será nulo. Verificou-se que 89 aeroportos não tinham limites de cidade correspondentes e um aeroporto tinha um campo de região de correspondência nulo. Isso resultou em 90 aeroportos sem regiões 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 dividir esses resultados em várias linhas, uma para cada resultado. Isso também explica por que "Havaí" mostra resultados min_wkt e max_wkt diferentes: existem várias regiões com o mesmo nome, mas com limites diferentes.

Mapa de Kibana

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

Há uma nova opção de camada no menu Adicionar Camada chamada "ES|QL". Como todos os recursos geoespaciais descritos até agora, esta opção está no status de “visualização técnica”. Selecionar esta opção permite adicionar uma camada ao mapa com base nos resultados de uma consulta ES|QL. Por exemplo, você pode adicionar uma camada ao seu mapa que mostre todos os aeroportos do mundo.

Ou você poderia adicionar uma camada que mostra os polígonos no índice airport_city_boundaries, ou melhor ainda, como a complexa consulta ENRICH acima gera estatísticas sobre quantos aeroportos existem em cada região?

O que vem a seguir?

Você deve ter notado que nos dois exemplos acima, comprimimos outra função espacial ST_CENTROID_AGG. Esta é a função agregada usada no comando STATS e é o primeiro de muitos recursos de análise espacial que planejamos adicionar ao ES|QL. Quando tivermos mais para mostrar, postaremos no blog sobre isso!

Antes de chegarmos a isso, queremos contar um pouco mais sobre um recurso particularmente interessante que desenvolvemos: a capacidade de realizar pesquisas de distância espacial, um dos recursos de pesquisa espacial mais comumente usados ​​no Elasticsearch. Você pode imaginar como seria a sintaxe de uma pesquisa à distância? Talvez algo como a função OGC? Fique ligado no próximo blog desta série para saber mais!

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

 

Pronto para experimentar você mesmo? Comece seu teste gratuito .
Quer obter a certificação Elastic? Descubra quando começa o próximo treinamento de engenheiro do Elasticsearch !

 

Próximo:Pesquisa geoespacial com Elasticsearch ES|QL — Search Labs

Google: A transição para Rust reduziu significativamente as vulnerabilidades do Android. PostgreSQL 17 é lançado. Huawei anuncia UBMC aberto, o antigo reprodutor de música clássico Winamp, é oficialmente de código aberto. tornou-se uma marca registrada da Oracle? Open Source Daily | PostgreSQL 17; Como as empresas chinesas de IA contornam a proibição de chips dos EUA; A empresa iniciante "Zhihuijun" abriu o código-fonte AimRT, uma estrutura de desenvolvimento de tempo de execução para o campo da robótica moderna, Tcl/Tk 9.0, lançou o Meta e lançou o modelo de IA multimodal Llama 3.2
{{o.nome}}
{{m.nome}}

Acho que você gosta

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