摘要: 爲MongoDB中的數據構建倒排索引(Inverted Index),而後緩存到內存中,能夠大幅提高搜索性能。本文將經過爲電影數據構建演員索引,介紹兩種構建倒排索引的方法:MapReduce和Aggregation Pipeline。html
GitHub地址:git
做者: KiwenLaugithub
日期: 2016-09-11算法
倒排索引(Inverted Index),也稱爲反向索引,維基百科的定義是這樣的:mongodb
是一種索引方法,被用來存儲在全文搜索下某個單詞在一個文檔或者一組文檔中的存儲位置的映射。數據庫
這個定義比較學術,也就是比較反人類,忽略...編程
倒排索引是搜索引擎中的核心數據結構。搜索引擎的爬蟲獲取的網頁數據能夠視爲鍵值對,其中,Key是網頁地址(url),而Value是網頁內容。網頁的內容是由不少關鍵詞(word)組成的,能夠視爲關鍵詞數組。所以,爬蟲獲取的網頁數據能夠這樣表示:數組
<url1, [word2, word3]> <url2, [word2]> <url3, [word1, word2]>
可是,用戶是經過關鍵詞進行搜索的,直接使用原始數據進行查詢的話則須要遍歷全部鍵值對中的關鍵詞數組,效率是很是低的。緩存
所以,用於搜索的數據結構應該以關鍵詞(word)爲Key,以網頁地址(url)爲Value:數據結構
<word1, [url3]> <word2, [ur1, url2, url3]> <word3, [url1]>
這樣的話,查詢關鍵詞word2,當即可以獲取結果: [ur1, url2, url3]。
簡單地說,倒排索引就是把Key與Value對調以後的索引,構建倒排索引的目的是提高搜索性能。
MongoDB是文檔型數據庫,其數據有三個層級: 數據庫(database),集合(collection)和文檔(document),分別對應關係型數據庫中的三個層級的: 數據庫(database), 表(table),行(row)。MongDB中每一個的文檔是一個JSON文件,例如,本文使用的movie集合中的一個文檔以下所示:
{ "_id" : ObjectId("57d02d60b128567fc130287d"), "movie" : "Pride & Prejudice", "starList" : [ "Keira Knightley", "Matthew Macfadyen" ], "__v" : 0 }
該文檔一共有4個屬性:
_id: 文檔ID,由MongoDB自動生成。
__v: 文檔版本,由MongoDB的NodeJS接口Mongoose自動生成。
movie: 電影名稱。
starList: 演員列表。
可知,這個文檔表示電影《傲慢與偏見》,由女神凱拉·奈特莉主演。
忽略_id
與__v
,movie集合的數據以下:
{ "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"] }
其中,Key爲電影名稱(movie),而Value爲演員列表(starList)。
這時查詢Keira Knightley所主演的電影的NodeJS代碼以下:
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); });
注:本文全部代碼使用了MongoDB的NodeJS接口Mongoose,它與MongoDB Shell的接口基本一致。
代碼並不複雜,可是數據量大時查詢性能會不好,由於這個查詢須要:
遍歷整個movie集合的全部文檔
遍歷每一個文檔的startList數組
構建倒排索引能夠有效地提高搜索性能。本文將介紹MongoDB中兩種構建倒排索引的方法:MapReduce與Aggregation Pipeline。
MapReduce是由谷歌提出的編程模型,適用於多種大數據處理場景,在搜索引擎中,MapReduce能夠用於構建網頁數據的倒排索引,也能夠用於編寫網頁排序算法PageRank(由谷歌創始人佩奇和布林提出)。
MapReduce的輸入數據與輸出數據均爲鍵值對。MapReduce分爲兩個函數: Map與Reduce。
Map函數將輸入鍵值<k1, v1>
對進行變換,輸出中間鍵值對<k2, v2>
。
MapReduce框架會自動對中間鍵值對<k2, v2>
進行分組,Key相同的鍵值對會被合併爲一個鍵值對<k2, list(v2)>
。
Reduce函數對<k2, list(v2)>
的Value進行合併,生成結果鍵值對<k2, v3>
。
使用MapReduce構建倒排索引的NodeJS代碼以下:
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); });
代碼解釋:
Map函數的輸入數據是Movie集合中的各個文檔,在代碼中用this表示。文檔的movie與starList屬性構成鍵值對<movie, starList>
。Map函數遍歷starList,爲每一個start生成鍵值對<star, movieList>
。這時Key與Value進行了對調,且starList被拆分了,movieList僅包含單個movie。
MongoDB的MapReduce執行框架對成鍵值對<star, movieList>
進行分組,star相同的鍵值對會被合併爲一個鍵值對<star, list(movieList)>
。這一步是自動進行的,所以在代碼中並無體現。
Reduce函數的輸入數據是鍵值對<star, list(movieList)>
,在代碼中,star即爲key,而list(movieList)即爲values,二者爲Reduce函數的參數。Reduce函數合併list(movieList),從而獲得鍵值對<star, movieList>
,最終,movieList中將包含該star的全部movie。
在代碼中,Map函數與Reduce返回的鍵值對中的Value是一個對象{ movieList: movieList }
,而不是數組movieList
,所以代碼和結果都顯得比較奇怪。MongoDB的MapReduce框架不支持Reduce函數返回數組,所以只能將movieList放在對象裏面返回。
輸出結果:
[ { "_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" ] } } ]
Aggregation Pipeline,中文稱做聚合管道,用於彙總MongoDB中多個文檔中的數據,也能夠用於構建倒排索引。
Aggregation Pipeline進行各類聚合操做,而且能夠將多個聚合操做組合使用,相似於Linux中的管道操做,前一個操做的輸出是下一個操做的輸入。
使用Aggregation Pipeline構建倒排索引的NodeJS代碼以下:
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); });
代碼解釋:
$unwind: 將starList拆分,輸出結果(忽略_id
與__v
)爲:
[ { "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" } ]
$group: 根據文檔的starList屬性進行分組,而後將分組文檔的movie屬性合併爲movieList,輸出結果爲:
[ { "_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" ] } ]