倒排索引构建算法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、然后递归的执行,直到文档内容全部完成这一系列操作。

这里有一张示意图:

倒排索引构建算法BSBI和SPIMI_第1张图片

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

SPIMI

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

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

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

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

算法的代码实现

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

doc1.txt:

[java]  view plain copy print ?
  1. Mike studyed English hardly yesterday  
  2. He got the 100 at the last exam  
  3. He thinks English is very interesting  

doc2.txt:

[java]  view plain copy print ?
  1. Mike studyed English hardly yesterday  
  2. He got the 100 at the last exam  
  3. He thinks English is very interesting  
下面是文档信息预处理类PreTreatTool.java:

[java]  view plain copy print ?
  1. package InvertedIndex;  
  2.   
  3. import java.io.BufferedReader;  
  4. import java.io.File;  
  5. import java.io.FileNotFoundException;  
  6. import java.io.FileOutputStream;  
  7. import java.io.FileReader;  
  8. import java.io.IOException;  
  9. import java.io.PrintStream;  
  10. import java.util.ArrayList;  
  11. import java.util.regex.Matcher;  
  12. import java.util.regex.Pattern;  
  13.   
  14. /** 
  15.  * 文档预处理工具类 
  16.  *  
  17.  * @author lyq 
  18.  *  
  19.  */  
  20. public class PreTreatTool {  
  21.     // 一些无具体意义的过滤词  
  22.     public static String[] FILTER_WORDS = new String[] { "at""At""The",  
  23.             "the""is""very" };  
  24.   
  25.     // 批量文档的文件地址  
  26.     private ArrayList<String> docFilePaths;  
  27.     // 输出的有效词的存放路径  
  28.     private ArrayList<String> effectWordPaths;  
  29.   
  30.     public PreTreatTool(ArrayList<String> docFilePaths) {  
  31.         this.docFilePaths = docFilePaths;  
  32.     }  
  33.   
  34.     /** 
  35.      * 获取文档有效词文件路径 
  36.      *  
  37.      * @return 
  38.      */  
  39.     public ArrayList<String> getEFWPaths() {  
  40.         return this.effectWordPaths;  
  41.     }  
  42.   
  43.     /** 
  44.      * 从文件中读取数据 
  45.      *  
  46.      * @param filePath 
  47.      *            单个文件 
  48.      */  
  49.     private ArrayList<String> readDataFile(String filePath) {  
  50.         File file = new File(filePath);  
  51.         ArrayList<String[]> dataArray = new ArrayList<String[]>();  
  52.         ArrayList<String> words = new ArrayList<>();  
  53.   
  54.         try {  
  55.             BufferedReader in = new BufferedReader(new FileReader(file));  
  56.             String str;  
  57.             String[] tempArray;  
  58.             while ((str = in.readLine()) != null) {  
  59.                 tempArray = str.split(" ");  
  60.                 dataArray.add(tempArray);  
  61.             }  
  62.             in.close();  
  63.         } catch (IOException e) {  
  64.             e.getStackTrace();  
  65.         }  
  66.   
  67.         // 将每行词做拆分加入到总列表容器中  
  68.         for (String[] array : dataArray) {  
  69.             for (String word : array) {  
  70.                 words.add(word);  
  71.             }  
  72.         }  
  73.   
  74.         return words;  
  75.     }  
  76.   
  77.     /** 
  78.      * 对文档内容词汇进行预处理 
  79.      */  
  80.     public void preTreatWords() {  
  81.         String baseOutputPath = "";  
  82.         int endPos = 0;  
  83.         ArrayList<String> tempWords = null;  
  84.         effectWordPaths = new ArrayList<>();  
  85.   
  86.         for (String filePath : docFilePaths) {  
  87.             tempWords = readDataFile(filePath);  
  88.             filterWords(tempWords, true);  
  89.   
  90.             // 重新组装出新的输出路径  
  91.             endPos = filePath.lastIndexOf(".");  
  92.             baseOutputPath = filePath.substring(0, endPos);  
  93.   
  94.             writeOutOperation(tempWords, baseOutputPath + "-efword.txt");  
  95.             effectWordPaths.add(baseOutputPath + "-efword.txt");  
  96.         }  
  97.     }  
  98.   
  99.     /** 
  100.      *  
  101.      * 对文档中的词语进行过滤操作 
  102.      *  
  103.      * @param words 
  104.      *            待处理文档词语 
  105.      * @param canRepeated 
  106.      *            有效词是否可以重复 
  107.      */  
  108.     private void filterWords(ArrayList<String> words, boolean canRepeated) {  
  109.         boolean isFilterWord;  
  110.         // 做形容词匹配  
  111.         Pattern adjPattern;  
  112.         // 做动词时态的匹配  
  113.         Pattern formerPattern;  
  114.         // 数字匹配  
  115.         Pattern numberPattern;  
  116.         Matcher adjMatcher;  
  117.         Matcher formerMatcher;  
  118.         Matcher numberMatcher;  
  119.         ArrayList<String> deleteWords = new ArrayList<>();  
  120.   
  121.         adjPattern = Pattern.compile(".*(ly$|ful$|ing$)");  
  122.         formerPattern = Pattern.compile(".*ed$");  
  123.         numberPattern = Pattern.compile("[0-9]+(.[0-9]+)?");  
  124.   
  125.         String w;  
  126.         for (int i = 0; i < words.size(); i++) {  
  127.             w = words.get(i);  
  128.             isFilterWord = false;  
  129.   
  130.             for (String fw : FILTER_WORDS) {  
  131.                 if (fw.equals(w)) {  
  132.                     deleteWords.add(w);  
  133.                     isFilterWord = true;  
  134.                     break;  
  135.                 }  
  136.             }  
  137.   
  138.             if (isFilterWord) {  
  139.                 continue;  
  140.             }  
  141.   
  142.             adjMatcher = adjPattern.matcher(w);  
  143.             formerMatcher = formerPattern.matcher(w);  
  144.             numberMatcher = numberPattern.matcher(w);  
  145.   
  146.             // 将词语统一小写字母化  
  147.             w = w.toLowerCase();  
  148.   
  149.             // 如果是形容词,副词形式的或是纯数字的词,则进行过滤  
  150.             if (adjMatcher.matches() || numberMatcher.matches()) {  
  151.                 deleteWords.add(w);  
  152.             } else if (formerMatcher.matches()) {  
  153.                 // 如果是ed结尾表明是动词的在时态方面的变化,进行变化,转为原有动词的形式,截去最末尾2个额外添加的后缀词  
  154.                 w = w.substring(0, w.length() - 2);  
  155.             }  
  156.               
  157.             words.set(i, w);  
  158.         }  
  159.   
  160.         // 进行无效词的过滤  
  161.         words.removeAll(deleteWords);  
  162.         deleteWords.clear();  
  163.   
  164.         String s1;  
  165.         String s2;  
  166.   
  167.         // 进行词语的去重  
  168.         for (int i = 0; i < words.size() - 1; i++) {  
  169.             s1 = words.get(i);  
  170.   
  171.             for (int j = i + 1; j < words.size(); j++) {  
  172.                 s2 = words.get(j);  
  173.   
  174.                 // 找到存在相同的词了,就挑出循环  
  175.                 if (s1.equals(s2)) {  
  176.                     deleteWords.add(s1);  
  177.                     break;  
  178.                 }  
  179.             }  
  180.         }  
  181.   
  182.         // 删除多余重复的词语  
  183.         words.removeAll(deleteWords);  
  184.         words.addAll(deleteWords);  
  185.     }  
  186.   
  187.     /** 
  188.      * 将数据写出到磁盘文件操作,如果文件已经存在,则在文件尾部进行内容追加 
  189.      *  
  190.      * @param buffer 
  191.      *            当前写缓冲中的数据 
  192.      * @param filePath 
  193.      *            输出地址 
  194.      */  
  195.     private void writeOutOperation(ArrayList<String> buffer, String filePath) {  
  196.         StringBuilder strBuilder = new StringBuilder();  
  197.   
  198.         // 将缓冲中的数据组成字符写入到文件中  
  199.         for (String word : buffer) {  
  200.             strBuilder.append(word);  
  201.             strBuilder.append("\n");  
  202.         }  
  203.   
  204.         try {  
  205.             File file = new File(filePath);  
  206.             PrintStream ps = new PrintStream(new FileOutputStream(file));  
  207.             ps.print(strBuilder.toString());// 往文件里写入字符串  
  208.         } catch (FileNotFoundException e) {  
  209.             // TODO Auto-generated catch block  
  210.             e.printStackTrace();  
  211.         }  
  212.     }  
  213.   
  214. }  
