Indexar e consultar dados de localização GeoJSON no Azure Cosmos DB for NoSQL

APLICA-SE A: NoSQL

Os dados geoespaciais no Azure Cosmos DB for NoSQL permitem armazenar informações de localização e executar consultas comuns, incluindo, mas não se limitando a:

  • Descobrir se um local está dentro de uma área definida
  • Medir a distância entre dois locais
  • Determinar se um caminho se cruza com um local ou uma área

Esse guia orienta você ao longo do processo de criar dados geoespaciais, indexar os dados e, em seguida, consultar os dados em um contêiner.

Pré-requisitos

Criar um contêiner e uma política de indexação

Todos os contêineres incluem uma política de indexação padrão que irá indexar os dados geoespaciais com sucesso. Para criar uma política de indexação personalizada, crie uma conta e especifique um arquivo JSON com a configuração da política. Nesta seção, um índice espacial personalizado é usado para um contêiner recém-criado.

  1. Abra um terminal.

  2. Crie uma variável de shell para o nome da conta e do grupo de recursos do seu Azure Cosmos DB for NoSQL.

    # Variable for resource group name
    resourceGroupName="<name-of-your-resource-group>"
    
    # Variable for account name
    accountName="<name-of-your-account>"
    
  3. Crie um novo banco de dados chamado cosmicworks usando az cosmosdb sql database create.

    az cosmosdb sql database create \
        --resource-group "<resource-group-name>" \
        --account-name "<nosql-account-name>" \
        --name "cosmicworks" \
        --throughput 400
    
  4. Crie um novo arquivo JSON chamado index-policy.json e adicione ao arquivo o objeto JSON a seguir.

    {
      "indexingMode": "consistent",
      "automatic": true,
      "includedPaths": [
        {
          "path": "/*"
        }
      ],
      "excludedPaths": [
        {
          "path": "/\"_etag\"/?"
        }
      ],
      "spatialIndexes": [
        {
          "path": "/location/*",
          "types": [
            "Point",
            "Polygon"
          ]
        }
      ]
    }
    
  5. Use um az cosmosdb sql container create para criar um novo contêiner chamado locations com um caminho de chave de partição de /region.

    az cosmosdb sql container create \
        --resource-group "<resource-group-name>" \
        --account-name "<nosql-account-name>" \
        --database-name "cosmicworks" \
        --name "locations" \
        --partition-key-path "/category" \
        --idx @index-policy.json
    
  6. Para terminar, obtenha o ponto de extremidade de conta para a sua conta usando az cosmosdb show e uma consulta JMESPath.

    az cosmosdb show \
        --resource-group "<resource-group-name>" \
        --name "<nosql-account-name>" \
        --query "documentEndpoint"
    
  7. Anote o ponto de extremidade da conta porque você vai precisar dele na próxima seção.

Criar um SDK de aplicativo de console do .NET

