大数据第九天

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用于生成倒排索引。除此之外,还可以利用复合键值对等实现包含更多信息的倒排索引。



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迭代器)


 //自己定义的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侧的连接中











 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值