文档类Document.java:

[java]  view plain copy print ?
  1. package InvertedIndex;  
  2.   
  3. import java.util.ArrayList;  
  4.   
  5. /** 
  6.  * 文档类 
  7.  * @author lyq 
  8.  * 
  9.  */  
  10. public class Document {  
  11.     //文档的唯一标识  
  12.     int docId;  
  13.     //文档的文件地址  
  14.     String filePath;  
  15.     //文档中的有效词  
  16.     ArrayList<String> effectWords;  
  17.       
  18.     public Document(ArrayList<String> effectWords, String filePath){  
  19.         this.effectWords = effectWords;  
  20.         this.filePath = filePath;  
  21.     }  
  22.       
  23.     public Document(ArrayList<String> effectWords, String filePath, int docId){  
  24.         this(effectWords, filePath);  
  25.         this.docId = docId;  
  26.     }  
  27. }  
BSBI算法工具类BSBITool.java:

[java]  view plain copy print ?
  1. package InvertedIndex;  
  2.   
  3. import java.io.BufferedReader;  
  4. import java.io.File;  
  5. import java.io.FileNotFoundException;  
  6. import java.io.FileOutputStream;  
  7. import java.io.FileReader;  
  8. import java.io.IOException;  
  9. import java.io.PrintStream;  
  10. import java.util.ArrayList;  
  11. import java.util.HashMap;  
  12. import java.util.Map;  
  13.   
  14. /** 
  15.  * BSBI基于磁盘的外部排序算法 
  16.  *  
  17.  * @author lyq 
  18.  *  
  19.  */  
  20. public class BSBITool {  
  21.     // 文档唯一标识ID  
  22.     public static int DOC_ID = 0;  
  23.   
  24.     // 读缓冲区的大小  
  25.     private int readBufferSize;  
  26.     // 写缓冲区的大小  
  27.     private int writeBufferSize;  
  28.     // 读入的文档的有效词文件地址  
  29.     private ArrayList<String> effectiveWordFiles;  
  30.     // 倒排索引输出文件地址  
  31.     private String outputFilePath;  
  32.     // 读缓冲 1  
  33.     private String[][] readBuffer1;  
  34.     // 读缓冲2  
  35.     private String[][] readBuffer2;  
  36.     // 写缓冲区  
  37.     private String[][] writeBuffer;  
  38.     // 有效词与hashcode的映射  
  39.     private Map<String, String> code2word;  
  40.   
  41.     public BSBITool(ArrayList<String> effectiveWordFiles, int readBufferSize,  
  42.             int writeBufferSize) {  
  43.         this.effectiveWordFiles = effectiveWordFiles;  
  44.         this.readBufferSize = readBufferSize;  
  45.         this.writeBufferSize = writeBufferSize;  
  46.   
  47.         initBuffers();  
  48.     }  
  49.   
  50.     /** 
  51.      * 初始化缓冲区的设置 
  52.      */  
  53.     private void initBuffers() {  
  54.         readBuffer1 = new String[readBufferSize][2];  
  55.         readBuffer2 = new String[readBufferSize][2];  
  56.         writeBuffer = new String[writeBufferSize][2];  
  57.     }  
  58.   
  59.     /** 
  60.      * 从文件中读取有效词并进行编码替换 
  61.      *  
  62.      * @param filePath 
  63.      *            返回文档 
  64.      */  
  65.     private Document readEffectWords(String filePath) {  
  66.         long hashcode = 0;  
  67.   
  68.         String w;  
  69.         Document document;  
  70.         code2word = new HashMap<String, String>();  
  71.         ArrayList<String> words;  
  72.   
  73.         words = readDataFile(filePath);  
  74.   
  75.         for (int i = 0; i < words.size(); i++) {  
  76.             w = words.get(i);  
  77.   
  78.             hashcode = BKDRHash(w);  
  79.             hashcode = hashcode % 10000;  
  80.   
  81.             // 将有效词的hashcode取模值作为对应的代表  
  82.             code2word.put(hashcode + "", w);  
  83.             w = hashcode + "";  
  84.   
  85.             words.set(i, w);  
  86.         }  
  87.   
  88.         document = new Document(words, filePath, DOC_ID);  
  89.         DOC_ID++;  
  90.   
  91.         return document;  
  92.     }  
  93.   
  94.     /** 
  95.      * 将字符做哈希值的转换 
  96.      *  
  97.      * @param str 
  98.      *            待转换字符 
  99.      * @return 
  100.      */  
  101.     private long BKDRHash(String str) {  
  102.         int seed = 31/* 31 131 1313 13131 131313 etc.. */  
  103.         long hash = 0;  
  104.         int i = 0;  
  105.   
  106.         for (i = 0; i < str.length(); i++) {  
  107.             hash = (hash * seed) + (str.charAt(i));  
  108.         }  
  109.   
  110.         return hash;  
  111.   
  112.     }  
  113.   
  114.     /** 
  115.      * 根据输入的有效词输出倒排索引文件 
  116.      */  
  117.     public void outputInvertedFiles() {  
  118.         int index = 0;  
  119.         String baseFilePath = "";  
  120.         outputFilePath = "";  
  121.         Document doc;  
  122.         ArrayList<String> tempPaths;  
  123.         ArrayList<String[]> invertedData1;  
  124.         ArrayList<String[]> invertedData2;  
  125.   
  126.         tempPaths = new ArrayList<>();  
  127.         for (String filePath : effectiveWordFiles) {  
  128.             doc = readEffectWords(filePath);  
  129.             writeOutFile(doc);  
  130.   
  131.             index = doc.filePath.lastIndexOf(".");  
  132.             baseFilePath = doc.filePath.substring(0, index);  
  133.             writeOutOperation(writeBuffer, baseFilePath + "-temp.txt");  
  134.   
  135.             tempPaths.add(baseFilePath + "-temp.txt");  
  136.         }  
  137.   
  138.         outputFilePath = baseFilePath + "-bsbi-inverted.txt";  
  139.   
  140.         // 将中间产生的倒排索引数据进行总的合并并输出到一个文件中  
  141.         for (int i = 1; i < tempPaths.size(); i++) {  
  142.             if (i == 1) {  
  143.                 invertedData1 = readInvertedFile(tempPaths.get(0));  
  144.             } else {  
  145.                 invertedData1 = readInvertedFile(outputFilePath);  
  146.             }  
  147.   
  148.             invertedData2 = readInvertedFile(tempPaths.get(i));  
  149.   
  150.             mergeInvertedData(invertedData1, invertedData2, false,  
  151.                     outputFilePath);  
  152.   
  153.             writeOutOperation(writeBuffer, outputFilePath, false);  
  154.         }  
  155.     }  
  156.   
  157.     /** 
  158.      * 将文档的最终的倒排索引结果写出到文件 
  159.      *  
  160.      * @param doc 
  161.      *            待处理文档 
  162.      */  
  163.     private void writeOutFile(Document doc) {  
  164.         // 在读缓冲区中是否需要再排序  
  165.         boolean ifSort = true;  
  166.         int index = 0;  
  167.         String baseFilePath;  
  168.         String[] temp;  
  169.         ArrayList<String> tempWords = (ArrayList<String>) doc.effectWords  
  170.                 .clone();  
  171.         ArrayList<String[]> invertedData1;  
  172.         ArrayList<String[]> invertedData2;  
  173.   
  174.         invertedData1 = new ArrayList<>();  
  175.         invertedData2 = new ArrayList<>();  
  176.   
  177.         // 将文档的数据平均拆分成2份,用于读入后面的2个缓冲区中  
  178.         for (int i = 0; i < tempWords.size() / 2; i++) {  
  179.             temp = new String[2];  
  180.             temp[0] = tempWords.get(i);  
  181.             temp[1] = doc.docId + "";  
  182.             invertedData1.add(temp);  
  183.   
  184.             temp = new String[2];  
  185.             temp[0] = tempWords.get(i + tempWords.size() / 2);  
  186.             temp[1] = doc.docId + "";  
  187.             invertedData2.add(temp);  
  188.         }  
  189.   
  190.         // 如果是奇数个,则将最后一个补入  
  191.         if (tempWords.size() % 2 == 1) {  
  192.             temp = new String[2];  
  193.             temp[0] = tempWords.get(tempWords.size() - 1);  
  194.             temp[1] = doc.docId + "";  
  195.             invertedData2.add(temp);  
  196.         }  
  197.   
  198.         index = doc.filePath.lastIndexOf(".");  
  199.         baseFilePath = doc.filePath.substring(0, index);  
  200.         mergeInvertedData(invertedData1, invertedData2, ifSort, baseFilePath  
  201.                 + "-temp.txt");  
  202.     }  
  203.   
  204.     /** 
  205.      * 合并读缓冲区数据写到写缓冲区中,用到了归并排序算法 
  206.      *  
  207.      * @param outputPath 
  208.      *            写缓冲区的写出的路径 
  209.      */  
  210.     private void mergeWordBuffers(String outputPath) {  
  211.         int i = 0;  
  212.         int j = 0;  
  213.         int num1 = 0;  
  214.         int num2 = 0;  
  215.         // 写缓冲区下标  
  216.         int writeIndex = 0;  
  217.   
  218.         while (readBuffer1[i][0] != null && readBuffer2[j][0] != null) {  
  219.             num1 = Integer.parseInt(readBuffer1[i][0]);  
  220.             num2 = Integer.parseInt(readBuffer2[j][0]);  
  221.   
  222.             // 如果缓冲1小,则优先存缓冲1到写缓冲区中  
  223.             if (num1 < num2) {  
  224.                 writeBuffer[writeIndex][0] = num1 + "";  
  225.                 writeBuffer[writeIndex][1] = readBuffer1[i][1];  
  226.   
  227.                 i++;  
  228.             } else if (num2 < num1) {  
  229.                 writeBuffer[writeIndex][0] = num2 + "";  
  230.                 writeBuffer[writeIndex][1] = readBuffer1[j][1];  
  231.   
  232.                 j++;  
  233.             } else if (num1 == num2) {  
  234.                 // 如果两个缓冲区中的数字一样,说明是同个有效词,先进行合并再写入  
  235.                 writeBuffer[writeIndex][0] = num1 + "";  
  236.                 writeBuffer[writeIndex][1] = readBuffer1[i][1] + ":"  
  237.                         + readBuffer2[j][1];  
  238.   
  239.                 i++;  
  240.                 j++;  
  241.             }  
  242.   
  243.             // 写的指针往后挪一位  
  244.             writeIndex++;  
  245.   
  246.             // 如果写满写缓冲区时,进行写出到文件操作  
  247.             if (writeIndex >= writeBufferSize) {  
  248.                 writeOutOperation(writeBuffer, outputPath);  
  249.                 writeIndex = 0;  
  250.             }  
  251.         }  
  252.   
  253.         if (readBuffer1[i][0] == null) {  
  254.             writeRemainReadBuffer(readBuffer2, j, outputPath);  
  255.         }  
  256.   
  257.         if (readBuffer2[j][0] == null) {  
  258.             writeRemainReadBuffer(readBuffer1, j, outputPath);  
  259.         }  
  260.     }  
  261.   
  262.     /** 
  263.      * 将数据写出到磁盘文件操作,如果文件已经存在,则在文件尾部进行内容追加 
  264.      *  
  265.      * @param buffer 
  266.      *            当前写缓冲中的数据 
  267.      * @param filePath 
  268.      *            输出地址 
  269.      */  
  270.     private void writeOutOperation(String[][] buffer, String filePath) {  
  271.         String word;  
  272.         StringBuilder strBuilder = new StringBuilder();  
  273.   
  274.         // 将缓冲中的数据组成字符写入到文件中  
  275.         for (String[] array : buffer) {  
  276.             if (array[0] == null) {  
  277.                 continue;  
  278.             }  
  279.   
  280.             word = array[0];  
  281.   
  282.             strBuilder.append(word);  
  283.             strBuilder.append(" ");  
  284.             strBuilder.append(array[1]);  
  285.             strBuilder.append("\n");  
  286.         }  
  287.   
  288.         try {  
  289.             File file = new File(filePath);  
  290.             PrintStream ps = new PrintStream(new FileOutputStream(file));  
  291.             ps.print(strBuilder.toString());// 往文件里写入字符串  
  292.         } catch (FileNotFoundException e) {  
  293.             // TODO Auto-generated catch block  
  294.             e.printStackTrace();  
  295.         }  
  296.     }  
  297.       
  298.     /** 
  299.      * 将数据写出到磁盘文件操作,如果文件已经存在,则在文件尾部进行内容追加 
  300.      *  
  301.      * @param buffer 
  302.      *            当前写缓冲中的数据 
  303.      * @param filePath 
  304.      *            输出地址 
  305.      * @param isCoded 
  306.      *            是否以编码的方式输出 
  307.      */  
  308.     private void writeOutOperation(String[][] buffer, String filePath, boolean isCoded) {  
  309.         String word;  
  310.         StringBuilder strBuilder = new StringBuilder();  
  311.   
  312.         // 将缓冲中的数据组成字符写入到文件中  
  313.         for (String[] array : buffer) {  
  314.             if (array[0] == null) {  
  315.                 continue;  
  316.             }  
  317.   
  318.             if(!isCoded){  
  319.                 word = code2word.get(array[0]);  
  320.             }else{  
  321.                 word = array[0];  
  322.             }  
  323.   
  324.             strBuilder.append(word);  
  325.             strBuilder.append(" ");  
  326.             strBuilder.append(array[1]);  
  327.             strBuilder.append("\n");  
  328.         }  
  329.   
  330.         try {  
  331.             File file = new File(filePath);  
  332.             PrintStream ps = new PrintStream(new FileOutputStream(file));  
  333.             ps.print(strBuilder.toString());// 往文件里写入字符串  
  334.         } catch (FileNotFoundException e) {  
  335.             // TODO Auto-generated catch block  
  336.             e.printStackTrace();  
  337.         }  
  338.     }  
  339.   
  340.     /** 
  341.      * 将剩余的读缓冲区中的数据读入写缓冲区中 
  342.      *  
  343.      * @param remainBuffer 
  344.      *            读缓冲区的剩余缓冲 
  345.      * @param currentReadPos 
  346.      *            当前的读取位置 
  347.      * @param outputPath 
  348.      *            写缓冲区的写出文件路径 
  349.      */  
  350.     private void writeRemainReadBuffer(String[][] remainBuffer,  
  351.             int currentReadPos, String outputPath) {  
  352.         while (remainBuffer[currentReadPos][0] != null  
  353.                 && currentReadPos < readBufferSize) {  
  354.             removeRBToWB(remainBuffer[currentReadPos]);  
  355.   
  356.             currentReadPos++;  
  357.   
  358.             // 如果写满写缓冲区时,进行写出到文件操作  
  359.             if (writeBuffer[writeBufferSize - 1][0] != null) {  
  360.                 writeOutOperation(writeBuffer, outputPath);  
  361.             }  
  362.         }  
  363.   
  364.     }  
  365.   
  366.     /** 
  367.      * 将剩余读缓冲区中的数据通过插入排序的方式插入写缓冲区 
  368.      *  
  369.      * @param record 
  370.      */  
  371.     private void removeRBToWB(String[] record) {  
  372.         int insertIndex = 0;  
  373.         int endIndex = 0;  
  374.         long num1;  
  375.         long num2;  
  376.         long code = Long.parseLong(record[0]);  
  377.   
  378.         // 如果写缓冲区目前为空,则直接加入  
  379.         if (writeBuffer[0][0] == null) {  
  380.             writeBuffer[0] = record;  
  381.             return;  
  382.         }  
  383.   
  384.         // 寻找待插入的位置  
  385.         for (int i = 0; i < writeBufferSize - 1; i++) {  
  386.             if (writeBuffer[i][0] == null) {  
  387.                 endIndex = i;  
  388.                 break;  
  389.             }  
  390.   
  391.             num1 = Long.parseLong(writeBuffer[i][0]);  
  392.   
  393.             if (writeBuffer[i + 1][0] == null) {  
  394.                 if (code > num1) {  
  395.                     endIndex = i + 1;  
  396.                     insertIndex = i + 1;  
  397.                 }  
  398.             } else {  
  399.                 num2 = Long.parseLong(writeBuffer[i + 1][0]);  
  400.   
  401.                 if (code > num1 && code < num2) {  
  402.                     insertIndex = i + 1;  
  403.                 }  
  404.             }  
  405.         }  
  406.   
  407.         // 进行插入操作,相关数据进行位置迁移  
  408.         for (int i = endIndex; i > insertIndex; i--) {  
  409.             writeBuffer[i] = writeBuffer[i - 1];  
  410.         }  
  411.         writeBuffer[insertIndex] = record;  
  412.     }  
  413.   
  414.     /** 
  415.      * 将磁盘中的2个倒排索引数据进行合并 
  416.      *  
  417.      * @param invertedData1 
  418.      *            倒排索引为文件数据1 
  419.      * @param invertedData2 
  420.      *            倒排索引文件数据2 
  421.      * @param isSort 
  422.      *            是否需要对缓冲区中的数据进行排序 
  423.      * @param outputPath 
  424.      *            倒排索引输出文件地址 
  425.      */  
  426.     private void mergeInvertedData(ArrayList<String[]> invertedData1,  
  427.             ArrayList<String[]> invertedData2, boolean ifSort, String outputPath) {  
  428.         int rIndex1 = 0;  
  429.         int rIndex2 = 0;  
  430.   
  431.         // 重新初始化缓冲区  
  432.         initBuffers();  
  433.   
  434.         while (invertedData1.size() > 0 && invertedData2.size() > 0) {  
  435.             readBuffer1[rIndex1][0] = invertedData1.get(0)[0];  
  436.             readBuffer1[rIndex1][1] = invertedData1.get(0)[1];  
  437.   
  438.             readBuffer2[rIndex2][0] = invertedData2.get(0)[0];  
  439.             readBuffer2[rIndex2][1] = invertedData2.get(0)[1];  
  440.   
  441.             invertedData1.remove(0);  
  442.             invertedData2.remove(0);  
  443.             rIndex1++;  
  444.             rIndex2++;  
  445.   
  446.             if (rIndex1 == readBufferSize) {  
  447.                 if (ifSort) {  
  448.                     wordBufferSort(readBuffer1);  
  449.                     wordBufferSort(readBuffer2);  
  450.                 }  
  451.   
  452.                 mergeWordBuffers(outputPath);  
  453.                 initBuffers();  
  454.             }  
  455.         }  
  456.   
  457.         if (ifSort) {  
  458.             wordBufferSort(readBuffer1);  
  459.             wordBufferSort(readBuffer2);  
  460.         }  
  461.   
  462.         mergeWordBuffers(outputPath);  
  463.         readBuffer1 = new String[readBufferSize][2];  
  464.         readBuffer2 = new String[readBufferSize][2];  
  465.   
  466.         if (invertedData1.size() == 0 && invertedData2.size() > 0) {  
  467.             readRemainDataToRB(invertedData2, outputPath);  
  468.         } else if (invertedData1.size() > 0 && invertedData2.size() == 0) {  
  469.             readRemainDataToRB(invertedData1, outputPath);  
  470.         }  
  471.     }  
  472.   
  473.     /** 
  474.      * 剩余的有效词数据读入读缓冲区 
  475.      *  
  476.      * @param remainData 
  477.      *            剩余数据 
  478.      * @param outputPath 
  479.      *            输出文件路径 
  480.      */  
  481.     private void readRemainDataToRB(ArrayList<String[]> remainData,  
  482.             String outputPath) {  
  483.         int rIndex = 0;  
  484.         while (remainData.size() > 0) {  
  485.             readBuffer1[rIndex][0] = remainData.get(0)[0];  
  486.             readBuffer1[rIndex][1] = remainData.get(0)[1];  
  487.             remainData.remove(0);  
  488.   
  489.             rIndex++;  
  490.   
  491.             // 读缓冲 区写满,进行写入到写缓冲区中  
  492.             if (readBuffer1[readBufferSize - 1][0] != null) {  
  493.                 wordBufferSort(readBuffer1);  
  494.   
  495.                 writeRemainReadBuffer(readBuffer1, 0, outputPath);  
  496.                 initBuffers();  
  497.             }  
  498.         }  
  499.   
  500.         wordBufferSort(readBuffer1);  
  501.   
  502.         writeRemainReadBuffer(readBuffer1, 0, outputPath);  
  503.   
  504.     }  
  505.   
  506.     /** 
  507.      * 缓冲区数据进行排序 
  508.      *  
  509.      * @param buffer 
  510.      *            缓冲空间 
  511.      */  
  512.     private void wordBufferSort(String[][] buffer) {  
  513.         String[] temp;  
  514.         int k = 0;  
  515.   
  516.         long num1 = 0;  
  517.         long num2 = 0;  
  518.         for (int i = 0; i < buffer.length - 1; i++) {  
  519.             // 缓冲区可能没填满  
  520.             if (buffer[i][0] == null) {  
  521.                 continue;  
  522.             }  
  523.   
  524.             k = i;  
  525.             for (int j = i + 1; j < buffer.length; j++) {  
  526.                 // 缓冲区可能没填满  
  527.                 if (buffer[j][0] == null) {  
  528.                     continue;  
  529.                 }  
  530.                 // 获取2个缓冲区小块的起始编号值  
  531.                 num1 = Long.parseLong(buffer[k][0]);  
  532.                 num2 = Long.parseLong(buffer[j][0]);  
  533.   
  534.                 if (num2 < num1) {  
  535.                     k = j;  
  536.                 }  
  537.             }  
  538.   
  539.             if (k != i) {  
  540.                 temp = buffer[k];  
  541.                 buffer[k] = buffer[i];  
  542.                 buffer[i] = temp;  
  543.             }  
  544.         }  
  545.     }  
  546.   
  547.     /** 
  548.      * 从文件中读取倒排索引数据 
  549.      *  
  550.      * @param filePath 
  551.      *            单个文件 
  552.      */  
  553.     private ArrayList<String[]> readInvertedFile(String filePath) {  
  554.         File file = new File(filePath);  
  555.         ArrayList<String[]> dataArray = new ArrayList<String[]>();  
  556.   
  557.         try {  
  558.             BufferedReader in = new BufferedReader(new FileReader(file));  
  559.             String str;  
  560.             String[] tempArray;  
  561.             while ((str = in.readLine()) != null) {  
  562.                 tempArray = str.split(" ");  
  563.                 dataArray.add(tempArray);  
  564.             }  
  565.             in.close();  
  566.         } catch (IOException e) {  
  567.             e.getStackTrace();  
  568.         }  
  569.   
  570.         return dataArray;  
  571.     }  
  572.   
  573.     /** 
  574.      * 从文件中读取数据 
  575.      *  
  576.      * @param filePath 
  577.      *            单个文件 
  578.      */  
  579.     private ArrayList<String> readDataFile(String filePath) {  
  580.         File file = new File(filePath);  
  581.         ArrayList<String[]> dataArray = new ArrayList<String[]>();  
  582.         ArrayList<String> words = new ArrayList<>();  
  583.   
  584.         try {  
  585.             BufferedReader in = new BufferedReader(new FileReader(file));  
  586.             String str;  
  587.             String[] tempArray;  
  588.             while ((str = in.readLine()) != null) {  
  589.                 tempArray = str.split(" ");  
  590.                 dataArray.add(tempArray);  
  591.             }  
  592.             in.close();  
  593.         } catch (IOException e) {  
  594.             e.getStackTrace();  
  595.         }  
  596.   
  597.         // 将每行词做拆分加入到总列表容器中  
  598.         for (String[] array : dataArray) {  
  599.             for (String word : array) {  
  600.                 if (!word.equals("")) {  
  601.                     words.add(word);  
  602.                 }  
  603.             }  
  604.         }  
  605.   
  606.         return words;  
  607.     }  
  608. }  