O SDK do .NET para o Azure Cosmos DB for NoSQL fornece classes para objetos GeoJSON comuns. Use esse SDK para simplificar o processo de adicionar objetos geográficos ao seu contêiner.

  1. Abra um terminal em um diretório vazio.

  2. Crie um novo aplicativo .NET usando o comando dotnet new com o modelo de console .

    dotnet new console
    
  3. Importe o pacote NuGet Microsoft.Azure.Cosmos usando o comando dotnet add package.

    dotnet add package Microsoft.Azure.Cosmos --version 3.*
    

    Aviso

    Atualmente, o Entity Framework não dá suporte a dados espaciais no Azure Cosmos DB for NoSQL. Use um dos SDKs do Azure Cosmos DB for NoSQL para obter um suporte fortemente tipado ao GeoJSON.

  4. Importe o pacote NuGet Azure.Identity.

    dotnet add package Azure.Identity --version 1.*
    
  5. Compile o projeto com o dotnet buildcomando.

    dotnet build
    
  6. Abra o ambiente de desenvolvedor integrado (IDE) de sua escolha no mesmo diretório que o seu aplicativo de console do .NET.

  7. Abra o arquivo Program.cs recém-criado e exclua qualquer código existente. Adicione o uso de diretivas para os namespaces Microsoft.Azure.Cosmos, Microsoft.Azure.Cosmos.Linq e Microsoft.Azure.Cosmos.Spatial.

    using Microsoft.Azure.Cosmos;
    using Microsoft.Azure.Cosmos.Linq;
    using Microsoft.Azure.Cosmos.Spatial;
    
  8. Adicione outra diretiva de uso para o namespace Azure.Identity.

    using Azure.Identity;
    
  9. Crie uma nova variável do tipo credential chamada DefaultAzureCredential.

    DefaultAzureCredential credential = new();
    
  10. Crie uma variável de cadeia de caracteres chamada endpoint com o ponto de extremidade da sua conta do Azure Cosmos DB for NoSQL.

    string endpoint = "<nosql-account-endpoint>";
    
  11. Crie uma nova instância da classe CosmosClient transmitindo-a para connectionString e encapsulando-a em uma instrução using.

    using CosmosClient client = new (connectionString);
    
  12. Recupere uma referência ao contêiner criado anteriormente (cosmicworks/locations) na conta do Azure Cosmos DB for NoSQL usando CosmosClient.GetDatabase e, em seguida, Database.GetContainer. Armazene o resultado em uma variável chamada container.

    var container = client.GetDatabase("cosmicworks").GetContainer("locations");
    
  13. Salve o arquivo Program.cs.

Adicionar dados geoespaciais

O SDK do .NET inclui vários tipos no namespace Microsoft.Azure.Cosmos.Spatial para representar objetos GeoJSON comuns. Esses tipos simplificam o processo de adicionar novas informações de localização aos itens em um contêiner.

  1. Crie um novo arquivo chamado Office.cs. No arquivo, adicione uma diretiva using a Microsoft.Azure.Cosmos.Spatial e crie um Office tipo de registro com essas propriedades:

    Type Descrição Valor padrão
    id string Identificador exclusivo
    name string Nome do escritório
    local Point Ponto geográfico GeoJSON
    category string Valor da chave de partição business-office
    using Microsoft.Azure.Cosmos.Spatial;
    
    public record Office(
        string id,
        string name,
        Point location,
        string category = "business-office"
    );
    

    Observação

    Esse registro inclui uma propriedade Point que representa uma posição específica no GeoJSON. Para obter mais informações, confira Ponto GeoJSON.

  2. Crie outro arquivo novo chamado Category.cs. Adicione outro tipo de registro chamado Region com essas propriedades:

    Type Descrição Valor padrão
    id string Identificador exclusivo
    name string Nome do escritório
    local Polygon Forma geográfica GeoJSON
    category string Valor da chave de partição business-region
    using Microsoft.Azure.Cosmos.Spatial;
    
    public record Region(
        string id,
        string name,
        Polygon location,
        string category = "business-region"
    );
    

    Observação

    Esse registro inclui uma propriedade Polygon que representa uma forma composta de linhas desenhadas entre diversos locais no GeoJSON. Para obter mais informações, confira Polígono GeoJSON.

  3. Crie outro arquivo novo chamado Result.cs. Adicione um tipo de registro chamado Result com essas duas propriedades:

    Type Descrição
    name string Nome do resultado equiparado
    distanceKilometers decimal Distância em quilômetros
    public record Result(
        string name,
        decimal distanceKilometers
    );
    
  4. Salve os arquivos Office.cs, Region.cs e Result.cs.

  5. Abra o arquivo Program.cs outra vez.

  6. Crie um novo Polygon em uma variável chamada mainCampusPolygon.

    Polygon mainCampusPolygon = new (
        new []
        {
            new LinearRing(new [] {
                new Position(-122.13237, 47.64606),
                new Position(-122.13222, 47.63376),
                new Position(-122.11841, 47.64175),
                new Position(-122.12061, 47.64589),
                new Position(-122.13237, 47.64606),
            })
        }
    );
    
  7. Crie uma nova variável Region chamada mainCampusRegion usando o polígono, o identificador único 1000 e o nome Main Campus.

    Region mainCampusRegion = new ("1000", "Main Campus", mainCampusPolygon);
    
  8. Use Container.UpsertItemAsync para adicionar a região ao contêiner. Escreva as informações da região no console.

    await container.UpsertItemAsync<Region>(mainCampusRegion);
    Console.WriteLine($"[UPSERT ITEM]\t{mainCampusRegion}");
    

    Dica

    Este guia usa upsert em vez de inserir para que você possa executar o script várias vezes sem causar um conflito entre os identificadores únicos. Para obter mais informações sobre operações upsert, consulte como criar itens.

  9. Crie uma nova variável Point chamada headquartersPoint. Use essa variável para criar uma nova variável Office chamada headquartersOffice usando o ponto, o identificador único 0001 e o nome Headquarters.

    Point headquartersPoint = new (-122.12827, 47.63980);
    Office headquartersOffice = new ("0001", "Headquarters", headquartersPoint);
    
  10. Crie outra Point variável chamada researchPoint. Use essa variável para criar uma outra variável Office chamada researchOffice usando o respectivo ponto, o identificador único 0002 e o nome Research and Development.

    Point researchPoint = new (-96.84369, 46.81298);
    Office researchOffice = new ("0002", "Research and Development", researchPoint);
    
  11. Crie um TransactionalBatch para fazer upsert de ambas as variáveis Office como uma única transação. Em seguida, grave as informações dos dois escritórios no console.

    TransactionalBatch officeBatch = container.CreateTransactionalBatch(new PartitionKey("business-office"));
    officeBatch.UpsertItem<Office>(headquartersOffice);
    officeBatch.UpsertItem<Office>(researchOffice);
    await officeBatch.ExecuteAsync();
    
    Console.WriteLine($"[UPSERT ITEM]\t{headquartersOffice}");
    Console.WriteLine($"[UPSERT ITEM]\t{researchOffice}");
    

    Observação

    Para obter mais informações sobre transações, confira operações em lotes transacionais.

  12. Salve o arquivo Program.cs.

  13. Execute o aplicativo em um terminal usando dotnet run. Observe que a saída da execução do aplicativo inclui informações sobre os três itens recém-criados.

    dotnet run
    
    [UPSERT ITEM]   Region { id = 1000, name = Main Campus, location = Microsoft.Azure.Cosmos.Spatial.Polygon, category = business-region }
    [UPSERT ITEM]   Office { id = 0001, name = Headquarters, location = Microsoft.Azure.Cosmos.Spatial.Point, category = business-office }
    [UPSERT ITEM]   Office { id = 0002, name = Research and Development, location = Microsoft.Azure.Cosmos.Spatial.Point, category = business-office }
    

