上一篇文章: MongoDB指南---1六、聚合
下一篇文章: MongoDB指南---1八、聚合命令
MapReduce是聚合工具中的明星,它很是強大、很是靈活。有些問題過於複雜,沒法使用聚合框架的查詢語言來表達,這時可使用MapReduce。MapReduce使用JavaScript做爲「查詢語言」,所以它可以表達任意複雜的邏輯。然而,這種強大是有代價的:MapReduce很是慢,不該該用在實時的數據分析中。
MapReduce可以在多臺服務器之間並行執行。它會將一個大問題分割爲多個小問題,將各個小問題發送到不一樣的機器上,每臺機器只負責完成一部分工做。全部機器都完成時,再將這些零碎的解決方案合併爲一個完整的解決方案。
MapReduce須要幾個步驟。最開始是映射(map),將操做映射到集合中的每一個文檔。這個操做要麼「無做爲」,要麼「產生一些鍵和X個值」。而後就是中間環節,稱做洗牌(shuffle),按照鍵分組,並將產生的鍵值組成列表放到對應的鍵中。化簡(reduce)則把列表中的值化簡成一個單值。這個值被返回,而後接着進行洗牌,直到每一個鍵的列表只有一個值爲止,這個值也就是最終結果。
下面會多舉幾個MapReduce的例子,這個工具很是強大,但也有點複雜。web
用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返回的文檔包含不少與操做有關的元信息。框架
這是存放MapReduce結果的集合名。這是個臨時集合,MapReduce的鏈接關閉後它就被自動刪除了。本章稍後會介紹如何指定一個好一點的名字以及將結果集合持久化。函數
操做花費的時間,單位是毫秒。工具
這個內嵌文檔主要用做調試,其中包含3個鍵。網站
發送到map函數的文檔個數。this
在map函數中emit被調用的次數。
結果集合中的文檔數量。
對結果集合進行查詢會發現原有集合的全部鍵及其計數:
···
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的最終結果。
假設有個網站,人們能夠提交其餘網頁的連接,好比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列表和表示該標籤流行程度的分數。
前面兩個例子只用到了mapreduce、map和reduce鍵。這3個鍵是必需的,可是MapReduce命令還有不少可選的鍵。
能夠將reduce的結果發送給這個鍵,這是整個處理過程的最後一步。
若是爲值爲true,那麼在鏈接關閉時會將臨時結果集合保存下來,不然不保存。
輸出集合的名稱。若是設置了這選項,系統會自動設置keeptemp : true。
在發往map函數前,先用指定條件過濾文檔。
在發往map前先給文檔排序(與limit一同使用很是有用)。
發往map函數的文檔數量的上限。
能夠在JavaScript代碼中使用的變量。
是否記錄詳細的服務器日誌。
和group命令同樣,MapReduce也可使用finalize函數做爲參數。它會在最後一個reduce輸出結果後執行,而後將結果存到臨時集合中。
返回體積比較大的結果集對MapReduce不是什麼大不了的事情,由於它不像group那樣有4 MB的限制。然而,信息老是要傳遞出去的,一般來講,finalize是計算平均數、裁剪數組、清除多餘信息的好時機。
默認狀況下,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也沒有問題,如此往復直到無窮都沒問題!
有時須要對集合的一部分執行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就不能有效發揮做用。
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)了。
還有個用於調試的詳細輸出選項。若是想看看MapReduce的運行過程,能夠將"verbose"指定爲true。
也能夠用print把map、reduce、finalize過程當中的信息輸出到服務器日誌上。
上一篇文章: MongoDB指南---1六、聚合
下一篇文章: MongoDB指南---1八、聚合命令