SPIMI算法工具类SPIMITool.java:

[java]  view plain copy print ?
  1. package InvertedIndex;  
  2.   
  3. import java.io.BufferedReader;  
  4. import java.io.File;  
  5. import java.io.FileNotFoundException;  
  6. import java.io.FileOutputStream;  
  7. import java.io.FileReader;  
  8. import java.io.IOException;  
  9. import java.io.PrintStream;  
  10. import java.util.ArrayList;  
  11.   
  12. /** 
  13.  * SPIMI内存式单边扫描构建算法 
  14.  * @author lyq 
  15.  * 
  16.  */  
  17. public class SPIMITool {  
  18.     //倒排索引输出文件地址  
  19.     private String outputFilePath;  
  20.     // 读入的文档的有效词文件地址  
  21.     private ArrayList<String> effectiveWordFiles;  
  22.     // 内存缓冲区,不够还能够在增加空间  
  23.     private ArrayList<String[]> buffers;  
  24.       
  25.     public SPIMITool(ArrayList<String> effectiveWordFiles){  
  26.         this.effectiveWordFiles = effectiveWordFiles;  
  27.     }  
  28.       
  29.     /** 
  30.      * 从文件中读取数据 
  31.      *  
  32.      * @param filePath 
  33.      *            单个文件 
  34.      */  
  35.     private ArrayList<String> readDataFile(String filePath) {  
  36.         File file = new File(filePath);  
  37.         ArrayList<String[]> dataArray = new ArrayList<String[]>();  
  38.         ArrayList<String> words = new ArrayList<>();  
  39.   
  40.         try {  
  41.             BufferedReader in = new BufferedReader(new FileReader(file));  
  42.             String str;  
  43.             String[] tempArray;  
  44.             while ((str = in.readLine()) != null) {  
  45.                 tempArray = str.split(" ");  
  46.                 dataArray.add(tempArray);  
  47.             }  
  48.             in.close();  
  49.         } catch (IOException e) {  
  50.             e.getStackTrace();  
  51.         }  
  52.   
  53.         // 将每行词做拆分加入到总列表容器中  
  54.         for (String[] array : dataArray) {  
  55.             for (String word : array) {  
  56.                 words.add(word);  
  57.             }  
  58.         }  
  59.   
  60.         return words;  
  61.     }  
  62.    
  63.       
  64.     /** 
  65.      * 根据已有的文档数据进行倒排索引文件的构建 
  66.      * @param docs 
  67.      * 文档集合 
  68.      */  
  69.     private void writeInvertedIndex(ArrayList<Document> docs){  
  70.         ArrayList<String> datas;  
  71.         String[] recordData;  
  72.           
  73.         buffers = new ArrayList<>();  
  74.         for(Document tempDoc: docs){  
  75.             datas = tempDoc.effectWords;  
  76.               
  77.             for(String word: datas){  
  78.                 recordData = new String[2];  
  79.                 recordData[0] = word;  
  80.                 recordData[1] = tempDoc.docId + "";  
  81.                   
  82.                 addRecordToBuffer(recordData);  
  83.             }  
  84.         }  
  85.           
  86.         //最后将数据写出到磁盘中  
  87.         writeOutOperation(buffers, outputFilePath);  
  88.     }  
  89.       
  90.     /** 
  91.      * 将新读入的数据记录读入到内存缓冲中,如果存在则加入到倒排记录表中 
  92.      * @param insertedData 
  93.      * 待插入的数据 
  94.      */  
  95.     private void addRecordToBuffer(String[] insertedData){  
  96.         boolean isContained = false;  
  97.         String wordName;  
  98.           
  99.         wordName = insertedData[0];  
  100.         for(String[] array: buffers){  
  101.             if(array[0].equals(wordName)){  
  102.                 isContained = true;  
  103.                 //添加倒排索引记录,以:隔开  
  104.                 array[1] += ":" + insertedData[1];  
  105.                   
  106.                 break;  
  107.             }  
  108.         }  
  109.           
  110.         //如果没有包含,则说明是新的数据,直接添加  
  111.         if(!isContained){  
  112.             buffers.add(insertedData);  
  113.         }  
  114.     }  
  115.       
  116.     /** 
  117.      * 将数据写出到磁盘文件操作,如果文件已经存在,则在文件尾部进行内容追加 
  118.      * @param buffer 
  119.      * 当前写缓冲中的数据 
  120.      * @param filePath 
  121.      * 输出地址 
  122.      */  
  123.     private void writeOutOperation(ArrayList<String[]> buffer, String filePath) {  
  124.         StringBuilder strBuilder = new StringBuilder();  
  125.           
  126.         //将缓冲中的数据组成字符写入到文件中  
  127.         for(String[] array: buffer){  
  128.             strBuilder.append(array[0]);  
  129.             strBuilder.append(" ");  
  130.             strBuilder.append(array[1]);  
  131.             strBuilder.append("\n");  
  132.         }  
  133.           
  134.         try {  
  135.             File file = new File(filePath);  
  136.             PrintStream ps = new PrintStream(new FileOutputStream(file));  
  137.             ps.println(strBuilder.toString());// 往文件里写入字符串  
  138.         } catch (FileNotFoundException e) {  
  139.             // TODO Auto-generated catch block  
  140.             e.printStackTrace();  
  141.         }  
  142.     }  
  143.       
  144.     /** 
  145.      * 构造倒排索引文件 
  146.      */  
  147.     public void createInvertedIndexFile(){  
  148.         int docId = 1;  
  149.         String baseFilePath;  
  150.         String fileName;  
  151.         String p;  
  152.         int index1 = 0;  
  153.         int index2 = 0;  
  154.         Document tempDoc;  
  155.         ArrayList<String> words;  
  156.         ArrayList<Document> docs;  
  157.           
  158.         outputFilePath = "spimi";  
  159.         docs = new ArrayList<>();  
  160.         p = effectiveWordFiles.get(0);  
  161.         //提取文件名称  
  162.         index1 = p.lastIndexOf("\\");  
  163.         baseFilePath = p.substring(0, index1+1);  
  164.         outputFilePath = baseFilePath + "spimi";  
  165.           
  166.         for(String path: effectiveWordFiles){  
  167.             //获取文档有效词  
  168.             words = readDataFile(path);  
  169.             tempDoc = new Document(words, path, docId);  
  170.               
  171.             docId++;  
  172.             docs.add(tempDoc);  
  173.               
  174.             //提取文件名称  
  175.             index1 = path.lastIndexOf("\\");  
  176.             index2 = path.lastIndexOf(".");  
  177.             fileName = path.substring(index1+1, index2);  
  178.               
  179.             outputFilePath += "-" + fileName;  
  180.         }  
  181.         outputFilePath += ".txt";  
  182.           
  183.         //根据文档数据进行倒排索引文件的创建  
  184.         writeInvertedIndex(docs);  
  185.     }  
  186.   
  187. }  
