MongoDB指南---15、特殊的索引和集合:地理空间索引、使用GridFS存储文件

liulufei 2019-11-17

上一篇文章:MongoDB指南---14、特殊的索引和集合:固定集合、TTL索引、全文本索引
下一篇文章:MongoDB指南---16、聚合

地理空间索引

MongoDB支持几种类型的地理空间索引。其中最常用的是2dsphere索引(用于地球表面类型的地图)和2d索引(用于平面地图和时间连续的数据)。
2dsphere允许使用GeoJSON格式(http://www.geojson.org)指定点、线和多边形。点可以用形如[longitude, latitude]([经度,纬度])的两个元素的数组表示:

{
    "name" : "New York City",
    "loc" : {
        "type" : "Point",
        "coordinates" : [50, 2] 
    }
}

线可以用一个由点组成的数组来表示:

{
    "name" : "Hudson River",
    "loc" : {
        "type" : "Line",
        "coordinates" : [[0,1], [0,2], [1,2]]
    }
}

多边形的表示方式与线一样(都是一个由点组成的数组),但是"type"不同:

{
    "name" : "New England",
    "loc" : {
        "type" : "Polygon",
        "coordinates" : [[0,1], [0,2], [1,2]]
    }
}

"loc"字段的名字可以是任意的,但是其中的子对象是由GeoJSON指定的,不能改变。
在ensureIndex中使用"2dsphere"选项就可以创建一个地理空间索引:

> db.world.ensureIndex({"loc" : "2dsphere"})

地理空间查询的类型

可以使用多种不同类型的地理空间查询:交集(intersection)、包含(within)以及接近(nearness)。查询时,需要将希望查找的内容指定为形如{"$geometry" : geoJsonDesc}的GeoJSON对象。
例如,可以使用"$geoIntersects"操作符找出与查询位置相交的文档:

> var eastVillage = {
... "type" : "Polygon",
... "coordinates" : [
... [-73.9917900, 40.7264100],
... [-73.9917900, 40.7321400],
... [-73.9829300, 40.7321400],
... [-73.9829300, 40.7264100]
... ]}
> db.open.street.map.find(
... {"loc" : {"$geoIntersects" : {"$geometry" : eastVillage}}})

这样就会找到所有与East Village区域有交集的文档。
可以使用"$within"查询完全包含在某个区域的文档,例如:“East Village有哪些餐馆?”

> db.open.street.map.find({"loc" : {"$within" : {"$geometry" : eastVillage}}})

与第一个查询不同,这次不会返回那些只是经过East Village(比如街道)或者部分重叠(比如用于表示曼哈顿的多边形)的文档。
最后,可以使用"$near"查询附近的位置:

> db.open.street.map.find({"loc" : {"$near" : {"$geometry" : eastVillage}}})

注意,"$near"是唯一一个会对查询结果进行自动排序的地理空间操作符:"$near"的返回结果是按照距离由近及远排序的。
地理位置查询有一点非常有趣:不需要地理空间索引就可以使用"$geoIntersects"或者"$within"("$near"需要使用索引)。但是,建议在用于表示地理位置的字段上建立地理空间索引,这样可以显著提高查询速度。

 复合地理空间索引

如果有其他类型的索引,可以将地理空间索引与其他字段组合在一起使用,以便对更复杂的查询进行优化。上面提到过一种可能的查询:“East Village有哪些餐馆?”。如果仅仅使用地理空间索引,我们只能查找到East Village内的所有东西,但是如果要将“restaurants”或者是“pizza”单独查询出来,就需要使用其他索引中的字段了:

> db.open.street.map.ensureIndex({"tags" : 1, "location" : "2dsphere"})

然后就能够很快地找到East Village内的披萨店了:

> db.open.street.map.find({"loc" : {"$within" : {"$geometry" : eastVillage}},
... "tags" : "pizza"})

其他索引字段可以放在"2dsphere"字段前面也可以放在后面,这取决于我们希望首先使用其他索引的字段进行过滤还是首先使用位置进行过滤。应该将那个能够过滤掉尽可能多的结果的字段放在前面。

 2D索引

对于非球面地图(游戏地图、时间连续的数据等),可以使用"2d"索引代替"2dsphere":

> db.hyrule.ensureIndex({"tile" : "2d"})

"2d"索引用于扁平表面,而不是球体表面。"2d"索引不应该用在球体表面上,否则极点附近会出现大量的扭曲变形。
文档中应该使用包含两个元素的数组表示2d索引字段(写作本书时,这个字段还不是GeoJSON文档)。示例如下:

{
    "name" : "Water Temple",
    "tile" : [ 32, 22 ]
}

"2d"索引只能对点进行索引。可以保存一个由点组成的数组,但是它只会被保存为由点组成的数组,不会被当成线。特别是对于"$within"查询来说,这是一项重要的区别。如果将街道保存为由点组成的数组,那么如果其中的某个点位于给定的形状之内,这个文档就会与$within相匹配。但是,由这些点组成的线并不一定完全包含在这个形状之内。
默认情况下,地理空间索引是假设你的值都介于-180~180。可以根据需要在ensureIndex中设置更大或者更小的索引边界值:

> db.star.trek.ensureIndex({"light-years" : "2d"}, {"min" : -1000, "max" : 1000})

这会创建一个2000×2000大小的空间索引。
使用"2d"索引进行查询比使用"2dsphere"要简单许多。可以直接使用"$near"或者"$within",而不必带有"$geometry"子对象。可以直接指定坐标:

> db.hyrule.find({"tile" : {"$near" : [20, 21]}})

这样会返回hyrule集合内的全部文档,按照距离(20,21)这个点的距离排序。如果没有指定文档数量限制,默认最多返回100个文档。如果不需要这么多结果,应该根据需要设置返回文档的数量以节省服务器资源。例如,下面的代码只会返回距离(20,21)最近的10个文档:

> db.hyrule.find({"tile" : {"$near" : [20, 21]}}).limit(10)

"$within"可以查询出某个形状(矩形、圆形或者是多边形)范围内的所有文档。如果要使用矩形,可以指定"$box"选项:

> db.hyrule.find({"tile" : {"$within" : {"$box" : [[10, 20], [15, 30]]}}})

"$box"接受一个两元素的数组:第一个元素指定左下角的坐标,第二个元素指定右上角的坐标。
类似地,可以使用"$center"选项返回圆形范围内的所有文档,这个选项也是接受一个两元素数组作为参数:第一个元素是一个点,用于指定圆心;第二个参数用于指定半径:

> db.hyrule.find({"tile" : {"$within" : {"$center" : [[12, 25], 5]}}})

还可以使用多个点组成的数组来指定多边形:

> db.hyrule.find(
... {"tile" : {"$within" : {"$polygon" : [[0, 20], [10, 0], [-10, 0]]}}})

这个例子会查询出包含给定三角形内的点的所有文档。列表中的最后一个点会被连接到第一个点,以便组成多边形。

使用GridFS存储文件

GridFS是MongoDB的一种存储机制,用来存储大型二进制文件。下面列出了使用GridFS作为文件存储的理由。

  • 使用GridFS能够简化你的栈。如果已经在使用MongoDB,那么可以使用GridFS来代替独立的文件存储工具。
  • GridFS会自动平衡已有的复制或者为MongoDB设置的自动分片,所以对文件存储做故障转移或者横向扩展会更容易。
  • 当用于存储用户上传的文件时,GridFS可以比较从容地解决其他一些文件系统可能会遇到的问题。例如,在GridFS文件系统中,如果在同一个目录下存储大量的文件,没有任何问题。
  • 在GridFS中,文件存储的集中度会比较高,因为MongoDB是以2 GB为单位来分配数据文件的。

GridFS也有一些缺点。

  • GridFS的性能比较低:从MongoDB中访问文件,不如直接从文件系统中访问文件速度快。
  • 如果要修改GridFS上的文档,只能先将已有文档删除,然后再将整个文档重新保存。MongoDB将文件作为多个文档进行存储,所以它无法在同一时间对文件中的所有块加锁。

通常来说,如果你有一些不常改变但是经常需要连续访问的大文件,那么使用GridFS再合适不过了。

 GridFS入门

使用GridFS最简单的方式是使用mongofiles工具。所有的MongoDB发行版中都包含了mongofiles,可以用它在GridFS中上传文件、下载文件、查看文件列表、搜索文件,以及删除文件。
与其他的命令行工具一样,运行mongofiles --help就可以查看它的可用选项了。
在下面这个会话中,首先用mongofiles从文件系统中上传一个文件到GridFS,然后列出GridFS中的所有文件,最后再将之前上传过的文件从GridFS中下载下来:

$ echo "Hello, world" > foo.txt
$ ./mongofiles put foo.txt
connected to: 127.0.0.1
added file: { _id: ObjectId('4c0d2a6c3052c25545139b88'),
                filename: "foo.txt", length: 13, chunkSize: 262144,
                uploadDate: new Date(1275931244818),
                md5: "a7966bf58e23583c9a5a4059383ff850" }
done!
$ ./mongofiles list
connected to: 127.0.0.1
foo.txt 13
$ rm foo.txt
$ ./mongofiles get foo.txt
connected to: 127.0.0.1
done write to: foo.txt
$ cat foo.txt
Hello,world

在上面的例子中,使用mongofiles执行了三种基本操作:put、list和get。put操作可以将文件系统中选定的文件上传到GridFS;list操作可以列出GridFS中的文件;get操作与put相反,用于将GridFS中的文件下载到文件系统中。mongofiles还支持另外两种操作:用于在GridFS中搜索文件的search操作和用于从GridFS中删除文件的delete操作。

 在MongoDB驱动程序中使用GridFS

所有客户端驱动程序都提供了GridFS API。例如,可以用PyMongo(MongoDB的Python驱动程序)执行与上面直接使用mongofiles一样的操作:

>>> from pymongo import Connection
>>> import gridfs
>>> db = Connection().test
>>> fs = gridfs.GridFS(db)
>>> file_id = fs.put("Hello, world", filename="foo.txt")
>>> fs.list()
[u'foo.txt']
>>> fs.get(file_id).read()
'Hello, world'

PyMongo中用于操作GridFS的API与mongofiles非常像:可以很方便地执行put、get和list操作。几乎所有MongoDB驱动程序都遵循这种基本模式对GridFS进行操作,当然通常也会提供一些更高级的功能。关于特定驱动程序对GridFS的操作,可以查询相关驱动程序的文件。

 揭开GridFS的面纱

GridFS是一种轻量级的文件存储规范,用于存储MongoDB中的普通文档。MongoDB服务器几乎不会对GridFS请求做“特殊”处理,所有处理都由客户端的驱动程序和工具负责。
GridFS背后的理念是:可以将大文件分割为多个比较大的块,将每个块作为独立的文档进行存储。由于MongoDB支持在文档中存储二进制数据,所以可以将块存储的开销降到非常低。除了将文件的每一个块单独存储之外,还有一个文档用于将这些块组织在一起并存储该文件的元信息。
GridFS中的块会被存储到专用的集合中。块默认使用的集合是fs.chunks,不过可以修改为其他集合。在块集合内部,各个文档的结构非常简单:

{
    "_id" : ObjectId("..."),
    "n" : 0,
    "data" : BinData("..."),
    "files_id" : ObjectId("...")
}

与其他的MongoDB文档一样,块也都拥有一个唯一的"_id"。另外,还有如下几个键。

  • "files_id"

块所属文件的元信息。

  • "n"

块在文件中的相对位置。

  • "data"

块所包含的二进制数据。

每个文件的元信息被保存在一个单独的集合中,默认情况下这个集合是fs.files。这个文件集合中的每一个文档表示GridFS中的一个文件,文档中可以包含与这个文件相关的任意用户自定义元信息。除用户自定义的键之外,还有几个键是GridFS规范规定必须要有的。

  • "_id"

文件的唯一id,这个值就是文件的每个块文档中"files_id"的值。

  • "length"

文件所包含的字节数。

  • "chunkSize"

组成文件的每个块的大小,单位是字节。这个值默认是256 KB,可以在需要时进行调整。

  • "uploadDate"

文件被上传到GridFS的日期。

  • "md5"

文件内容的md5校验值,这个值由服务器端计算得到。

这些必须字段中最有意思(或者说能够见名知意)的一个可能是"md5"。"md5"字段的值是由MongoDB服务器使用filemd5命令得到的,这个命令可以用来计算上传到GridFS的块的md5校验值。这意味着,用户可以通过检查文件的md5校验值来确保文件上传正确。
如上面所说,在fs.files中,除了这些必须字段外,可以使用任何自定义的字段来保存必需的文件元信息。可能你希望在文件元信息中保存文件的下载次数、MIME类型或者用户评分。
只要理解了GridFS底层的规范,自己就可以很容易地实现一些驱动程序没有提供的辅助功能。例如,可以使用distinct命令得到GridFS中保存文件的文件名集合(集合中的每个文件名都是唯一的)。

> db.fs.files.distinct("filename")
[ "foo.txt" , "bar.txt" , "baz.txt" ]

这样,在加载或者收集文件相关信息时,应用程序可以拥有非常大的灵活性.

上一篇文章:MongoDB指南---14、特殊的索引和集合:固定集合、TTL索引、全文本索引
下一篇文章:MongoDB指南---16、聚合

相关推荐