Consultar dados geoespaciais usando a consulta NoSQL

Os tipos no namespace Microsoft.Azure.Cosmos.Spatial podem ser usados como entradas de dados para uma consulta parametrizada NoSQL para usar funções integradas como ST_DISTANCE.

  1. Abra o arquivo Program.cs.

  2. Criar uma nova variável string chamada nosql com a consulta é usado nesta seção para medir a distância entre pontos.

    string nosqlString = @"
        SELECT
            o.name,
            NumberBin(distanceMeters / 1000, 0.01) AS distanceKilometers
        FROM
            offices o
        JOIN
            (SELECT VALUE ROUND(ST_DISTANCE(o.location, @compareLocation))) AS distanceMeters
        WHERE
            o.category = @partitionKey AND
            distanceMeters > @maxDistance
    ";
    

    Dica

    Essa consulta coloca a função geoespacial dentro de uma subconsulta para simplificar o processo de reutilização do valor já calculado várias vezes nas cláusulas SELECT e WHERE.

  3. Crie uma nova variável QueryDefinition chamada query usando a variável nosqlString como um parâmetro. Em seguida, use o método fluente QueryDefinition.WithParameter várias vezes para adicionar esses parâmetros à consulta:

    Valor
    @maxDistance 2000
    @partitionKey "business-office"
    @compareLocation new Point(-122.11758, 47.66901)
    var query = new QueryDefinition(nosqlString)
        .WithParameter("@maxDistance", 2000)
        .WithParameter("@partitionKey", "business-office")
        .WithParameter("@compareLocation", new Point(-122.11758, 47.66901));
    
  4. Crie um novo iterador usando Container.GetItemQueryIterator<>, o tipo genérico Result e a variável query. Em seguida, use uma combinação de um loop while e foreach para iterar todos os resultados em cada página de resultados. Gere cada resultado para o console.

    var distanceIterator = container.GetItemQueryIterator<Result>(query);
    while (distanceIterator.HasMoreResults)
    {
        var response = await distanceIterator.ReadNextAsync();
        foreach (var result in response)
        {
            Console.WriteLine($"[DISTANCE KM]\t{result}");
        }
    }
    

    Observação

    Para obter mais informações sobre como enumerar os resultados da consulta, confira itens de consulta.

  5. Salve o arquivo Program.cs.

  6. Execute o aplicativo em um terminal usando dotnet run. Observe que a saída agora inclui os resultados da consulta.

    dotnet run
    
    [DISTANCE KM]   Result { name = Headquarters, distanceKilometers = 3.34 }
    [DISTANCE KM]   Result { name = Research and Development, distanceKilometers = 1907.43 }
    

