Skip to main content

MongoDB地理索引初探

本文内容主要译自MongoDB官方文档。第一部分主要以一个简单的示例演示了如何使用MongoDB的2dsphere索引和相应的操作符来寻找附近的饭店,第二部分则主要介绍了GeoJSON对象的类型,及其代表的含义。第三部分是一点小小的总结。

 
00:00/00:00

一个简单的示例:寻找附近的饭店

概述

MongoDB的地理空间索引可以帮助我们在包含地理空间形状和点集的结合上高效地执行空间查询。本章节将简单介绍地理空间索引的概念,然后展示$geoWithin$geoIntersects 以及 $geoNear的使用。
为了展示地理空间功能的能力并且比较不同方法之间的区别,本章节将会指导大家如何写一系列为简单的地理空间应用的查询。

假定我们在设计一个移动应用来帮助用户寻找纽约的饭店。该应用必须:

  • 使用 $geoIntersects 确定用户当前的邻近区域
  • 使用 $geoWithin 显示在该邻近区域的饭店数目
  • 使用 $nearSphere 找到距离用户一定距离范围内的饭店
    本章节将使用 2dsphere 索引来查询球面几何上的数据。

平面和球面集合的差异

地理空间查询既可以使用平面几何,也可以使用球面几何,根据使用的查询和索引类型来决定。 2dsphere 索引只能支持球面几何,而 2d索引同时支持平面和球面几何。然而,在 2dsphere索引上使用球面几何的查询将会更高效和准确,因此我们应该在地理空间字段上使用 2dsphere索引。

下面的表格展示了每个地理空间操作符将会使用什么类型的几何:

查询类型 几何类型 备注
$near(GeoJSON点,2dsphere索引) 球面
$near(传统坐标,2d索引) 平面
$nearSphere(GeoJSON点,2dsphere索引) 球面
$nearSphere(传统坐标,2d索引) 球面 使用GeoJSON点替换
$geoWithin:{$geometry:...} 球面
$geoWithin:{$box:...} 平面
$geoWithin:{$polygon:...} 平面
$geoWithin:{$center:...} 平面
$geoWithin:{$centerSphere:...} 球面
$geoIntersects 球面

$geoNear命令和 $geoNear聚合操作符在使用传统坐标时会以弧度进行操作,在使用GeoJSON点的时候则以米为单位进行操作。

变形

在一个地图上进行可视化时,由于映射三维空间的本质,球面几何将会变形,例如将地球映射到一个平面上。例如,以经纬度点(0,0),(80,0),(80,80),(0,80)为例,下图展示了这个区域覆盖的范围:

 

查找饭店

准备活动

分别下载示例数据集:neighborhoodsrestaurant
wget https://raw.githubusercontent.com/mongodb/docs-assets/geospatial/neighborhoods.json
wget https://raw.githubusercontent.com/mongodb/docs-assets/geospatial/restaurants.json

它们分别包括了邻居和饭店的集合。

在下载好数据集之后,将它们导入数据库:
mongoimport <path to restaurants.json> -d <database name> -c restaurants
mongoimport <path to neighborhoods.json> -d <database name> -c neighborhoods

geoNear命令需要在地理空间索引上执行,几乎每次都能提高 $geoWithin$geoIntersects查询的性能。

由于数据是地理相关的,使用mongo shell在每个集合上创建2dsphere索引:
db.restaurants.createIndex({ location: "2dsphere" })
db.neighborhoods.createIndex({ geometry: "2dsphere" })

创建成功后看到如下信息:
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}

查看数据

mongo shell中查看新创建的 restaurants集合中的一条数据:
db.restaurants.findOne()

查询可能会返回如下的文档:
{
location: {
type: "Point",
coordinates: [-73.856077, 40.848447]
},
name: "Morris Park Bake Shop"
}

该饭店文档对应下图中的位置:

由于本章节使用2dsphere索引,那么location字段中的地理数据必须符合GeoJSON格式。

接下来,查看neighborhoods集合上的一条数据:
db.neighborhoods.findOne()

这个查询将会返回如下类型的文档:
{
geometry: {
type: "Polygon",
coordinates: [[
[ -73.99, 40.75 ],
...
[ -73.98, 40.76 ],
[ -73.99, 40.75 ]
]]
},
name: "Hell's Kitchen"
}

这个几何对应下图中标记的区域:

 

找到当前的居民区

假设用户的移动设备能够提供一个用户非常精确的位置,那么使用 $geoIntersects 来找到用户当前所在的居民区就是非常容易的。

