前言:本文是對博文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);
修改后編譯運行的效果圖如下(用了另外一份更大的數據文件進行測試):