算法测试类Client.java:

[java]  view plain copy print ?
  1. package InvertedIndex;  
  2.   
  3. import java.util.ArrayList;  
  4.   
  5. /** 
  6.  * 倒排索引测试类 
  7.  * @author lyq 
  8.  * 
  9.  */  
  10. public class Client {  
  11.     public static void main(String[] args){  
  12.         //读写缓冲区的大小  
  13.         int readBufferSize;  
  14.         int writeBufferSize;  
  15.         String baseFilePath;  
  16.         PreTreatTool preTool;  
  17.         //BSBI基于磁盘的外部排序算法  
  18.         BSBITool bTool;  
  19.         //SPIMI内存式单边扫描构建算法  
  20.         SPIMITool sTool;  
  21.         //有效词文件路径  
  22.         ArrayList<String> efwFilePaths;  
  23.         ArrayList<String> docFilePaths;  
  24.           
  25.         readBufferSize = 10;  
  26.         writeBufferSize = 20;  
  27.         baseFilePath = "C:\\Users\\lyq\\Desktop\\icon\\";  
  28.         docFilePaths = new ArrayList<>();  
  29.         docFilePaths.add(baseFilePath + "doc1.txt");  
  30.         docFilePaths.add(baseFilePath + "doc2.txt");  
  31.           
  32.         //文档预处理工具类  
  33.         preTool = new PreTreatTool(docFilePaths);  
  34.         preTool.preTreatWords();  
  35.           
  36.         //预处理完获取有效词文件路径  
  37.         efwFilePaths = preTool.getEFWPaths();  
  38.         bTool = new BSBITool(efwFilePaths, readBufferSize, writeBufferSize);  
  39.         bTool.outputInvertedFiles();  
  40.           
  41.         sTool = new SPIMITool(efwFilePaths);  
  42.         sTool.createInvertedIndexFile();  
  43.     }  
  44. }  