假设用户所在位置的经纬度为:-73.93414657,40.82302903。为了找到当前的居民区,我们可以以GeoJSON的格式使用特定的$geometry字段:
db.neighborhoods.findOne({ geometry: { $geoIntersects: { $geometry: { type: "Point", coordinates: [ -73.93414657, 40.82302903 ] } } } })

其中,geometry是一个包含地理坐标数据、需要匹配的字段,type是我们所需要匹配的对象类型,coordinates是我们所需要匹配的坐标。
该查询将会返回下面的结果:
{
"_id" : ObjectId("55cb9c666c522cafdb053a68"),
"geometry" : {
"type" : "Polygon",
"coordinates" : [
[
[
-73.93383000695911,
40.81949109558767
],
...
]
]
},
"name" : "Central Harlem North-Polo Grounds"
}

找到居民区所有的饭店

我们也可以查询所有包含给定居民区的饭店。在mongo shell中运行下面的脚本以找到包含该用户的区域,然后计算在该区域内的饭店数:

var neighborhood = db.neighborhoods.findOne( { geometry: { $geoIntersects: { $geometry: { type: "Point", coordinates: [ -73.93414657, 40.82302903 ] } } } } )

db.restaurants.find( { location: { $geoWithin: { $geometry: neighborhood.geometry } } } ).count()

其中,location是一个包含地理坐标数据、需要匹配的字段。该查询将会告诉你在请求的区域内有127个饭店,可视化如下图所示:

 

找到一定范围内的饭店

为了找到与某个点距离在一定范围内的饭店,我们可以使用 $geoWithin$centerSphere 来返回结果(未排序),或者如果需要根据距离远近的排序结果的话,可以结合使用 $maxDistancenearSphere来进行查询。

$geoWithin返回未排序结果

为了找到在一个圆形区域内的饭店,使用 $geoWithin$centerSphere 。其中, $centerSphere 是MongoDB特有的语法,通过指定中心点和弧度半径来表示圆形区域。

$geoWithin 不会以任何特定的顺序返回文档,因此它有可能会首先展示离用户最远的文档。

下面的命令将会返回离用户5公里以内的所有饭店:
db.restaurants.find({ location: { $geoWithin: { $centerSphere: [ [ -73.93414657, 40.82302903 ], 5 / 3963.2 ] } } })
其中,location是一个包含地理坐标数据、需要匹配的字段。 $centerSphere的第二个参数接收弧度制的半径,因此我们必须使用它来除以地球的半径(单位为公里)。查阅相关文档了解更多关于距离单位之间的换算。

使用 $nearSphere进行排序

你可以使用 $nearSphere$maxDistance 参数(单位为米)。下面的代码将会按照从近到远的顺序返回距离该用户5公里以内的所有饭店:
var METERS_PER_MILE = 1609.34
db.restaurants.find({ location: { $nearSphere: { $geometry: { type: "Point", coordinates: [ -73.93414657, 40.82302903 ] }, $maxDistance: 5 * METERS_PER_MILE } } })

其中,location是一个包含地理坐标数据、需要匹配的字段。

GeoJSON对象

概述

MongoDB支持以下类型的GeoJSON对象类型:

  • 线
  • 多边形
  • 多点
  • 多线
  • 多个多边形
  • 几何集合

要存储GeoJSON数据的话,在文档中使用 type字段来指定GeoJSON对象类型以及 coordinates 对象来指定对象的坐标:
{ type: "<GeoJSON type>" , coordinates: <coordinates> }
一般以经度、纬度的顺序来排列坐标。

MongoDB默认使用WGS84基准作为GeoJSON默认的坐标参考系统。

接下来依次介绍各种类型。

