Queries geoespaciais do MongoDB em C#
Avalie esse Tutorial
Se você já deu uma olhada em um mapa para encontrar os locais de lanchonete mais próximos de você, provavelmente usou uma query geoespacial sob o capô! Utilizando objetosGeoJSONdo para armazenar dados geoespaciais no MongoDB Atlas, você pode criar suas próprias queries geoespaciais para seu aplicativo. Neste tutorial, veremos como trabalhar com queries geoespaciais no driver C# do MongoDB.
As queries geoespaciais permitem a você trabalhar com dados geoespaciais. Seja em um espaço 2d (como um mapa plano) ou 3d (ao visualizar uma representação esférica do mundo), os dados geoespaciais permitem que você encontre áreas e lugares em referência a um ponto ou conjunto de pontos.
Isso pode parecer complicado, mas você provavelmente já encontrou esses casos de uso no dia a dia: procurar pontos de interesse em uma nova cidade que você esteja explorando, descobrir quais cafés são mais próximos de você ou encontrar todas as confeiteiros em um raio de três quilômetros raio da sua posição atual (para a ciência!).
Esses tipos de queries podem ser feitas facilmente com operadores especiais de query geoespacial no MongoDB. E, para nossa sorte, esses operadores também são implementados na maioria dos drivers do MongoDB, incluindo o driver C# que usaremos neste tutorial.
Um aspecto importante do trabalho com dados geoespaciais é algo chamado formatoGeoJSON. É um padrão aberto para representar características geográficas simples e facilita o trabalho com dados geoespaciais. Veja como são alguns dos tipos de objetos GeoJSON:
1 // Point GeoJSON type 2 { 3 "type" : "Point", 4 "coordinates" : [-115.20146200000001, 36.114704000000003] 5 } 6 7 // Polygon GeoJSON type 8 { 9 "type": "Polygon", 10 "coordinates": [ 11 [ 12 [100.0, 0.0], 13 [101.0, 0.0], 14 [101.0, 1.0], 15 [100.0, 1.0], 16 [100.0, 0.0] 17 ] 18 ] 19 }
Enquanto o MongoDB suporta armazenar seus dados geoespaciais como legacy coordinate pairs, é preferível trabalhar com o formato GeoJSON , pois ele torna as queries complicadas possíveis e muito mais simples.
💡 Seja trabalhando com coordenadas no formato GeoJSON ou como legacy coordinate pairs, as consultas exigem que a longitude seja passada primeiro, seguida pela latitude. Isso pode parecer "retrógrado" em comparação com o que você pode estar acostumado, mas tenha certeza de que esse formato realmente segue a ordem
(X, Y)
da matemática! Lembre-se disso, pois as consultas geoespaciais do MongoDB também exigirão que as coordenadas sejam passadas em [longitude, latitude]
formato, quando aplicável.Tudo bem, vamos começar com o tutorial!
- Visual Studio Community (2019 ou superior)
Para facilitar o acompanhamento deste tutorial, trabalharemos com os conjuntos de dados
restaurants
e neighborhoods
, ambos disponíveis publicamente em nossa documentação. Ambos são arquivosJSON
que contêm uma quantidade significativa de dados de restaurantes e bairros de Nova York já no formato GeoJSON!Esses arquivos são diferentes do conjunto de dados
sample_restaurants
que pode ser carregado no Atlas! Embora os nomes das coleções sejam os mesmos, os arquivos JSON que estou pedindo para você baixar já têm dados no formato GeoJSON, que serão necessários para este tutorial.} Quando você chegar à Etapa 5 de importação de dados para o cluster (Executar mongoimport), certifique-se de acompanhar os nomes
database
e collection
que você passa para o comando. Vamos precisar deles mais tarde! Se você quiser usar os mesmos nomes que neste tutorial, meu banco de dados se chama sample-geo
, e minhas collections se chamam restaurants
e neighborhoods
.Por último, para trabalhar com dados geoespaciais, um índice2dsphere precisa ser criado para cada collection. Você pode fazer isso no portal do MongoDB Atlas.
Primeiro, navegue até seu cluster e clique em "Browse Collections":
Você será direcionado para a sua lista de coleções. Encontre os dados do seu restaurante (se estiver acompanhando, será uma coleção chamada
restaurants
dentro do banco de dadossample-geo
).Com a coleção selecionada, clique na aba "Índices":
Clique no botão "CREATE INDEX" para abrir o assistente de criação de índice. Na seção "Campos", você especificará em qual campo criar um índice, bem como em qual tipo de índice. Para nosso tutorial, limpe a entrada, copie e cole o seguinte:
1 { "location": "2dsphere" }
Clique em "Revisar". Você será solicitado a confirmar a criação de um índice em
sample-geo.restaurants
no campo { "location": "2dsphere" }
(lembre-se, se você não estiver usando os mesmos nomes de banco de dados e coleção, confirme se o índice está sendo criado em yourDatabaseName.yourCollectionName
). Clique em "Confirmar".Da mesma forma, encontre os dados da sua vizinhança (
sample-geo.neighborhoods
, a menos que você tenha usado nomes diferentes). Selecione sua coleçãoneighborhoods
e faça a mesma coisa, desta vez criando este índice:1 { "geometry": "2dsphere" }
Quase instantaneamente, os índices serão criados. Você saberá que o índice foi criado com êxito depois de vê-lo listado na guia Índices da coleção selecionada.
Agora, você está pronto para trabalhar com os dados do seu restaurante e bairro!
Para mostrar essas amostras, trabalharemos dentro do contexto de um programa de console simples. Implementaremos cada operador de query geoespacial como seu próprio método e registraremos a query MongoDB correspondente que ele executa.
Após criar um novo projeto de console, adicione o driver MongoDB ao seu projeto usando o Gerenciador de pacotes ou o .NET CLI:
Gerente de pacotes
1 Install-Package MongoDB.Driver
.NET CLI
1 dotnet add package MongoDB.Driver
Em seguida, adicione as seguintes dependências ao seu
Program.cs
arquivo :1 using MongoDB.Bson; 2 using MongoDB.Bson.IO; 3 using MongoDB.Bson.Serialization; 4 using MongoDB.Driver; 5 using MongoDB.Driver.GeoJsonObjectModel; 6 using System;
Para todos os nossos exemplos, usaremos as seguintes classes
Restaurant
e Neighborhood
como nossos modelos:1 public class Restaurant 2 { 3 public ObjectId Id { get; set; } 4 public GeoJsonPoint<GeoJson2DCoordinates> Location { get; set; } 5 public string Name { get; set; } 6 }
1 public class Neighborhood 2 { 3 public ObjectId Id { get; set; } 4 public GeoJsonPoint<GeoJson2DCoordinates> Geometry { get; set; } 5 public string Name { get; set; } 6 }
Adicione ambos ao seu aplicativo. Para simplificar, adicionei-os como classes adicionais no meu arquivo
Program.cs
.Em seguida, precisamos nos conectar ao nosso cluster. Coloque o seguinte código dentro do método
Main
do seu programa:1 // Be sure to update yourUsername, yourPassword, yourClusterName, and yourProjectId to your own! 2 // Similarly, also update "sample-geo", "restaurants", and "neighborhoods" to whatever you've named your database and collections. 3 var client = new MongoClient("mongodb+srv://yourUsername:yourPassword@yourClusterName.yourProjectId.mongodb.net/sample-geo?retryWrites=true&w=majority"); 4 var database = client.GetDatabase("sample-geo"); 5 var restaurantCollection = database.GetCollection<Restaurant>("restaurants"); 6 var neighborhoodCollection = database.GetCollection<Neighborhood>("neighborhoods");
Finalmente, adicionaremos um método auxiliar chamado
Log()
dentro de nossa classeProgram
. Isto pegará as queries geoespaciais que escrevemos em C# e registrará a Query MongoDB correspondente no console. Isso nos dá uma maneira fácil de copiá-lo e usá-lo em outro lugar.1 private static void Log(string exampleName, FilterDefinition<Restaurant> filter) 2 { 3 var serializerRegistry = BsonSerializer.SerializerRegistry; 4 var documentSerializer = serializerRegistry.GetSerializer<Restaurant>(); 5 var rendered = filter.Render(documentSerializer, serializerRegistry); 6 Console.WriteLine($"{exampleName} example:"); 7 Console.WriteLine(rendered.ToJson(new JsonWriterSettings { Indent = true })); 8 Console.WriteLine(); 9 }
Agora temos nossa estrutura montada. Agora podemos criar os métodos de consulta geoespacial!
Como o MongoDB tem operadores dedicados para queries geoespaciais, podemos aproveitar o construtor de definição de filtrodo driver C# para construir queries seguras contra erros de digitação. O uso do construtor de definições de filtro também oferece segurança em tempo de compilação e suporte à refatoração no Visual Studio, o que o torna uma ótima maneira de trabalhar com queries geoespaciais.
O filtro
.Near
implementa o operador de query geoespacial$near . Utilize isto quando você deseja retornar objetos geoespaciais que estão próximos a um ponto central, com resultados classificados do mais próximo para o mais distante.Em nosso programa, vamos criar um método
NearExample()
que faça isso. Vamos procurar restaurantes que estejam ano máximo 10,000 metros de distância e a pelo menos 2,000 metros de distância de uma Magnolia Bakery (na Bleecker Street) em Nova York:1 private static void NearExample(IMongoCollection<Restaurant> collection) 2 { 3 // Instantiate builder 4 var builder = Builders<Restaurant>.Filter; 5 6 // Set center point to Magnolia Bakery on Bleecker Street 7 var point = GeoJson.Point(GeoJson.Position(-74.005, 40.7358879)); 8 9 // Create geospatial query that searches for restaurants at most 10,000 meters away, 10 // and at least 2,000 meters away from Magnolia Bakery (AKA, our center point) 11 var filter = builder.Near(x => x.Location, point, maxDistance: 10000, minDistance: 2000); 12 13 // Log filter we've built to the console using our helper method 14 Log("$near", filter); 15 }
É isso ai! Sempre que chamamos esse método, uma query
$near
será gerada que você pode copiar e colar do console. Sinta-se livre para colar essa query no explorador de dados no Atlas para ver quais restaurantes correspondem ao filtro (não se lembre de alterar "Location"
para um"location"
em letra minúscula ao trabalhar no Atlas). Em uma publicação futura, abordaremos como visualizar esses resultados em um mapa!Por enquanto, você pode chamar esse método (e todos os outros métodos a seguir) a partir do método
Main
da seguinte forma:1 static void Main(string[] args) 2 { 3 var client = new MongoClient("mongodb+srv://yourUsername:yourPassword@yourClusterName.yourProjectId.mongodb.net/sample-geo?retryWrites=true&w=majority"); 4 var database = client.GetDatabase("sample-geo"); 5 var restaurantCollection = database.GetCollection<Restaurant>("restaurants"); 6 var neighborhoodCollection = database.GetCollection<Neighborhood>("neighborhoods"); 7 8 NearExample(restaurantCollection); 9 // Add other methods here as you create them 10 }
Sinta-se à vontade para modificar este código! Altere o ponto central mudando as coordenadas ou deixe que o método aceite variáveis para os parâmetros
point
, maxDistance
e minDistance
em vez de codificá-los.Na maioria dos casos de uso,
.Near
resolverá o problema. Ele mede distâncias contra um plano plano 2d (plano euclidiano) que será preciso para a maioria das aplicações. No entanto, se você precisar que as consultas sejam executadas em geometria esférica 3d ao medir distâncias, use o filtro.NearSphere
(que implementa o operador$nearSphere
). Ele aceita os mesmos parâmetros que.Near
, mas calculará distâncias usando geometria esférica.O filtro
.GeoWithin
implementa o operador de query geoespacial$geoWithin . Utilize isto quando você deseja retornar objetos geoespaciais que existem inteiramente dentro de uma forma especificada, seja um legacy coordinate pairs Polygon
, MultiPolygon
ou forma definida por GeoJSON. Como você verá em um exemplo posterior, esta forma pode ser um círculo e pode ser gerada utilizando o operador$center
.Para implementar isso em nosso programa, vamos criar um método
GeoWithinExample()
que procure restaurantes em uma área, especificamente nesta área:No código, descrevemos esta área como um polígono e usamos ele como uma lista de pontos:
1 private static void GeoWithinExample(IMongoCollection<Restaurant> collection) 2 { 3 var builder = Builders<Restaurant>.Filter; 4 5 // Build polygon area to search within. 6 // This must always begin and end with the same coordinate 7 // to "close" the polygon and fully surround the area. 8 var coordinates = new GeoJson2DCoordinates[] 9 { 10 GeoJson.Position(-74.0011869, 40.752482), 11 GeoJson.Position(-74.007384, 40.743641), 12 GeoJson.Position(-74.001856, 40.725631), 13 GeoJson.Position(-73.978511, 40.726793), 14 GeoJson.Position(-73.974408, 40.755243), 15 GeoJson.Position(-73.981669, 40.766716), 16 GeoJson.Position(-73.998423, 40.763535), 17 GeoJson.Position(-74.0011869, 40.752482), 18 }; 19 var polygon = GeoJson.Polygon(coordinates); 20 21 // Create geospatial query that searches for restaurants that fully fall within the polygon. 22 var filter = builder.GeoWithin(x => x.Location, polygon); 23 24 // Log the filter we've built to the console using our helper method. 25 Log("$geoWithin", filter); 26 }
O filtro
.GeoIntersects
implementa o operador de query geoespacial$geoIntersects . Utilize isto quando você deseja retornar objetos geoespaciais que abrangem a mesma área que um objeto especificado, geralmente um ponto.Para o nosso programa, vamos criar um método
GeoIntersectsExample()
que verifica se um ponto especificado está dentro de um dos bairros armazenados em nossa coleção de bairros:1 private static void GeoIntersectsExample(IMongoCollection<Neighborhood> collection) 2 { 3 var builder = Builders<Neighborhood>.Filter; 4 5 // Set specified point. For example, the location of a user (with granted permission) 6 var point = GeoJson.Point(GeoJson.Position(-73.996284, 40.720083)); 7 8 // Create geospatial query that searches for neighborhoods that intersect with specified point. 9 // In other words, return results where the intersection of a neighborhood and the specified point is non-empty. 10 var filter = builder.GeoIntersects(x => x.Geometry, point); 11 12 // Log the filter we've built to the console using our helper method. 13 Log("$geoIntersects", filter); 14 }
💡 Para este método, um método
Log()
sobrecarregado que aceite um FilterDefinition
do tipo Neighborhood
precisa ser criado.Como vimos, o
$geoWithin
operador retorna objetos geoespaciais que existem inteiramente dentro de uma forma especificada. Podemos definir essa forma como um círculo usando o $center
operador .Vamos criar um método
GeoWithinCenterExample()
em nosso programa. Este método procurará todos os restaurantes existentes dentro de um círculo que centralizamos na ponte do Brooklyn:1 private static void GeoWithinCenterExample(IMongoCollection<Restaurant> collection) 2 { 3 var builder = Builders<Restaurant>.Filter; 4 5 // Set center point to Brooklyn Bridge 6 var point = GeoJson.Point(GeoJson.Position(-73.99631, 40.705396)); 7 8 // Create geospatial query that searches for restaurants that fall within a radius of 20 (units used by the coordinate system) 9 var filter = builder.GeoWithinCenter(x => x.Location, point.Coordinates.X, point.Coordinates.Y, 20); 10 Log("$geoWithin.$center", filter); 11 }
Outra maneira de fazer query de locais é combinando os operadores de query geoespacial
$geoWithin
e $centerSphere
. Ele difere do operador$center
de algumas maneiras:$centerSphere
usa a geometria esférica, enquanto$center
usa a geometria plana para os cálculos.$centerSphere
funciona com objetos GeoJSON e legacy coordinate pairs, enquanto$center
só funciona e retorna legacy coordinate pairs.$centerSphere
usa radianos para distância, o que requer cálculos adicionais para produzir uma consulta precisa.$center
usa as unidades usadas pelo sistema de coordenadas e pode ser menos preciso para algumas consultas.
Veremos nosso método de exemplo em um momento, mas primeiro, um pequeno contexto de como calcular radianos para geometria esférica!
💡 Uma coisa importante sobre trabalhar com
$centerSphere
(e quaisquer outros operadores geoespaciais que usam geometria esférica) é que ele usa radianos para distância. Isso significa que as unidades de distância usadas nas consultas (milhas ou quilômetros) primeiro precisam ser convertidas em radianos. O uso de radianos considera corretamente a natureza esférica do objeto que estamos medindo (geralmente a Terra) e permite que o operador$centerSphere
calcule as distâncias corretamente.Use esta tabela útil para converter entre distâncias e radianos:
Conversão | Descrição | Exemplo de cálculo |
---|---|---|
Distância (milhas) para radianos | Divida a distância pelo raio da esfera (por exemplo, a Terra) em milhas. O raio equitorial da Terra em milhas é aproximadamente 3,963.2 . | Pesquisar objetos com um raio de 100 milhas: 100 / 3963.2 |
Distância (quilômetros) para radianos | Divida a distância pelo raio da esfera (por exemplo, a Terra) em quilômetros. O raio equatorial da Terra em quilômetros é de aproximadamente 6,378.1 . | Pesquise objetos com raio de 100 quilômetros: 100 / 6378.1 |
radianos para distância (milhas) | Multiplique a medida em radianos pelo raio da esfera (por exemplo, a Terra). O raio equitorial da Terra em milhas é aproximadamente 3,963.2 . | Encontre a medida em radianos de 50 em milhas: 50 * 3963.2 |
radianos para distância (quilômetros) | Multiplique a medida do radiano pelo raio da esfera (por exemplo, a Terra). O raio equitorial da Terra em quilômetros é de aproximadamente 6,378.1 . | Encontre a medida em radianos de 50 em quilômetros: 50 * 6378.1 |
Para o nosso programa, vamos criar um
GeoWithinCenterSphereExample()
que busca todos os restaurantes em um raio de três milhas do Apollo Theater no Harlem:1 private static void GeoWithinCenterSphereExample(IMongoCollection<Restaurant> collection) 2 { 3 var builder = Builders<Restaurant>.Filter; 4 5 // Set center point to Apollo Theater in Harlem 6 var point = GeoJson.Point(GeoJson.Position(-73.949995, 40.81009)); 7 8 // Create geospatial query that searches for restaurants that fall within a 3-mile radius of Apollo Theater. 9 // Notice how we pass our 3-mile radius parameter as radians (3 / 3963.2). This ensures accurate calculations with the $centerSphere operator. 10 var filter = builder.GeoWithinCenterSphere(x => x.Location, point.Coordinates.X, point.Coordinates.Y, 3 / 3963.2); 11 12 // Log the filter we've built to the console using our helper method. 13 Log("$geoWithin.$centerSphere", filter); 14 }
Como vimos, trabalhar com queries geoespaciais do MongoDB em C# é possível por meio de seu suporte para os operadores de consulta geoespacial. Em outro tutorial, veremos como visualizar nossos resultados de consulta geoespacial em um mapa!
Se você tiver alguma dúvida ou estiver preso, não hesite em publicar em nossos MongoDB Community! E se você encontrou este tutorial útil, não se lembre de avaliá-lo e deixar qualquer feedback. Isso nos ajuda a melhorar nossos artigos para que eles sejam incríveis para todos!