MongoDB指南---1七、MapReduce

上一篇文章: MongoDB指南---1六、聚合
下一篇文章: MongoDB指南---1八、聚合命令

MapReduce是聚合工具中的明星,它很是强大、很是灵活。有些问题过于复杂,没法使用聚合框架的查询语言来表达,这时可使用MapReduce。MapReduce使用JavaScript做为“查询语言”,所以它可以表达任意复杂的逻辑。然而,这种强大是有代价的:MapReduce很是慢,不该该用在实时的数据分析中。
MapReduce可以在多台服务器之间并行执行。它会将一个大问题分割为多个小问题,将各个小问题发送到不一样的机器上,每台机器只负责完成一部分工做。全部机器都完成时,再将这些零碎的解决方案合并为一个完整的解决方案。
MapReduce须要几个步骤。最开始是映射(map),将操做映射到集合中的每一个文档。这个操做要么“无做为”,要么“产生一些键和X个值”。而后就是中间环节,称做洗牌(shuffle),按照键分组,并将产生的键值组成列表放到对应的键中。化简(reduce)则把列表中的值化简成一个单值。这个值被返回,而后接着进行洗牌,直到每一个键的列表只有一个值为止,这个值也就是最终结果。
下面会多举几个MapReduce的例子,这个工具很是强大,但也有点复杂。web

 示例1:找出集合中的全部键

用MapReduce来解决这个问题有点大材小用,不过仍是一种了解其机制的不错的方式。要是已经知道MapReduce的原理,则直接跳到本节最后,看看MongoDB中MapReduce的使用注意事项。
MongoDB会假设你的模式是动态的,因此并不跟踪记录每一个文档中的键。一般找到集合中全部文档全部键的最好方式就是用MapReduce。在本例中,会记录每一个键出现了多少次。内嵌文档中的键就不计算了,但给map函数作个简单修改就能实现这个功能了。
在映射环节,咱们但愿获得集合中每一个文档的全部键。map函数使用特别的emit函数“返回”要处理的值。emit会给MapReduce一个键(相似于前面$group所使用的键)和一个值。这里用emit将文档某个键的计数(count)返回({count : 1})。咱们想为每一个键单独计数,因此为文档中的每一个键调用一次emit。this就是当前映射文档的引用:数据库

> map = function() {
... for (var key in this) {
...     emit(key, {count : 1});
... }};

这样就有了许许多多{count : 1}文档,每个都与集合中的一个键相关。这种由一个或多个{count : 1}文档组成的数组,会传递给reduce函数。reduce函数有两个参数,一个是key,也就是emit返回的第一个值,还有另一个数组,由一个或者多个与键对应的{count : 1}文档组成。segmentfault

> reduce = function(key, emits) {
... total = 0;
... for (var i in emits) {
...     total += emits[i].count;
... }
... return {"count" : total};
... }

reduce必定要可以在以前的map阶段或者前一个reduce阶段的结果上反复执行。因此reduce返回的文档必须能做为reduce的第二个参数的一个元素。例如,x键映射到了3个文档{count : 1,id : 1}、{count : 1,id : 2}和{count : 1,id : 3},其中id键只用于区分不一样的文档。MongoDB可能会这样调用reduce:数组

> r1 = reduce("x", [{count : 1, id : 1}, {count : 1, id : 2}])
{count : 2}
> r2 = reduce("x", [{count : 1, id : 3}])
{count : 1}
> reduce("x", [r1, r2])
{count : 3}

不能认为第二个参数老是初始文档之一(好比{count:1})或者长度固定。reduce应该能处理emit文档和其余reduce返回结果的各类组合。
总之,MapReduce函数可能会是下面这样:服务器

> mr = db.runCommand({"mapreduce" : "foo", "map" : map, "reduce" : reduce})
{
    "result" : "tmp.mr.mapreduce_1266787811_1",
    "timeMillis" : 12,
    "counts" : {
        "input" : 6
        "emit" : 14
        "output" : 5
    },
    "ok" : true
}

MapReduce返回的文档包含不少与操做有关的元信息。框架

  • "result" : "tmp.mr.mapreduce_1266787811_1"

这是存放MapReduce结果的集合名。这是个临时集合,MapReduce的链接关闭后它就被自动删除了。本章稍后会介绍如何指定一个好一点的名字以及将结果集合持久化。函数

  • "timeMillis" : 12

操做花费的时间,单位是毫秒。工具

  • "counts" : { ... }

这个内嵌文档主要用做调试,其中包含3个键。网站

  • "input" : 6

发送到map函数的文档个数。this

  • "emit" : 14

在map函数中emit被调用的次数。

  • "output" : 5

结果集合中的文档数量。
对结果集合进行查询会发现原有集合的全部键及其计数:
···

db[mr.result].find()
{ "_id" : "_id", "value" : { "count" : 6 } }
{ "_id" : "a", "value" : { "count" : 4 } }
{ "_id" : "b", "value" : { "count" : 2 } }
{ "_id" : "x", "value" : { "count" : 1 } }
{ "_id" : "y", "value" : { "count" : 1 } }
···
这个结果集中的每一个"_id"对应原集合中的一个键,"value"键的值就是reduce的最终结果。

 示例2:网页分类

