共计 7951 个字符,预计需要花费 20 分钟才能阅读完成。
上一篇文章:MongoDB 指南 —13、索引类型、索引管理
下一篇文章:
本章介绍 MongoDB 中一些特殊的集合和索引类型,包括:
- 用于类队列数据的固定集合(capped collection);
- 用于缓存的 TTL 索引;
- 用于简单字符串搜索的全文本索引;
- 用于二维平面和球体空间的地理空间索引;
- 用于存储大文件的 GridFS。
固定集合
MongoDB 中的“普通”集合是动态创建的,而且可以自动增长以容纳更多的数据。MongoDB 中还有另一种不同类型的集合,叫做固定集合,固定集合需要事先创建好,而且它的大小是固定的(如图 6 - 1 所示)。说到固定大小的集合,有一个很有趣的问题:向一个已经满了的固定集合中插入数据会怎么样?答案是,固定集合的行为类似于循环队列。如果已经没有空间了,最老的文档会被删除以释放空间,新插入的文档会占据这块空间(如图 6 - 2 所示)。也就是说,当固定集合被占满时,如果再插入新文档,固定集合会自动将最老的文档从集合中删除。
图 6 -1 新文档被插入到队列末尾
图 6 -2 如果队列已经被占满,那么最老的文档会被之后插入的新文档覆盖
固定集合的访问模式与 MongoDB 中的大部分集合不同:数据被顺序写入磁盘上的固定空间。因此它们在碟式磁盘(spinning disk)上的写入速度非常快,尤其是集合拥有专用磁盘时(这样就不会因为其他集合的一些随机性的写操作而“中断”)。
固定集合不能被分片。
固定集合可以用于记录日志,尽管它们不够灵活。虽然可以在创建时指定集合大小,但无法控制什么时候数据会被覆盖。
创建固定集合
不同于普通集合,固定集合必须在使用之前先显式创建。可以使用 create 命令创建固定集合。在 shell 中,可以使用 createCollection 函数:
> db.createCollection("my_collection", {"capped" : true, "size" : 100000});
{"ok" : true}
上面的命令创建了一个名为 my_collection 大小为 100 000 字节的固定集合。
除了大小,createCollection 还能够指定固定集合中文档的数量:
> db.createCollection("my_collection2",
... {"capped" : true, "size" : 100000, "max" : 100});
{"ok" : true}
可以使用这种方式来保存最新的 10 则新闻,或者是将每个用户的文档数量限制为 1000。
固定集合创建之后,就不能改变了(如果需要修改固定集合的属性,只能将它删除之后再重建)。因此,在创建大的固定集合之前应该仔细想清楚它的大小。
为固定集合指定文档数量限制时,必须同时指定固定集合的大小。不管先达到哪一个限制,之后插入的新文档就会把最老的文档挤出集合:固定集合的文档数量不能超过文档数量限制,固定集合的大小也不能超过大小限制。
创建固定集合时还有另一个选项,可以将已有的某个常规集合转换为固定集合,可以使用 convertToCapped 命令实现。下面的例子将 test 集合转换为一个大小为 10 000 字节的固定集合:
> db.runCommand({"convertToCapped" : "test", "size" : 10000});
{"ok" : true}
无法将固定集合转换为非固定集合(只能将其删除)。
自然排序
对固定集合可以进行一种特殊的排序,称为自然排序(natural sort)。自然排序返回结果集中文档的顺序就是文档在磁盘上的顺序(如图 6 - 3 所示)。
图 6 -3 使用 {“$natural” : 1} 进行排序
对大多数集合来说,自然排序的意义不大,因为文档的位置经常变动。但是,固定集合中的文档是按照文档被插入的顺序保存的,自然顺序就是文档的插入顺序。因此,自然排序得到的文档是从旧到新排列的。当然也可以按照从新到旧的顺序排列(如图 6 - 4 所示)。
> db.my_collection.find().sort({"$natural" : -1})
图 6 -4 使用 {“$natural” : -1} 进行排序
循环游标
循环游标(tailable cursor)是一种特殊的游标,当循环游标的结果集被取光后,游标不会被关闭。循环游标的灵感来自 tail - f 命令(循环游标跟这个命令有点儿相似),会尽可能久地持续提取输出结果。由于循环游标在结果集取光之后不会被关闭,因此,当有新文档插入到集合中时,循环游标会继续取到结果。由于普通集合并不维护文档的插入顺序,所以循环游标只能用在固定集合上。
循环游标通常用于当文档被插入到“工作队列”(其实就是个固定集合)时对新插入的文档进行处理。如果超过 10 分钟没有新的结果,循环游标就会被释放,因此,当游标被关闭时自动重新执行查询是非常重要的。下面是一个在 PHP 中使用循环游标的例子(不能在 mongo shell 中使用循环游标):
$cursor = $collection->find()->tailable();
while (true) {if (!$cursor->hasNext()) {if ($cursor->dead()) {break;}
sleep(1);
}
else {while ($cursor->hasNext()) {do_stuff($cursor->getNext());
}
}
}
这个游标会不断对查询结果进行处理,或者是等待新的查询结果,直到游标被关闭(超过 10 分钟没有新的结果或者人为中止查询操作)。
没有_id 索引的集合
默认情况下,每个集合都有一个 ”_id” 索引。但是,如果在调用 createCollection 创建集合时指定 autoIndexId 选项为 false,创建集合时就不会自动在 ”_id” 上创建索引。实践中不建议这么使用,但是对于只有插入操作的集合来说,这确实可以带来速度的稍许提升。
如果创建了一个没有 ”_id” 索引的集合,那就永远都不能复制它所在的 mongod 了。复制操作要求每个集合上都要有 ”_id” 索引(对于复制操作,能够唯一标识集合中的每一个文档是非常重要的)。
在 2.2 版本之前,固定集合默认是没有 ”_id” 索引的,除非显式地将 autoIndexId 置为 true。如果正在使用旧版的固定集合,要确保你的应用程序能够填充 ”_id” 字段(大多数驱动程序会自动填充 ”_id” 字段),然后使用 ensureIndex 命令创建 ”_id” 索引。
记住,”_id” 索引必须是唯一索引。不同于其他索引,”_id” 索引一经创建就无法删除了,因此在生产环境中创建索引之前先自己实践一下是非常重要的。所以创建 ”_id” 索引必须一次成功!如果创建的 ”_id” 索引不合规范,就只能删除集合再重建了。
TTL 索引
上一节已经讲过,对于固定集合中的内容何时被覆盖,你只拥有非常有限的控制权限。如果需要更加灵活的老化移出系统(age-out system),可以使用 TTL 索引(time-to-live index,具有生命周期的索引),这种索引允许为每一个文档设置一个超时时间。一个文档到达预设置的老化程度之后就会被删除。这种类型的索引对于缓存问题(比如会话的保存)非常有用。
在 ensureIndex 中指定 expireAfterSecs 选项就可以创建一个 TTL 索引:
> // 超时时间为 24 小时
> db.foo.ensureIndex({"lastUpdated" : 1}, {"expireAfterSecs" : 60*60*24})
这样就在 ”lastUpdated” 字段上建立了一个 TTL 索引。如果一个文档的 ”lastUpdated” 字段存在并且它的值是日期类型,当服务器时间比文档的 ”lastUpdated” 字段的时间晚 expireAfterSecs 秒时,文档就会被删除。
为了防止活跃的会话被删除,可以在会话上有活动发生时将 ”lastUpdated” 字段的值更新为当前时间。只要 ”lastUpdated” 的时间距离当前时间达到 24 小时,相应的文档就会被删除。
MongoDB 每分钟对 TTL 索引进行一次清理,所以不应该依赖以秒为单位的时间保证索引的存活状态。可以使用 collMod 命令修改 expireAfterSecs 的值:
> db.runCommand({"collMod" : "someapp.cache", "expireAfterSecs" : 3600})
在一个给定的集合上可以有多个 TTL 索引。TTL 索引不能是复合索引,但是可以像“普通”索引一样用来优化排序和查询。
全文本索引
MongoDB 有一个特殊类型的索引用于在文档中搜索文本。前面几章都是使用精确匹配和正则表达式来查询字符串,但是这些技术有一些限制。使用正则表达式搜索大块文本的速度非常慢,而且无法处理语言的理解问题(比如 entry 与 entries 应该算是匹配的)。使用全文本索引可以非常快地进行文本搜索,就如同内置了多种语言分词机制的支持一样。
创建任何一种索引的开销都比较大,而创建全文本索引的成本更高。在一个操作频繁的集合上创建全文本索引可能会导致 MongoDB 过载,所以应该是离线状态下创建全文本索引,或者是在对性能没要求时。创建全文本索引时要特别小心谨慎,内存可能会不够用(除非你有 SSD)。
全文本索引也会导致比“普通”索引更严重的性能问题,因为所有字符串都需要被分解、分词,并且保存到一些地方。因此,可能会发现拥有全文本索引的集合的写入性能比其他集合要差。全文本索引也会降低分片时的数据迁移速度:将数据迁移到其他分片时,所有文本都需要重新进行索引。
写作本书时,全文本索引仍然只是一个处于“试验阶段”的功能,所以需要专门启用这个功能才能进行使用。启动 MongoDB 时指定 –setParameter textSearch Enabled=true 选项,或者在运行时执行 setParameter 命令,都可以启用全文本索引:
> db.adminCommand({"setParameter" : 1, "textSearchEnabled" : true})
假如我们使用这个非官方的 Hacker News JSON API(http://api.ihackernews.com)将最近的一些文章加载到了 MongoDB 中。
为了进行文本搜索,首先需要创建一个 ”text” 索引:
> db.hn.ensureIndex({"title" : "text"})
现在,必须通过 text 命令才能使用这个索引(写作本书时,全文本索引还不能用在“普通”查询中):
test> db.runCommand({"text" : "hn", "search" : "ask hn"})
{
"queryDebugString" : "ask|hn||||||",
"language" : "english",
"results" : [
{
"score" : 2.25,
"obj" : {"_id" : ObjectId("50dcab296803fa7e4f000011"),
"title" : "Ask HN: Most valuable skills you have?",
"url" : "/comments/4974230",
"id" : 4974230,
"commentCount" : 37,
"points" : 31,
"postedAgo" : "2 hours ago",
"postedBy" : "bavidar"
}
},
{
"score" : 0.5625,
"obj" : {"_id" : ObjectId("50dcab296803fa7e4f000001"),
"title" : "Show HN: How I turned an old book...",
"url" : "http://www.howacarworks.com/about",
"id" : 4974055,
"commentCount" : 44,
"points" : 95,
"postedAgo" : "2 hours ago",
"postedBy" : "AlexMuir"
}
},
{
"score" : 0.5555555555555556,
"obj" : {"_id" : ObjectId("50dcab296803fa7e4f000010"),
"title" : "Show HN: ShotBlocker - iOS Screenshot detector...",
"url" : "https://github.com/clayallsopp/ShotBlocker",
"id" : 4973909,
"commentCount" : 10,
"points" : 17,
"postedAgo" : "3 hours ago",
"postedBy" : "10char"
}
}
],
"stats" : {
"nscanned" : 4,
"nscannedObjects" : 0,
"n" : 3,
"timeMicros" : 89
},
"ok" : 1 }
匹配到的文档是按照相关性降序排列的:”Ask HN” 位于第一位,然后是两个部分匹配的文档。每个对象前面的 ”score” 字段描述了每个结果与查询的匹配程度。
如你所见,这个搜索是不区分大小写不的,至少对于 [a-zA-Z] 这些字符是这样。全文本索引会使用 toLower 将单词变为小写,但这是与本地化相关的,所以某些语言的用户可能会发现 MongoDB 会不可预测性地变得区分大小写,这取决于 toLower 在不同字符集上的行为。MongoDB 一直在努力提高对不同字符集的支持。
全文本索引只会对字符串数据进行索引:其他的数据类型会被忽略,不会包含在索引中。一个集合上最多只能有一个全文本索引,但是全文本索引可以包含多个字段:
> db.blobs.ensureIndex({"title" : "text", "desc" : "text", "author" : "text"})
与“普通”的多键索引不同,全文本索引中的字段顺序不重要:每个字段都被同等对待。可以为每个字段指定不同的权重来控制不同字段的相对重要性:
> db.hn.ensureIndex({"title" : "text", "desc" : "text", "author" : "text"},
... {"weights" : {"title" : 3, "author" : 2}})
默认的权重是 1,权重的范围可以是 1~1 000 000 000。使用上面的代码设置权重之后,”title” 字段成为其中最重要的字段,”author” 其次,最后是 ”desc”(没有指定,因此它的权重是默认值 1)。
索引一经创建,就不能改变字段的权重了(除非删除索引再重建),所以在生产环境中创建索引之前应该先在测试数据集上实际操作一下。
对于某些集合,我们可能并不知道每个文档所包含的字段。可以使用 ”$**” 在文档的所有字符串字段上创建全文本索引:这不仅会对顶级的字符串字段建立索引,也会搜索嵌套文档和数组中的字符串字段:
> db.blobs.ensureIndex({"$**" : "text"})
也可以为 ”$**” 设置权重:
> db.hn.ensureIndex({"whatever" : "text"},
... {"weights" : {"title" : 3, "author" : 1, "$**" : 2}})
“whatever” 可以指代任何东西。在设置权重时指明了是对所有字段进行索引,因此 MongoDB 并不要求你明确给出字段列表。
搜索语法
默认情况下,MongoDB 会使用 OR 连接查询中的每个词:“ask OR hn”。这是执行全文本查询最有效的方式,但是也可以进行短语的精确匹配,以及使用 NOT。为了精确查询“ask hn”这个短语,可以用双引号将查询内容括起来:
> db.runCommand({text: "hn", search: "\"ask hn\""})
{
"queryDebugString" : "ask|hn||||ask hn||",
"language" : "english",
"results" : [
{
"score" : 2.25,
"obj" : {"_id" : ObjectId("50dcab296803fa7e4f000011"),
"title" : "Ask HN: Most valuable skills you have?",
"url" : "/comments/4974230",
"id" : 4974230,
"commentCount" : 37,
"points" : 31,
"postedAgo" : "2 hours ago",
"postedBy" : "bavidar"
}
}
],
"stats" : {
"nscanned" : 4,
"nscannedObjects" : 0,
"n" : 1,
"nfound" : 1,
"timeMicros" : 20392
},
"ok" : 1
}
这比使用 OR 的匹配慢一些,因为 MongoDB 首先要执行一个 OR 匹配,然后再对匹配结果进行 AND 匹配。
可以将查询字符串的一部分指定为字面量匹配,另一部分仍然是普通匹配:
> db.runCommand({text: "hn", search: "\"ask hn\"ipod"})
这会精确搜索 ”ask hn” 这个短语,也会可选地搜索 ”ipod”。
也可以使用 ”-“ 字符指定特定的词不要出现在搜索结果中:
> db.runCommand({text: "hn", search: "-startup vc"})
这样就会返回匹配“vc”但是不包含“startup”这个词的文档。
优化全文本搜索
有几种方式可以优化全文本搜索。如果能够使用某些查询条件将搜索结果的范围变小,可以创建一个由其他查询条件前缀和全文本字段组成的复合索引:
> db.blog.ensureIndex({"date" : 1, "post" : "text"})
这就是局部的全文本索引,MongoDB 会基于上面例子中的 ”date” 先将搜索范围分散为多个比较小的树。这样,对于特定日期的文档进行全文本查询就会快很多了。
也可以使用其他查询条件后缀,使索引能够覆盖查询。例如,如果要返回 ”author” 和 ”post” 字段,可以基于这两个字段创建一个复合索引:
> db.blog.ensureIndex({"post" : "text", "author" : 1})
前缀和后缀形式也可以组合在一起使用:
> db.blog.ensureIndex({"date" : 1, "post" : "text", "author" : 1})
这里的前缀索引字段和后缀索引字段都不可以是多键字段。
创建全文本索引会自动在集合上启用 usePowerOf2Sizes 选项,这个选项可以控制空间的分配方式。这个选项能够提高写入速度,所以不要禁用它。
在其他语言中搜索
当一个文档被插入之后(或者索引第一次被创建之后),MongoDB 会查找索引字段,对字符串进行分词,将其减小为一个基本单元(essential unit)。然后,不同语言的分词机制是不同的,所以必须指定索引或者文档使用的语言。文本类型的索引允许指定 ”default_language” 选项,它的默认值是 ”english”,可以被设置为多种其他语言(MongoDB 的在线文档提供了最新的支持语言列表)。
例如,要创建一个法语的索引,可以这么做:
> db.users.ensureIndex({"profil" : "text", "intérêts" : "text"},
... {"default_language" : "french"})
这样,这个索引就会默认使用法语的分词机制,除非指定了其他的分词机制。如果在插入文档时指定 ”language” 字段,就可以为每个文档分别指定分词时使用的语言:
> db.users.insert({"username" : "swedishChef",
... "profile" : "Bork de bork", language : "swedish"})