--- title: MongoDB中的空间索引 date: 2021-01-21 author: ac tags: - MongoDB - geojson categories: - Database --- > 为了支持对地理空间坐标数据的高效查询,MongoDB提供了两个特殊的索引:2d索引(返回结果时使用平面几何)和2dsphere索引(返回结果时使用球面几何)。 ### 1. `MongoDB`中的地理空间数据 在`MongoDB`中,用文档记录地球球体(地理坐标)上的位置信息,可以将数据存储为`GeoJSON`对象,如果用文档记录几何平面(投影坐标)上的位置信息,可以将数据存储为`legacy coordinate pairs`传统坐标对。 > `mongodb`对地理坐标的`GeoJSON`对象进行的空间查询操作,使用的空间参考是`WGS84`。 #### 1.1 [GeoJSON](https://tools.ietf.org/html/rfc7946#section-3.1)对象 `GeoJSON`是一种基于`JSON`格式的地理空间数据交换格式。它定义了几种类型的`JSON`对象,通过这些`JSON`对象或其组合来表示地理空间数据的特征、性质和空间范围等信息。 `GeoJSON`默认使用的是地理坐标参考系统(`WGS-84`),单位是十进制的度。 一个`GeoJSON`对象可以是[SFSQL](https://www.ogc.org/standards/sfs)规范中定义的七种几何类型(`Point`、`MultiPoint`、`LineString`、`MultiLineString`,`Polygon`,`MultiPolygon`,`GeometryCollection`)。 `GeoJSON`表示的这些几何类型与`WKT`和`WKB`的很相似。 ```json //WKT:Point(102.0, 0.5)对应下面的geojson { "type": "Point", "coordinates": [102.0, 0.5] } //WKT:LineString(102.0 0.0,103.0 1.0,104.0 0.0) { "type":"LineString", "coordinates": [ [102.0, 0.0], [103.0, 1.0], [104.0, 0.0], ] } /* * WKT: Polygon( (100.0 0.0,101.0 0.0,101.0 1.0,100.0 1.0,100.0 0.0) ) */ { "type": "Polygon", "coordinates": [ [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0] ] ] } /** *WKT: MultiPoint((100.0 0.0),(101.0 1.0)) */ { "type": "MultiPoint", "coordinates": [ [100.0, 0.0], [101.0, 1.0] ] } /** *WKT: MultiLineString((100.0 0.0,101.0 1.0),(102.0 2.0,103.0 3.0)) */ { "type": "MultiLineString", "coordinates": [ [ [100.0, 0.0], [101.0, 1.0] ], [ [102.0, 2.0], [103.0, 3.0] ] ] } /**WTK: *MultiPolygon( ((102.0 2.0,103.0 2.0,103.0 3.0,102.0 3.0,102.0 2.0)), ( (100.0 0.0,101.0 0.0,100.1 1.0,100.0 1.0,100.0 0.0), (100.2 0.2,100.2 0.8,100.8 0.8,100.8 0.2,100.2 0.2) ) ) */ { "type": "MultiPolygon", "coordinates": [ [ [ [102.0, 2.0], [103.0, 2.0], [103.0, 3.0], [102.0, 3.0], [102.0, 2.0] ] ], [ [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0] ], [ [100.2, 0.2], [100.2, 0.8], [100.8, 0.8], [100.8, 0.2], [100.2, 0.2] ] ] ] } /**WKT: *GeometryCollection( POINT(100.0 0.0), LINESTRING(101.0 0.0,102.0 1.0) ) */ { "type": "GeometryCollection", "geometries": [{ "type": "Point", "coordinates": [100.0, 0.0] }, { "type": "LineString", "coordinates": [ [101.0, 0.0], [102.0, 1.0] ] }] } ``` 在`SFSQL`中,`coordinate`是一`n`个数字组成的数组,用来表示`n`维空间下点`Point`的位置信息。`Geometry Object`都有一个`coordinates`属性来表示几何体中的点位信息。 需要注意的是`Polygon`类型的`GeoJSON`对象,`Polygon`是由`Linearing`数组构成,第一个`Linearing`是面的外边界(`Exterior boundary`),其余的为面内的“洞”(`Interior boundary`),且不能相交或重叠,也不能共享边界。 ![image-20210201160139172](./images/image-20210201160139172.png) > `LinearRing`是一段封闭的分段的线状路径,(coordinates 的成员数组)至少4个坐标点,三个坐标可以确定 `LinearRing`,第四个坐标用于闭合,与第一个坐标相同。 > > 一个`LinearRing`必须遵循右手定则,**外边界是逆时针的,而“洞”是顺时针方向**。 另外,`GeoJSON`的类型还包括`Feature`和`FeatureCollection`两种。 `Feature`类型的`GeoJSON`对象必须包含一个`geometry`属性,且值为上述几何类型中的一种,及其它属性`perproties`。`FeatureCollection`包含一个`Feature`数组对象。 ``` Feature ↙ ↘ Geometry properties ↙ ↓ ↘ Point Polyline Polygon MultiPoint MultiLineString MultiPolygon GeometryCollection ``` 示例: `Feature`类型的`GeoJSON`: ```json { "type": "Feature", "properties": { "name": "测试点" }, "geometry": { "type": "Point", "coordinates": [ 113.95560264587402, 22.51267588902413 ] } } ``` `FeatureCollection`类型的`GeoJSON`: ```json { "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": { "name": "测试线" }, "geometry": { "type": "LineString", "coordinates": [ [ 113.96212577819824, 22.515649230084094 ], [ 113.96092414855956, 22.49241582330295 ] ] } }, { "type": "Feature", "properties": { "name": "测试点" }, "geometry": { "type": "Point", "coordinates": [ 113.95560264587402, 22.51267588902413 ] } } ] ``` `GeoJSON`的类型是**【不可扩展】**的,只有固定的上面简述的九种。其中`FeatureCollection`是最常用的一种,像`WFS`服务,将响应格式设为`application/json`时,服务就会返回一个`FeatureCollection`类型的`GeoJSON`对象。 > > 注意:"geometry type" 的值是大小写敏感的。 > `MongoDB`中支持的`GeoJSON`对象类型只有上述简述的[SFSQL](https://www.ogc.org/standards/sfs)规范中的七种`geometry type`。文档存储`GeoJSON`对象数据,通常是作为属性值嵌入到文档中,格式如下: ```json :{ type:, coordinates: } ``` - 必须有一个`type`属性,且值为`GeoJSON object type` - 必须有一个`coordinates`属性,用于表示几何对象的点位信息。 如果`coordinates`是经纬度的地理坐标,则其有效的经度值在-180到180之间,两者都包括在内,有效的纬度在-90到90之间。 #### 1.2 Legacy Coordinate Pairs 对于平面坐标,建议是存储成`Legacy Coordinate Pairs`坐标对,可以使用`2d`索引。 存储坐标对数据可以使用数组或嵌入文档的形式: ```json //数组(优先考虑) : [, ] 或 : [, ] ``` ```json //嵌入文档 : { : , : } 或 : { : , : } ``` 从上面的结构可以看出,坐标对的形式其实只适合存储`Point`类型的数据。 ### 2. 地理空间索引 **地理空间索引(`Geospatial Index`)** 为了支持对地理空间坐标数据的有效查询,`MongoDB`提供了两种特殊的索引:使用平面几何数据(投影坐标)的**二维索引(2d indexes)**和使用球面几何(地理坐标)数据的**二维球面索引(2dsphere indexes)**。 #### 2.1 `2dsphere indexes` `2dsphere`索引支持在地球球体上的几何计算和支持所有`MongoDB`地理空间查询(包含,相交和接近等)。 `2dsphere`索引支持存储为`GeoJSON`对象和传统坐标对的数据。但对于传统坐标对,索引需要将数据转换为`GeoJSON`中的点类型。 **创建`2dsphere`索引** 创建一个`2dsphere`索引,可以使用`db.collection.createIndex()`方法,指定索引类型为`2dsphere`: ```shell //单字段索引 db.collection.createIndex({: "2dsphere"}) ``` 其中的``字段的值应为`GeoJSON`对象或`legacy coordinates pair`传统坐标对。如果在`2dsphere`索引字段中插入带有非几何数据的文档,或者在集合中的非几何数据的字段上构建`2dsphere`索引,则会操作失败(索引字段的限制)。 创建包含`2dsphere`索引的复合索引,可以包含多个位置信息的几何字段和非地理空间信息的字段: ```shell //复合索引 db.sphere.createIndex({"location":"2dsphere","name":1}) ``` #### 2.2 `2d indexes` `2d indexes`支持平面几何上的计算和查询,虽然该索引支持通过`$nearSphere`查询球面上的几何计算,但对于球面上的计算,还是尽可能的使用`2dsphere`索引。 **创建`2d`索引** 创建一个`2d`索引,可以使用`db.collection.ceateIndex()`方法,指定索引类型为`2d`: ```shell db.collection.createIndex( { : "2d" } ) ``` 索引字段`location field`的值必须是`legacy coordinates pair` ### 3. 空间查询 > `MongoDB`中的空间查询是基于空间索引基础之上的,所以进行空间查询前,因先创建地理空间索引。 #### 3.1 查询操作 `MongoDB`提供以下空间查询操作: | name | Description | | ---------------- | ------------------------------------------------------------ | | `$geoIntersects` | 查询几何对象与指定的`GeoJSON`对象相交的文档。`2dsphere` 索引支持该操作。 | | `$geoWithin` | 查询几何对象在指定的`GeoJSON`对象边界内的文档。`2dsphere`和`2d`索引都支持该操作。 | | `$near` | 返回几何对象在指定点附近的文档。`2dsphere`和`2d`索引都支持该操作。 | | `$nearSphere` | 返回球体上某点附近的地理空间对象文档。`2dsphere`和`2d`索引都支持该操作。 | #### 3.2 几何操作符 | name | Description | format | | --------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | `$box` | 在`$geoWithin`操作中使用传统坐标对(`legacy coordinate pairs`)指定矩形,只有`2d index`支持。 | { \: { $geoWithin: { $box: [ [ \ ], [ \ ] ] } } } | | `$center` | 在`$geoWithin`操作中使用传统坐标对(`legacy coordinate pairs`)指定圆形,只有`2d index`支持。 | { \: { $geoWithin: { $center: [ [ \,\ ] , \ ] } } } | | `$centerSphere` | 当使用球面几何的地理空间查询时,在`$geoWithin`操作中使用传统坐标对或`GeoJSON`对象,`2d `和`2dsphere`都支持 | { \: { $geoWithin: { $centerSphere: [ [\, \ ], \ ] } } } | | `$geometry` | 用于在空间查询操作中使用`GeoJSON`对象指定输入的几何对象。`2d `和`2dsphere`都支持。 | $geometry: { type: "\", coordinates: [ \ ] } | | `$maxDistance` | 用于过滤`$near`和`$nearSphere`查询结果,指定最大距离。单位由坐标系决定(对于`GeoJSON`的点对象使用米为单位)。`2d `和`2dsphere`都支持。 | db.places.find( { loc: { $near: [ -74 , 40 ], $maxDistance: 10 } } ) | | `$minDistance` | 用于过滤`$near`和`$nearSphere`操作的查询结果,限定结果文档中的几何对象到中心点的最小距离。`2d `和`2dsphere`索引都支持。 | db.places.find( { location: { $nearSphere: { $geometry: { type : "Point", coordinates : [ -73.9667, 40.78 ] }, $minDistance: 1000, $maxDistance: 5000 }} } ) | | `$polygon` | 为`$geoWithin`查询指定一个使用传统坐标对的多边形。只有`2d`索引支持该操作。 | db.places.find( { loc: { $geoWithin: { $polygon: [ [ 0 , 0 ], [ 3 , 6 ], [ 6 , 0 ] ] } } } ) | | `$uniqueDocs` | 地理空间查询不返回重复的结果。从2.6开始就被废弃了,`$uniqueDocs`操作符对结果没有影响。 | | `$geoIntersects`操作使用`$geometry`指定`GeoJSON`对象 ```shell { : { $geoIntersects: { $geometry: { type: "" , coordinates: [ ] } } } } ``` `$geoWithin`操作也是使用`$geometry`指定一个`Polygon`或`MultiPolygon`类型的`GeoJSON`对象作为输入: ```json { : { $geoWithin: { $geometry: { type: <"Polygon" or "MultiPolygon"> , coordinates: [ ] } } } } ``` `$near`操作与`$maxDistance`和`$minDistance`操作符一起使用,返回以指定点为中心点,在限定距离范围内的文档。`$near`操作的输入可以是`GeoJSON`格式的数据也可以是坐标对的数据,对空间索引的要求有区别: - 对`GeoJSON`类型的点,需要使用`2dsphere`索引 - 对坐标对格式的点数据,需要使用`2d`索引 ```json //GeoJSON Point,unit is meters { : { $near: { $geometry: { type: "Point" , coordinates: [ , ] }, $maxDistance: , $minDistance: } } } // legacy coordinates,unit is radians { $near: [ , ], $maxDistance: } ``` `$nearSphere`操作是针对地理坐标进行计算的,返回指定球面上距离中心点在某段范围内的文档。当然,地理坐标您可以存储为`GeoJSON`的格式,也可以存储为传统坐标对的形式。 - 当文档中的几何数据格式是`GeoJSON`时,建议使用`GeoJSON`类型的点作为输入,且使用`2dsphere`索引; - 当文档中的位置信息格式是传统坐标对时,使用传统坐标对作为输入,且使用`2d`索引。其实`$nearSphere`操作也可以在`GeoJSON`格式的数据上使用`2d`索引。 ```json //GeoJSON 格式输入,单位为米 { $nearSphere: { $geometry: { type : "Point", coordinates : [ , ] }, $minDistance: , $maxDistance: } } //传统坐标对格式输入,单位为弧度 { $nearSphere: [ , ], $minDistance: , $maxDistance: } ``` #### 3.3 实例 ##### 数据准备 ![image-20210126112951124](./images/image-20210126112951124.png) ```shell > use geodata switched to db geodata > db.sphere.insert( {"name":"测试点","location":{"type": "Point","coordinates": [113.92024040222168,22.548708470991805]}}) > db.sphere.insert( {"name":"线段1","location":{"type": "LineString","coordinates": [[113.92993927001953,22.535707699328004],[113.91483306884766,22.504310546471817]]}}) > db.sphere.insert( {"name":"线段2","location":{"type": "LineString","coordinates": [[113.92204284667969,22.572487200676317],[113.99070739746094,22.518265717308317]]}}) > db.sphere.insert( {"name": "线段3","location":{"type": "LineString","coordinates": [[113.97457122802734,22.562976200808055],[113.9725112915039,22.484644051870895]]}}) > db.sphere.insert( {"name": "面1","location":{"type": "Polygon","coordinates": [[[113.89663696289062,22.581997544284242],[113.89869689941406,22.53507348402533],[113.90865325927733,22.491940013104305],[113.95397186279297,22.554098675696263],[113.89663696289062,22.581997544284242]]]}}) ``` ##### 创建`2dsphere`索引 ```shell > db.sphere.createIndex({"location":"2dsphere"}) { "createdCollectionAutomatically" : false, "numIndexesBefore" : 1, "numIndexesAfter" : 2, "ok" : 1 } > db.sphere.getIndexes() [ { "v" : 2, "key" : { "_id" : 1 }, "name" : "_id_" }, { "v" : 2, "key" : { "location" : "2dsphere" }, "name" : "location_2dsphere", "2dsphereIndexVersion" : 3 } ] ``` ##### 空间查询操作 使用线段2作为输入,执行`geoIntersects`操作: ```shell > db.sphere.find({location:{ ... $geoIntersects: { ... $geometry: { ... "type": "LineString", ... "coordinates": [ ... [113.92204284667969,22.572487200676317], ... [113.99070739746094,22.518265717308317] ... ] ... } ... } ... }}) { "_id" : ObjectId("600f9a8d264f9b09033f1fe7"), "name" : "线段2", "location" : { "type" : "LineString", "coordinates" : [ [ 113.92204284667969, 22.572487200676317 ], [ 113.99070739746094, 22.518265717308317 ] ] } } { "_id" : ObjectId("600f9a9f264f9b09033f1fe9"), "name" : "面1", "location" : { "type" : "Polygon", "coordinates" : [ [ [ 113.89663696289062, 22.581997544284242 ], [ 113.89869689941406, 22.53507348402533 ], [ 113.90865325927733, 22.491940013104305 ], [ 113.95397186279297, 22.554098675696263 ], [ 113.89663696289062, 22.581997544284242 ] ] ] } } { "_id" : ObjectId("600f9a98264f9b09033f1fe8"), "name" : "线段3", "location" : { "type" : "LineString", "coordinates" : [ [ 113.97457122802734, 22.562976200808055 ], [ 113.9725112915039, 22.484644051870895 ] ] } } > ``` 使用面1作为输入,执行`$geoWithin`操作 ```shell > db.sphere.find({location:{$geoWithin: {$geometry: {"type": "Polygon","coordinates": [[[113.89663696289062,22.581997544284242],[113.89869689941406,22.53507348402533],[113.90865325927733,22.491940013104305],[113.95397186279297,22.554098675696263],[113.89663696289062,22.581997544284242]]]}}}}) { "_id" : ObjectId("600f9a9f264f9b09033f1fe9"), "name" : "面1", "location" : { "type" : "Polygon", "coordinates" : [ [ [ 113.89663696289062, 22.581997544284242 ], [ 113.89869689941406, 22.53507348402533 ], [ 113.90865325927733, 22.491940013104305 ], [ 113.95397186279297, 22.554098675696263 ], [ 113.89663696289062, 22.581997544284242 ] ] ] } } { "_id" : ObjectId("600f9a0c264f9b09033f1fe6"), "name" : "线段1", "location" : { "type" : "LineString", "coordinates" : [ [ 113.92993927001953, 22.535707699328004 ], [ 113.91483306884766, 22.504310546471817 ] ] } } { "_id" : ObjectId("600f92c5264f9b09033f1fe5"), "name" : "测试点", "location" : { "type" : "Point", "coordinates" : [ 113.92024040222168, 22.548708470991805 ] } } ``` 使用测试点作为输入,执行`$nearSphere`操作。结果会按距离中心点距离,由远到近进行排序。 ```shell db.sphere.find({ location:{ $nearSphere:{ $geometry:{ type:"Point", coordinates:[113.92024040222168,22.548708470991805] }, $minDistance: 100, $maxDistance: 10000 } } }) { "_id" : ObjectId("600f9a0c264f9b09033f1fe6"), "name" : "线段1", "location" : { "type" : "LineString", "coordinates" : [ [ 113.92993927001953, 22.535707699328004 ], [ 113.91483306884766, 22.504310546471817 ] ] } } { "_id" : ObjectId("600f9a8d264f9b09033f1fe7"), "name" : "线段2", "location" : { "type" : "LineString", "coordinates" : [ [ 113.92204284667969, 22.572487200676317 ], [ 113.99070739746094, 22.518265717308317 ] ] } } { "_id" : ObjectId("600f9a98264f9b09033f1fe8"), "name" : "线段3", "location" : { "type" : "LineString", "coordinates" : [ [ 113.97457122802734, 22.562976200808055 ], [ 113.9725112915039, 22.484644051870895 ] ] } } ``` ### 参考文章 [1] `2d Index Internals` https://docs.mongodb.com/manual/core/geospatial-indexes/ [2] `The GeoJSON Format` https://tools.ietf.org/html/rfc7946#section-3.1 [3] `GeoJSON Objects` https://docs.mongodb.com/manual/reference/geojson/ [4] `Simple Feature Access - Part 2: SQL Option` https://www.ogc.org/standards/sfs [5] `Geospatial Query Operators` https://docs.mongodb.com/manual/reference/operator/query-geospatial/