大数据管理与分析实验报告
实验目的
倒排索引(Inverted Index)被用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置的映射,是目前几乎所有支持全文索引的搜索引擎都需要依赖的一个数据结构。通过对倒排索引的编程实现,熟练掌握MapReduce 程序在集群上的提交与执行过程,加深对MapReduce 编程框架的理解。
实验平台
- 操作系统:Ubuntu Kylin
- Hadoop 版本:2.10.1
- JDK 版本:1.8
- Java IDE:Eclipse 3.8
实验内容
在本地eclipse 上编写带词频属性的对英文文档的文档倒排索引程序,要求程序能够实现对stop-words(如a,an,the,in,of 等词)的去除,能够统计单词在每篇文档中出现的频率。
实验要求
实验结果的输出类似如下格式:
标准输出存放在hdfs 上/output 目录,使用diff 命令判断自己的输出结果与标准输出的差异
diff <(hdfs dfs -cat /output/part-r-00000) <(cat /home/用户名/Desktop/part-r-00000)
实验思路
最简单的倒排索引和WordCount难度相近,仅仅是输出的value改为了word所在document,而document的名称可由FileSplit获取到。这里涉及到MapReduce的工作流程,一个split里的所有内容必定是属于同一document的,所以用FileSplit得到片所对应的文件名。
接下来难度升级,不仅输出document,还要输出word在document中出现的频率。这时要注意处理算法与实现细节上的不同,根据算法,词频可以通过遍历一遍,维护一个计数得到,但在具体实现时可以用combiner帮助我们统计,这时mapper的输出为<<word, document>, 1>,而combiner将mapper的输出做一个汇总,即计算了词频。然后Partitioner中用word作为key蒙骗过原本的Partitioner,使相同的word哈希到相同位置,使用循环+队列结构进行reduce,当下一个key不同时,输出上一个队列,当下一个document不同时,更新要输出的Text,否则进行计数。最后一个队列用cleanup()函数处理掉。
最后加入停用词表,使用setup()函数进行初始化工作时将停用词表放在一个集合中,map操作时根据检查是否在集合中做一下判断即可。另,由于只统计字母与数字所组成的word,将输入的value进行过滤,用toLowerCase()函数全部转小写,再对每个字符做判断即可。
实现代码
由于Ubuntu里没装输入法工具,注释基本没有,但代码不是很难,属于一看即懂的那种。
package org.apache.hadoop.examples;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeSet;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.partition.HashPartitioner;
public class exp2 {
public static class InvertedIndexMapper extends Mapper<Object, Text, Text, IntWritable> {
Set<String> stop_words;
@Override
protected void setup(Context context) throws IOException, InterruptedException {
stop_words = new TreeSet<String>();
Configuration conf = context.getConfiguration();
BufferedReader reader = new BufferedReader(new InputStreamReader(FileSystem.get(conf).open(new Path("/stop_words/stop_words_eng.txt"))));
String line;
while ((line = reader.readLine()) != null) {
StringTokenizer itr = new StringTokenizer(line);
while (itr.hasMoreTokens()) {
stop_words.add(itr.nextToken());
}
}
reader.close();
}
@Override
protected void map(Object key, Text value, Context context) throws IOException, InterruptedException {
FileSplit fileSplit = (FileSplit)context.getInputSplit();
String fileName = fileSplit.getPath().getName();
String line = value.toString().toLowerCase();
StringBuilder new_line = new StringBuilder();
for (int i = 0; i < line.length(); ++i) {
char c = line.charAt(i);
if (c >= '0' && c <= '9' || c >= 'a' && c <= 'z') {
new_line.append(c);
} else {
new_line.append(' ');
}
}
StringTokenizer itr = new StringTokenizer(new_line.toString().trim());
while (itr.hasMoreTokens()) {
String str = itr.nextToken();
if (stop_words.contains(str)) continue;
context.write(new Text(str+","+fileName), new IntWritable(1));
}
}
}
public static class Combiner extends Reducer<Text, IntWritable, Text, IntWritable> {
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
int sum = 0;
for (IntWritable val : values) {
sum += val.get();
}
context.write(key, new IntWritable(sum));
}
}
public static class Partitioner extends HashPartitioner<Text, IntWritable> {
@Override
public int getPartition(Text key, IntWritable value, int numReduceTasks) {
Text term = new Text(key.toString().split(",")[0]);
return super.getPartition(term, value, numReduceTasks);
}
}
public static class InvertedIndexReducer extends Reducer<Text, IntWritable, Text, Text> {
private String last_word = null;
private String last_doc = null;
private int count = 0;
private int total = 0;
private StringBuilder str = new StringBuilder();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
String[] token = key.toString().split(",");
if (last_word == null) last_word = token[0];
if (last_doc == null) last_doc = token[1];
if (!token[0].equals(last_word)) { // different word
str.append("<" + last_doc + "," + count + ">;<total," + total + ">.");
context.write(new Text(last_word), new Text(str.toString()));
str = new StringBuilder();
last_word = token[0];
last_doc = token[1];
count = 0;
for (IntWritable value : values) {
count += value.get();
}
total = count;
return;
}
if (!token[1].equals(last_doc)) {
str.append("<" + last_doc + "," + count + ">;");
last_doc = token[1];
count = 0;
for (IntWritable value : values) {
count += value.get();
}
total += count;
return;
}
for (IntWritable value : values) {
count += value.get();
total += value.get();
}
}
@Override
protected void cleanup(Context context) throws IOException, InterruptedException {
str.append("<" + last_doc + "," + count + ">;<total," + total + ">.");
context.write(new Text(last_word), new Text(str.toString()));
super.cleanup(context);
}
}
public static void main(String[] args) {
if(args.length != 2) {
System.err.println("Usage: Relation <in> <out>");
System.exit(2);
}
try {
Configuration conf = new Configuration();
Job job = Job.getInstance(conf, "invert index");
job.setJarByClass(exp2.class);
job.setInputFormatClass(TextInputFormat.class);
job.setMapperClass(InvertedIndexMapper.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
job.setCombinerClass(Combiner.class);
job.setPartitionerClass(Partitioner.class);
job.setReducerClass(InvertedIndexReducer.class);
FileInputFormat.addInputPath(job, new Path(args[0])); // "/input"
FileOutputFormat.setOutputPath(job, new Path(args[1])); // "/output"
System.exit(job.waitForCompletion(true) ? 0 : 1);
} catch (Exception e) {
e.printStackTrace();
}
}
}
实验结果
反思总结
首先是setup与cleanup这两个函数的使用,如果不是网上查资料,都不知道还有这个功能可以方便地做一些初始化和最后的清理工作。
其次,调试时才知道setOutputKeyClass()和setOutputValueClass()函数指的是map的输入输出类型,一直都当作最终输出,中途一直报错就是因为没改成新的输出类型(此处存疑),也因此在代码里特将其和setMapperClass()放在一起。
再次,重新看实验零WordCount时发现Eclipse里可以直接看hdfs文件(虽然每次运行完代码都要refresh一下,但还是比命令行快吧)。为了方便调试也可以直接设置输入输出路径然后在Eclipse里run查看结果。
最后感慨一句,课上学到的算法和实际写出来的东西还是有很大不同,比如词频的统计,并且本次实验基本上将MaoReduce的流程函数跑了个遍,对于理解记忆也是很有帮助的。另,成功跑出结果也是很令人兴奋。