算法的输出:

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

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

[java]  view plain copy print ?
  1. mike  
  2. study  
  3. yesterday  
  4. got  
  5. last  
  6. exam  
  7. thinks  
  8. english  
  9. he  
可以看见,一些修饰词什么的已经被我过滤掉了。

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

[java]  view plain copy print ?
  1. 1426 0  
  2. 1542 0  
  3. 2540 0  
  4. 3056 0  
  5. 3325 0  
  6. 4326 0  
  7. 4897 0  
  8. 6329 0  
  9. 7327 0  
还有文档2的临时文件:

[java]  view plain copy print ?
  1. 1426 1  
  2. 1542 1  
  3. 2540 1  
  4. 3056 1  
  5. 3325 1  
  6. 4326 1  
  7. 4897 1  
  8. 6329 1  
  9. 7327 1  
将这2个文档的信息进行合并最终输出的倒排索引文件为:

[java]  view plain copy print ?
  1. yesterday 0:1  
  2. mike 0:1  
  3. got 0:1  
  4. english 0:1  
  5. he 0:1  
  6. last 0:1  
  7. thinks 0:1  
  8. study 0:1  
  9. exam 0:1  
同样的SPIMI算法输出的结果:

[java]  view plain copy print ?
  1. mike 1:2  
  2. study 1:2  
  3. yesterday 1:2  
  4. got 1:2  
  5. last 1:2  
  6. exam 1:2  
  7. thinks 1:2  
  8. english 1:2  
  9. he 1:2  

算法小结

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

你可能感兴趣的:(倒排索引构建算法BSBI和SPIMI)