点(Point

示例如下:
{ type: "Point", coordinates: [ 40, 5 ] }

线(LineString

对于LineString类型, coordinate 成员必须是两个或多个坐标的数组。示例如下:
{ type: "LineString", coordinates: [ [ 40, 5 ], [ 41, 6 ] ] }

一个线性环由4个或更多坐标构成。第一个和最后一个坐标相等(它们表示相同的坐标)。尽管线性环没有被显式表示为一个GeoJSON几何类型,但是它在多边形几何类型定义中有提及到。

多边形(Polygon

多边形由一个GeoJSON线性环坐标数组的数组组成。这些线性环闭合的线段。闭合的线段至少有4个坐标对,并且第一个坐标和最后一个坐标相同。
一个弯曲平面上两个点组成的线不一定等同于在一个平面上这两个点确定的线。在弯曲平面上两个点连接的线是测地线。仔细检查点的坐标,以避免共边、重合以及其它交叉类型的错误。

单环多边形

下面的示例展示了一个外环的GeoJSON多边形,不包括内环(或者小洞)。第一个和最后一个坐标必须相同以关闭这个多边形。
{
type: "Polygon",
coordinates: [ [ [ 0 , 0 ] , [ 3 , 6 ] , [ 6 , 1 ] , [ 0 , 0 ] ] ]
}

对于单环的多边形,这个环不能自交叉。

多环多边形

对于多环多边形:

  • 第一个描述的环必须是外环
  • 外环不能自交叉
  • 所有内环必须全部包含在外环中
  • 内环之间不能交叉或覆盖。内环不能共边

下面的示例表示了包含一个内环的GeoJSON多边形:
{
type : "Polygon",
coordinates : [
[ [ 0 , 0 ] , [ 3 , 6 ] , [ 6 , 1 ] , [ 0 , 0 ] ],
[ [ 2 , 2 ] , [ 3 , 3 ] , [ 4 , 2 ] , [ 2 , 2 ] ]
]
}

多点(MultiPoint

GeoJSON多点嵌入文档包含一系列点。
{
type: "MultiPoint",
coordinates: [
[ -73.9580, 40.8003 ],
[ -73.9498, 40.7968 ],
[ -73.9737, 40.7648 ],
[ -73.9814, 40.7681 ]
]
}

多线(MultiLineString

示例如下:
{
type: "MultiLineString",
coordinates: [
[ [ -73.96943, 40.78519 ], [ -73.96082, 40.78095 ] ],
[ [ -73.96415, 40.79229 ], [ -73.95544, 40.78854 ] ],
[ [ -73.97162, 40.78205 ], [ -73.96374, 40.77715 ] ],
[ [ -73.97880, 40.77247 ], [ -73.97036, 40.76811 ] ]
]
}

多个多边形(MultiPolygon

示例如下:
{
type: "MultiPolygon",
coordinates: [
[ [ [ -73.958, 40.8003 ], [ -73.9498, 40.7968 ], [ -73.9737, 40.7648 ], [ -73.9814, 40.7681 ], [ -73.958, 40.8003 ] ] ],
[ [ [ -73.958, 40.8003 ], [ -73.9498, 40.7968 ], [ -73.9737, 40.7648 ], [ -73.958, 40.8003 ] ] ]
]
}

几何集合(GeometryCollection)

GeoJSON类型GeometryCollection的存储示例如下:
{
type: "GeometryCollection",
geometries: [
{
type: "MultiPoint",
coordinates: [
[ -73.9580, 40.8003 ],
[ -73.9498, 40.7968 ],
[ -73.9737, 40.7648 ],
[ -73.9814, 40.7681 ]
]
},
{
type: "MultiLineString",
coordinates: [
[ [ -73.96943, 40.78519 ], [ -73.96082, 40.78095 ] ],
[ [ -73.96415, 40.79229 ], [ -73.95544, 40.78854 ] ],
[ [ -73.97162, 40.78205 ], [ -73.96374, 40.77715 ] ],
[ [ -73.97880, 40.77247 ], [ -73.97036, 40.76811 ] ]
]
}
]
}

总结

本文只是直接将官方文档的示例直接运行了一遍,初步了解了一下MongoDB地理索引相关的一些操作符,$geoWithin$geoInsects以及$geoNear等操作符的使用。一些总结和反思如下:

  • 地理查询相关的功能已经挺完善了,可以查询到某个坐标点所在的几何对象、可以查询到某个坐标特定距离内的所有坐标或对象集合等等
  • 创建索引之后查询速度非常快,例如,在官方提供的示例数据集中所有操作的结果返回时间均在毫秒级别
  • 该功能的应用场景非常广泛,基本上所有的基于位置的服务都可以使用到(外卖、POI推荐、共享单车...)
  • 我看neighborhoods集合中数据类型基本上都是Polygon或者是Multipolygon,可是感觉现实生活中能够获得满足这两个对象要求的数据并不多,然后关于MultiPointMultiLineString类型与点的匹配,可能需要根据实际情况将点扩展到一个图形中来判断交叉
  • 下一步可以深入了解下地理索引的原理以及具体的匹配方法,从而对其查询的准确性和查询效率有进一步的认识

打赏
微信扫一扫支付
微信logo微信扫一扫, 打赏作者吧~