MongoDB优化之倒排索引
2011037399
8年前
<p>摘要:为MongoDB中的数据构建倒排索引(Inverted Index),然后缓存到内存中,可以大幅提升搜索性能。本文将通过为电影数据构建演员索引,介绍两种构建倒排索引的方法: <a href="/misc/goto?guid=4959715429719036431" rel="nofollow,noindex">MapReduce</a> 和 <a href="/misc/goto?guid=4959715429810600774" rel="nofollow,noindex">Aggregation Pipeline</a> 。</p> <p> </p> <p style="text-align:center"><img src="https://simg.open-open.com/show/6771416ef9a5c79237136bd4ea4e4fb1.png"></p> <h2><strong>一. 倒排索引</strong></h2> <p>倒排索引(Inverted Index),也称为反向索引, <a href="/misc/goto?guid=4959715429890902720" rel="nofollow,noindex">维基百科</a> 的定义是这样的:</p> <p>是一种索引方法,被用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置的映射。</p> <p>这个定义比较学术,也就是比较反人类,忽略...</p> <p>倒排索引是搜索引擎中的核心数据结构。搜索引擎的爬虫获取的网页数据可以视为键值对,其中,Key是网页地址(url),而Value是网页内容。网页的内容是由很多关键词(word)组成的,可以视为关键词数组。因此,爬虫获取的网页数据可以这样表示:</p> <pre> <code class="language-javascript"><url1, [word2, word3]> <url2, [word2]> <url3, [word1, word2]></code></pre> <p>但是,用户是通过关键词进行搜索的,直接使用原始数据进行查询的话则需要遍历所有键值对中的关键词数组,效率是非常低的。</p> <p>因此,用于搜索的数据结构应该以关键词(word)为Key,以网页地址(url)为Value:</p> <pre> <code class="language-javascript"><word1, [url3]> <word2, [ur1, url2, url3]> <word3, [url1]></code></pre> <p>这样的话,查询关键词word2,立即能够获取结果: [ur1, url2, url3]。</p> <p>简单地说, <strong>倒排索引就是把Key与Value对调之后的索引</strong> ,构建倒排索引的目的是提升搜索性能。</p> <h2><strong>二. 测试数据</strong></h2> <p><a href="/misc/goto?guid=4959715429977860337" rel="nofollow,noindex">MongoDB</a> 是文档型数据库,其数据有三个层级: 数据库(database),集合(collection)和文档(document),分别对应关系型数据库中的三个层级的: 数据库(database), 表(table),行(row)。MongDB中每个的文档是一个JSON文件,例如,本文使用的movie集合中的一个文档如下所示:</p> <pre> <code class="language-javascript">{ "_id" : ObjectId("57d02d60b128567fc130287d"), "movie" : "Pride & Prejudice", "starList" : [ "Keira Knightley", "Matthew Macfadyen" ], "__v" : 0 }</code></pre> <p>该文档一共有4个属性:</p> <ul> <li> <p>_id: 文档ID,由MongoDB自动生成。</p> </li> <li> <p>__v: 文档版本,由MongoDB的NodeJS接口Mongoose自动生成。</p> </li> <li> <p>movie: 电影名称。</p> </li> <li> <p>starList: 演员列表。</p> </li> </ul> <p>可知,这个文档表示电影 <a href="/misc/goto?guid=4959715430061464666" rel="nofollow,noindex">《傲慢与偏见》</a> ,由女神 <a href="/misc/goto?guid=4959715430144829095" rel="nofollow,noindex">凯拉·奈特莉</a> 主演。</p> <p>忽略 _id 与 __v ,movie集合的数据如下:</p> <pre> <code class="language-javascript">{ "movie": "Pride & Prejudice", "starList": ["Keira Knightley", "Matthew Macfadyen"] }, { "movie": "Begin Again", "starList": ["Keira Knightley", "Mark Ruffalo"] }, { "movie": "The Imitation Game", "starList": ["Keira Knightley", "Benedict Cumberbatch"] }</code></pre> <p>其中,Key为电影名称(movie),而Value为演员列表(starList)。</p> <p>这时查询Keira Knightley所主演的电影的NodeJS <a href="/misc/goto?guid=4959715430215763599" rel="nofollow,noindex">代码</a> 如下:</p> <pre> <code class="language-javascript">Movie.find( { starList: "Keira Knightley" }, { _id: 0, movie: 1 }, function(err, results) { if (err) { console.log(err); process.exit(1); } console.log("search movie success:\n"); console.log(JSON.stringify(results, null, 4)); process.exit(0); });</code></pre> <ul> <li> <p>注:本文所有代码使用了MongoDB的NodeJS接口 <a href="/misc/goto?guid=4958534319495195936" rel="nofollow,noindex">Mongoose</a> ,它与MongoDB Shell的接口基本一致。</p> </li> </ul> <p>代码并不复杂,但是数据量大时查询性能会很差,因为这个查询需要:</p> <ul> <li> <p>遍历整个movie集合的所有文档</p> </li> <li> <p>遍历每个文档的startList数组</p> </li> </ul> <p>构建倒排索引可以有效地提升搜索性能。本文将介绍MongoDB中两种构建倒排索引的方法: <a href="/misc/goto?guid=4959715429719036431" rel="nofollow,noindex">MapReduce</a> 与 <a href="/misc/goto?guid=4959715429810600774" rel="nofollow,noindex">Aggregation Pipeline</a> 。</p> <h2><strong>三 MapReduce</strong></h2> <p><a href="/misc/goto?guid=4958540746211559437" rel="nofollow,noindex">MapReduce</a> 是由谷歌提出的编程模型,适用于多种大数据处理场景,在搜索引擎中,MapReduce可以用于构建网页数据的倒排索引,也可以用于编写网页排序算法PageRank(由谷歌创始人佩奇和布林提出)。</p> <p>MapReduce的输入数据与输出数据均为键值对。MapReduce分为两个函数: Map与Reduce。</p> <ul> <li> <p>Map函数将输入键值 <k1, v1> 对进行变换,输出中间键值对 <k2, v2> 。</p> </li> <li> <p>MapReduce框架会自动对中间键值对 <k2, v2> 进行分组,Key相同的键值对会被合并为一个键值对 <k2, list(v2)> 。</p> </li> <li> <p>Reduce函数对 <k2, list(v2)> 的Value进行合并,生成结果键值对 <k2, v3> 。</p> </li> </ul> <p>使用MapReduce构建倒排索引的NodeJS <a href="/misc/goto?guid=4959715430380452000" rel="nofollow,noindex">代码</a> 如下:</p> <pre> <code class="language-javascript">var option = {}; option.map = function() { var movie = this.movie; this.starList.forEach(function(star) { emit(star, { movieList: [movie] }); }); }; option.reduce = function(key, values) { var movieList = []; values.forEach(function(value) { movieList.push(value.movieList[0]); }); return { movieList: movieList }; }; Movie.mapReduce(option, function(err, results) { if (err) { console.log(err); process.exit(1); } console.log("create inverted index success:\n"); console.log(JSON.stringify(results, null, 4)); process.exit(0); });</code></pre> <p>代码解释:</p> <ul> <li> <p>Map函数的输入数据是Movie集合中的各个文档,在代码中用this表示。文档的movie与starList属性构成键值对 <movie, starList> 。Map函数遍历starList,为每个start生成键值对 <star, movieList> 。这时Key与Value进行了对调,且starList被拆分了,movieList仅包含单个movie。</p> </li> <li> <p>MongoDB的MapReduce执行框架对成键值对 <star, movieList> 进行分组,star相同的键值对会被合并为一个键值对 <star, list(movieList)> 。这一步是自动进行的,因此在代码中并没有体现。</p> </li> <li> <p>Reduce函数的输入数据是键值对 <star, list(movieList)> ,在代码中,star即为key,而list(movieList)即为values,两者为Reduce函数的参数。Reduce函数合并list(movieList),从而得到键值对 <star, movieList> ,最终,movieList中将包含该star的所有movie。</p> </li> </ul> <p>在代码中,Map函数与Reduce返回的键值对中的Value是一个对象 { movieList: movieList } ,而不是数组 movieList ,因此代码和结果都显得比较奇怪。MongoDB的MapReduce框架不支持Reduce函数返回数组,因此只能将movieList放在对象里面返回。</p> <p>输出结果:</p> <pre> <code class="language-javascript">[ { "_id": "Benedict Cumberbatch", "value": { "movieList": [ "The Imitation Game" ] } }, { "_id": "Keira Knightley", "value": { "movieList": [ "Pride & Prejudice", "Begin Again", "The Imitation Game" ] } }, { "_id": "Mark Ruffalo", "value": { "movieList": [ "Begin Again" ] } }, { "_id": "Matthew Macfadyen", "value": { "movieList": [ "Pride & Prejudice" ] } } ]</code></pre> <h2><strong>四. Aggregation Pipeline</strong></h2> <p><a href="/misc/goto?guid=4959715429810600774" rel="nofollow,noindex">Aggregation Pipeline</a> ,中文称作聚合管道,用于汇总MongoDB中多个文档中的数据,也可以用于构建倒排索引。</p> <p>Aggregation Pipeline进行各种 <a href="/misc/goto?guid=4959715430472867849" rel="nofollow,noindex">聚合操作</a> ,并且可以将多个聚合操作组合使用,类似于Linux中的管道操作,前一个操作的输出是下一个操作的输入。</p> <p>使用Aggregation Pipeline构建倒排索引的NodeJS <a href="/misc/goto?guid=4959715430554142999" rel="nofollow,noindex">代码</a> 如下:</p> <pre> <code class="language-javascript">Movie.aggregate([ { "$unwind": "$starList" }, { "$group": { "_id": "$starList", "movieList": { "$push": "$movie" } } }, { "$project": { "_id": 0, "star": "$_id", "movieList": 1 } }], function(err, results) { if (err) { console.log(err); process.exit(1); } console.log("create inverted index success:\n"); console.log(JSON.stringify(results, null, 4)); process.exit(0); });</code></pre> <p>代码解释:</p> <ul> <li> <p>$unwind: 将starList拆分,输出结果(忽略 _id 与 __v )为:</p> </li> </ul> <pre> <code class="language-javascript">[ { "movie": "Pride & Prejudice", "starList": "Keira Knightley" }, { "movie": "Pride & Prejudice", "starList": "Matthew Macfadyen" }, { "movie": "Begin Again", "starList": "Keira Knightley" }, { "movie": "Begin Again", "starList": "Mark Ruffalo" }, { "movie": "The Imitation Game", "starList": "Keira Knightley" }, { "movie": "The Imitation Game", "starList": "Benedict Cumberbatch" } ]</code></pre> <ul> <li> <p>$group: 根据文档的starList属性进行分组,然后将分组文档的movie属性合并为movieList,输出结果为:</p> </li> </ul> <pre> <code class="language-javascript">[ { "_id": "Benedict Cumberbatch", "movieList": [ "The Imitation Game" ] }, { "_id": "Matthew Macfadyen", "movieList": [ "Pride & Prejudice" ] }, { "_id": "Mark Ruffalo", "movieList": [ "Begin Again" ] }, { "_id": "Keira Knightley", "movieList": [ "Pride & Prejudice", "Begin Again", "The Imitation Game" ] } ]</code></pre> <p> </p> <p>来自:https://segmentfault.com/a/1190000006885552</p> <p> </p>