1. 概述

本教程中,我们将探索MongoDB对地理空间的支持。

我们将讨论如何在MongoDB中存储地理空间数据,以及如何索引和搜索位置。其中用到了neargeoWithingeoIntersects地理空间查询方法。

2. 存储地理空间数据

首先,我们来看下如何在MongoDB中存储地理位置信息。

MongoDB支持多种GeoJSON类型来存储位置信息。 本文中,我们将主要使用Point(点)和Polygon(多边形)类型。

2.1. Point 点

这是最基础和常用的GeoJSON类型,它用来表示网格上的一个特定点

例如,在places collection中,有一个location字段,用来保存我们的坐标点:

{
  "name": "Big Ben",
  "location": {
    "coordinates": [-0.1268194, 51.5007292],
    "type": "Point"
  }
}

注意第一个表示经度(longitude),第二个为纬度(latitude)。

2.2. Polygon 多边形

多边形是一个稍稍复杂点的GeoJSON类型。

我们可以使用多边形来定义一个具有外边界的区域,如果需要还可以包含内部孔。

例子:

{
  "name": "Hyde Park",
  "location": {
    "coordinates": [
      [
        [-0.159381, 51.513126],
        [-0.189615, 51.509928],
        [-0.187373, 51.502442],
        [-0.153019, 51.503464],
        [-0.159381, 51.513126]
      ]
    ],
    "type": "Polygon"
  }
}

上面例子中,我们定义了一个表示外边界点的数组。 注意最后一个点要等于第一个点,以形成封闭的区域。

请注意,我们需要沿逆时针方向定义外环边界,顺时针方向定义内环(孔)边界。

除了上述的点和多边形外,还有许多其他类型,例如LineString,MultiPoint,MultiPolygon,MultiLineString和GeometryCollection。

3. 地理空间索引

为了搜索我们插入的地理空间数据,我们需要先为location字段创建索引。

有2种索引方式:2d2dsphere

开始之前,我们先定义places collection:

MongoClient mongoClient = new MongoClient();
MongoDatabase db = mongoClient.getDatabase("myMongoDb");
collection = db.getCollection("places");

3.1. 2d 索引

2d索引用于2D平面位置搜索查询。

下面是通过Java代码的方式,为location创建2d索引:

collection.createIndex(Indexes.geo2d("location"));

当然,也可以在mongo shell中执行:

db.places.createIndex({location:"2d"})

3.2. 2dsphere 索引

2dsphere索引用于地球表面类型的地图

类似的,用Java代码创建2dspheresuoy索引:

collection.createIndex(Indexes.geo2dsphere("location"));

或者mongo shell:

db.places.createIndex({location:"2dsphere"})

4. 地理空间搜索

4.1. Near 附近查询

我们可以使用near查询来搜索给定距离内的点。

near查询支持2d2dsphere索引。

在下一个示例中,我们将搜索小于1km且大于10米范围内的记录:

@Test
public void givenNearbyLocation_whenSearchNearby_thenFound() {
    Point currentLoc = new Point(new Position(-0.126821, 51.495885));
 
    FindIterable<Document> result = collection.find(
      Filters.near("location", currentLoc, 1000.0, 10.0));

    assertNotNull(result.first());
    assertEquals("Big Ben", result.first().get("name"));
}

对应的mongo shell查询:

db.places.find({
  location: {
    $near: {
      $geometry: {
        type: "Point",
        coordinates: [-0.126821, 51.495885]
      },
      $maxDistance: 1000,
      $minDistance: 10
    }
  }
})

结果从近到远排序。

如果我们搜索的位置非常远,则附近位置结果为空:

@Test
public void givenFarLocation_whenSearchNearby_thenNotFound() {
    Point currentLoc = new Point(new Position(-0.5243333, 51.4700223));
 
    FindIterable<Document> result = collection.find(
      Filters.near("location", currentLoc, 5000.0, 10.0));

    assertNull(result.first());
}

nearSphere方法,作用与near完全相同,只是它使用球形几何来计算距离。

4.2. Within 查询

geoWithin查询完全包含在给定区域中的位置,如圆、长方体或多边形。 同样支持2d和2dsphere索引。