假设有个网站,人们能够提交其余网页的连接,好比reddit(http://www.reddit.com)。提交者能够给这个连接添加标签,代表主题,好比politics、geek或者icanhascheezburger。能够用MapReduce找出哪一个主题最为热门,热门与否由最近的投票决定。
首先,创建一个map函数,发出(emit)标签和一个基于流行度和新旧程度的值。

map = function() {
    for (var i in this.tags) {
        var recency = 1/(new Date() - this.date); 
        var score = recency * this.score;

        emit(this.tags[i], {"urls" : [this.url], "score" : score});
    }
};

如今就化简同一个标签的全部值,以获得这个标签的分数:

reduce = function(key, emits) {
    var total = {urls : [], score : 0}
    for (var i in emits) {
        emits[i].urls.forEach(function(url) {
            total.urls.push(url);
        }
        total.score += emits[i].score;
    }
    return total;
};

最终的集合包含每一个标签的URL列表和表示该标签流行程度的分数。

 MongoDB和MapReduce

前面两个例子只用到了mapreduce、map和reduce键。这3个键是必需的,可是MapReduce命令还有不少可选的键。

  • "finalize" : function

能够将reduce的结果发送给这个键,这是整个处理过程的最后一步。

  • "keeptemp" : boolean

若是为值为true,那么在链接关闭时会将临时结果集合保存下来,不然不保存。

  • "out" : string

输出集合的名称。若是设置了这选项,系统会自动设置keeptemp : true。

  • "query" : document

在发往map函数前,先用指定条件过滤文档。

  • "sort" : document

在发往map前先给文档排序(与limit一同使用很是有用)。

  • "limit" : integer

发往map函数的文档数量的上限。

  • "scope" : document

能够在JavaScript代码中使用的变量。

  • "verbose" : boolean

是否记录详细的服务器日志。

1. finalize函数

和group命令同样,MapReduce也可使用finalize函数做为参数。它会在最后一个reduce输出结果后执行,而后将结果存到临时集合中。
返回体积比较大的结果集对MapReduce不是什么大不了的事情,由于它不像group那样有4 MB的限制。然而,信息老是要传递出去的,一般来讲,finalize是计算平均数、裁剪数组、清除多余信息的好时机。

2. 保存结果集合

默认状况下,Mongo会在执行MapReduce时建立一个临时集合,集合名是系统选的一个不太经常使用的名字,将"mr"、执行MapReduce的集合名、时间戳以及数据库做业ID,用“.”连成一个字符串,这就是临时集合的名字。结果产生形如mr.stuff.18234210220.2这样的名字。MongoDB会在调用的链接关闭时自动销毁这个集合(也能够在用完以后手动删除)。若是但愿保存这个集合,就要将keeptemp选项指定为true。
若是要常用这个临时集合,你可能想给它起个好点的名字。利用out选项(该选项接受字符串做为参数)就能够为临时集合指定一个易读易懂的名字。若是用了out选项,就没必要指定keeptemp : true了,由于指定out选项时系统会将keeptemp设置为true。即使你取了一个很是好的名字,MongoDB也会在MapReduce的中间过程使用自动生成的集合名。处理完成后,会自动将临时集合的名字更改成你指定的集合名,这个重命名的过程是原子性的。也就是说,若是屡次对同一个集合调用MapReduce,也不会在操做中遇到集合不完整的状况。
MapReduce产生的集合就是一个普通的集合,在这个集合上执行MapReduce彻底没有问题,或者在前一个MapReduce的结果上执行MapReduce也没有问题,如此往复直到无穷都没问题!

3. 对文档子集执行MapReduce

有时须要对集合的一部分执行MapReduce。只需在传给map函数前使用查询对文档进行过滤就行了。
每一个传递给map函数的文档都要先反序列化,从BSON对象转换为JavaScript对象,这个过程很是耗时。若是事先知道只须要对集合的一部分文档执行MapReduce,那么在map以前先对文档进行过滤能够极大地提升map速度。能够经过"query"、"limit"和"sort"等键对文档进行过滤。
"query"键的值是一个查询文档。一般查询返回的结果会传递给map函数。例如,有一个作跟踪分析的应用程序,如今咱们须要上周的总结摘要,只要使用以下命令对上周的文档执行MapReduce就行了:

> db.runCommand({"mapreduce" : "analytics", "map" : map, "reduce" : reduce,
                 "query" : {"date" : {"$gt" : week_ago}}})

sort选项和limit一块儿使用时一般可以发挥很是大的做用。limit也能够单独使用,用来截取一部分文档发送给map函数。
若是在上个例子中想分析最近10 000个页面的访问次数(而不是最近一周的),就可使用limit和sort:

> db.runCommand({"mapreduce" : "analytics", "map" : map, "reduce" : reduce,
                 "limit" : 10000, "sort" : {"date" : -1}})

query、limit、sort能够随意组合,可是若是不使用limit的话,sort就不能有效发挥做用。

4. 使用做用域

MapReduce能够为map、reduce、finalize函数都采用一种代码类型。但多数语言里,能够指定传递代码的做用域。然而MapReduce会忽略这个做用域。它有本身的做用域键"scope",若是想在MapReduce中使用客户端的值,则必须使用这个参数。能够用“变量名 : 值”这样的普通文档来设置该选项,而后在map、reduce和finalize函数中就能使用了。做用域在这些函数内部是不变的。例如,上一节的例子使用1/(newDate() - this.date)计算页面的新旧程度。能够将当前日期做为做用域的一部分传递进去:

> db.runCommand({"mapreduce" : "webpages", "map" : map, "reduce" : reduce,
                 "scope" : {now : new Date()}})

这样,在map函数中就能计算1/(now - this.date)了。

5. 得到更多的输出

还有个用于调试的详细输出选项。若是想看看MapReduce的运行过程,能够将"verbose"指定为true。
也能够用print把map、reduce、finalize过程当中的信息输出到服务器日志上。

上一篇文章: MongoDB指南---1六、聚合
下一篇文章: MongoDB指南---1八、聚合命令
相关文章
相关标签/搜索