meface/docs/article/db/mongodb_2dindexes.md

578 lines
20 KiB
Markdown
Raw Normal View History

2023-11-17 10:54:23 +08:00
---
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/