Consultar dados geoespaciais usando LINQ

A funcionalidade LINQ to NoSQL no SDK do .NET é compatível com a inclusão de tipos geoespaciais nas expressões de consulta. Mais ainda, o SDK inclui métodos de extensão que são mapeados para funções integradas equivalentes:

Método de extensão Função integrada
Distance() ST_DISTANCE
Intersects() ST_INTERSECTS
IsValid() ST_ISVALID
IsValidDetailed() ST_ISVALIDDETAILED
Within() ST_WITHIN
  1. Abra o arquivo Program.cs.

  2. Recupere o item Region do contêiner com um identificador único de 1000 e o armazene em uma variável chamada region.

    Region region = await container.ReadItemAsync<Region>("1000", new PartitionKey("business-region"));
    
  3. Use o método Container.GetItemLinqQueryable<> para obter um LINQ consultável e crie a consulta LINQ fluentemente executando essas três ações:

    1. Use o método de extensão Queryable.Where<> para filtrar apenas para itens com uma category equivalente a "business-office".

    2. Use Queryable.Where<> novamente para filtrar apenas para locais dentro da propriedade location da variável da region usando Geometry.Within().

    3. Traduza a expressão LINQ em um iterador de feed usando CosmosLinqExtensions.ToFeedIterator<>.

    var regionIterator = container.GetItemLinqQueryable<Office>()
        .Where(o => o.category == "business-office")
        .Where(o => o.location.Within(region.location))
        .ToFeedIterator<Office>();
    

    Importante

    Nesse exemplo, a propriedade de localização do escritório tem um ponto e a propriedade de localização da região tem um polígono. ST_WITHIN está determinando se o ponto do escritório está dentro do polígono da região.

  4. Use uma combinação de um loop while e foreach para iterar todos os resultados em cada página de resultados. Gere cada resultado para o console.

    while (regionIterator.HasMoreResults)
    {
        var response = await regionIterator.ReadNextAsync();
        foreach (var office in response)
        {
            Console.WriteLine($"[IN REGION]\t{office}");
        }
    }
    
  5. Salve o arquivo Program.cs.

  6. Execute o aplicativo uma última vez em um terminal usando dotnet run. Observe que a saída agora inclui os resultados da segunda consulta baseada em LINQ.

    dotnet run
    
    [IN REGION]     Office { id = 0001, name = Headquarters, location = Microsoft.Azure.Cosmos.Spatial.Point, category = business-office }
    

Limpar os recursos

Remova seu banco de dados após concluir este guia.

  1. Abra um terminal e crie uma variável de shell para o nome da sua conta e grupo de recursos.

    # Variable for resource group name
    resourceGroupName="<name-of-your-resource-group>"
    
    # Variable for account name
    accountName="<name-of-your-account>"
    
  2. Use az cosmosdb sql database delete para remover o banco de dados.

    az cosmosdb sql database delete \
        --resource-group "<resource-group-name>" \
        --account-name "<nosql-account-name>" \
        --name "cosmicworks"
    

Próximas etapas