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

10次阅读

共计 6263 个字符,预计需要花费 16 分钟才能阅读完成。

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

地理空间索引

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"]

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

正文完
 0