从 WordCount 到文档的倒排索引详解
概述
倒排索引源于實際應用中需要根據屬性的值來查找記錄。這種索引表中的每一項都包括一個屬性值和具有該屬性值的各記錄的地址。由于不是由記錄來確定屬性值,而是由屬性值來確定記錄的位置,因而稱為倒排索引(inverted index)。帶有倒排索引的文件我們稱為倒排索引文件,簡稱倒排文件(inverted file)。
——摘自《百度百科》
版權說明
著作權歸作者所有。
商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
本文作者:Q-WHai
發表日期: 2016年6月13日
本文鏈接:https://qwhai.blog.csdn.net/article/details/51659657
來源:CSDN
更多內容:分類 >> 大數據之 Hadoop
前言
在很多大數據的應用場景中我們都有可能看到倒排索引的身影,我第一次接觸倒排索引是在學習 Lucene 全文檢索框架的時候。本文會從倒排索引開始說明,再補充講解倒排索引文檔及帶權重的倒排索引文檔。你是不是想說這些不都是同一個東西么?顯然,他們不是同一個東西。
倒排索引
根據上面的概述所言,我相信你應該已經對倒排索引的原理有了一個初步的認識。如果你有一些編程的功底,那么基于某種編程語言寫一個倒排索引的程序應該不難。只是如何將這個倒排索引程序翻譯成 Hadoop 中 的 MapReduce 程序呢?這是需要探討的問題。
如果你理解了前面的 WordCount 程序的話,尤其是 Mapper 和 Reducer 的過程,那么倒排索引對你來說也就是一菜一碟了。
輸入的文件格式
要讓 MapReduce 程序正確的運行,我們首先要確保 MapReduce 的輸入是正確的。反映在文件中就是文件的格式需要提前被規定好。現在我們的文件輸入格式定義如下:
<key_name>:<value1> <value2> <value3> ... <value4>在輸入的格式中,我們把 key 與 values 用英文冒號( : )分隔開了。而 value 與 value 之間則是使用空格分隔開。
假設這里的 key 是一個代表文件的文件名稱,后面的 values 則是文件中的內容(單詞)。根據格式我們編寫了如下的幾組測試樣例:
file01
file02
file02:hi today funny dayfile03
file03:face day face world分布式運行的過程
對于文件從本地上傳到 HDFS 的過程不是本文的討論范圍,如果你感興趣可以查閱相關資料,理解上也并不困難。
當倒排索引的 MR 程序運行時,其過程大致可以用下圖進行表示:
MapReduce 程序編寫
這里的代碼邏輯與代碼結構與之前的 WordCount 程序有很多相似的地方。而與 WordCount 程序不同的是,InvertedIndex 在 Mapper 中需要對文件中的內容進行區別對待,因為文件的最開始是文件名的 key;在 Reducer 中累加的不是單詞的個數,而是 value 字符串的適當疊加。具體代碼如下:
import java.io.IOException; import java.util.StringTokenizer;import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Mapper; import org.apache.hadoop.mapreduce.Reducer;public class InvertedCoreMR {public static class CoreMapper extends Mapper<Object, Text, Text, Text> {@Overrideprotected void map(Object key, Text value, Mapper<Object, Text, Text, Text>.Context context)throws IOException, InterruptedException {String[] splits = value.toString().split(":");StringTokenizer tokenizer = new StringTokenizer(splits[1]);while (tokenizer.hasMoreTokens()) {context.write(new Text(tokenizer.nextToken()), new Text(splits[0]));}}}public static class CoreReducer extends Reducer<Text, Text, Text, Text> {@Overrideprotected void reduce(Text key, Iterable<Text> values, Reducer<Text, Text, Text, Text>.Context context)throws IOException, InterruptedException {if (null == values) {return;}StringBuffer filesBuffer = new StringBuffer();boolean firstFlag = true;for (Text invertedFile : values) {filesBuffer.append((firstFlag ? "" : ", ") + invertedFile.toString());firstFlag = false;}context.write(key, new Text(filesBuffer.toString()));}} }InvertedIndex 的客戶端程序與 WordCount 的客戶端基本上是一樣的。
import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Job; import org.apache.hadoop.mapreduce.lib.input.FileInputFormat; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; import org.apache.hadoop.util.Tool; import org.apache.hadoop.util.ToolRunner; import org.demo.index.inverted.core.InvertedCoreMR;public class InvertedClient extends Configuration implements Tool {public static void main(String[] args) {InvertedClient client = new InvertedClient();try {ToolRunner.run(client, args);} catch (Exception e) {e.printStackTrace();}}@Overridepublic Configuration getConf() {return this;}@Overridepublic void setConf(Configuration arg0) {}@Overridepublic int run(String[] args) throws Exception {Job job = new Job(getConf(), "File Inverted Index");job.setJarByClass(InvertedCoreMR.class);job.setMapperClass(InvertedCoreMR.CoreMapper.class);job.setReducerClass(InvertedCoreMR.CoreReducer.class);job.setOutputKeyClass(Text.class);job.setOutputValueClass(Text.class);FileInputFormat.addInputPath(job, new Path(args[0]));FileOutputFormat.setOutputPath(job, new Path(args[1]));return job.waitForCompletion(true) ? 0 : 1;} }結果展示
day file02, file03 face file03, file03 funny file02 hello file01, file01 hi file02 today file02, file01 world file01, file03優化
在上面的結果展示中可以看到 face 和 hello 這兩個單詞分別出現在了兩個相同的文件中。這不是一個好的用戶體驗,所以需要優化。優化的邏輯就是過濾。如果你有一些項目經驗的話,你可能會已經想到要 BloomFilter 或是字典樹之類的。這兩種過濾方案的確是很好的,只是這里我就不使用 BloomFilter 和字典樹了,感覺有一些“大材小用”了。直接使用 Java 提供的 HashSet 會更好一些。我一直比較提倡的做法就是,不要在一個小功能上使用大工具或是大框架,這樣會讓你的程序顯得臃腫肥大,且沒有什么實用價值。
可能你會說這里不應該直接使用過濾,應該進行詞頻的統計。是的,的確是要做詞頻統計,你先別急,咱們一步步來,后面會作介紹的。現在只對重復的文件名進行過濾就 ok 了。
修改之后,結果就要好用得多了。
day file03, file02 face file03 funny file02 hello file01 hi file02 today file02, file01 world file01, file03倒排索引文檔
在上面的倒排索引中,我們是人為給文件添加 key,也就是文件名來達到單詞映射文件名的目的。可是,如果我們的輸入文件中并不符合我們 MapReduce 程序的格式要求,那么之前的做法就與我們的愿望相悖了。我們要讓文檔檢索的時候更具一般性,那么就不能限定文件名,而是應該讓程序去動態獲取。
所以,這里我們首先介紹一下在 MapReduce 程序中如何動態獲取文件名,這一點是關鍵。
動態獲取文件名需要用到的兩個類分別是:InputSplit, FileSplit
它們所在的包名分別為:
我們可以在 Mapper 中通過如下兩句代碼獲取文件名:
InputSplit inputSplit = context.getInputSplit(); String fileName = ((FileSplit)inputSplit).getPath().toString();于是,我們修改了 CoreMapper.map() 方法:
public static class CoreMapper extends Mapper<Object, Text, Text, Text> {@Overrideprotected void map(Object key, Text value, Mapper<Object, Text, Text, Text>.Context context)throws IOException, InterruptedException {InputSplit inputSplit = context.getInputSplit();String fileName = ((FileSplit)inputSplit).getPath().toString();StringTokenizer tokenizer = new StringTokenizer(value.toString());Set<String> filterSet = new HashSet<>();while (tokenizer.hasMoreTokens()) {String label = tokenizer.nextToken();if (filterSet.contains(label)) {continue;}filterSet.add(label);context.write(new Text(label), new Text(fileName));}} }運行 Hadoop 程序,然后,我們就可以得到如下的結果:
day file:/home/hadoop/temp/inverted/file03, file:/home/hadoop/temp/inverted/file02 face file:/home/hadoop/temp/inverted/file03 funny file:/home/hadoop/temp/inverted/file02 hello file:/home/hadoop/temp/inverted/file01 hi file:/home/hadoop/temp/inverted/file02 today file:/home/hadoop/temp/inverted/file02, file:/home/hadoop/temp/inverted/file01 world file:/home/hadoop/temp/inverted/file01, file:/home/hadoop/temp/inverted/file03這樣我們就完成了更加一般性的倒排索引文檔方案,程序的運行結果也展示了我們想要達到的效果。
帶權重的倒排索引文檔
上面對文檔中的重復數據是采用 HashSet 過濾,而在實際應用中我們不能這樣一概而論。比如,文檔 Doc1 中單詞總數為 100 個,單詞 word1 的個數為 20 個,比率為 20% ;而文檔 Doc2 中的單詞總數為 1000 個,單詞 word1 的個數為 30 個,比率為 3%。如果我們不進行詞頻統計,那么這兩個文檔中 word1 的重要性是一樣的。這顯然與實際情況相悖。
所以這里我們還需要對每個單詞進行詞頻統計。
我們修改了 Mapper.map() 方法,具體代碼如下:
上面代碼的邏輯就是,如果發現一個單詞已經存在就將其記數器 +1. 如果單詞不存在,就新建記數器。記數器的選擇是 HashMap,這很好用,而且方便輕巧。
在 Reducer.reduce() 方法中需要修改的地方不多,因為要展示詞頻,所以首先要獲取詞頻,并把詞頻在結果中顯示出來即可。具體代碼如下:
public static class CoreReducer extends Reducer<Text, Text, Text, Text> {@Overrideprotected void reduce(Text key, Iterable<Text> values, Reducer<Text, Text, Text, Text>.Context context)throws IOException, InterruptedException {if (null == values) {return;}StringBuffer filesBuffer = new StringBuffer();boolean firstFlag = true;Set<String> filterSet = new HashSet<String>();for (Text invertedFile : values) {String fileName = invertedFile.toString().split(",")[0];int wordFreq = Integer.parseInt(invertedFile.toString().split(",")[1]);if (filterSet.contains(fileName)) {continue;}filterSet.add(fileName);filesBuffer.append((firstFlag ? "[" : ", [") + fileName + " : " + wordFreq + "]");firstFlag = false;}context.write(key, new Text(filesBuffer.toString()));} }修改后的結果展示
day [file:/home/hadoop/temp/inverted/file02 : 1], [file:/home/hadoop/temp/inverted/file03 : 1] face [file:/home/hadoop/temp/inverted/file03 : 2] funny [file:/home/hadoop/temp/inverted/file02 : 1] hello [file:/home/hadoop/temp/inverted/file01 : 2] hi [file:/home/hadoop/temp/inverted/file02 : 1] today [file:/home/hadoop/temp/inverted/file02 : 1], [file:/home/hadoop/temp/inverted/file01 : 1] world [file:/home/hadoop/temp/inverted/file01 : 1], [file:/home/hadoop/temp/inverted/file03 : 1]上面就是整個倒排索引的全部內容了,如果你任何疑問,歡迎留言。一起討論,一起進步。后面,我將更新 N 個 MapReduce 共同執行和 TF-IDF 的 MapReduce 實現(網絡上的那些 MR 版的 TF-IDF 文章有點看不下去了。。。),敬請期待。
GitHub
- https://github.com/Hadoop-league/InvertedIndex
征集
如果你也需要使用ProcessOn這款在線繪圖工具,可以使用如下邀請鏈接進行注冊:
https://www.processon.com/i/56205c2ee4b0f6ed10838a6d
總結
以上是生活随笔為你收集整理的从 WordCount 到文档的倒排索引详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: HBase Shell 的基本操作
- 下一篇: MapReduce进阶:多MapRedu