1.倒排索引
"倒排索引"是文档检索系统中最常用的数据结构,被广泛地应用于全文搜索引擎。它主要是用来存储某个单词(或词组)在一个文档或一组文档中的存储位置的映射,即提供了一种根据内容来查找文档的方式。由于不是根据文档来确定文档所包含的内容,而是进行相反的操作,因而称为倒排索引(Inverted Index)。
1 实例描述
通常情况下,倒排索引由一个单词(或词组)以及相关的文档列表组成,文档列表中的文档或者是标识文档的ID号,或者是指文档所在位置的URL
在实际应用中,还需要给每个文档添加一个权值,用来指出每个文档与搜索内容的相关度
样例输入:
1)file1:
MapReduce is simple
2)file2:
MapReduce is powerful is simple
3)file3:
Hello MapReduce bye MapReduce
期望输出:
MapReduce file1.txt:1;file2.txt:1;file3.txt:2;
is file1.txt:1;file2.txt:2;
simple file1.txt:1;file2.txt:1;
powerful file2.txt:1;
Hello file3.txt:1;
bye file3.txt:1;
2 问题分析
实现"倒排索引"只要关注的信息为:单词、文档URL及词频。但是在实现过程中,索引文件的格式会略有所不同,以避免重写OutPutFormat类
3.实现步骤
1)Map过程
首先使用默认的TextInputFormat类对输入文件进行处理,得到文本中每行的偏移量及其内容。显然,Map过程首先必须分析输入的<key,value>对,得到倒排索引中需要的三个信息:单词、文档URL和词频
存在两个问题:
第一,<key,value>对只能有两个值,在不使用Hadoop自定义数据类型的情况下,需要根据情况将其中两个值合并成一个值,作为key或value值;
第二,通过一个Reduce过程无法同时完成词频统计和生成文档列表,所以必须增加一个Combine过程完成词频统计。
单词和URL组成key值(如"MapReduce:file1.txt"),将词频作为value,这样做的好处是可以利用MapReduce框架自带的Map端排序,将同一文档的相同单词的词频组成列表,传递给Combine过程,实现类似于WordCount的功能。
2)Combine过程
经过map方法处理后,Combine过程将key值相同的value值累加,得到一个单词在文档在文档中的词频,如果直接将图所示的输出作为Reduce过程的输入,在Shuffle过程时将面临一个问题:所有具有相同单词的记录(由单词、URL和词频组成)应该交由同一个Reducer处理,但当前的key值无法保证这一点,所以必须修改key值和value值。
这次将单词作为key值,URL和词频组成value值(如"file1.txt:1")。可以利用MapReduce框架默认的HashPartitioner类完成Shuffle过程,将相同单词的所有记录发送给同一个Reducer进行处理
3)Reduce过程
经过上述两个过程后,Reduce过程只需将相同key值的value值组合成倒排索引文件所需的格式即可,剩下的事情就可以直接交给MapReduce框架进行处理了
4)需要解决的问题
本倒排索引在文件数目上没有限制,但是单词文件不宜过大(具体值与默认HDFS块大小及相关配置有关),要保证每个文件对应一个split。否则,由于Reduce过程没有进一步统计词频,最终结果可能会出现词频未统计完全的单词。可以通过重写InputFormat类将每个文件为一个split,避免上述情况。或者执行两次MapReduce,第一次MapReduce用于统计词频,第二次MapReduce用于生成倒排索引。除此之外,还可以利用复合键值对等实现包含更多信息的倒排索引。
"倒排索引"是文档检索系统中最常用的数据结构,被广泛地应用于全文搜索引擎。它主要是用来存储某个单词(或词组)在一个文档或一组文档中的存储位置的映射,即提供了一种根据内容来查找文档的方式。由于不是根据文档来确定文档所包含的内容,而是进行相反的操作,因而称为倒排索引(Inverted Index)。
1 实例描述
通常情况下,倒排索引由一个单词(或词组)以及相关的文档列表组成,文档列表中的文档或者是标识文档的ID号,或者是指文档所在位置的URL
在实际应用中,还需要给每个文档添加一个权值,用来指出每个文档与搜索内容的相关度
样例输入:
1)file1:
MapReduce is simple
2)file2:
MapReduce is powerful is simple
3)file3:
Hello MapReduce bye MapReduce
期望输出:
MapReduce file1.txt:1;file2.txt:1;file3.txt:2;
is file1.txt:1;file2.txt:2;
simple file1.txt:1;file2.txt:1;
powerful file2.txt:1;
Hello file3.txt:1;
bye file3.txt:1;
2 问题分析
实现"倒排索引"只要关注的信息为:单词、文档URL及词频。但是在实现过程中,索引文件的格式会略有所不同,以避免重写OutPutFormat类
3.实现步骤
1)Map过程
首先使用默认的TextInputFormat类对输入文件进行处理,得到文本中每行的偏移量及其内容。显然,Map过程首先必须分析输入的<key,value>对,得到倒排索引中需要的三个信息:单词、文档URL和词频
存在两个问题:
第一,<key,value>对只能有两个值,在不使用Hadoop自定义数据类型的情况下,需要根据情况将其中两个值合并成一个值,作为key或value值;
第二,通过一个Reduce过程无法同时完成词频统计和生成文档列表,所以必须增加一个Combine过程完成词频统计。
单词和URL组成key值(如"MapReduce:file1.txt"),将词频作为value,这样做的好处是可以利用MapReduce框架自带的Map端排序,将同一文档的相同单词的词频组成列表,传递给Combine过程,实现类似于WordCount的功能。
2)Combine过程
经过map方法处理后,Combine过程将key值相同的value值累加,得到一个单词在文档在文档中的词频,如果直接将图所示的输出作为Reduce过程的输入,在Shuffle过程时将面临一个问题:所有具有相同单词的记录(由单词、URL和词频组成)应该交由同一个Reducer处理,但当前的key值无法保证这一点,所以必须修改key值和value值。
这次将单词作为key值,URL和词频组成value值(如"file1.txt:1")。可以利用MapReduce框架默认的HashPartitioner类完成Shuffle过程,将相同单词的所有记录发送给同一个Reducer进行处理
3)Reduce过程
经过上述两个过程后,Reduce过程只需将相同key值的value值组合成倒排索引文件所需的格式即可,剩下的事情就可以直接交给MapReduce框架进行处理了
4)需要解决的问题
本倒排索引在文件数目上没有限制,但是单词文件不宜过大(具体值与默认HDFS块大小及相关配置有关),要保证每个文件对应一个split。否则,由于Reduce过程没有进一步统计词频,最终结果可能会出现词频未统计完全的单词。可以通过重写InputFormat类将每个文件为一个split,避免上述情况。或者执行两次MapReduce,第一次MapReduce用于统计词频,第二次MapReduce用于生成倒排索引。除此之外,还可以利用复合键值对等实现包含更多信息的倒排索引。
2、二次排序
在map阶段
1.使用job.setInputFormatClass定义的InputFormat将输入的数据集分割成小数据块
调用自定义Map的map方法,将一个个<LongWritable, Text>对输入给Map的map方法。输出应该符合自定义Map中定义的输出<IntPair, IntWritable>。
最终生成一个List<IntPair, IntWritable>。
2.在map阶段的最后,会先调用job.setPartitionerClass对这个List进行分区,每个分区映射到一个reducer
每个分区内又调用job.setSortComparatorClass设置的key比较函数类排序, 是一个二次排序。
如果没有通过job.setSortComparatorClass设置key比较函数类,则使用key的实现的compareTo方法。
使用IntPair实现的compareTo方法,
在reduce阶段
1.reducer接收到所有映射到这个reducer的map输出后,也是会调用job.setSortComparatorClass设置的key比较函数类对所有数据对排序
2.然后开始构造一个key对应的value迭代器,使用jobjob.setGroupingComparatorClass设置的分组函数类
只要这个比较器比较的两个key相同,他们就属于同一个组,它们的value放在一个value迭代器
3.最后进入Reducer的reduce方法,reduce方法的输入是所有的(key和它的value迭代器)
3.mapreduce连接
1、reduce side join
在reduce端进行表的连接,该方法的特点就是操作简单,缺点是map端shffule后传递给reduce端的数据量过大,极大的降低了性能
连接方法:
(1)map端读入输入数据,以连接键为Key,待连接的内容为value,但是value需要添加特别的标识,表示的内容为表的表示,即若value来自于表1,则标识位设置为1,若来自表2,则设置为2,然后将map的内容输出到reduce
(2)reduce端接收来自map端shuffle后的结果,即<key, values>内容,然后遍历values,对每一个value进行处理,主要的处理过程是:判断每一个标志位,如果来自1表,则将value放置在特地为1表创建的数组之中,若来自2表,则将value放置在为2表创建的数组中,最后对两个数组进行求笛卡儿积,然后输出结果,即为最终表的连接结果。
2、map side join
在map端进行表的连接,对表的大小有要求,首先有一个表必须足够小,可以读入内存,另外的一个表很大,与reduce端连接比较,map端的连接,不会产生大量数据的传递,而是在map端连接完毕之后就进行输出,效率极大的提高
连接方法:
(1)首先要重写Mapper类下面的setup方法,因为这个方法是先于map方法执行的,将较小的表先读入到一个HashMap中。
(2)重写map函数,一行行读入大表的内容,逐一的与HashMap中的内容进行比较,若Key相同,则对数据进行格式化处理,然后直接输出。
Reduce侧连接
使用分布式缓存API,完成两个数据集的连接操作
2 问题分析
MapReduce连接取决于数据集的规模及分区方式
如果一个数据集很大而另外一个数据集很小,小的分发到集群中的每一个节点
mapper阶段读取大数据集中的数据
reducer获取本节点上的数据(也就是小数据集中的数据)并完成连接操作
之后在map/reduce函数中可以通过context来访问到缓存的文件,一般是重写setup方法来进行初始化:
protected void setup(Context context) throws IOException, InterruptedException {
super.setup(context);
if (context.getCacheFiles() != null && context.getCacheFiles().length > 0) {
String path = context.getLocalCacheFiles()[0].getName();
File itermOccurrenceMatrix = new File(path);
FileReader fileReader = new FileReader(itermOccurrenceMatrix);
BufferedReader bufferedReader = new BufferedReader(fileReader);
String s;
while ((s = bufferedReader.readLine()) != null) {
//TODO:读取每行内容进行相关的操作
}
bufferedReader.close();
fileReader.close();
}
}
3.实现步骤
1)把数据放到缓存中的方法
public void addCacheFile(URI uri);
public void addCacheArchive(URI uri);// 以上两组方法将文件或存档添加到分布式缓存
public void setCacheFiles(URI[] files);
public void setCacheArchives(URI[] archives);// 以上两组方法将一次性向分布式缓存中添加一组文件或存档
public void addFileToClassPath(Path file);
public void addArchiveToClassPath(Path archive);// 以上两组方法将文件或存档添加到 MapReduce 任务的类路径
在缓存中可以存放两类对象:文件(files)和存档(achives)。
文件被直接放置在任务节点上,而存档则会被解档之后再将具体文件放置在任务节点上。
2)其次掌握在map或者reduce任务中,使用API从缓存中读取数据。
可以通过 getFileClassPaths()和getArchivesClassPaths()方法获取被添加到任务的类路径下的文件和文档。
Reduce端连接
2 问题分析
我们使用 TextPair 类构建组合键,包括气象站ID 和“标记”。
标记” 是一个虚拟的字段,其唯一目的是对记录排序,使气象站记录比天气记录先到达。
一种简单的做法就是:对于气象站记录,设置“标记”的值设为 0;
对于天气记录,设置“标记”的值设为1。
Map侧的连接
两个数据集中一个非常小,可以让小数据集存入缓存。在作业开始这些文件会被复制到运行task的节点上。 一开始,它的setup方法会检索缓存文件。
Map侧连接需要满足条件
与reduce侧连接不同,Map侧连接需要等待参与连接的数据集满足如下条件
1.除了连接键外,所有的输入都必须按照连接键排序。
输入的各种数据集必须有相同的分区数。
所有具有相同键的记录需要放在同一分区中。
当Map任务对其他Mapreduce作业的结果进行处理时(Cleanup时),Map侧的连接条件都自动满足
CompositeInputFormat类用于执行Map侧的连接,而输入和连接类型的配置可以通过属性指定
2.如果其中一个数据集足够小,旁路的分布式通道可以用在Map侧的连接中
在map阶段
1.使用job.setInputFormatClass定义的InputFormat将输入的数据集分割成小数据块
调用自定义Map的map方法,将一个个<LongWritable, Text>对输入给Map的map方法。输出应该符合自定义Map中定义的输出<IntPair, IntWritable>。
最终生成一个List<IntPair, IntWritable>。
2.在map阶段的最后,会先调用job.setPartitionerClass对这个List进行分区,每个分区映射到一个reducer
每个分区内又调用job.setSortComparatorClass设置的key比较函数类排序, 是一个二次排序。
如果没有通过job.setSortComparatorClass设置key比较函数类,则使用key的实现的compareTo方法。
使用IntPair实现的compareTo方法,
在reduce阶段
1.reducer接收到所有映射到这个reducer的map输出后,也是会调用job.setSortComparatorClass设置的key比较函数类对所有数据对排序
2.然后开始构造一个key对应的value迭代器,使用jobjob.setGroupingComparatorClass设置的分组函数类
只要这个比较器比较的两个key相同,他们就属于同一个组,它们的value放在一个value迭代器
3.最后进入Reducer的reduce方法,reduce方法的输入是所有的(key和它的value迭代器)
//自己定义的key类应该实现WritableComparable接口
public static class IntPair implements WritableComparable<IntPair> {
int first;
int second;
/**
* Set the left and right values.
*/
public void set(int left, int right) {
first = left;
second = right;
}
public int getFirst() {
return first;
}
public int getSecond() {
return second;
}
}
@Override
//反序列化,从流中的二进制转换成IntPair
public void readFields(DataInput in) throws IOException {
// TODO Auto-generated method stub
first = in.readInt();
second = in.readInt();
}
@Override
//序列化,将IntPair转化成使用流传送的二进制
public void write(DataOutput out) throws IOException {
// TODO Auto-generated method stub
out.writeInt(first);
out.writeInt(second);
}
@Override
//key的比较
public int compareTo(IntPair o) {
// TODO Auto-generated method stub
if (first != o.first) {
return first < o.first ? -1 : 1;
} else if (second != o.second) {
return second < o.second ? -1 : 1;
} else {
return 0;
}
}
//新定义类应该重写的两个方法
@Override
//The hashCode() method is used by the HashPartitioner (the default partitioner in MapReduce)
public int hashCode() {
return first * 157 + second;
}
@Override
public boolean equals(Object right) {
if (right == null)
return false;
if (this == right)
return true;
if (right instanceof IntPair) {
IntPair r = (IntPair) right;
return r.first == first && r.second == second;
} else {
return false;
}
}
分区函数类。根据first确定Partition
public static class FirstPartitioner extends Partitioner<IntPair,IntWritable>{
@Override
public int getPartition(IntPair key, IntWritable value,
int numPartitions) {
return Math.abs(key.getFirst() * 127) % numPartitions;
}
}
分组函数类。只要first相同就属于同一个组
/*//第一种方法,实现接口RawComparator
public static class GroupingComparator implements RawComparator<IntPair> {
@Override
public int compare(IntPair o1, IntPair o2) {
int l = o1.getFirst();
int r = o2.getFirst();
return l == r ? 0 : (l < r ? -1 : 1);
}
@Override
//一个字节一个字节的比,直到找到一个不相同的字节,然后比这个字节的大小作为两个字节流的大小比较结果。
public int compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2){
// TODO Auto-generated method stub
return WritableComparator.compareBytes(b1, s1, Integer.SIZE/8,
b2, s2, Integer.SIZE/8);
}
}*/
//第二种方法,继承WritableComparator
public static class GroupingComparator extends WritableComparator {
protected GroupingComparator() {
super(IntPair.class, true);
}
@Override
//Compare two WritableComparables.
public int compare(WritableComparable w1, WritableComparable w2) {
IntPair ip1 = (IntPair) w1;
IntPair ip2 = (IntPair) w2;
int l = ip1.getFirst();
int r = ip2.getFirst();
return l == r ? 0 : (l < r ? -1 : 1);
}
}
// 自定义map
public static class Map extends
Mapper<LongWritable, Text, IntPair, IntWritable> {
private final IntPair intkey = new IntPair();
private final IntWritable intvalue = new IntWritable();
public void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
String line = value.toString();
StringTokenizer tokenizer = new StringTokenizer(line);
int left = 0;
int right = 0;
if (tokenizer.hasMoreTokens()) {
left = Integer.parseInt(tokenizer.nextToken());
if (tokenizer.hasMoreTokens())
right = Integer.parseInt(tokenizer.nextToken());
intkey.set(left, right);
intvalue.set(right);
context.write(intkey, intvalue);
}
}
}
// 自定义reduce
public static class Reduce extends
Reducer<IntPair, IntWritable, Text, IntWritable> {
private final Text left = new Text();
private static final Text SEPARATOR =
new Text("------------------------------------------------");
public void reduce(IntPair key, Iterable<IntWritable> values,
Context context) throws IOException, InterruptedException {
context.write(SEPARATOR, null);
left.set(Integer.toString(key.getFirst()));
for (IntWritable val : values) {
context.write(left, val);
}
}
}
3.mapreduce连接
1、reduce side join
在reduce端进行表的连接,该方法的特点就是操作简单,缺点是map端shffule后传递给reduce端的数据量过大,极大的降低了性能
连接方法:
(1)map端读入输入数据,以连接键为Key,待连接的内容为value,但是value需要添加特别的标识,表示的内容为表的表示,即若value来自于表1,则标识位设置为1,若来自表2,则设置为2,然后将map的内容输出到reduce
(2)reduce端接收来自map端shuffle后的结果,即<key, values>内容,然后遍历values,对每一个value进行处理,主要的处理过程是:判断每一个标志位,如果来自1表,则将value放置在特地为1表创建的数组之中,若来自2表,则将value放置在为2表创建的数组中,最后对两个数组进行求笛卡儿积,然后输出结果,即为最终表的连接结果。
2、map side join
在map端进行表的连接,对表的大小有要求,首先有一个表必须足够小,可以读入内存,另外的一个表很大,与reduce端连接比较,map端的连接,不会产生大量数据的传递,而是在map端连接完毕之后就进行输出,效率极大的提高
连接方法:
(1)首先要重写Mapper类下面的setup方法,因为这个方法是先于map方法执行的,将较小的表先读入到一个HashMap中。
(2)重写map函数,一行行读入大表的内容,逐一的与HashMap中的内容进行比较,若Key相同,则对数据进行格式化处理,然后直接输出。
Reduce侧连接
使用分布式缓存API,完成两个数据集的连接操作
2 问题分析
MapReduce连接取决于数据集的规模及分区方式
如果一个数据集很大而另外一个数据集很小,小的分发到集群中的每一个节点
mapper阶段读取大数据集中的数据
reducer获取本节点上的数据(也就是小数据集中的数据)并完成连接操作
之后在map/reduce函数中可以通过context来访问到缓存的文件,一般是重写setup方法来进行初始化:
protected void setup(Context context) throws IOException, InterruptedException {
super.setup(context);
if (context.getCacheFiles() != null && context.getCacheFiles().length > 0) {
String path = context.getLocalCacheFiles()[0].getName();
File itermOccurrenceMatrix = new File(path);
FileReader fileReader = new FileReader(itermOccurrenceMatrix);
BufferedReader bufferedReader = new BufferedReader(fileReader);
String s;
while ((s = bufferedReader.readLine()) != null) {
//TODO:读取每行内容进行相关的操作
}
bufferedReader.close();
fileReader.close();
}
}
Configuration config=context.getConfiguration();
FileSystem fs=FileSystem.get(config);
FSDataInputStream in=fs.open(new Path(path));
Text line=new Text(“ ”);
LineReader lineReader=new LineReader(in,config);
int offset=0;
do{
offset=lineReader.readLine(line);
//读入path中一行到Text类型的line中,返回字节数
if(offset>0){
String[] tokens=line.toString().split(“,”); countryCodesTreeMap.put(tokens[0],tokens[1]);
}
}while(offset!=0);
得到的path为本地文件系统上的路径
这里的getLocalCacheFiles方法也被注解为过时了,只能使用context.getCacheFiles方法,和getLocalCacheFiles不同的是,getCacheFiles得到的路径是HDFS上的文件路径,如果使用这个方法,那么程序中读取的就不再试缓存在各个节点上的数据了,相当于共同访问HDFS上的同一个文件。
可以直接通过符号连接来跳过getLocalCacheFiles获得本地的文件
3.实现步骤
1)把数据放到缓存中的方法
public void addCacheFile(URI uri);
public void addCacheArchive(URI uri);// 以上两组方法将文件或存档添加到分布式缓存
public void setCacheFiles(URI[] files);
public void setCacheArchives(URI[] archives);// 以上两组方法将一次性向分布式缓存中添加一组文件或存档
public void addFileToClassPath(Path file);
public void addArchiveToClassPath(Path archive);// 以上两组方法将文件或存档添加到 MapReduce 任务的类路径
在缓存中可以存放两类对象:文件(files)和存档(achives)。
文件被直接放置在任务节点上,而存档则会被解档之后再将具体文件放置在任务节点上。
2)其次掌握在map或者reduce任务中,使用API从缓存中读取数据。
可以通过 getFileClassPaths()和getArchivesClassPaths()方法获取被添加到任务的类路径下的文件和文档。
Reduce端连接
2 问题分析
我们使用 TextPair 类构建组合键,包括气象站ID 和“标记”。
标记” 是一个虚拟的字段,其唯一目的是对记录排序,使气象站记录比天气记录先到达。
一种简单的做法就是:对于气象站记录,设置“标记”的值设为 0;
对于天气记录,设置“标记”的值设为1。
Map侧的连接
两个数据集中一个非常小,可以让小数据集存入缓存。在作业开始这些文件会被复制到运行task的节点上。 一开始,它的setup方法会检索缓存文件。
Map侧连接需要满足条件
与reduce侧连接不同,Map侧连接需要等待参与连接的数据集满足如下条件
1.除了连接键外,所有的输入都必须按照连接键排序。
输入的各种数据集必须有相同的分区数。
所有具有相同键的记录需要放在同一分区中。
当Map任务对其他Mapreduce作业的结果进行处理时(Cleanup时),Map侧的连接条件都自动满足
CompositeInputFormat类用于执行Map侧的连接,而输入和连接类型的配置可以通过属性指定
2.如果其中一个数据集足够小,旁路的分布式通道可以用在Map侧的连接中