在这篇文章里面,咱们会演示如何在 MongoDB 中使用 MapReduce 操做。
咱们会用 dummy-json
这个包来生成一些虚假的数据,而后用 Mongojs
javascript
若是想要快速看到结果,能够到 这里 里看看。java
MongoDB 是一个 NoSQL 数据库,不像 MySQL 、MSSQL 和 Oracle DB 那样,MongoDB 使用集合(collections) 来代替表(tables)。同时,它用集合中的文档(documents)来代替表中的行(rows)。还有最好的一点是,全部文档都保存成 JSON 格式!你能够到这里学更多关于 MongoDB 的知识。node
你能够从 这里 下载安装 MongoDB。git
若是之前没用过 MongoDB,那么你能够记住下面这些命令:github
Command | Result |
---|---|
mongod |
启动 MongoDB 服务 |
mongo |
进入 MongoDB Shell |
show dbs |
显示全部数据库列表 |
use <db name> |
进入指定的数据库 |
show collections |
进入数据库以后,显示该数据库中全部的集合 |
db.collectionName.find() |
显示该集合中全部文档 |
db.collectionName.findOne() |
显示该集合中第一个文档 |
db.collectionName.find().pretty() |
显示漂亮的 JSON 格式 |
db.collectionName.insert({key: value}) |
插入一条新的记录 |
db.collectionName.update({ condition: value}, {$set: {key: value}}, {upsert: true}) |
会更新指定的文档,设置指定的值。若是 upsert 为 true ,当没有找到匹配的文档时,会建立一条新的记录 |
db.collectionName.remove({}) |
移除集合中的全部文档 |
db.collectionName.remove({key: value}) |
移除集合中匹配到的文档 |
弄清楚 MapReduce 是如何运做的是很是重要的,若是对 MapReduce 过程不了解的话,你在运行 MapReduce 时极可能得不到你想要的结果。web
从 mongodb.org 上的解析:mongodb
Map-reduce 是一种数据处理范例,用于将大量的数据变成有用的聚合结果。 对于 map-reduce 操做,MongoDB 提供了 mapReduce 的数据库命令。shell
在这很是简单的术语里面,mapReduce 命令接受两个基本的输入:mapper 函数和 reducer 函数。数据库
Mapper 是一个匹配数据的过程,它会在集合中查询咱们想要处理的字段,而后根据咱们指定的 key 去分组,再把这些 key-value 对交给 reducer 函数,由它来处理这些匹配到的数据。npm
咱们来看看下面这些数据:
[ { name: foo, price: 9 }, { name: foo, price: 12 }, { name: bar, price: 8 }, { name: baz, price: 3 }, { name: baz, price: 5 } ]
咱们想要计算出相同名字下的所须要的价钱。咱们将会用这个数据经过 Mapper 和 Reducer 去得到结果。
当咱们让 Mapper 去处理上面的数据时,会生成以下的结果:
Key | Value |
---|---|
foo | [9,12] |
bar | [8] |
baz | [3,5] |
看到了吗?它用相同的 key 去分组数据。在咱们的例子中,是用 name 分组。这些结果会发送到 Reducer 中。
如今,在 reducer 中,咱们会获得上面表格中的第一行数据,而后迭代这些数据而后把它们加起来,这就是第一行数据的总和。而后 reducer 会对第二行数据作一样的事情,直到全部行被处理完。
最终的输出结果以下:
Name | Total |
---|---|
foo | 21 |
bar | 8 |
baz | 8 |
如今你明白为何 Mapper 会叫 Mapper 了吧 ! (由于它会建立一份数据的映射)
也明白了为何 Reducer 会叫 Reducer 了吧 ! (由于它会把 Mapper 生成的数据概括成一个简单的形式)
若是你运行一些例子,你就会知道它是怎么工做的拉。你也能够从官方文档 中了解更多细节。
正如上文所说,咱们能够在 mongo shell 中直接查询和看到输出结果。可是,为了让教程更加丰富,咱们会构建一个 Nodejs 项目,在里面运行咱们以前的任务。
咱们会用 mongojs
去实现咱们的 MapReduce。你能够用一样的代码跑在 mongo shell 里面,会看到一样的结果。
咱们会用 dummy-json
去建立一些虚假的数据。你能够在 这里 找到更多的信息。而后咱们会在这些虚假数据上面运行 MapReduce 命令,生成一些有意义的结果。
咱们开始吧!
首先,你要安装 Nodejs,你能够看看 这里。而后你要建立一个叫 mongoDBMapReduce 的目录。咱们将会建立 package.json
文件来保存项目的详细信息。
运行 npm init
而后填入你喜欢的东西,建立完 package.json
后,咱们要添加项目的依赖。
运行 npm i mongojs dummy-json --save-dev
,而后等几分钟以后,咱们项目的依赖就安装好了。
下一步,咱们要用 dummy-json
模块来生成虚假数据。
在项目的根目录建立一个名叫 dataGen.js
的文件,咱们会把数据生成的逻辑保存到一个独立的文件里面。若是之后须要添加更多的数据,你能够运行这个文件。
把下面的内容复制到 dataGen.js
里面:
var mongojs = require('mongojs'); var db = mongojs('mapReduceDB', ['sourceData']); var fs = require('fs'); var dummyjson = require('dummy-json'); var helpers = { gender: function() { return ""+ Math.random() > 0.5 ? 'male' : 'female'; }, dob : function() { var start = new Date(1900, 0, 1), end = new Date(); return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())); }, hobbies : function () { var hobbysList = []; hobbysList[0] = []; hobbysList[0][0] = ["Acrobatics", "Meditation", "Music"]; hobbysList[0][1] = ["Acrobatics", "Photography", "Papier-Mache"]; hobbysList[0][2] = [ "Papier-Mache"]; return hobbysList[0][Math.floor(Math.random() * hobbysList[0].length)]; } }; console.log("Begin Parsing >>"); var template = fs.readFileSync('schema.hbs', {encoding: 'utf8'}); var result = dummyjson.parse(template, {helpers: helpers}); console.log("Begin Database Insert >>"); db.sourceData.remove(function (argument) { console.log("DB Cleanup Completd"); }); db.sourceData.insert(JSON.parse(result), function (err, docs) { console.log("DB Insert Completed"); });
第1-4行,咱们引入了全部依赖。
第2行,咱们建立了一个叫 mapReduceDB
的数据库。在数据库里面,建立了一个叫 sourceData
的集合。
第6-23行,是 Handlebar 的 helper。你能够到 dummy-json
中了解更多信息。
第27-28行,咱们读取了 schema.hbs
文件 (咱们接着会建立这个文件),而后把它解析成 JSON。
第32行,在插入新数据以前,咱们要先把旧数据清除掉。若是你想保留旧数据,把这部分注释掉就行了。
第36行,把生成的数据插入数据库。
接着,咱们要在项目根目录建立一个叫 schema.hbs
的文件。这里面会包括 JSON 文档的结构。把下面的内容复制到文件里面:
[ {{#repeat 9999}} { "id": {{index}}, "name": "{{firstName}} {{lastName}}", "email": "{{email}}", "work": "{{company}}", "dob" : "{{dob}}", "age": {{number 1 99}}, "gender" : "{{gender}}", "salary" : {{number 999 99999}}, "hobbies" : "{{hobbies}}" } {{/repeat}} ]
注意 第2行,咱们会生成 9999 个文档。
打开一个新的终端,运行 mongod
,启动 MongoDB 服务。而后回到原来的终端,运行 node dataGen.js
。
若是一切正常,会显示以下结果:
$ node dataGen.js Begin Parsing >> Begin Database Insert >> DB Cleanup Completed DB Insert Completed
而后按 ctrl + c 杀掉 Node 程序。要验证是否插入成功,咱们能够打开一个新的终端,运行 mongo
命令进入 mongo shell。
> use mapReduceDB > db.sourceData.findOne() { "id": 0, "name": "Leanne Flinn", "email": "leanne.flinn@unilogic.com", "work": "Unilogic", "dob": "Sun Mar 14 1909 12:45:53 GTM+0530 (LST)", "age": 27, "gender": "male", "salary": 16660, "hobbies": "Acrobatics,Photography,Papier-Mache", "_id": Object("57579f702fa6c7651e504fe2") } > db.sourceData.count() 9999
如今咱们有 9999 个虚假用户的数据,让咱们试着把数据变得有意义
首先,在项目根目录建立一个 example1.js
的文件,咱们要进行 MapReduce 操做,去计算男女的数量。
咱们只须要让 Mapper 以性别做为 key,把值做为 1。由于一个用户不是男就是女。因此,Mapper 的输出会是下面这样:
Key | Value |
---|---|
Male | [1,1,1...] |
Female | [1,1,1,1,1...] |
在 Reducer 中,咱们会得到上面两行数据,咱们要作的是把每一行中的值求和,表示该性别的总数。最终的输出结果以下:
Key | Value |
---|---|
Male | 5031 |
Female | 4968 |
好了,如今咱们能够写代码去实现了。在 example1.js
中,咱们要先引入所须要的依赖。
var mongojs = require('mongojs'); var db = mongojs('mapReduceDB', ['sourceData', 'example1_results']);
注意 第2行,第一个参数是数据库的名字,第二个参数表示集合的数组。example1_results
集合用来保存结果。
接下来,咱们加上 mapper 和 reducer 函数:
var mapper = function () { emit(this.gender, 1); }; var reducer = function(gender, count){ return Array.sum(count); };
在第2行中, this
表示当前的文档,所以 this.gender
会做为 mapper 的 key,它的值要么是 male
,要么是 female
。而 emit()
将会把数据发送到一个临时保存数据的地方,做为 mapper 的结果。
在第5行中,咱们简单地把每一个性别的全部值加起来。
最后,加上执行逻辑:
db.sourceData.mapReduce( mapper, reducer, { out : "example1_results" } ); db.example1_results.find(function (err, docs) { if(err) console.log(err); console.log(docs); });
在第5行中,咱们设置了输出的集合名。
在第9行中,咱们会从 example1_results
集合取得结果并显示它。
咱们能够在终端运行试试:
$ node example1.js [ { _id: 'female', value: 4968 }, { _id: 'male': value: 5031 } ]
个人数量可能和你的不同,但男女总数应该是 9999 !
若是你想在 mongo shell 中运行上面的例子,你能够粘贴下面这些代码到终端里面:
mapper = function () { emit(this.gender, 1); }; reducer = function(gender, count){ return Array.sum(count); }; db.sourceData.mapReduce( mapper, reducer, { out : "example1_results" } ); db.example1_results.find()
而后你就会看到同样的结果,很简单吧!
在项目根目录建立一个 example2.js
的文件。在这里,咱们要把全部用户根据性别分组,而后分别找每一个性别中最老和最年轻的用户。这个例子比前面的稍微复杂一点。
在 mapper 中,咱们要以性别做为 key,而后以 object 做为 value。这个 object 要包含用户的年龄和名字。年龄是用来作计算用的,而名字只是用来显示给人看的。
Key | Value |
---|---|
Male | [{age: 9, name: 'John'}, ...] |
Female | [{age: 19, name: 'Rita'}, ...] |
咱们的 reducer 会比前一个例子要复杂一点。咱们要检查全部和性别相关的年龄,找到年龄最大和最小的用户。最终的输出结果是这样的:
Key | Value |
---|---|
Male | {min: {name: 'harry', age: 1}, max: {name: 'Alex', age: 99} } |
Female | {min: {name: 'Loli', age: 10}, max: {name: 'Mary', age: 98} } |
如今打开 example2.js
,粘贴下面的内容进去:
var mongojs = require('mongojs'); var db = mongojs('mapReduceDB', ['sourceData', 'example2_results']); var mapper = function () { var x = {age : this.age, name : this.name}; emit(this.gender, {min : x , max : x}); }; var reducer = function(key, values){ var res = values[0]; for (var i = 1; i < values.length; i++) { if(values[i].min.age < res.min.age) res.min = {name : values[i].min.name, age : values[i].min.age}; if (values[i].max.age > res.max.age) res.max = {name : values[i].max.name, age : values[i].max.age}; }; return res; }; db.sourceData.mapReduce( mapper, reducer, { out : "example2_results" } ); db.example2_results.find(function (err, docs) { if(err) console.log(err); console.log(JSON.stringify(docs)); });
在第6行,咱们构建了一个 object,把它做为 value 发送。
在第13-18行,咱们迭代了全部 object,检查当前的 object 的年龄是否大于或小于前一个 object 的年龄,若是是,就会更新 res.max
或者 res.min
。
在第第27行,咱们把结果输出到 example2_results
中。
咱们能够运行一下这个例子:
$ node example2.js [ { _id: 'female', value: { min: [Object], max: [Object] } }, { _id: 'male', value: { min: [Object], max: [Object] } } ]
在咱们最后的例子中,咱们会看看有多少用户有相同的兴趣爱好。咱们在项目根目录建立一个叫 example3.js
的文件。用户数据长这样子:
{ "id": 0, "name": "Leanne Flinn", "email": "leanne.flinn@unilogic.com", "work": "Unilogic", "dob": "Sun Mar 14 1909 12:45:53 GTM+0530 (LST)", "age": 27, "gender": "male", "salary": 16660, "hobbies": "Acrobatics,Photography,Papier-Mache", "_id": Object("57579f702fa6c7651e504fe2") }
如你所见,每一个用户的兴趣爱好列表都用逗号分隔。咱们会找出有多少用户有表演杂技的爱好等等。
在这个场景下,咱们的 mapper 会复杂一点。咱们要为每一个用户的兴趣爱好发送一个新的 key-value 对。这样,每一个用户的每一个兴趣爱好都会触发一次计算。最终咱们会获得以下的结果:
Key | Value |
---|---|
Acrobatics | [1,1,1,1,1,1,….] |
Meditation | [1,1,1,1,1,1,….] |
Music | [1,1,1,1,1,1,….] |
Photography | [1,1,1,1,1,1,….] |
Papier-Mache | [1,1,1,1,1,1,….] |
在这里,咱们只要简单地为每种兴趣爱好求和就行了。最终咱们会获得下面的结果:
Key | Value |
---|---|
Acrobatics | 6641 |
Meditation | 3338 |
Music | 3338 |
Photography | 3303 |
Papier-Mache | 6661 |
var mongojs = require('mongojs'); var db = mongojs('mapReduceDB', ['sourceData', 'example3_results']); var mapper = function () { var hobbys = this.hobbies.split(','); for (i in hobbys) { emit(hobbys[i], 1); } }; var reducer = function (key, values) { var count = 0; for (index in values) { count += values[index]; } return count; }; db.sourceData.mapReduce( mapper, reducer, { out : "example3_results" } ); db.example3_results.find(function (err, docs) { if(err) console.log(err); console.log(docs); });
注意第7-9行,咱们迭代了每一个兴趣爱好,而后发送了一次记数。
第13-18行能够用 Array.sum(values)
来代替,这样是另一种作相同事情的方式。最终咱们获得的结果:
$ node example3.js [ { _id: 'Acrobatics', value: 6641 }, { _id: 'Meditation', value: 3338 }, { _id: 'Music', value: 3338 }, { _id: 'Photography', value: 6661 }, { _id: 'Papier-Mache', value: 3303 } ]
这就是 MongoDB 中运行 MapReduce 的方法了。但要记住,有时候一个简单的查询就能完成你想要的事情的。
http://scarletsky.github.io/2016/06/12/mapreduce-in-mongodb/