• 倒排索引构建算法BSBI和SPIMI


    参考文献:

    http://www.cnblogs.com/fly1988happy/archive/2012/04/01/2429000.html

    http://blog.csdn.net/v_july_v/article/details/7109500

    我的数据挖掘算法:https://github.com/linyiqun/DataMiningAlgorithm
    我的算法库:https://github.com/linyiqun/lyq-algorithms-lib

    算法介绍

    在信息搜索领域,构建索引一直是是一种非常有效的方式,但是当搜索引擎面对的是海量数据的时候,你如果要从茫茫人海的数据中去找出数据,显然这不是一个很好的办法。于是倒排索引这个概念就被提了出来。再说倒排索引概念之前,先要理解一下,一般的索引检索信息的方式。比如原始的数据源假设都是以文档的形式被分开,文档1拥有一段内容,文档2也富含一段内容,文档3同样如此。然后给定一个关键词,要搜索出与此关键词相关的文档,自然而然我们联想到的办法就是一个个文档的内容去比较,判断是否含有此关键词,如果含有则返回这个文档的索引地址,如果不是接着用后面的文档去比,这就有点类似于字符串的匹配类似。很显然,当数据量非常巨大的时候,这种方式并不适用。原来的这种方式可以理解为是索引-->关键词,而倒排索引的形式则是关键词--->索引位置,也就是说,给出一个关键词信息,我能立马根据倒排索引的信息得出他的位置。当然,这里说的是倒排索引最后要达到的效果,至于是用什么方式实现,就不止一种了,本文所述的就是其中比较出名的BSBI和SPIMI算法。

    算法的原理

    这里首先给出一个具体的实例来了解一般的构造过程,先避开具体的实现方式,给定下面一组词句。

    Doc1:Mike spoken English Frequently at home.And he can write English every day.

    Doc2::Mike plays football very well.

    首先我们必须知道,我们需要的是一些关键的信息,诸如一些修饰词等等都需要省略,动词的时态变化等都需要还原,如果代词指的是同个人也能够省略,于是上面的句子可以简化成

    Doc1:Mike spoken English home.write English.

    Doc2:Mike play football.

    下面进行索引的倒排构建,因为Mike出现在文档1和文档2 中,所以Mike:{1, 2}后面的词的构造同样的道理。最后的关系就会构成词对应于索引位置的映射关系。理解了这个过程之后呢,可以介绍一下本文主要要说的BSBI(基于磁盘的外部排序构建索引)和SPIMI(内存单遍扫描构建索引)算法了,一般来说,后者比前者常用。

    BSBI

    此算法的主要步骤如下:

    1、将文档中的词进行id的映射,这里可以用hash的方法去构造

    2、将文档分割成大小相等的部分。

    3、将每部分按照词ID对上文档ID的方式进行排序

    4、将每部分排序好后的结果进行合并,最后写出到磁盘中。

    5、然后递归的执行,直到文档内容全部完成这一系列操作。

    这里有一张示意图:


    在算法的过程中会用到读缓冲区和写缓冲区,至于期间的大小多少如何配置都是看个人的,我在后面的代码实现中也有进行设置。至于其中的排序算法的选择,一般建议使用效果比较好的快速排序算法,但是我在后面为了方便,直接用了自己更熟悉的冒泡排序算法,这个也看个人。

    SPIMI

    接下来说说SPIMI算法,就是内存单遍扫描算法,这个算法与上面的算法一上来就有直接不同的特点就是他无须做id的转换,还是采用了词对索引的直接关联。还有1个比较大的特点是他不经过排序,直接按照先后顺序构建索引,算法的主要步骤如下:

    1、对每个块构造一个独立的倒排索引。

    2、最后将所有独立的倒排索引进行合并就OK了。

    本人为了方便就把这个算法的实现简洁化了,直接在内存中完成所有的构建工作。望读者稍加注意。SPIMI相对比较的简单,这里就不给出截图了。

    算法的代码实现

    首先是文档的输入数据,采用了2个一样的文档,我也是实在想不出有更好的测试数据了

    doc1.txt:

    Mike studyed English hardly yesterday
    He got the 100 at the last exam
    He thinks English is very interesting

    doc2.txt:

    Mike studyed English hardly yesterday
    He got the 100 at the last exam
    He thinks English is very interesting
    
    下面是文档信息预处理类PreTreatTool.java:

    package InvertedIndex;
    
    import java.io.BufferedReader;
    import java.io.File;
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.FileReader;
    import java.io.IOException;
    import java.io.PrintStream;
    import java.util.ArrayList;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    
    /**
     * 文档预处理工具类
     * 
     * @author lyq
     * 
     */
    public class PreTreatTool {
    	// 一些无具体意义的过滤词
    	public static String[] FILTER_WORDS = new String[] { "at", "At", "The",
    			"the", "is", "very" };
    
    	// 批量文档的文件地址
    	private ArrayList<String> docFilePaths;
    	// 输出的有效词的存放路径
    	private ArrayList<String> effectWordPaths;
    
    	public PreTreatTool(ArrayList<String> docFilePaths) {
    		this.docFilePaths = docFilePaths;
    	}
    
    	/**
    	 * 获取文档有效词文件路径
    	 * 
    	 * @return
    	 */
    	public ArrayList<String> getEFWPaths() {
    		return this.effectWordPaths;
    	}
    
    	/**
    	 * 从文件中读取数据
    	 * 
    	 * @param filePath
    	 *            单个文件
    	 */
    	private ArrayList<String> readDataFile(String filePath) {
    		File file = new File(filePath);
    		ArrayList<String[]> dataArray = new ArrayList<String[]>();
    		ArrayList<String> words = new ArrayList<>();
    
    		try {
    			BufferedReader in = new BufferedReader(new FileReader(file));
    			String str;
    			String[] tempArray;
    			while ((str = in.readLine()) != null) {
    				tempArray = str.split(" ");
    				dataArray.add(tempArray);
    			}
    			in.close();
    		} catch (IOException e) {
    			e.getStackTrace();
    		}
    
    		// 将每行词做拆分加入到总列表容器中
    		for (String[] array : dataArray) {
    			for (String word : array) {
    				words.add(word);
    			}
    		}
    
    		return words;
    	}
    
    	/**
    	 * 对文档内容词汇进行预处理
    	 */
    	public void preTreatWords() {
    		String baseOutputPath = "";
    		int endPos = 0;
    		ArrayList<String> tempWords = null;
    		effectWordPaths = new ArrayList<>();
    
    		for (String filePath : docFilePaths) {
    			tempWords = readDataFile(filePath);
    			filterWords(tempWords, true);
    
    			// 重新组装出新的输出路径
    			endPos = filePath.lastIndexOf(".");
    			baseOutputPath = filePath.substring(0, endPos);
    
    			writeOutOperation(tempWords, baseOutputPath + "-efword.txt");
    			effectWordPaths.add(baseOutputPath + "-efword.txt");
    		}
    	}
    
    	/**
    	 * 
    	 * 对文档中的词语进行过滤操作
    	 * 
    	 * @param words
    	 *            待处理文档词语
    	 * @param canRepeated
    	 *            有效词是否可以重复
    	 */
    	private void filterWords(ArrayList<String> words, boolean canRepeated) {
    		boolean isFilterWord;
    		// 做形容词匹配
    		Pattern adjPattern;
    		// 做动词时态的匹配
    		Pattern formerPattern;
    		// 数字匹配
    		Pattern numberPattern;
    		Matcher adjMatcher;
    		Matcher formerMatcher;
    		Matcher numberMatcher;
    		ArrayList<String> deleteWords = new ArrayList<>();
    
    		adjPattern = Pattern.compile(".*(ly$|ful$|ing$)");
    		formerPattern = Pattern.compile(".*ed$");
    		numberPattern = Pattern.compile("[0-9]+(.[0-9]+)?");
    
    		String w;
    		for (int i = 0; i < words.size(); i++) {
    			w = words.get(i);
    			isFilterWord = false;
    
    			for (String fw : FILTER_WORDS) {
    				if (fw.equals(w)) {
    					deleteWords.add(w);
    					isFilterWord = true;
    					break;
    				}
    			}
    
    			if (isFilterWord) {
    				continue;
    			}
    
    			adjMatcher = adjPattern.matcher(w);
    			formerMatcher = formerPattern.matcher(w);
    			numberMatcher = numberPattern.matcher(w);
    
    			// 将词语统一小写字母化
    			w = w.toLowerCase();
    
    			// 如果是形容词,副词形式的或是纯数字的词,则进行过滤
    			if (adjMatcher.matches() || numberMatcher.matches()) {
    				deleteWords.add(w);
    			} else if (formerMatcher.matches()) {
    				// 如果是ed结尾表明是动词的在时态方面的变化,进行变化,转为原有动词的形式,截去最末尾2个额外添加的后缀词
    				w = w.substring(0, w.length() - 2);
    			}
    			
    			words.set(i, w);
    		}
    
    		// 进行无效词的过滤
    		words.removeAll(deleteWords);
    		deleteWords.clear();
    
    		String s1;
    		String s2;
    
    		// 进行词语的去重
    		for (int i = 0; i < words.size() - 1; i++) {
    			s1 = words.get(i);
    
    			for (int j = i + 1; j < words.size(); j++) {
    				s2 = words.get(j);
    
    				// 找到存在相同的词了,就挑出循环
    				if (s1.equals(s2)) {
    					deleteWords.add(s1);
    					break;
    				}
    			}
    		}
    
    		// 删除多余重复的词语
    		words.removeAll(deleteWords);
    		words.addAll(deleteWords);
    	}
    
    	/**
    	 * 将数据写出到磁盘文件操作,如果文件已经存在,则在文件尾部进行内容追加
    	 * 
    	 * @param buffer
    	 *            当前写缓冲中的数据
    	 * @param filePath
    	 *            输出地址
    	 */
    	private void writeOutOperation(ArrayList<String> buffer, String filePath) {
    		StringBuilder strBuilder = new StringBuilder();
    
    		// 将缓冲中的数据组成字符写入到文件中
    		for (String word : buffer) {
    			strBuilder.append(word);
    			strBuilder.append("
    ");
    		}
    
    		try {
    			File file = new File(filePath);
    			PrintStream ps = new PrintStream(new FileOutputStream(file));
    			ps.print(strBuilder.toString());// 往文件里写入字符串
    		} catch (FileNotFoundException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    	}
    
    }
    
    文档类Document.java:

    package InvertedIndex;
    
    import java.util.ArrayList;
    
    /**
     * 文档类
     * @author lyq
     *
     */
    public class Document {
    	//文档的唯一标识
    	int docId;
    	//文档的文件地址
    	String filePath;
    	//文档中的有效词
    	ArrayList<String> effectWords;
    	
    	public Document(ArrayList<String> effectWords, String filePath){
    		this.effectWords = effectWords;
    		this.filePath = filePath;
    	}
    	
    	public Document(ArrayList<String> effectWords, String filePath, int docId){
    		this(effectWords, filePath);
    		this.docId = docId;
    	}
    }
    
    BSBI算法工具类BSBITool.java:

    package InvertedIndex;
    
    import java.io.BufferedReader;
    import java.io.File;
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.FileReader;
    import java.io.IOException;
    import java.io.PrintStream;
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * BSBI基于磁盘的外部排序算法
     * 
     * @author lyq
     * 
     */
    public class BSBITool {
    	// 文档唯一标识ID
    	public static int DOC_ID = 0;
    
    	// 读缓冲区的大小
    	private int readBufferSize;
    	// 写缓冲区的大小
    	private int writeBufferSize;
    	// 读入的文档的有效词文件地址
    	private ArrayList<String> effectiveWordFiles;
    	// 倒排索引输出文件地址
    	private String outputFilePath;
    	// 读缓冲 1
    	private String[][] readBuffer1;
    	// 读缓冲2
    	private String[][] readBuffer2;
    	// 写缓冲区
    	private String[][] writeBuffer;
    	// 有效词与hashcode的映射
    	private Map<String, String> code2word;
    
    	public BSBITool(ArrayList<String> effectiveWordFiles, int readBufferSize,
    			int writeBufferSize) {
    		this.effectiveWordFiles = effectiveWordFiles;
    		this.readBufferSize = readBufferSize;
    		this.writeBufferSize = writeBufferSize;
    
    		initBuffers();
    	}
    
    	/**
    	 * 初始化缓冲区的设置
    	 */
    	private void initBuffers() {
    		readBuffer1 = new String[readBufferSize][2];
    		readBuffer2 = new String[readBufferSize][2];
    		writeBuffer = new String[writeBufferSize][2];
    	}
    
    	/**
    	 * 从文件中读取有效词并进行编码替换
    	 * 
    	 * @param filePath
    	 *            返回文档
    	 */
    	private Document readEffectWords(String filePath) {
    		long hashcode = 0;
    
    		String w;
    		Document document;
    		code2word = new HashMap<String, String>();
    		ArrayList<String> words;
    
    		words = readDataFile(filePath);
    
    		for (int i = 0; i < words.size(); i++) {
    			w = words.get(i);
    
    			hashcode = BKDRHash(w);
    			hashcode = hashcode % 10000;
    
    			// 将有效词的hashcode取模值作为对应的代表
    			code2word.put(hashcode + "", w);
    			w = hashcode + "";
    
    			words.set(i, w);
    		}
    
    		document = new Document(words, filePath, DOC_ID);
    		DOC_ID++;
    
    		return document;
    	}
    
    	/**
    	 * 将字符做哈希值的转换
    	 * 
    	 * @param str
    	 *            待转换字符
    	 * @return
    	 */
    	private long BKDRHash(String str) {
    		int seed = 31; /* 31 131 1313 13131 131313 etc.. */
    		long hash = 0;
    		int i = 0;
    
    		for (i = 0; i < str.length(); i++) {
    			hash = (hash * seed) + (str.charAt(i));
    		}
    
    		return hash;
    
    	}
    
    	/**
    	 * 根据输入的有效词输出倒排索引文件
    	 */
    	public void outputInvertedFiles() {
    		int index = 0;
    		String baseFilePath = "";
    		outputFilePath = "";
    		Document doc;
    		ArrayList<String> tempPaths;
    		ArrayList<String[]> invertedData1;
    		ArrayList<String[]> invertedData2;
    
    		tempPaths = new ArrayList<>();
    		for (String filePath : effectiveWordFiles) {
    			doc = readEffectWords(filePath);
    			writeOutFile(doc);
    
    			index = doc.filePath.lastIndexOf(".");
    			baseFilePath = doc.filePath.substring(0, index);
    			writeOutOperation(writeBuffer, baseFilePath + "-temp.txt");
    
    			tempPaths.add(baseFilePath + "-temp.txt");
    		}
    
    		outputFilePath = baseFilePath + "-bsbi-inverted.txt";
    
    		// 将中间产生的倒排索引数据进行总的合并并输出到一个文件中
    		for (int i = 1; i < tempPaths.size(); i++) {
    			if (i == 1) {
    				invertedData1 = readInvertedFile(tempPaths.get(0));
    			} else {
    				invertedData1 = readInvertedFile(outputFilePath);
    			}
    
    			invertedData2 = readInvertedFile(tempPaths.get(i));
    
    			mergeInvertedData(invertedData1, invertedData2, false,
    					outputFilePath);
    
    			writeOutOperation(writeBuffer, outputFilePath, false);
    		}
    	}
    
    	/**
    	 * 将文档的最终的倒排索引结果写出到文件
    	 * 
    	 * @param doc
    	 *            待处理文档
    	 */
    	private void writeOutFile(Document doc) {
    		// 在读缓冲区中是否需要再排序
    		boolean ifSort = true;
    		int index = 0;
    		String baseFilePath;
    		String[] temp;
    		ArrayList<String> tempWords = (ArrayList<String>) doc.effectWords
    				.clone();
    		ArrayList<String[]> invertedData1;
    		ArrayList<String[]> invertedData2;
    
    		invertedData1 = new ArrayList<>();
    		invertedData2 = new ArrayList<>();
    
    		// 将文档的数据平均拆分成2份,用于读入后面的2个缓冲区中
    		for (int i = 0; i < tempWords.size() / 2; i++) {
    			temp = new String[2];
    			temp[0] = tempWords.get(i);
    			temp[1] = doc.docId + "";
    			invertedData1.add(temp);
    
    			temp = new String[2];
    			temp[0] = tempWords.get(i + tempWords.size() / 2);
    			temp[1] = doc.docId + "";
    			invertedData2.add(temp);
    		}
    
    		// 如果是奇数个,则将最后一个补入
    		if (tempWords.size() % 2 == 1) {
    			temp = new String[2];
    			temp[0] = tempWords.get(tempWords.size() - 1);
    			temp[1] = doc.docId + "";
    			invertedData2.add(temp);
    		}
    
    		index = doc.filePath.lastIndexOf(".");
    		baseFilePath = doc.filePath.substring(0, index);
    		mergeInvertedData(invertedData1, invertedData2, ifSort, baseFilePath
    				+ "-temp.txt");
    	}
    
    	/**
    	 * 合并读缓冲区数据写到写缓冲区中,用到了归并排序算法
    	 * 
    	 * @param outputPath
    	 *            写缓冲区的写出的路径
    	 */
    	private void mergeWordBuffers(String outputPath) {
    		int i = 0;
    		int j = 0;
    		int num1 = 0;
    		int num2 = 0;
    		// 写缓冲区下标
    		int writeIndex = 0;
    
    		while (readBuffer1[i][0] != null && readBuffer2[j][0] != null) {
    			num1 = Integer.parseInt(readBuffer1[i][0]);
    			num2 = Integer.parseInt(readBuffer2[j][0]);
    
    			// 如果缓冲1小,则优先存缓冲1到写缓冲区中
    			if (num1 < num2) {
    				writeBuffer[writeIndex][0] = num1 + "";
    				writeBuffer[writeIndex][1] = readBuffer1[i][1];
    
    				i++;
    			} else if (num2 < num1) {
    				writeBuffer[writeIndex][0] = num2 + "";
    				writeBuffer[writeIndex][1] = readBuffer1[j][1];
    
    				j++;
    			} else if (num1 == num2) {
    				// 如果两个缓冲区中的数字一样,说明是同个有效词,先进行合并再写入
    				writeBuffer[writeIndex][0] = num1 + "";
    				writeBuffer[writeIndex][1] = readBuffer1[i][1] + ":"
    						+ readBuffer2[j][1];
    
    				i++;
    				j++;
    			}
    
    			// 写的指针往后挪一位
    			writeIndex++;
    
    			// 如果写满写缓冲区时,进行写出到文件操作
    			if (writeIndex >= writeBufferSize) {
    				writeOutOperation(writeBuffer, outputPath);
    				writeIndex = 0;
    			}
    		}
    
    		if (readBuffer1[i][0] == null) {
    			writeRemainReadBuffer(readBuffer2, j, outputPath);
    		}
    
    		if (readBuffer2[j][0] == null) {
    			writeRemainReadBuffer(readBuffer1, j, outputPath);
    		}
    	}
    
    	/**
    	 * 将数据写出到磁盘文件操作,如果文件已经存在,则在文件尾部进行内容追加
    	 * 
    	 * @param buffer
    	 *            当前写缓冲中的数据
    	 * @param filePath
    	 *            输出地址
    	 */
    	private void writeOutOperation(String[][] buffer, String filePath) {
    		String word;
    		StringBuilder strBuilder = new StringBuilder();
    
    		// 将缓冲中的数据组成字符写入到文件中
    		for (String[] array : buffer) {
    			if (array[0] == null) {
    				continue;
    			}
    
    			word = array[0];
    
    			strBuilder.append(word);
    			strBuilder.append(" ");
    			strBuilder.append(array[1]);
    			strBuilder.append("
    ");
    		}
    
    		try {
    			File file = new File(filePath);
    			PrintStream ps = new PrintStream(new FileOutputStream(file));
    			ps.print(strBuilder.toString());// 往文件里写入字符串
    		} catch (FileNotFoundException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    	}
    	
    	/**
    	 * 将数据写出到磁盘文件操作,如果文件已经存在,则在文件尾部进行内容追加
    	 * 
    	 * @param buffer
    	 *            当前写缓冲中的数据
    	 * @param filePath
    	 *            输出地址
    	 * @param isCoded
    	 *            是否以编码的方式输出
    	 */
    	private void writeOutOperation(String[][] buffer, String filePath, boolean isCoded) {
    		String word;
    		StringBuilder strBuilder = new StringBuilder();
    
    		// 将缓冲中的数据组成字符写入到文件中
    		for (String[] array : buffer) {
    			if (array[0] == null) {
    				continue;
    			}
    
    			if(!isCoded){
    				word = code2word.get(array[0]);
    			}else{
    				word = array[0];
    			}
    
    			strBuilder.append(word);
    			strBuilder.append(" ");
    			strBuilder.append(array[1]);
    			strBuilder.append("
    ");
    		}
    
    		try {
    			File file = new File(filePath);
    			PrintStream ps = new PrintStream(new FileOutputStream(file));
    			ps.print(strBuilder.toString());// 往文件里写入字符串
    		} catch (FileNotFoundException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    	}
    
    	/**
    	 * 将剩余的读缓冲区中的数据读入写缓冲区中
    	 * 
    	 * @param remainBuffer
    	 *            读缓冲区的剩余缓冲
    	 * @param currentReadPos
    	 *            当前的读取位置
    	 * @param outputPath
    	 *            写缓冲区的写出文件路径
    	 */
    	private void writeRemainReadBuffer(String[][] remainBuffer,
    			int currentReadPos, String outputPath) {
    		while (remainBuffer[currentReadPos][0] != null
    				&& currentReadPos < readBufferSize) {
    			removeRBToWB(remainBuffer[currentReadPos]);
    
    			currentReadPos++;
    
    			// 如果写满写缓冲区时,进行写出到文件操作
    			if (writeBuffer[writeBufferSize - 1][0] != null) {
    				writeOutOperation(writeBuffer, outputPath);
    			}
    		}
    
    	}
    
    	/**
    	 * 将剩余读缓冲区中的数据通过插入排序的方式插入写缓冲区
    	 * 
    	 * @param record
    	 */
    	private void removeRBToWB(String[] record) {
    		int insertIndex = 0;
    		int endIndex = 0;
    		long num1;
    		long num2;
    		long code = Long.parseLong(record[0]);
    
    		// 如果写缓冲区目前为空,则直接加入
    		if (writeBuffer[0][0] == null) {
    			writeBuffer[0] = record;
    			return;
    		}
    
    		// 寻找待插入的位置
    		for (int i = 0; i < writeBufferSize - 1; i++) {
    			if (writeBuffer[i][0] == null) {
    				endIndex = i;
    				break;
    			}
    
    			num1 = Long.parseLong(writeBuffer[i][0]);
    
    			if (writeBuffer[i + 1][0] == null) {
    				if (code > num1) {
    					endIndex = i + 1;
    					insertIndex = i + 1;
    				}
    			} else {
    				num2 = Long.parseLong(writeBuffer[i + 1][0]);
    
    				if (code > num1 && code < num2) {
    					insertIndex = i + 1;
    				}
    			}
    		}
    
    		// 进行插入操作,相关数据进行位置迁移
    		for (int i = endIndex; i > insertIndex; i--) {
    			writeBuffer[i] = writeBuffer[i - 1];
    		}
    		writeBuffer[insertIndex] = record;
    	}
    
    	/**
    	 * 将磁盘中的2个倒排索引数据进行合并
    	 * 
    	 * @param invertedData1
    	 *            倒排索引为文件数据1
    	 * @param invertedData2
    	 *            倒排索引文件数据2
    	 * @param isSort
    	 *            是否需要对缓冲区中的数据进行排序
    	 * @param outputPath
    	 *            倒排索引输出文件地址
    	 */
    	private void mergeInvertedData(ArrayList<String[]> invertedData1,
    			ArrayList<String[]> invertedData2, boolean ifSort, String outputPath) {
    		int rIndex1 = 0;
    		int rIndex2 = 0;
    
    		// 重新初始化缓冲区
    		initBuffers();
    
    		while (invertedData1.size() > 0 && invertedData2.size() > 0) {
    			readBuffer1[rIndex1][0] = invertedData1.get(0)[0];
    			readBuffer1[rIndex1][1] = invertedData1.get(0)[1];
    
    			readBuffer2[rIndex2][0] = invertedData2.get(0)[0];
    			readBuffer2[rIndex2][1] = invertedData2.get(0)[1];
    
    			invertedData1.remove(0);
    			invertedData2.remove(0);
    			rIndex1++;
    			rIndex2++;
    
    			if (rIndex1 == readBufferSize) {
    				if (ifSort) {
    					wordBufferSort(readBuffer1);
    					wordBufferSort(readBuffer2);
    				}
    
    				mergeWordBuffers(outputPath);
    				initBuffers();
    			}
    		}
    
    		if (ifSort) {
    			wordBufferSort(readBuffer1);
    			wordBufferSort(readBuffer2);
    		}
    
    		mergeWordBuffers(outputPath);
    		readBuffer1 = new String[readBufferSize][2];
    		readBuffer2 = new String[readBufferSize][2];
    
    		if (invertedData1.size() == 0 && invertedData2.size() > 0) {
    			readRemainDataToRB(invertedData2, outputPath);
    		} else if (invertedData1.size() > 0 && invertedData2.size() == 0) {
    			readRemainDataToRB(invertedData1, outputPath);
    		}
    	}
    
    	/**
    	 * 剩余的有效词数据读入读缓冲区
    	 * 
    	 * @param remainData
    	 *            剩余数据
    	 * @param outputPath
    	 *            输出文件路径
    	 */
    	private void readRemainDataToRB(ArrayList<String[]> remainData,
    			String outputPath) {
    		int rIndex = 0;
    		while (remainData.size() > 0) {
    			readBuffer1[rIndex][0] = remainData.get(0)[0];
    			readBuffer1[rIndex][1] = remainData.get(0)[1];
    			remainData.remove(0);
    
    			rIndex++;
    
    			// 读缓冲 区写满,进行写入到写缓冲区中
    			if (readBuffer1[readBufferSize - 1][0] != null) {
    				wordBufferSort(readBuffer1);
    
    				writeRemainReadBuffer(readBuffer1, 0, outputPath);
    				initBuffers();
    			}
    		}
    
    		wordBufferSort(readBuffer1);
    
    		writeRemainReadBuffer(readBuffer1, 0, outputPath);
    
    	}
    
    	/**
    	 * 缓冲区数据进行排序
    	 * 
    	 * @param buffer
    	 *            缓冲空间
    	 */
    	private void wordBufferSort(String[][] buffer) {
    		String[] temp;
    		int k = 0;
    
    		long num1 = 0;
    		long num2 = 0;
    		for (int i = 0; i < buffer.length - 1; i++) {
    			// 缓冲区可能没填满
    			if (buffer[i][0] == null) {
    				continue;
    			}
    
    			k = i;
    			for (int j = i + 1; j < buffer.length; j++) {
    				// 缓冲区可能没填满
    				if (buffer[j][0] == null) {
    					continue;
    				}
    				// 获取2个缓冲区小块的起始编号值
    				num1 = Long.parseLong(buffer[k][0]);
    				num2 = Long.parseLong(buffer[j][0]);
    
    				if (num2 < num1) {
    					k = j;
    				}
    			}
    
    			if (k != i) {
    				temp = buffer[k];
    				buffer[k] = buffer[i];
    				buffer[i] = temp;
    			}
    		}
    	}
    
    	/**
    	 * 从文件中读取倒排索引数据
    	 * 
    	 * @param filePath
    	 *            单个文件
    	 */
    	private ArrayList<String[]> readInvertedFile(String filePath) {
    		File file = new File(filePath);
    		ArrayList<String[]> dataArray = new ArrayList<String[]>();
    
    		try {
    			BufferedReader in = new BufferedReader(new FileReader(file));
    			String str;
    			String[] tempArray;
    			while ((str = in.readLine()) != null) {
    				tempArray = str.split(" ");
    				dataArray.add(tempArray);
    			}
    			in.close();
    		} catch (IOException e) {
    			e.getStackTrace();
    		}
    
    		return dataArray;
    	}
    
    	/**
    	 * 从文件中读取数据
    	 * 
    	 * @param filePath
    	 *            单个文件
    	 */
    	private ArrayList<String> readDataFile(String filePath) {
    		File file = new File(filePath);
    		ArrayList<String[]> dataArray = new ArrayList<String[]>();
    		ArrayList<String> words = new ArrayList<>();
    
    		try {
    			BufferedReader in = new BufferedReader(new FileReader(file));
    			String str;
    			String[] tempArray;
    			while ((str = in.readLine()) != null) {
    				tempArray = str.split(" ");
    				dataArray.add(tempArray);
    			}
    			in.close();
    		} catch (IOException e) {
    			e.getStackTrace();
    		}
    
    		// 将每行词做拆分加入到总列表容器中
    		for (String[] array : dataArray) {
    			for (String word : array) {
    				if (!word.equals("")) {
    					words.add(word);
    				}
    			}
    		}
    
    		return words;
    	}
    }
    
    SPIMI算法工具类SPIMITool.java:

    package InvertedIndex;
    
    import java.io.BufferedReader;
    import java.io.File;
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.FileReader;
    import java.io.IOException;
    import java.io.PrintStream;
    import java.util.ArrayList;
    
    /**
     * SPIMI内存式单边扫描构建算法
     * @author lyq
     *
     */
    public class SPIMITool {
    	//倒排索引输出文件地址
    	private String outputFilePath;
    	// 读入的文档的有效词文件地址
    	private ArrayList<String> effectiveWordFiles;
    	// 内存缓冲区,不够还能够在增加空间
    	private ArrayList<String[]> buffers;
    	
    	public SPIMITool(ArrayList<String> effectiveWordFiles){
    		this.effectiveWordFiles = effectiveWordFiles;
    	}
    	
    	/**
    	 * 从文件中读取数据
    	 * 
    	 * @param filePath
    	 *            单个文件
    	 */
    	private ArrayList<String> readDataFile(String filePath) {
    		File file = new File(filePath);
    		ArrayList<String[]> dataArray = new ArrayList<String[]>();
    		ArrayList<String> words = new ArrayList<>();
    
    		try {
    			BufferedReader in = new BufferedReader(new FileReader(file));
    			String str;
    			String[] tempArray;
    			while ((str = in.readLine()) != null) {
    				tempArray = str.split(" ");
    				dataArray.add(tempArray);
    			}
    			in.close();
    		} catch (IOException e) {
    			e.getStackTrace();
    		}
    
    		// 将每行词做拆分加入到总列表容器中
    		for (String[] array : dataArray) {
    			for (String word : array) {
    				words.add(word);
    			}
    		}
    
    		return words;
    	}
     
    	
    	/**
    	 * 根据已有的文档数据进行倒排索引文件的构建
    	 * @param docs
    	 * 文档集合
    	 */
    	private void writeInvertedIndex(ArrayList<Document> docs){
    		ArrayList<String> datas;
    		String[] recordData;
    		
    		buffers = new ArrayList<>();
    		for(Document tempDoc: docs){
    			datas = tempDoc.effectWords;
    			
    			for(String word: datas){
    				recordData = new String[2];
    				recordData[0] = word;
    				recordData[1] = tempDoc.docId + "";
    				
    				addRecordToBuffer(recordData);
    			}
    		}
    		
    		//最后将数据写出到磁盘中
    		writeOutOperation(buffers, outputFilePath);
    	}
    	
    	/**
    	 * 将新读入的数据记录读入到内存缓冲中,如果存在则加入到倒排记录表中
    	 * @param insertedData
    	 * 待插入的数据
    	 */
    	private void addRecordToBuffer(String[] insertedData){
    		boolean isContained = false;
    		String wordName;
    		
    		wordName = insertedData[0];
    		for(String[] array: buffers){
    			if(array[0].equals(wordName)){
    				isContained = true;
    				//添加倒排索引记录,以:隔开
    				array[1] += ":" + insertedData[1];
    				
    				break;
    			}
    		}
    		
    		//如果没有包含,则说明是新的数据,直接添加
    		if(!isContained){
    			buffers.add(insertedData);
    		}
    	}
    	
    	/**
    	 * 将数据写出到磁盘文件操作,如果文件已经存在,则在文件尾部进行内容追加
    	 * @param buffer
    	 * 当前写缓冲中的数据
    	 * @param filePath
    	 * 输出地址
    	 */
    	private void writeOutOperation(ArrayList<String[]> buffer, String filePath) {
    		StringBuilder strBuilder = new StringBuilder();
    		
    		//将缓冲中的数据组成字符写入到文件中
    		for(String[] array: buffer){
    			strBuilder.append(array[0]);
    			strBuilder.append(" ");
    			strBuilder.append(array[1]);
    			strBuilder.append("
    ");
    		}
    		
    		try {
    			File file = new File(filePath);
    			PrintStream ps = new PrintStream(new FileOutputStream(file));
    			ps.println(strBuilder.toString());// 往文件里写入字符串
    		} catch (FileNotFoundException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    	}
    	
    	/**
    	 * 构造倒排索引文件
    	 */
    	public void createInvertedIndexFile(){
    		int docId = 1;
    		String baseFilePath;
    		String fileName;
    		String p;
    		int index1 = 0;
    		int index2 = 0;
    		Document tempDoc;
    		ArrayList<String> words;
    		ArrayList<Document> docs;
    		
    		outputFilePath = "spimi";
    		docs = new ArrayList<>();
    		p = effectiveWordFiles.get(0);
    		//提取文件名称
    		index1 = p.lastIndexOf("\");
    		baseFilePath = p.substring(0, index1+1);
    		outputFilePath = baseFilePath + "spimi";
    		
    		for(String path: effectiveWordFiles){
    			//获取文档有效词
    			words = readDataFile(path);
    			tempDoc = new Document(words, path, docId);
    			
    			docId++;
    			docs.add(tempDoc);
    			
    			//提取文件名称
    			index1 = path.lastIndexOf("\");
    			index2 = path.lastIndexOf(".");
    			fileName = path.substring(index1+1, index2);
    			
    			outputFilePath += "-" + fileName;
    		}
    		outputFilePath += ".txt";
    		
    		//根据文档数据进行倒排索引文件的创建
    		writeInvertedIndex(docs);
    	}
    
    }
    
    算法测试类Client.java:

    package InvertedIndex;
    
    import java.util.ArrayList;
    
    /**
     * 倒排索引测试类
     * @author lyq
     *
     */
    public class Client {
    	public static void main(String[] args){
    		//读写缓冲区的大小
    		int readBufferSize;
    		int writeBufferSize;
    		String baseFilePath;
    		PreTreatTool preTool;
    		//BSBI基于磁盘的外部排序算法
    		BSBITool bTool;
    		//SPIMI内存式单边扫描构建算法
    		SPIMITool sTool;
    		//有效词文件路径
    		ArrayList<String> efwFilePaths;
    		ArrayList<String> docFilePaths;
    		
    		readBufferSize = 10;
    		writeBufferSize = 20;
    		baseFilePath = "C:\Users\lyq\Desktop\icon\";
    		docFilePaths = new ArrayList<>();
    		docFilePaths.add(baseFilePath + "doc1.txt");
    		docFilePaths.add(baseFilePath + "doc2.txt");
    		
    		//文档预处理工具类
    		preTool = new PreTreatTool(docFilePaths);
    		preTool.preTreatWords();
    		
    		//预处理完获取有效词文件路径
    		efwFilePaths = preTool.getEFWPaths();
    		bTool = new BSBITool(efwFilePaths, readBufferSize, writeBufferSize);
    		bTool.outputInvertedFiles();
    		
    		sTool = new SPIMITool(efwFilePaths);
    		sTool.createInvertedIndexFile();
    	}
    }
    
    算法的输出:

    为了模拟出真实性,算法的输出都是以文件的形式。

    首先是预处理类处理之后的有效词文件doc1-efword.txt和doc2-efword.txt:

    mike
    study
    yesterday
    got
    last
    exam
    thinks
    english
    he
    
    可以看见,一些修饰词什么的已经被我过滤掉了。

    下面是BSBI算法生成的中间文件,就是映射成编码的文件,也许你看了这些数值真实表示的是什么词语:

    1426 0
    1542 0
    2540 0
    3056 0
    3325 0
    4326 0
    4897 0
    6329 0
    7327 0
    
    还有文档2的临时文件:

    1426 1
    1542 1
    2540 1
    3056 1
    3325 1
    4326 1
    4897 1
    6329 1
    7327 1
    
    将这2个文档的信息进行合并最终输出的倒排索引文件为:

    yesterday 0:1
    mike 0:1
    got 0:1
    english 0:1
    he 0:1
    last 0:1
    thinks 0:1
    study 0:1
    exam 0:1
    
    同样的SPIMI算法输出的结果:

    mike 1:2
    study 1:2
    yesterday 1:2
    got 1:2
    last 1:2
    exam 1:2
    thinks 1:2
    english 1:2
    he 1:2
    

    算法小结

    我在实现算法的过程中无疑低估了此算法的难度,尤其是BSBI的实现,因为中间读写缓冲区在做数据操作的时候,各种情况需要判断,诸如写缓冲区满了的时候要刷出到磁盘上,读缓冲区满的时候要通过归并排序移入读缓冲区中,这里面的判断实在过多,加上之前早期没有想到这个问题,导致算法可读性不是很好,就索性把缓冲区设大,先走通这个流程,所以这个算法大家还是以理解为主,就不要拿来实际运用了,同样对于SPIMI算法一样的道理,算法实现在这里帮助大家更好的理解吧,还有很多不足的地方。还有1点是文档内容预处理的时候,我只是象征性的进行过滤,真实的信息过滤实现复杂程度远远超过我所写的,这里包括了修饰词,时态词的变化,副词等等,这些有时还需要语义挖掘的一些知识来解决,大家意会即可。

  • 相关阅读:
    【cdq分治】【P4390】[BOI2007]Mokia 摩基亚
    【树上莫队】【SP10707】 COT2
    【费用流】【网络流24题】【P1251】 餐巾计划问题
    【费用流】【网络流24题】【P4014】 分配问题
    【MST】P2323 [HNOI2006]公路修建问题
    【组合数学】【P5216】DLS采花
    【线段树】【P4198】 楼房重建
    【整体二分】【P3527】 [POI2011]MET-Meteors
    【线性基/神仙题】P4151 [WC2011]最大XOR和路径
    【枚举&数据结构】【P2484】 [SDOI2011]打地鼠
  • 原文地址:https://www.cnblogs.com/bianqi/p/12183956.html
Copyright © 2020-2023  润新知