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八、聚合命令
相關文章
相關標籤/搜索