meface/docs/article/db/mongodb_2dindexes.md

578 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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
<field>:{
type:<GeoJSON type>,
coordinates:<coordinates>
}
```
- 必须有一个`type`属性,且值为`GeoJSON object type`
- 必须有一个`coordinates`属性,用于表示几何对象的点位信息。
如果`coordinates`是经纬度的地理坐标,则其有效的经度值在-180到180之间两者都包括在内有效的纬度在-90到90之间。
#### 1.2 Legacy Coordinate Pairs
对于平面坐标,建议是存储成`Legacy Coordinate Pairs`坐标对,可以使用`2d`索引。
存储坐标对数据可以使用数组或嵌入文档的形式:
```json
//数组(优先考虑)
<field>: [<x>, <y>]
<field>: [<longitude>, <latitude>]
```
```json
//嵌入文档
<field>: { <field1>: <x>, <field2>: <y> }
<field>: { <field1>: <longitude>, <field2>: <latitude> }
```
从上面的结构可以看出,坐标对的形式其实只适合存储`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({<location field>: "2dsphere"})
```
其中的`<location field>`字段的值应为`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( { <location field> : "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`支持。 | { \<location field>: { $geoWithin: { $box: [ [ \<bottom left coordinates> ], [ \<upper right coordinates> ] ] } } } |
| `$center` | 在`$geoWithin`操作中使用传统坐标对(`legacy coordinate pairs`)指定圆形,只有`2d index`支持。 | { \<location field>: { $geoWithin: { $center: [ [ \<x>,\<y> ] , \<radius> ] } } } |
| `$centerSphere` | 当使用球面几何的地理空间查询时,在`$geoWithin`操作中使用传统坐标对或`GeoJSON`对象,`2d `和`2dsphere`都支持 | { \<location field>: { $geoWithin: { $centerSphere: [ [\<x>, \<y> ], \<radius> ] } } } |
| `$geometry` | 用于在空间查询操作中使用`GeoJSON`对象指定输入的几何对象。`2d `和`2dsphere`都支持。 | $geometry: { type: "\<GeoJSON object type>", coordinates: [ \<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
{
<location field>: {
$geoIntersects: {
$geometry: {
type: "<GeoJSON object type>" ,
coordinates: [ <coordinates> ]
}
}
}
}
```
`$geoWithin`操作也是使用`$geometry`指定一个`Polygon`或`MultiPolygon`类型的`GeoJSON`对象作为输入:
```json
{
<location field>: {
$geoWithin: {
$geometry: {
type: <"Polygon" or "MultiPolygon"> ,
coordinates: [ <coordinates> ]
}
}
}
}
```
`$near`操作与`$maxDistance`和`$minDistance`操作符一起使用,返回以指定点为中心点,在限定距离范围内的文档。`$near`操作的输入可以是`GeoJSON`格式的数据也可以是坐标对的数据,对空间索引的要求有区别:
- 对`GeoJSON`类型的点,需要使用`2dsphere`索引
- 对坐标对格式的点数据,需要使用`2d`索引
```json
//GeoJSON Point,unit is meters
{
<location field>: {
$near: {
$geometry: {
type: "Point" ,
coordinates: [ <longitude> , <latitude> ]
},
$maxDistance: <distance in meters>,
$minDistance: <distance in meters>
}
}
}
// legacy coordinates,unit is radians
{
$near: [ <x>, <y> ],
$maxDistance: <distance in radians>
}
```
`$nearSphere`操作是针对地理坐标进行计算的,返回指定球面上距离中心点在某段范围内的文档。当然,地理坐标您可以存储为`GeoJSON`的格式,也可以存储为传统坐标对的形式。
- 当文档中的几何数据格式是`GeoJSON`时,建议使用`GeoJSON`类型的点作为输入,且使用`2dsphere`索引;
- 当文档中的位置信息格式是传统坐标对时,使用传统坐标对作为输入,且使用`2d`索引。其实`$nearSphere`操作也可以在`GeoJSON`格式的数据上使用`2d`索引。
```json
//GeoJSON 格式输入,单位为米
{
$nearSphere: {
$geometry: {
type : "Point",
coordinates : [ <longitude>, <latitude> ]
},
$minDistance: <distance in meters>,
$maxDistance: <distance in meters>
}
}
//传统坐标对格式输入,单位为弧度
{
$nearSphere: [ <x>, <y> ],
$minDistance: <distance in radians>,
$maxDistance: <distance in radians>
}
```
#### 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/