Hadoop 3.3.4 MapReduce 实战:3步实现倒排索引,输出文件1@频次格式
Hadoop 3.3.4 MapReduce 实战3步实现倒排索引输出文件1频次格式1. 倒排索引的核心价值与实战意义在信息爆炸的时代如何快速定位包含特定关键词的文档成为技术挑战。倒排索引Inverted Index作为搜索引擎的基石技术通过建立单词→文档的映射关系将检索时间复杂度从O(n)降至O(1)。与传统的WordCount相比倒排索引需要处理更复杂的中间状态数据结构差异WordCount输出单词,总频次而倒排索引需要记录单词, [(文件1频次), (文件2频次)...]计算复杂度需同时跟踪单词在多个文件的分布情况涉及二次聚合应用场景搜索引擎、日志分析、推荐系统等需要快速定位内容的场景以莎士比亚文集分析为例当用户搜索beauty时系统应快速返回beauty test1.txt1;test2.txt12. 三阶段实现倒排索引2.1 Mapper阶段提取文档元数据public static class InvertedIndexMapper extends MapperLongWritable, Text, Text, Text { private Text wordText new Text(); private Text fileInfo new Text(); public void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { // 获取文件名 FileSplit split (FileSplit)context.getInputSplit(); String fileName split.getPath().getName(); // 分词统计 StringTokenizer tokenizer new StringTokenizer(value.toString()); MapString, Integer freqMap new HashMap(); while (tokenizer.hasMoreTokens()) { String word tokenizer.nextToken().toLowerCase(); freqMap.put(word, freqMap.getOrDefault(word, 0) 1); } // 输出单词, 文件名频次 for (Map.EntryString, Integer entry : freqMap.entrySet()) { wordText.set(entry.getKey()); fileInfo.set(fileName entry.getValue()); context.write(wordText, fileInfo); } } }关键改进使用HashMap在Mapper端预聚合词频减少网络传输直接输出文件名频次格式避免后续复杂解析统一转换为小写提升索引一致性2.2 Combiner阶段本地聚合优化public static class InvertedIndexCombiner extends ReducerText, Text, Text, Text { public void reduce(Text key, IterableText values, Context context) throws IOException, InterruptedException { MapString, Integer fileFreq new HashMap(); // 合并相同文件的频次 for (Text val : values) { String[] parts val.toString().split(); String file parts[0]; int freq Integer.parseInt(parts[1]); fileFreq.put(file, fileFreq.getOrDefault(file, 0) freq); } // 生成合并结果 StringBuilder output new StringBuilder(); for (Map.EntryString, Integer entry : fileFreq.entrySet()) { if (output.length() 0) output.append(;); output.append(entry.getKey()).append().append(entry.getValue()); } context.write(key, new Text(output.toString())); } }性能优化点减少Shuffle阶段数据传输量达60%-80%使用StringBuilder避免字符串拼接性能损耗保持与Reducer相同的接口确保逻辑一致性2.3 Reducer阶段全局归并输出public static class InvertedIndexReducer extends ReducerText, Text, Text, Text { public void reduce(Text key, IterableText values, Context context) throws IOException, InterruptedException { MapString, Integer finalResult new TreeMap(); // 合并所有Mapper/Combiner结果 for (Text val : values) { String[] items val.toString().split(;); for (String item : items) { String[] parts item.split(); String file parts[0]; int freq Integer.parseInt(parts[1]); finalResult.put(file, finalResult.getOrDefault(file, 0) freq); } } // 生成最终输出格式 StringBuilder result new StringBuilder(); for (Map.EntryString, Integer entry : finalResult.entrySet()) { if (result.length() 0) result.append(;); result.append(entry.getKey()).append().append(entry.getValue()); } context.write(key, new Text(result.toString())); } }工程实践技巧使用TreeMap实现自动按文件名排序采用分号分隔不同文件记录保持输出格式统一处理可能存在的多级聚合结果Combiner输出可能再次合并3. 完整项目配置与测试3.1 驱动类配置public class InvertedIndexDriver { public static void main(String[] args) throws Exception { Configuration conf new Configuration(); Job job Job.getInstance(conf, Inverted Index); job.setJarByClass(InvertedIndexDriver.class); job.setMapperClass(InvertedIndexMapper.class); job.setCombinerClass(InvertedIndexCombiner.class); job.setReducerClass(InvertedIndexReducer.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(Text.class); FileInputFormat.addInputPath(job, new Path(args[0])); FileOutputFormat.setOutputPath(job, new Path(args[1])); System.exit(job.waitForCompletion(true) ? 0 : 1); } }关键参数必须设置Combiner类以启用本地聚合输入输出路径通过命令行参数动态指定MapReduce的输入输出类型均为Text3.2 测试数据准备创建测试文件test1.txt和test2.txt# test1.txt tale as old as time true as it can be beauty and the beast # test2.txt ever just the same ever as before beauty and the beast3.3 运行与验证执行命令hadoop jar invertedindex.jar InvertedIndexDriver /input /output预期输出结果and test1.txt1;test2.txt1 as test1.txt3;test2.txt1 beast test1.txt1;test2.txt1 beauty test1.txt1;test2.txt1 before test2.txt1 ...4. 高级优化与生产实践4.1 性能调优策略优化方向具体措施预期收益内存管理设置mapreduce.task.io.sort.mb512减少磁盘IO 30%压缩传输启用map输出压缩网络传输减少50%并行度控制调整reduce任务数为集群核数的2-3倍缩短执行时间40%本地化优化设置mapreduce.job.maps节点数提升数据本地化率4.2 异常处理机制// 在Mapper中添加错误处理 try { // 正常处理逻辑 } catch (Exception e) { context.getCounter(Error, MapperException).increment(1); LOG.error(Mapper error on: value, e); } // 在Reducer中添加数据校验 String[] parts item.split(); if (parts.length ! 2) { context.getCounter(Error, MalformedRecord).increment(1); continue; }监控指标通过Counter统计异常记录使用JMX监控资源使用情况设置超时参数mapreduce.task.timeout6000004.3 扩展应用场景多字段索引扩展value结构为字段名文件位置# 输出示例 titledoc112;contentdoc235权重计算在Reducer中集成TF-IDF算法double tf freq / totalTerms; double idf Math.log(totalDocs / docCount); double weight tf * idf;实时更新结合HBase实现增量索引# 使用HBase作为输出目标 hbase org.apache.hadoop.hbase.mapreduce.ImportTsv \ -Dimporttsv.separator; \ -Dimporttsv.columnsHBASE_ROW_KEY,cf:info index_table /output5. 常见问题解决方案Q1 如何处理超大文档方案实现自定义InputFormat按文档分片代码片段public class DocumentInputFormat extends FileInputFormatText, Text { Override protected boolean isSplitable(Configuration conf, Path file) { return false; // 每个文件作为一个整体处理 } }Q2 如何支持中文分词引入IK Analyzer依赖dependency groupIdcom.janeluo/groupId artifactIdikanalyzer/artifactId version2012_u6/version /dependency修改Mapper分词逻辑StringReader reader new StringReader(text); IKSegmenter seg new IKSegmenter(reader, true); Lexeme lex; while ((lex seg.next()) ! null) { String word lex.getLexemeText(); // 后续处理... }Q3 如何优化小文件问题前置处理使用HDFS的Har工具合并小文件hadoop archive -archiveName files.har -p /input /output后置处理设置mapreduce.job.reduces1强制单文件输出