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

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

地理空间索引

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

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

线能够用一个由点组成的数组来表示:json

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

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

{
    "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内的披萨店了:rest

> 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指南---1四、特殊的索引和集合:固定集合、TTL索引、全文本索引
下一篇文章: MongoDB指南---1六、聚合
相关文章
相关标签/搜索