概念:
倒排索引(Inverted index),也常被称为反向索引、置入档案或反向档案,是一种索引方法,
被用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置的映射。
它是文档检索系统中最常用的数据结构
求出每个关键词在哪个文档中的第几行出现了几次
输入数据格式:
有两个文件data1.txt和data2.txt,文件中的内容就是普通的文本。每个单词就是一个关键词
data1.txt的内容:
zhangsan love zhouba
lisi love zhengshi lisi love wujiu
wangwu love zhaoliu
lisi zhouba zhangsan sunqi
data2.txt的内容:
hello zhangsan
hello zhouba
hello lisi
输出数据格式:
huangixaoming data1.txt,2,2;data1.txt,4,1;data2.txt,3,1
意义:
关键词 lisi 在第一份文档 data1.txt 中的第 2 行出现了 2 次
关键词 lisi 在第一份文档 data1.txt 中的第 4 行出现了 1 次
关键词 lisi 在第二份文档 data2.txt 中的第 3 行出现了 1 次
求得的最终结果:
zhengshi data1.txt,2,1
hello data2.txt,3,1;data2.txt,2,1;data2.txt,1,1
zhangsan data2.txt,1,1;data1.txt,4,1;data1.txt,1,1
lisi data1.txt,4,1;data1.txt,2,2;data2.txt,3,1
wangwu data1.txt,3,1
zhaoliu data1.txt,3,1
love data1.txt,3,1;data1.txt,1,1;data1.txt,2,2
sunqi data1.txt,4,1
zhouba data1.txt,1,1;data1.txt,4,1;data2.txt,2,1
wujiu data1.txt,2,1
实现思想
- 在setup中获取文件名
修改默认组件以记录行号。
在大文件读取时,只能采用改写默认输入组件的方式进行。因为一个大文件会被切分成多个split并行执行,所以在mapper中声明一个属性记录行号的做法,会导致每一个块的每一行都从头算起,不可行
- 在map中以单词名称作为key;文件名,行号,出现次数作为value输出。在reduce中进行合并。
MapReduce主体逻辑
package exam_answer.three;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.InputSplit;
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.output.FileOutputFormat;
public class InvertedIndexMR_1 {
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
job.setJarByClass(InvertedIndexMR_1.class);
job.setMapperClass(InvertedIndexMR_1_Mapper.class);
job.setReducerClass(InvertedIndexMR_1_Reducer.class);
//设置自定义的默认组件
job.setInputFormatClass(MyTextInputFormat.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(Text.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(Text.class);
FileInputFormat.addInputPath(job, new Path("E:\\bigdatatest\\input"));
Path outputPath = new Path("E:\\bigdatatest\\output");
FileSystem fs = FileSystem.get(conf);
if (fs.exists(outputPath)) {
fs.delete(outputPath, true);
}
FileOutputFormat.setOutputPath(job, outputPath);
boolean isDone = job.waitForCompletion(true);
System.exit(isDone ? 0 : 1);
}
public static class InvertedIndexMR_1_Mapper extends Mapper<LongWritable, Text, Text, Text> {
private String fileName = null;
private Text outkey = new Text();
private Text outvalue = new Text();
//用于每一行中的单词计数
private Map<String, Integer> wordsCountMap = new HashMap<String, Integer>();
@Override
protected void setup(Context context) throws IOException, InterruptedException {
/**
* 获取文件名
*/
InputSplit inputSplit = context.getInputSplit();
FileSplit fileSplit = (FileSplit)inputSplit;
fileName = fileSplit.getPath().getName();
}
/**
* map方法输出的key-value分别是:
* key :行号
* value :word1 word2 word3 ...
*/
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String[] split = value.toString().split(" ");
//把每一个词存入wordsCountMap并计数
for(String word : split){
if(wordsCountMap.containsKey(word)){
wordsCountMap.put(word, wordsCountMap.get(word) + 1);
}else{
wordsCountMap.put(word, 1);
}
}
//遍历wordsCountMap并输出key-value
for(String word : wordsCountMap.keySet()){
outkey.set(word);
outvalue.set(fileName + "," + key.get() + "," + wordsCountMap.get(word));
//输出格式:word 文件名,行号,个数
context.write(outkey, outvalue);
}
//读取一行后清空HashMap
wordsCountMap.clear();
}
}
public static class InvertedIndexMR_1_Reducer extends Reducer<Text, Text, Text, Text> {
private Text outvalue = new Text();
@Override
protected void reduce(Text key, Iterable<Text> values, Context context)
throws IOException, InterruptedException {
StringBuilder sb = new StringBuilder();
//合并
for(Text val : values){
sb.append(val.toString()).append(";");
}
//去除最后一个分号
String newStr = sb.toString().substring(0, sb.toString().length() - 1);
outvalue.set(newStr);
//输出格式:hello data2.txt,3,1;data2.txt,2,1;data2.txt,1,1
context.write(key, outvalue);
}
}
}
自定义输入组件
自定义输入组件可以直接复制TextInputForMat,和LineRecordReader两个类再其上进行改写。
MyTextInputFormat.class
只需要把createRecordReader()方法中的返回值改成自定义的MyLineRecordReader。读取组件的主体逻辑是在RecordReader中实现的。
@Override
public RecordReader<LongWritable, Text> createRecordReader(InputSplit split, TaskAttemptContext context) {
String delimiter = context.getConfiguration().get("textinputformat.record.delimiter");
byte[] recordDelimiterBytes = null;
if (null != delimiter)
recordDelimiterBytes = delimiter.getBytes(Charsets.UTF_8);
//修改为自定义的类
return new MyLineRecordReader(recordDelimiterBytes);
}
- MyLineRecordReader.class
因为只把map()的输入的key值从行偏移量改成行号,所以主体逻辑不需要变。nextKeyValue()方法是实现读取文件中的每一行数据的实现,在其中把原来设置key值的方法key.set(pos)改为key.set(lineCount),当然先要声明一个属性private long lineCount = 1; 最后还要在while循环中对lineCount++;
private long lineCount = 1;
public boolean nextKeyValue() throws IOException {
if (key == null) {
key = new LongWritable();
}
// key.set(pos);
//把map输入的key设为行号
key.set(lineCount);
if (value == null) {
value = new Text();
}
int newSize = 0;
// We always read one extra line, which lies outside the upper
// split limit i.e. (end - 1)
while (getFilePosition() <= end || in.needAdditionalRecordAfterSplit()) {
if (pos == 0) {
newSize = skipUtfByteOrderMark();
//对行数进行累加
lineCount++;
} else {
newSize = in.readLine(value, maxLineLength, maxBytesToConsume(pos));
pos += newSize;
//对行数进行累加
lineCount++;
}
if ((newSize == 0) || (newSize < maxLineLength)) {
break;
}
// line too long. try again
LOG.info("Skipped line of size " + newSize + " at pos " + (pos - newSize));
}
if (newSize == 0) {
key = null;
value = null;
return false;
} else {
return true;
}
}