下面例子中,我们查询以给定位置为中心,5km半径圆内的记录:

@Test
public void givenNearbyLocation_whenSearchWithinCircleSphere_thenFound() {
    double distanceInRad = 5.0 / 6371;
 
    FindIterable<Document> result = collection.find(
      Filters.geoWithinCenterSphere("location", -0.1435083, 51.4990956, distanceInRad));

    assertNotNull(result.first());
    assertEquals("Big Ben", result.first().get("name"));
}

备注,我们需要将距离从km转换为弧度(除以地球半径)。

对应的mongo shell查询语句:

db.places.find({
  location: {
    $geoWithin: {
      $centerSphere: [
        [-0.1435083, 51.4990956],
        0.0007848061528802386
      ]
    }
  }
})

下面,我们查询矩形框范围内的所有记录。我们需要通过左下角和右上角坐标定义框的范围:

@Test
public void givenNearbyLocation_whenSearchWithinBox_thenFound() {
    double lowerLeftX = -0.1427638;
    double lowerLeftY = 51.4991288;
    double upperRightX = -0.1256209;
    double upperRightY = 51.5030272;

    FindIterable<Document> result = collection.find(
      Filters.geoWithinBox("location", lowerLeftX, lowerLeftY, upperRightX, upperRightY));

    assertNotNull(result.first());
    assertEquals("Big Ben", result.first().get("name"));
}

对应的mongo shell查询:

db.places.find({
  location: {
    $geoWithin: {
      $box: [
        [-0.1427638, 51.4991288],
        [-0.1256209, 51.5030272]
      ]
    }
  }
})

最后,如果矩形或圆不能满足你的需求,你还可以使用多边形来定义更具体的区域

@Test
public void givenNearbyLocation_whenSearchWithinPolygon_thenFound() {
    ArrayList<List<Double>> points = new ArrayList<List<Double>>();
    points.add(Arrays.asList(-0.1439, 51.4952));
    points.add(Arrays.asList(-0.1121, 51.4989));
    points.add(Arrays.asList(-0.13, 51.5163));
    points.add(Arrays.asList(-0.1439, 51.4952));
 
    FindIterable<Document> result = collection.find(
      Filters.geoWithinPolygon("location", points));

    assertNotNull(result.first());
    assertEquals("Big Ben", result.first().get("name"));
}

对应的查询语句:

db.places.find({
  location: {
    $geoWithin: {
      $polygon: [
        [-0.1439, 51.4952],
        [-0.1121, 51.4989],
        [-0.13, 51.5163],
        [-0.1439, 51.4952]
      ]
    }
  }
})

我们仅定义了一个具有外边界的多边形,我们也可以在上面打孔。每个孔都是Point类型的List

geoWithinPolygon("location", points, hole1, hole2, ...)

4.3. Intersect 交集查询

geoWithin完全包含相比,geoIntersects查找至少与给定几何相交的记录。

只支持2dsphere索引。

下面例子中,查找与给定多边形相交的记录:

@Test
public void givenNearbyLocation_whenSearchUsingIntersect_thenFound() {
    ArrayList<Position> positions = new ArrayList<Position>();
    positions.add(new Position(-0.1439, 51.4952));
    positions.add(new Position(-0.1346, 51.4978));
    positions.add(new Position(-0.2177, 51.5135));
    positions.add(new Position(-0.1439, 51.4952));
    Polygon geometry = new Polygon(positions);
 
    FindIterable<Document> result = collection.find(
      Filters.geoIntersects("location", geometry));

    assertNotNull(result.first());
    assertEquals("Hyde Park", result.first().get("name"));
}

对应mongo shell查询:

db.places.find({
  location:{
    $geoIntersects:{
      $geometry:{
        type:"Polygon",
          coordinates:[
          [
            [-0.1439, 51.4952],
            [-0.1346, 51.4978],
            [-0.2177, 51.5135],
            [-0.1439, 51.4952]
          ]
        ]
      }
    }
  }
})

5. 总结

本文中,我们学习了如何在MongoDB中存储地理空间数据,并研究了2d索引和2dsphere索引的区别,以及如何在MongoDB使用地理空间查询。

惯例,完整的示例源码可从GitHub上获取。