海量数据处理之倒排索引
2012-08-14 10:58 javaspring 阅读(9517) 评论(0) 编辑 收藏 举报前言:本文是对博文http://blog.csdn.net/v_july_v/article/details/7085669的总结和引用
一,什么是倒排索引
问题描述:文档检索系统,查询那些文件包含了某单词,比如常见的学术论文的关键字搜索。
基本原理及要点:为何叫倒排索引?一种索引方法,被用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置的映射。
以英文为例,下面是要被索引的文本:
T0 = "it is what it is"
T1 = "what is it"
T2 = "it is a banana"
我们就能得到下面的反向文件索引:
"a": {2}
"banana": {2}
"is": {0, 1, 2}
"it": {0, 1, 2}
"what": {0, 1}
检索的条件"what","is"和"it"将对应集合的交集。
正向索引开发出来用来存储每个文档的单词的列表。正向索引的查询往往满足每个文档有序频繁的全文查询和每个单词在校验文档中的验证这样的查询。在正向索引中,文档占据了中心的位置,每个文档指向了一个它所包含的索引项的序列。也就是说文档指向了它包含的那些单词,而反向索引则是单词指向了包含它的文档,很容易看到这个反向的关系。
本章要介绍这样一个问题,对倒排索引中的关键词进行编码。那么,这个问题将分为两个个步骤:
- 首先,要提取倒排索引内词典文件中的关键词;
- 对提取出来的关键词进行编码。本章采取hash编码的方式。既然要用hash编码,那么最重要的就是要解决hash冲突的问题,下文会详细介绍。
24.1、正排索引与倒排索引
咱们先来看什么是倒排索引,以及倒排索引与正排索引之间的区别:
我们知道,搜索引擎的关键步骤就是建立倒排索引,所谓倒排索引一般表示为一个关键词,然后是它的频度(出现的次数),位置(出现在哪一篇文章或网页中,及有关的日期,作者等信息),它相当于为互联网上几千亿页网页做了一个索引,好比一本书的目录、标签一般。读者想看哪一个主题相关的章节,直接根据目录即可找到相关的页面。不必再从书的第一页到最后一页,一页一页的查找。
接下来,阐述下正排索引与倒排索引的区别:
一般索引(正排索引)
正排表是以文档的ID为关键字,表中记录文档中每个字的位置信息,查找时扫描表中每个文档中字的信息直到找出所有包含查询关键字的文档。正排表结构如图1所示,这种组织方法在建立索引的时候结构比较简单,建立比较方便且易于维护;因为索引是基于文档建立的,若是有新的文档假如,直接为该文档建立一个新的索引块,挂接在原来索引文件的后面。若是有文档删除,则直接找到该文档号文档对因的索引信息,将其直接删除。但是在查询的时候需对所有的文档进行扫描以确保没有遗漏,这样就使得检索时间大大延长,检索效率低下。
尽管正排表的工作原理非常的简单,但是由于其检索效率太低,除非在特定情况下,否则实用性价值不大。
倒排索引
倒排表以字或词为关键字进行索引,表中关键字所对应的记录表项记录了出现这个字或词的所有文档,一个表项就是一个字表段,它记录该文档的ID和字符在该文档中出现的位置情况。由于每个字或词对应的文档数量在动态变化,所以倒排表的建立和维护都较为复杂,但是在查询的时候由于可以一次得到查询关键字所对应的所有文档,所以效率高于正排表。在全文检索中,检索的快速响应是一个最为关键的性能,而索引建立由于在后台进行,尽管效率相对低一些,但不会影响整个搜索引擎的效率。
倒排表的结构图如图2:
倒排表的索引信息保存的是字或词后继数组模型、互关联后继数组模型条在文档内的位置,在同一篇文档内相邻的字或词条的前后关系没有被保存到索引文件内。
24.2、倒排索引中提取关键词
倒排索引是搜索引擎之基石。建成了倒排索引后,用户要查找某个query,如在搜索框输入某个关键词:“结构之法”后,搜索引擎不会再次使用爬虫又一个一个去抓取每一个网页,从上到下扫描网页,看这个网页有没有出现这个关键词,而是会在它预先生成的倒排索引文件中查找和匹配包含这个关键词“结构之法”的所有网页。找到了之后,再按相关性度排序,最终把排序后的结果显示给用户。
如下,即是一个倒排索引文件(不全),我们把它取名为big_index,文件中每一较短的,不包含有“#####”符号的便是某个关键词,及这个关键词的出现次数。现在要从这个大索引文件中提取出这些关键词,--Firelf--,-11,-Winter-,.,007,007:天降杀机,02Chan..如何做到呢?一行一行的扫描整个索引文件么?
何意?之前已经说过:倒排索引包含词典和倒排记录表两个部分,词典一般有词项(或称为关键词)和词项频率(即这个词项或关键词出现的次数),倒排记录表则记录着上述词项(或关键词)所出现的位置,或出现的文档及网页ID等相关信息。
最简单的讲,就是要提取词典中的词项(关键词):--Firelf--,-11,-Winter-,.,007,007:天降杀机,02Chan...。
--Firelf--(关键词)
8(出现次数)
我们可以试着这么解决:通过查找#####便可判断某一行出现的词是不是关键词,但如果这样做的话,便要扫描整个索引文件的每一行,代价实在巨大。如何提高速度呢?对了,关键词后面的那个出现次数为我们问题的解决起到了很好的作用,如下注释所示:
//
本身没有##### 的行判定为关键词行,后跟这个关键词的行数N(即词项频率)
// 接下来,截取关键词--Firelf--,然后读取后面关键词的行数N
// 再跳过N行(滤过和避免扫描中间的倒排记录表信息)
// 读取下一个关键词..
有朋友指出,上述方法虽然减少了扫描的行数,但并没有减少I0开销。读者是否有更好地办法?欢迎随时交流。
24.2、为提取出来的关键词编码
爱思考的朋友可能会问,上述从倒排索引文件中提取出那些关键词(词项)的操作是为了什么呢?其实如我个人微博上12月12日所述的Hash词典编码:
词典文件的编码:1、词典怎么生成(存储和构造词典);2、如何运用hash对输入的汉字进行编码;3、如何更好的解决冲突,即不重复以及追加功能。具体例子为:事先构造好词典文件后,输入一个词,要求找到这个词的编码,然后将其编码输出。且要有不断能添加词的功能,不得重复。
步骤应该是如下:1、读索引文件;2、提取索引中的词出来;3、词典怎么生成,存储和构造词典;4、词典文件的编码:不重复与追加功能。编码比如,输入中国,他的编码可以为10001,然后输入银行,他的编码可以为10002。只要实现不断添加词功能,以及不重复即可,词典类的大文件,hash最重要的是怎样避免冲突。
也就是说,现在我要对上述提取出来后的关键词进行编码,采取何种方式编码呢?暂时用hash函数编码。编码之后的效果将是每一个关键词都有一个特定的编码,如下图所示(与上文big_index文件比较一下便知):
--Firelf-- 对应编码为:135942
-11 对应编码为:106101
....
但细心的朋友一看上图便知,其中第34~39行显示,有重复的编码,那么如何解决这个不重复编码的问题呢?
用hash表编码?但其极易产生冲突碰撞,为什么?请看:
哈希表是一种查找效率极高的数据结构,很多语言都在内部实现了哈希表。PHP中的哈希表是一种极为重要的数据结构,不但用于表示Array数据类型,还在Zend虚拟机内部用于存储上下文环境信息(执行上下文的变量及函数均使用哈希表结构存储)。
理想情况下哈希表插入和查找操作的时间复杂度均为O(1),任何一个数据项可以在一个与哈希表长度无关的时间内计算出一个哈希值(key),然后在常量时间内定位到一个桶(术语bucket,表示哈希表中的一个位置)。当然这是理想情况下,因为任何哈希表的长度都是有限的,所以一定存在不同的数据项具有相同哈希值的情况,此时不同数据项被定为到同一个桶,称为碰撞(collision)。
哈希表的实现需要解决碰撞问题,碰撞解决大体有两种思路,
- 第一种是根据某种原则将被碰撞数据定为到其它桶,例如线性探测——如果数据在插入时发生了碰撞,则顺序查找这个桶后面的桶,将其放入第一个没有被使用的桶;
- 第二种策略是每个桶不是一个只能容纳单个数据项的位置,而是一个可容纳多个数据的数据结构(例如链表或红黑树),所有碰撞的数据以某种数据结构的形式组织起来。
不论使用了哪种碰撞解决策略,都导致插入和查找操作的时间复杂度不再是O(1)。以查找为例,不能通过key定位到桶就结束,必须还要比较原始key(即未做哈希之前的key)是否相等,如果不相等,则要使用与插入相同的算法继续查找,直到找到匹配的值或确认数据不在哈希表中。
PHP是使用单链表存储碰撞的数据,因此实际上PHP哈希表的平均查找复杂度为O(L),其中L为桶链表的平均长度;而最坏复杂度为O(N),此时所有数据全部碰撞,哈希表退化成单链表。下图PHP中正常哈希表和退化哈希表的示意图。
哈希表碰撞攻击就是通过精心构造数据,使得所有数据全部碰撞,人为将哈希表变成一个退化的单链表,此时哈希表各种操作的时间均提升了一个数量级,因此会消耗大量CPU资源,导致系统无法快速响应请求,从而达到拒绝服务攻击(DoS)的目的。
可以看到,进行哈希碰撞攻击的前提是哈希算法特别容易找出碰撞,如果是MD5或者SHA1那基本就没戏了,幸运的是(也可以说不幸的是)大多数编程语言使用的哈希算法都十分简单(这是为了效率考虑),因此可以不费吹灰之力之力构造出攻击数据.(上述五段文字引自:http://www.codinglabs.org/html/hash-collisions-attack-on-php.html)。
24.4、暴雪的Hash算法
值得一提的是,在解决Hash冲突的时候,搞的焦头烂额,结果今天上午在自己的博客内的一篇文章(十一、从头到尾彻底解析Hash表算法)内找到了解决办法:网上流传甚广的暴雪的Hash算法。 OK,接下来,咱们回顾下暴雪的hash表算法:
“接下来,咱们来具体分析一下一个最快的Hash表算法。
我们由一个简单的问题逐步入手:有一个庞大的字符串数组,然后给你一个单独的字符串,让你从这个数组中查找是否有这个字符串并找到它,你会怎么做?
有一个方法最简单,老老实实从头查到尾,一个一个比较,直到找到为止,我想只要学过程序设计的人都能把这样一个程序作出来,但要是有程序员把这样的程序交给用户,我只能用无语来评价,或许它真的能工作,但...也只能如此了。
最合适的算法自然是使用HashTable(哈希表),先介绍介绍其中的基本知识,所谓Hash,一般是一个整数,通过某种算法,可以把一个字符串"压缩" 成一个整数。当然,无论如何,一个32位整数是无法对应回一个字符串的,但在程序中,两个字符串计算出的Hash值相等的可能非常小,下面看看在MPQ中的Hash算法:
函数prepareCryptTable以下的函数生成一个长度为0x500(合10进制数:1280)的cryptTable[0x500]
01.//函数prepareCryptTable以下的函数生成一个长度为0x500(合10进制数:1280)的cryptTable[0x500] 02.void prepareCryptTable() 03.{ 04. unsigned long seed = 0x00100001, index1 = 0, index2 = 0, i; 05. 06. for( index1 = 0; index1 < 0x100; index1++ ) 07. { 08. for( index2 = index1, i = 0; i < 5; i++, index2 += 0x100 ) 09. { 10. unsigned long temp1, temp2; 11. 12. seed = (seed * 125 + 3) % 0x2AAAAB; 13. temp1 = (seed & 0xFFFF) << 0x10; 14. 15. seed = (seed * 125 + 3) % 0x2AAAAB; 16. temp2 = (seed & 0xFFFF); 17. 18. cryptTable[index2] = ( temp1 | temp2 ); 19. } 20. } 21.}
函数HashString以下函数计算lpszFileName 字符串的hash值,其中dwHashType 为hash的类型,
01.//函数HashString以下函数计算lpszFileName 字符串的hash值,其中dwHashType 为hash的类型, 02.unsigned long HashString(const char *lpszkeyName, unsigned long dwHashType ) 03.{ 04. unsigned char *key = (unsigned char *)lpszkeyName; 05. unsigned long seed1 = 0x7FED7FED; 06. unsigned long seed2 = 0xEEEEEEEE; 07. int ch; 08. 09. while( *key != 0 ) 10. { 11. ch = *key++; 12. seed1 = cryptTable[(dwHashType<<8) + ch] ^ (seed1 + seed2); 13. seed2 = ch + seed1 + seed2 + (seed2<<5) + 3; 14. } 15. return seed1; 16.}
Blizzard的这个算法是非常高效的,被称为"One-Way Hash"( A one-way hash is a an algorithm that is constructed in such a way that deriving the original string (set of strings, actually) is virtually impossible)。举个例子,字符串"unitneutralacritter.grp"通过这个算法得到的结果是0xA26067F3。
是不是把第一个算法改进一下,改成逐个比较字符串的Hash值就可以了呢,答案是,远远不够,要想得到最快的算法,就不能进行逐个的比较,通常是构造一个哈希表(Hash Table)来解决问题,哈希表是一个大数组,这个数组的容量根据程序的要求来定义,
例如1024,每一个Hash值通过取模运算 (mod) 对应到数组中的一个位置,这样,只要比较这个字符串的哈希值对应的位置有没有被占用,就可以得到最后的结果了,想想这是什么速度?是的,是最快的O(1),现在仔细看看这个算法吧:
01.typedef struct 02.{ 03. int nHashA; 04. int nHashB; 05. char bExists; 06. ...... 07.} SOMESTRUCTRUE; 08.//一种可能的结构体定义?
函数GetHashTablePos下述函数为在Hash表中查找是否存在目标字符串,有则返回要查找字符串的Hash值,无则,return -1.
01.//函数GetHashTablePos下述函数为在Hash表中查找是否存在目标字符串,有则返回要查找字符串的Hash值,无则,return -1. 02.int GetHashTablePos( har *lpszString, SOMESTRUCTURE *lpTable ) 03.//lpszString要在Hash表中查找的字符串,lpTable为存储字符串Hash值的Hash表。 04.{ 05. int nHash = HashString(lpszString); //调用上述函数HashString,返回要查找字符串lpszString的Hash值。 06. int nHashPos = nHash % nTableSize; 07. 08. if ( lpTable[nHashPos].bExists && !strcmp( lpTable[nHashPos].pString, lpszString ) ) 09. { //如果找到的Hash值在表中存在,且要查找的字符串与表中对应位置的字符串相同, 10. return nHashPos; //返回找到的Hash值 11. } 12. else 13. { 14. return -1; 15. } 16.}
看到此,我想大家都在想一个很严重的问题:“如果两个字符串在哈希表中对应的位置相同怎么办?”,毕竟一个数组容量是有限的,这种可能性很大。解决该问题的方法很多,我首先想到的就是用“链表”,感谢大学里学的数据结构教会了这个百试百灵的法宝,我遇到的很多算法都可以转化成链表来解决,只要在哈希表的每个入口挂一个链表,保存所有对应的字符串就OK了。事情到此似乎有了完美的结局,如果是把问题独自交给我解决,此时我可能就要开始定义数据结构然后写代码了。
然而Blizzard的程序员使用的方法则是更精妙的方法。基本原理就是:他们在哈希表中不是用一个哈希值而是用三个哈希值来校验字符串。”
“MPQ使用文件名哈希表来跟踪内部的所有文件。但是这个表的格式与正常的哈希表有一些不同。首先,它没有使用哈希作为下标,把实际的文件名存储在表中用于验证,实际上它根本就没有存储文件名。而是使用了3种不同的哈希:一个用于哈希表的下标,两个用于验证。这两个验证哈希替代了实际文件名。
当然了,这样仍然会出现2个不同的文件名哈希到3个同样的哈希。但是这种情况发生的概率平均是:1:18889465931478580854784,这个概率对于任何人来说应该都是足够小的。现在再回到数据结构上,Blizzard使用的哈希表没有使用链表,而采用"顺延"的方式来解决问题。”下面,咱们来看看这个网上流传甚广的暴雪hash算法:
函数GetHashTablePos中,lpszString 为要在hash表中查找的字符串;lpTable 为存储字符串hash值的hash表;nTableSize 为hash表的长度:
表;nTableSize 为hash表的长度: 01.//函数GetHashTablePos中,lpszString 为要在hash表中查找的字符串;lpTable 为存储字符串hash值的hash表;nTableSize 为hash表的长度: 02.int GetHashTablePos( char *lpszString, MPQHASHTABLE *lpTable, int nTableSize ) 03.{ 04. const int HASH_OFFSET = 0, HASH_A = 1, HASH_B = 2; 05. 06. int nHash = HashString( lpszString, HASH_OFFSET ); 07. int nHashA = HashString( lpszString, HASH_A ); 08. int nHashB = HashString( lpszString, HASH_B ); 09. int nHashStart = nHash % nTableSize; 10. int nHashPos = nHashStart; 11. 12. while ( lpTable[nHashPos].bExists ) 13. { 14.// 如果仅仅是判断在该表中时候存在这个字符串,就比较这两个hash值就可以了,不用对结构体中的字符串进行比较。 15.// 这样会加快运行的速度?减少hash表占用的空间?这种方法一般应用在什么场合? 16. if ( lpTable[nHashPos].nHashA == nHashA 17. && lpTable[nHashPos].nHashB == nHashB ) 18. { 19. return nHashPos; 20. } 21. else 22. { 23. nHashPos = (nHashPos + 1) % nTableSize; 24. } 25. 26. if (nHashPos == nHashStart) 27. break; 28. } 29. return -1; 30.}
上述程序解释:
- 计算出字符串的三个哈希值(一个用来确定位置,另外两个用来校验)
- 察看哈希表中的这个位置
- 哈希表中这个位置为空吗?如果为空,则肯定该字符串不存在,返回-1。
- 如果存在,则检查其他两个哈希值是否也匹配,如果匹配,则表示找到了该字符串,返回其Hash值。
- 移到下一个位置,如果已经移到了表的末尾,则反绕到表的开始位置起继续查询
- 看看是不是又回到了原来的位置,如果是,则返回没找到
- 回到3。
24.4、不重复Hash编码
有了上面的暴雪Hash算法。咱们的问题便可解决了。不过,有两点必须先提醒读者:1、Hash表起初要初始化;2、暴雪的Hash算法对于查询那样处理可以,但对插入就不能那么解决。
关键主体代码如下:
01.//函数prepareCryptTable以下的函数生成一个长度为0x500(合10进制数:1280)的cryptTable[0x500] 02.void prepareCryptTable() 03.{ 04. unsigned long seed = 0x00100001, index1 = 0, index2 = 0, i; 05. 06. for( index1 = 0; index1 <0x100; index1++ ) 07. { 08. for( index2 = index1, i = 0; i < 5; i++, index2 += 0x100) 09. { 10. unsigned long temp1, temp2; 11. seed = (seed * 125 + 3) % 0x2AAAAB; 12. temp1 = (seed & 0xFFFF)<<0x10; 13. seed = (seed * 125 + 3) % 0x2AAAAB; 14. temp2 = (seed & 0xFFFF); 15. cryptTable[index2] = ( temp1 | temp2 ); 16. } 17. } 18.} 19. 20.//函数HashString以下函数计算lpszFileName 字符串的hash值,其中dwHashType 为hash的类型, 21.unsigned long HashString(const char *lpszkeyName, unsigned long dwHashType ) 22.{ 23. unsigned char *key = (unsigned char *)lpszkeyName; 24. unsigned long seed1 = 0x7FED7FED; 25. unsigned long seed2 = 0xEEEEEEEE; 26. int ch; 27. 28. while( *key != 0 ) 29. { 30. ch = *key++; 31. seed1 = cryptTable[(dwHashType<<8) + ch] ^ (seed1 + seed2); 32. seed2 = ch + seed1 + seed2 + (seed2<<5) + 3; 33. } 34. return seed1; 35.} 36. 37.///////////////////////////////////////////////////////////////////// 38.//function: 哈希词典 编码 39.//parameter: 40.//author: lei.zhou 41.//time: 2011-12-14 42.///////////////////////////////////////////////////////////////////// 43.MPQHASHTABLE TestHashTable[nTableSize]; 44.int TestHashCTable[nTableSize]; 45.int TestHashDTable[nTableSize]; 46.key_list test_data[nTableSize]; 47. 48.//直接调用上面的hashstring,nHashPos就是对应的HASH值。 49.int insert_string(const char *string_in) 50.{ 51. const int HASH_OFFSET = 0, HASH_C = 1, HASH_D = 2; 52. unsigned int nHash = HashString(string_in, HASH_OFFSET); 53. unsigned int nHashC = HashString(string_in, HASH_C); 54. unsigned int nHashD = HashString(string_in, HASH_D); 55. unsigned int nHashStart = nHash % nTableSize; 56. unsigned int nHashPos = nHashStart; 57. int ln, ires = 0; 58. 59. while (TestHashTable[nHashPos].bExists) 60. { 61.// if (TestHashCTable[nHashPos] == (int) nHashC && TestHashDTable[nHashPos] == (int) nHashD) 62.// break; 63.// //... 64.// else 65. //如之前所提示读者的那般,暴雪的Hash算法对于查询那样处理可以,但对插入就不能那么解决 66. nHashPos = (nHashPos + 1) % nTableSize; 67. 68. if (nHashPos == nHashStart) 69. break; 70. } 71. 72. ln = strlen(string_in); 73. if (!TestHashTable[nHashPos].bExists && (ln < nMaxStrLen)) 74. { 75. TestHashCTable[nHashPos] = nHashC; 76. TestHashDTable[nHashPos] = nHashD; 77. 78. test_data[nHashPos] = (KEYNODE *) malloc (sizeof(KEYNODE) * 1); 79. if(test_data[nHashPos] == NULL) 80. { 81. printf("10000 EMS ERROR !!!!\n"); 82. return 0; 83. } 84. 85. test_data[nHashPos]->pkey = (char *)malloc(ln+1); 86. if(test_data[nHashPos]->pkey == NULL) 87. { 88. printf("10000 EMS ERROR !!!!\n"); 89. return 0; 90. } 91. 92. memset(test_data[nHashPos]->pkey, 0, ln+1); 93. strncpy(test_data[nHashPos]->pkey, string_in, ln); 94. *((test_data[nHashPos]->pkey)+ln) = 0; 95. test_data[nHashPos]->weight = nHashPos; 96. 97. TestHashTable[nHashPos].bExists = 1; 98. } 99. else 100. { 101. if(TestHashTable[nHashPos].bExists) 102. printf("30000 in the hash table %s !!!\n", string_in); 103. else 104. printf("90000 strkey error !!!\n"); 105. } 106. return nHashPos; 107.}
接下来要读取索引文件big_index对其中的关键词进行编码(为了简单起见,直接一行一行扫描读写,没有跳过行数了):
01.void bigIndex_hash(const char *docpath, const char *hashpath) 02.{ 03. FILE *fr, *fw; 04. int len; 05. char *pbuf, *p; 06. char dockey[TERM_MAX_LENG]; 07. 08. if(docpath == NULL || *docpath == '\0') 09. return; 10. 11. if(hashpath == NULL || *hashpath == '\0') 12. return; 13. 14. fr = fopen(docpath, "rb"); //读取文件docpath 15. fw = fopen(hashpath, "wb"); 16. if(fr == NULL || fw == NULL) 17. { 18. printf("open read or write file error!\n"); 19. return; 20. } 21. 22. pbuf = (char*)malloc(BUFF_MAX_LENG); 23. if(pbuf == NULL) 24. { 25. fclose(fr); 26. return ; 27. } 28. 29. memset(pbuf, 0, BUFF_MAX_LENG); 30. 31. while(fgets(pbuf, BUFF_MAX_LENG, fr)) 32. { 33. len = GetRealString(pbuf); 34. if(len <= 1) 35. continue; 36. p = strstr(pbuf, "#####"); 37. if(p != NULL) 38. continue; 39. 40. p = strstr(pbuf, " "); 41. if (p == NULL) 42. { 43. printf("file contents error!"); 44. } 45. 46. len = p - pbuf; 47. dockey[0] = 0; 48. strncpy(dockey, pbuf, len); 49. 50. dockey[len] = 0; 51. 52. int num = insert_string(dockey); 53. 54. dockey[len] = ' '; 55. dockey[len+1] = '\0'; 56. char str[20]; 57. itoa(num, str, 10); 58. 59. strcat(dockey, str); 60. dockey[len+strlen(str)+1] = '\0'; 61. fprintf (fw, "%s\n", dockey); 62. 63. } 64. free(pbuf); 65. fclose(fr); 66. fclose(fw); 67.}
主函数已经很简单了,如下:
01.int main() 02.{ 03. prepareCryptTable(); //Hash表起初要初始化 04. 05. //现在要把整个big_index文件插入hash表,以取得编码结果 06. bigIndex_hash("big_index.txt", "hashpath.txt"); 07. system("pause"); 08. 09. return 0; 10.}
程序运行后生成的hashpath.txt文件如下:
如上所示,采取暴雪的Hash算法并在插入的时候做适当处理,当再次对上文中的索引文件big_index进行Hash编码后,冲突问题已经得到初步解决。当然,还有待更进一步更深入的测试。
后续添上数目索引1~10000...
后来又为上述文件中的关键词编了码一个计数的内码,不过,奇怪的是,同样的代码,在Dev C++ 与VS2010上运行结果却不同(左边dev上计数从"1"开始,VS上计数从“1994014002”开始),如下图所示:
在上面的bigIndex_hashcode函数的基础上,修改如下,即可得到上面的效果:
01.void bigIndex_hashcode(const char *in_file_path, const char *out_file_path) 02.{ 03. FILE *fr, *fw; 04. int len, value; 05. char *pbuf, *pleft, *p; 06. char keyvalue[TERM_MAX_LENG], str[WORD_MAX_LENG]; 07. 08. if(in_file_path == NULL || *in_file_path == '\0') { 09. printf("input file path error!\n"); 10. return; 11. } 12. 13. if(out_file_path == NULL || *out_file_path == '\0') { 14. printf("output file path error!\n"); 15. return; 16. } 17. 18. fr = fopen(in_file_path, "r"); //读取in_file_path路径文件 19. fw = fopen(out_file_path, "w"); 20. 21. if(fr == NULL || fw == NULL) 22. { 23. printf("open read or write file error!\n"); 24. return; 25. } 26. 27. pbuf = (char*)malloc(BUFF_MAX_LENG); 28. pleft = (char*)malloc(BUFF_MAX_LENG); 29. if(pbuf == NULL || pleft == NULL) 30. { 31. printf("allocate memory error!"); 32. fclose(fr); 33. return ; 34. } 35. 36. memset(pbuf, 0, BUFF_MAX_LENG); 37. 38. int offset = 1; 39. while(fgets(pbuf, BUFF_MAX_LENG, fr)) 40. { 41. if (--offset > 0) 42. continue; 43. 44. if(GetRealString(pbuf) <= 1) 45. continue; 46. 47. p = strstr(pbuf, "#####"); 48. if(p != NULL) 49. continue; 50. 51. p = strstr(pbuf, " "); 52. if (p == NULL) 53. { 54. printf("file contents error!"); 55. } 56. 57. len = p - pbuf; 58. 59. // 确定跳过行数 60. strcpy(pleft, p+1); 61. offset = atoi(pleft) + 1; 62. 63. strncpy(keyvalue, pbuf, len); 64. keyvalue[len] = '\0'; 65. value = insert_string(keyvalue); 66. 67. if (value != -1) { 68. 69. // key value中插入空格 70. keyvalue[len] = ' '; 71. keyvalue[len+1] = '\0'; 72. 73. itoa(value, str, 10); 74. strcat(keyvalue, str); 75. 76. keyvalue[len+strlen(str)+1] = ' '; 77. keyvalue[len+strlen(str)+2] = '\0'; 78. 79. keysize++; 80. itoa(keysize, str, 10); 81. strcat(keyvalue, str); 82. 83. // 将key value写入文件 84. fprintf (fw, "%s\n", keyvalue); 85. 86. } 87. } 88. free(pbuf); 89. fclose(fr); 90. fclose(fw); 91.}
第二部分:于给定的文档生成倒排索引
第一节、索引的构建方法
根据信息检索导论(Christtopher D.Manning等著,王斌译)一书给的提示,我们可以选择两种构建索引的算法:BSBI算法,与SPIMI算法。
BSBI算法,基于磁盘的外部排序算法,此算法首先将词项映射成其ID的数据结构,如Hash映射。而后将文档解析成词项ID-文档ID对,并在内存中一直处理,直到累积至放满一个固定大小的块空间为止,我们选择合适的块大小,使之能方便加载到内存中并允许在内存中快速排序,快速排序后的块转换成倒排索引格式后写入磁盘。
建立倒排索引的步骤如下:
- 将文档分割成几个大小相等的部分;
- 对词项ID-文档ID进行排序;
- 将具有同一词项ID的所有文档ID放到倒排记录表中,其中每条倒排记录仅仅是一个文档ID;
- 将基于块的倒排索引写到磁盘上。
此算法假如说最后可能会产生10个块。其伪码如下:
- BSBI NDEXConSTRUCTION()
- n <- 0
- while(all documents have not been processed)
- do n<-n+1
- block <- PARSENEXTBLOCK() //文档分析
- BSBI-INVERT(block)
- WRITEBLOCKTODISK(block,fn)
- MERGEBLOCKS(f1,...,fn;fmerged)
(基于块的排序索引算法,该算法将每个块的倒排索引文件存入文件f1,...,fn中,最后合并成fmerged
如果该算法应用最后一步产生了10个块,那么接下来便会将10个块索引同时合并成一个索引文件。)
合并时,同时打开所有块对应的文件,内存中维护了为10个块准备的读缓冲区和一个为最终合并索引准备的写缓冲区。每次迭代中,利用优先级队列(如堆结构或类似的数据结构)选择最小的未处理的词项ID进行处理。如下图所示(图片引自深入搜索引擎--海里信息的压缩、索引和查询,梁斌译),分块索引,分块排序,最终全部合并(说实话,跟MapReduce还是有些类似的):
读入该词项的倒排记录表并合并,合并结果写回磁盘中。需要时,再次从文件中读入数据到每个读缓冲区(基于磁盘的外部排序算法的更多可以参考:程序员编程艺术第十章、如何给10^7个数据量的磁盘文件排序)。
BSBI算法主要的时间消耗在排序上,选择什么排序方法呢,简单的快速排序足矣,其时间复杂度为O(N*logN),其中N是所需要排序的项(词项ID-文档ID对)的数目的上界。
SPIMI算法,内存式单遍扫描索引算法
与上述BSBI算法不同的是:SPIMI使用词项而不是其ID,它将每个块的词典写入磁盘,对于写一块则重新采用新的词典,只要硬盘空间足够大,它能索引任何大小的文档集。
倒排索引 = 词典(关键词或词项+词项频率)+倒排记录表。建倒排索引的步骤如下:
- 从头开始扫描每一个词项-文档ID(信息)对,遇一词,构建索引;
- 继续扫描,若遇一新词,则再建一新索引块(加入词典,通过Hash表实现,同时,建一新的倒排记录表);若遇一旧词,则找到其倒排记录表的位置,添加其后
- 在内存内基于分块完成排序,后合并分块;
- 写入磁盘。
其伪码如下:
- SPIMI-Invert(Token_stream)
- output.file=NEWFILE()
- dictionary = NEWHASH()
- while (free memory available)
- do token <-next(token_stream) //逐一处理每个词项-文档ID对
- if term(token) !(- dictionary
- then postings_list = AddToDictionary(dictionary,term(token)) //如果词项是第一次出现,那么加入hash词典,同时,建立一个新的倒排索引表
- else postings_list = GetPostingList(dictionary,term(token)) //如果不是第一次出现,那么直接返回其倒排记录表,在下面添加其后
- if full(postings_list)
- then postings_list =DoublePostingList(dictionary,term(token))
- AddToPosTingsList (postings_list,docID(token)) //SPIMI与BSBI的区别就在于此,前者直接在倒排记录表中增加此项新纪录
- sorted_terms <- SortTerms(dictionary)
- WriteBlockToDisk(sorted_terms,dictionary,output_file)
- return output_file
SPIMI与BSBI的主要区别:
SPIMI当发现关键词是第一次出现时,会直接在倒排记录表中增加一项(与BSBI算法不同)。同时,与BSBI算法一开始就整理出所有的词项ID-文档ID,并对它们进行排序的做法不同(而这恰恰是BSBI的做法),这里的每个倒排记录表都是动态增长的(也就是说,倒排记录表的大小会不断调整),同时,扫描一遍就可以实现全体倒排记录表的收集。
SPIMI这样做有两点好处:
- 由于不需要排序操作,因此处理的速度更快,
- 由于保留了倒排记录表对词项的归属关系,因此能节省内存,词项的ID也不需要保存。这样,每次单独的SPIMI-Invert调用能够处理的块大小可以非常大,整个倒排索引的构建过程也可以非常高效。
但不得不提的是,由于事先并不知道每个词项的倒排记录表大小,算法一开始只能分配一个较小的倒排记录表空间,每次当该空间放满的时候,就会申请加倍的空间,
与此同时,自然而然便会浪费一部分空间(当然,此前因为不保存词项ID,倒也省下一点空间,总体而言,算作是抵销了)。
不过,至少SPIMI所用的空间会比BSBI所用空间少。当内存耗尽后,包括词典和倒排记录表的块索引将被写到磁盘上,但在此之前,为使倒排记录表按照词典顺序来加快最后的合并操作,所以要对词项进行排序操作。
小数据量与大数据量的区别
在小数据量时,有足够的内存保证该创建过程可以一次完成;
数据规模增大后,可以采用分组索引,然后再归并索 引的策略。该策略是,
- 建立索引的模块根据当时运行系统所在的计算机的内存大小,将索引分为 k 组,使得每组运算所需内存都小于系统能够提供的最大使用内存的大小。
- 按照倒排索引的生成算法,生成 k 组倒排索引。
- 然后将这 k 组索引归并,即将相同索引词对应的数据合并到一起,就得到了以索引词为主键的最终的倒排文件索引,即反向索引。
第二节、Hash表的构建与实现
如下,给定如下图所示的正排文档,每一行的信息分别为(中间用##########隔开):文档ID、订阅源(子频道)、 频道分类、 网站类ID(大频道)、时间、 md5、文档权值、关键词、作者等等。
要求基于给定的上述正排文档。生成如第二十四章所示的倒排索引文件(注,关键词所在的文章如果是同一个日期的话,是挨在同一行的,用“#”符号隔开):
我们知道:为网页建立全文索引是网页预处理的核心部分,包括分析网页和建立倒排文件。二者是顺序进行,先分析网页,后建立倒排文件(也称为反向索引),如图所示:
正如上图粗略所示,我们知道倒排索引创建的过程如下:
- 写爬虫抓取相关的网页,而后提取相关网页或文章中所有的关键词;
- 分词,找出所有单词;
- 过滤不相干的信息(如广告等信息);
- 构建倒排索引,关键词=>(文章ID 出现次数 出现的位置)
- 生成词典文件 频率文件 位置文件
- 压缩。
建相关的数据结构
根据给定的正排文档,我们可以建立如下的两个结构体表示这些信息:文档ID、订阅源(子频道)、 频道分类、 网站类ID(大频道)、时间、 md5、文档权值、关键词、作者等等。如下所示:
01.typedef struct key_node 02.{ 03. char *pkey; // 关键词实体 04. int count; // 关键词出现次数 05. int pos; // 关键词在hash表中位置 06. struct doc_node *next; // 指向文档结点 07.}KEYNODE, *key_list; 08. 09.key_list key_array[TABLE_SIZE]; 10. 11.typedef struct doc_node 12.{ 13. char id[WORD_MAX_LEN]; //文档ID 14. int classOne; //订阅源(子频道) 15. char classTwo[WORD_MAX_LEN]; //频道分类 16. int classThree; //网站类ID(大频道) 17. char time[WORD_MAX_LEN]; //时间 18. char md5[WORD_MAX_LEN]; //md5 19. int weight; //文档权值 20. struct doc_node *next; 21.}DOCNODE, *doc_list;
我们知道,通过第二十四章的暴雪的Hash表算法,可以比较好的避免相关冲突的问题。下面,我们再次引用其代码:
基于暴雪的Hash之上的改造算
01.//函数prepareCryptTable以下的函数生成一个长度为0x100的cryptTable[0x100] 02.void PrepareCryptTable() 03.{ 04. unsigned long seed = 0x00100001, index1 = 0, index2 = 0, i; 05. 06. for( index1 = 0; index1 <0x100; index1++ ) 07. { 08. for( index2 = index1, i = 0; i < 5; i++, index2 += 0x100) 09. { 10. unsigned long temp1, temp2; 11. seed = (seed * 125 + 3) % 0x2AAAAB; 12. temp1 = (seed & 0xFFFF)<<0x10; 13. seed = (seed * 125 + 3) % 0x2AAAAB; 14. temp2 = (seed & 0xFFFF); 15. cryptTable[index2] = ( temp1 | temp2 ); 16. } 17. } 18.} 19. 20.//函数HashString以下函数计算lpszFileName 字符串的hash值,其中dwHashType 为hash的类型, 21.unsigned long HashString(const char *lpszkeyName, unsigned long dwHashType ) 22.{ 23. unsigned char *key = (unsigned char *)lpszkeyName; 24. unsigned long seed1 = 0x7FED7FED; 25. unsigned long seed2 = 0xEEEEEEEE; 26. int ch; 27. 28. while( *key != 0 ) 29. { 30. ch = *key++; 31. seed1 = cryptTable[(dwHashType<<8) + ch] ^ (seed1 + seed2); 32. seed2 = ch + seed1 + seed2 + (seed2<<5) + 3; 33. } 34. return seed1; 35.} 36. 37.//按关键字查询,如果成功返回hash表中索引位置 38.key_list SearchByString(const char *string_in) 39.{ 40. const int HASH_OFFSET = 0, HASH_C = 1, HASH_D = 2; 41. unsigned int nHash = HashString(string_in, HASH_OFFSET); 42. unsigned int nHashC = HashString(string_in, HASH_C); 43. unsigned int nHashD = HashString(string_in, HASH_D); 44. unsigned int nHashStart = nHash % TABLE_SIZE; 45. unsigned int nHashPos = nHashStart; 46. 47. while (HashTable[nHashPos].bExists) 48. { 49. if (HashATable[nHashPos] == (int) nHashC && HashBTable[nHashPos] == (int) nHashD) 50. { 51. break; 52. //查询与插入不同,此处不需修改 53. } 54. else 55. { 56. nHashPos = (nHashPos + 1) % TABLE_SIZE; 57. } 58. 59. if (nHashPos == nHashStart) 60. { 61. break; 62. } 63. } 64. 65. if( key_array[nHashPos] && strlen(key_array[nHashPos]->pkey)) 66. { 67. return key_array[nHashPos]; 68. } 69. 70. return NULL; 71.} 72. 73.//按索引查询,如果成功返回关键字(此函数在本章中没有被用到,可以忽略) 74.key_list SearchByIndex(unsigned int nIndex) 75.{ 76. unsigned int nHashPos = nIndex; 77. if (nIndex < TABLE_SIZE) 78. { 79. if(key_array[nHashPos] && strlen(key_array[nHashPos]->pkey)) 80. { 81. return key_array[nHashPos]; 82. } 83. } 84. 85. return NULL; 86.} 87. 88.//插入关键字,如果成功返回hash值 89.int InsertString(const char *str) 90.{ 91. const int HASH_OFFSET = 0, HASH_A = 1, HASH_B = 2; 92. unsigned int nHash = HashString(str, HASH_OFFSET); 93. unsigned int nHashA = HashString(str, HASH_A); 94. unsigned int nHashB = HashString(str, HASH_B); 95. unsigned int nHashStart = nHash % TABLE_SIZE; 96. unsigned int nHashPos = nHashStart; 97. int len; 98. 99. while (HashTable[nHashPos].bExists) 100. { 101. nHashPos = (nHashPos + 1) % TABLE_SIZE; 102. 103. if (nHashPos == nHashStart) 104. break; 105. } 106. 107. len = strlen(str); 108. if (!HashTable[nHashPos].bExists && (len < WORD_MAX_LEN)) 109. { 110. HashATable[nHashPos] = nHashA; 111. HashBTable[nHashPos] = nHashB; 112. 113. key_array[nHashPos] = (KEYNODE *) malloc (sizeof(KEYNODE) * 1); 114. if(key_array[nHashPos] == NULL) 115. { 116. printf("10000 EMS ERROR !!!!\n"); 117. return 0; 118. } 119. 120. key_array[nHashPos]->pkey = (char *)malloc(len+1); 121. if(key_array[nHashPos]->pkey == NULL) 122. { 123. printf("10000 EMS ERROR !!!!\n"); 124. return 0; 125. } 126. 127. memset(key_array[nHashPos]->pkey, 0, len+1); 128. strncpy(key_array[nHashPos]->pkey, str, len); 129. *((key_array[nHashPos]->pkey)+len) = 0; 130. key_array[nHashPos]->pos = nHashPos; 131. key_array[nHashPos]->count = 1; 132. key_array[nHashPos]->next = NULL; 133. HashTable[nHashPos].bExists = 1; 134. return nHashPos; 135. } 136. 137. if(HashTable[nHashPos].bExists) 138. printf("30000 in the hash table %s !!!\n", str); 139. else 140. printf("90000 strkey error !!!\n"); 141. return -1; 142.}
有了这个Hash表,接下来,我们就可以把词插入Hash表进行存储了。
第三节、倒排索引文件的生成与实现
Hash表实现了(存于HashSearch.h中),还得编写一系列的函数,如下所示(所有代码还只是初步实现了功能,稍后在第四部分中将予以改进与优化):
- //处理空白字符和空白行
- int GetRealString(char *pbuf)
- {
- int len = strlen(pbuf) - 1;
- while (len > 0 && (pbuf[len] == (char)0x0d || pbuf[len] == (char)0x0a || pbuf[len] == ' ' || pbuf[len] == '\t'))
- {
- len--;
- }
- if (len < 0)
- {
- *pbuf = '\0';
- return len;
- }
- pbuf[len+1] = '\0';
- return len + 1;
- }
- //重新strcoll字符串比较函数
- int strcoll(const void *s1, const void *s2)
- {
- char *c_s1 = (char *)s1;
- char *c_s2 = (char *)s2;
- while (*c_s1 == *c_s2++)
- {
- if (*c_s1++ == '\0')
- {
- return 0;
- }
- }
- return *c_s1 - *--c_s2;
- }
- //从行缓冲中得到各项信息,将其写入items数组
- void GetItems(char *&move, int &count, int &wordnum)
- {
- char *front = move;
- bool flag = false;
- int len;
- move = strstr(move, "#####");
- if (*(move + 5) == '#')
- {
- flag = true;
- }
- if (move)
- {
- len = move - front;
- strncpy(items[count], front, len);
- }
- items[count][len] = '\0';
- count++;
- if (flag)
- {
- move = move + 10;
- } else
- {
- move = move + 5;
- }
- }
- //保存关键字相应的文档内容
- doc_list SaveItems()
- {
- doc_list infolist = (doc_list) malloc(sizeof(DOCNODE));
- strcpy_s(infolist->id, items[0]);
- infolist->classOne = atoi(items[1]);
- strcpy_s(infolist->classTwo, items[2]);
- infolist->classThree = atoi(items[3]);
- strcpy_s(infolist->time, items[4]);
- strcpy_s(infolist->md5, items[5]);
- infolist->weight = atoi(items[6]);
- return infolist;
- }
- //得到目录下所有文件名
- int GetFileName(char filename[][FILENAME_MAX_LEN])
- {
- _finddata_t file;
- long handle;
- int filenum = 0;
- //C:\Users\zhangxu\Desktop\CreateInvertedIndex\data
- if ((handle = _findfirst("C:\\Users\\zhangxu\\Desktop\\CreateInvertedIndex\\data\\*.txt", &file)) == -1)
- {
- printf("Not Found\n");
- }
- else
- {
- do
- {
- strcpy_s(filename[filenum++], file.name);
- } while (!_findnext(handle, &file));
- }
- _findclose(handle);
- return filenum;
- }
- //以读方式打开文件,如果成功返回文件指针
- FILE* OpenReadFile(int index, char filename[][FILENAME_MAX_LEN])
- {
- char *abspath;
- char dirpath[] = {"data\\"};
- abspath = (char *)malloc(ABSPATH_MAX_LEN);
- strcpy_s(abspath, ABSPATH_MAX_LEN, dirpath);
- strcat_s(abspath, FILENAME_MAX_LEN, filename[index]);
- FILE *fp = fopen (abspath, "r");
- if (fp == NULL)
- {
- printf("open read file error!\n");
- return NULL;
- }
- else
- {
- return fp;
- }
- }
- //以写方式打开文件,如果成功返回文件指针
- FILE* OpenWriteFile(const char *in_file_path)
- {
- if (in_file_path == NULL)
- {
- printf("output file path error!\n");
- return NULL;
- }
- FILE *fp = fopen(in_file_path, "w+");
- if (fp == NULL)
- {
- printf("open write file error!\n");
- }
- return fp;
- }
最后,主函数编写如下:
- int main()
- {
- key_list keylist;
- char *pbuf, *move;
- int filenum = GetFileName(filename);
- FILE *fr;
- pbuf = (char *)malloc(BUF_MAX_LEN);
- memset(pbuf, 0, BUF_MAX_LEN);
- FILE *fw = OpenWriteFile("index.txt");
- if (fw == NULL)
- {
- return 0;
- }
- PrepareCryptTable(); //初始化Hash表
- int wordnum = 0;
- for (int i = 0; i < filenum; i++)
- {
- fr = OpenReadFile(i, filename);
- if (fr == NULL)
- {
- break;
- }
- // 每次读取一行处理
- while (fgets(pbuf, BUF_MAX_LEN, fr))
- {
- int count = 0;
- move = pbuf;
- if (GetRealString(pbuf) <= 1)
- continue;
- while (move != NULL)
- {
- // 找到第一个非'#'的字符
- while (*move == '#')
- move++;
- if (!strcmp(move, ""))
- break;
- GetItems(move, count, wordnum);
- }
- for (int i = 7; i < count; i++)
- {
- // 将关键字对应的文档内容加入文档结点链表中
- if (keylist = SearchByString(items[i])) //到hash表内查询
- {
- doc_list infolist = SaveItems();
- infolist->next = keylist->next;
- keylist->count++;
- keylist->next = infolist;
- }
- else
- {
- // 如果关键字第一次出现,则将其加入hash表
- int pos = InsertString(items[i]); //插入hash表
- keylist = key_array[pos];
- doc_list infolist = SaveItems();
- infolist->next = NULL;
- keylist->next = infolist;
- if (pos != -1)
- {
- strcpy_s(words[wordnum++], items[i]);
- }
- }
- }
- }
- }
- // 通过快排对关键字进行排序
- qsort(words, WORD_MAX_NUM, WORD_MAX_LEN, strcoll);
- // 遍历关键字数组,将关键字及其对应的文档内容写入文件中
- for (int i = 0; i < WORD_MAX_NUM; i++)
- {
- keylist = SearchByString(words[i]);
- if (keylist != NULL)
- {
- fprintf(fw, "%s %d\n", words[i], keylist->count);
- doc_list infolist = keylist->next;
- for (int j = 0; j < keylist->count; j++)
- {
- //文档ID,订阅源(子频道) 频道分类 网站类ID(大频道) 时间 md5,文档权值
- fprintf(fw, "%s %d %s %d %s %s %d\n", infolist->id, infolist->classOne,
- infolist->classTwo, infolist->classThree, infolist->time, infolist->md5, infolist->weight);
- infolist = infolist->next;
- }
- }
- }
- free(pbuf);
- fclose(fr);
- fclose(fw);
- system("pause");
- return 0;
- }
程序编译运行后,生成的倒排索引文件为index.txt,其与原来给定的正排文档对照如下:
有没有发现关键词奥恰洛夫出现在的三篇文章是同一个日期1210的,貌似与本文开头指定的倒排索引格式要求不符?因为第二部分开头中,已明确说明:“注,关键词所在的文章如果是同一个日期的话,是挨在同一行的,用“#”符号隔开”。OK,有疑问是好事,代表你思考了,请直接转至下文第4部分。
第四节、程序需求功能的改进
4.1、对相同日期与不同日期的处理
细心的读者可能还是会注意到:在第二部分开头中,要求基于给定的上述正排文档。生成如第二十四章所示的倒排索引文件是下面这样子的,即是:
也就是说,上面建索引的过程本该是如下的:
与第一部分所述的SMIPI算法有什么区别?对的,就在于对在同一个日期的出现的关键词的处理。如果是遇一旧词,则找到其倒排记录表的位置:相同日期,添加到之前同一日期的记录之后(第一个记录的后面记下同一日期的记录数目);不同日期,另起一行新增记录。
相同(单个)日期,根据文档权值排序
不同日期,根据时间排序
代码主要修改如下:
- //function: 对链表进行冒泡排序
- void ListSort(key_list keylist)
- {
- doc_list p = keylist->next;
- doc_list final = NULL;
- while (true)
- {
- bool isfinish = true;
- while (p->next != final) {
- if (strcmp(p->time, p->next->time) < 0)
- {
- SwapDocNode(p);
- isfinish = false;
- }
- p = p->next;
- }
- final = p;
- p = keylist->next;
- if (isfinish || p->next == final) {
- break;
- }
- }
- }
- int main()
- {
- key_list keylist;
- char *pbuf, *move;
- int filenum = GetFileName(filename);
- FILE *frp;
- pbuf = (char *)malloc(BUF_MAX_LEN);
- memset(pbuf, 0, BUF_MAX_LEN);
- FILE *fwp = OpenWriteFile("index.txt");
- if (fwp == NULL) {
- return 0;
- }
- PrepareCryptTable();
- int wordnum = 0;
- for (int i = 0; i < filenum; i++)
- {
- frp = OpenReadFile(i, filename);
- if (frp == NULL) {
- break;
- }
- // 每次读取一行处理
- while (fgets(pbuf, BUF_MAX_LEN, frp))
- {
- int count = 0;
- move = pbuf;
- if (GetRealString(pbuf) <= 1)
- continue;
- while (move != NULL)
- {
- // 找到第一个非'#'的字符
- while (*move == '#')
- move++;
- if (!strcmp(move, ""))
- break;
- GetItems(move, count, wordnum);
- }
- for (int i = 7; i < count; i++) {
- // 将关键字对应的文档内容加入文档结点链表中
- // 如果关键字第一次出现,则将其加入hash表
- if (keylist = SearchByString(items[i])) {
- doc_list infolist = SaveItems();
- infolist->next = keylist->next;
- keylist->count++;
- keylist->next = infolist;
- } else {
- int pos = InsertString(items[i]);
- keylist = key_array[pos];
- doc_list infolist = SaveItems();
- infolist->next = NULL;
- keylist->next = infolist;
- if (pos != -1) {
- strcpy_s(words[wordnum++], items[i]);
- }
- }
- }
- }
- }
- // 通过快排对关键字进行排序
- qsort(words, WORD_MAX_NUM, WORD_MAX_LEN, strcoll);
- // 遍历关键字数组,将关键字及其对应的文档内容写入文件中
- int rownum = 1;
- for (int i = 0; i < WORD_MAX_NUM; i++) {
- keylist = SearchByString(words[i]);
- if (keylist != NULL) {
- doc_list infolist = keylist->next;
- char date[9];
- // 截取年月日
- for (int j = 0; j < keylist->count; j++)
- {
- strncpy_s(date, infolist->time, 8);
- date[8] = '\0';
- strncpy_s(infolist->time, date, 9);
- infolist = infolist->next;
- }
- // 对链表根据时间进行排序
- ListSort(keylist);
- infolist = keylist->next;
- int *count = new int[WORD_MAX_NUM];
- memset(count, 0, WORD_MAX_NUM);
- strcpy_s(date, infolist->time);
- int num = 0;
- // 得到单个日期的文档数目
- for (int j = 0; j < keylist->count; j++)
- {
- if (strcmp(date, infolist->time) == 0) {
- count[num]++;
- } else {
- count[++num]++;
- }
- strcpy_s(date, infolist->time);
- infolist = infolist->next;
- }
- fprintf(fwp, "%s %d %d\n", words[i], num + 1, rownum);
- WriteFile(keylist, num, fwp, count);
- rownum++;
- }
- }
- free(pbuf);
- // fclose(frp);
- fclose(fwp);
- system("pause");
- return 0;
- }
修改后编译运行,生成的index.txt文件如下:
4.2、为关键词添上编码
如上图所示,已经满足需求了。但可以再在每个关键词的背后添加一个计数表示索引到了第多少个关键词:
第五节、算法的二次改进
5.1、省去二次Hash
针对本文评论下读者的留言,做了下思考,自觉可以省去二次hash:
- for (int i = 7; i < count; i++)
- {
- // 将关键字对应的文档内容加入文档结点链表中
- //也就是说当查询到hash表中没有某个关键词之,后便会插入
- //而查询的时候,search会调用hashstring,得到了nHashC ,nHashD
- //插入的时候又调用了一次hashstring,得到了nHashA,nHashB
- //而如果查询的时候,是针对同一个关键词查询的,所以也就是说nHashC&nHashD,与nHashA&nHashB是相同的,无需二次hash
- //所以,若要改进,改的也就是下面这个if~else语句里头。July,2011.12.30。
- if (keylist = SearchByString(items[i])) //到hash表内查询
- {
- doc_list infolist = SaveItems();
- infolist->next = keylist->next;
- keylist->count++;
- keylist->next = infolist;
- }
- else
- {
- // 如果关键字第一次出现,则将其加入hash表
- int pos = InsertString(items[i]); //插入hash表
- keylist = key_array[pos];
- doc_list infolist = SaveItems();
- infolist->next = NULL;
- keylist->next = infolist;
- if (pos != -1)
- {
- strcpy_s(words[wordnum++], items[i]);
- }
- }
- }
- }
- }
- // 通过快排对关键字进行排序
- qsort(words, WORD_MAX_NUM, WORD_MAX_LEN, strcoll);
5.2、除去排序,针对不同日期的记录直接插入
- //对链表进行冒泡排序。这里可以改成快速排序:等到统计完所有有关这个关键词的文章之后,才能对他集体快排。
- //但其实完全可以用插入排序,不同日期的,根据时间的先后找到插入位置进行插入:
- //假如说已有三条不同日期的记录 A B C
- //来了D后,发现D在C之前,B之后,那么就必须为它找到B C之间的插入位置,
- //A B D C。July、2011.12.31。
- void ListSort(key_list keylist)
- {
- doc_list p = keylist->next;
- doc_list final = NULL;
- while (true)
- {
- bool isfinish = true;
- while (p->next != final) {
- if (strcmp(p->time, p->next->time) < 0) //不同日期的按最早到最晚排序
- {
- SwapDocNode(p);
- isfinish = false;
- }
- p = p->next;
- }
- final = p;
- p = keylist->next;
- if (isfinish || p->next == final) {
- break;
- }
- }
- }
综上5.1、5.2两节免去冒泡排序和,省去二次hash和免去冒泡排序,修改后如下:
- for (int i = 7; i < count; i++) {
- // 将关键字对应的文档内容加入文档结点链表中
- // 如果关键字第一次出现,则将其加入hash表
- InitHashValue(items[i], hashvalue);
- if (keynode = SearchByString(items[i], hashvalue)) {
- doc_list infonode = SaveItems();
- doc_list p = keynode->next;
- // 根据时间由早到晚排序
- if (strcmp(infonode->time, p->time) < 0) {
- //考虑infonode插入keynode后的情况
- infonode->next = p;
- keynode->next = infonode;
- } else {
- //考虑其他情况
- doc_list pre = p;
- p = p->next;
- while (p)
- {
- if (strcmp(infonode->time, p->time) > 0) {
- p = p->next;
- pre = pre->next;
- } else {
- break;
- }
- }
- infonode->next = p;
- pre->next = infonode;
- }
- keynode->count++;
- } else {
- int pos = InsertString(items[i], hashvalue);
- keynode = key_array[pos];
- doc_list infolist = SaveItems();
- infolist->next = NULL;
- keynode->next = infolist;
- if (pos != -1) {
- strcpy_s(words[wordnum++], items[i]);
- }
- }
- }
- }
- }
- // 通过快排对关键字进行排序
- qsort(words, WORD_MAX_NUM, WORD_MAX_LEN, strcoll);
修改后编译运行的效果图如下(用了另外一份更大的数据文件进行测试):