1. 程式人生 > >大資料相關概念總結

大資料相關概念總結

本貼從解決這類問題的方法入手,開闢一系列專題來解決海量資料問題。擬包含 以下幾個方面。

  1. Bloom Filter
  2. Hash
  3. Bit-Map
  4. 堆(Heap)
  5. 雙層桶劃分
  6. 資料庫索引
  7. 倒排索引(Inverted Index)
  8. 外排序
  9. Trie樹
  10. MapReduce

  在這些解決方案之上,再借助一定的例子來剖析海量資料處理問題的解決方案。

海量資料處理專題(二)——Bloom Filter

【什麼是Bloom Filter】 
Bloom Filter是一種空間效率很高的隨機資料結構,它利用位陣列很簡潔地表示一個集合,並能判斷一個元素是否屬於這個集合。Bloom Filter的這種高效是有一定代價的:在判斷一個元素是否屬於某個集合時,有可能會把不屬於這個集合的元素誤認為屬於這個集合(false positive)。因此,Bloom Filter不適合那些“零錯誤”的應用場合。而在能容忍低錯誤率的應用場合下,Bloom Filter通過極少的錯誤換取了儲存空間的極大節省。 這裡有一篇關於Bloom Filter的詳細介紹,不太懂的博友可以看看。 
【適用範圍】 
可以用來實現資料字典,進行資料的判重,或者集合求交集 
【基本原理及要點】 
對於原理來說很簡單,位陣列+k個獨立hash函式。將hash函式對應的值的位陣列置1,查詢時如果發現所有hash函式對應位都是1說明存在,很明顯這 個過程並不保證查詢的結果是100%正確的。同時也不支援刪除一個已經插入的關鍵字,因為該關鍵字對應的位會牽動到其他的關鍵字。所以一個簡單的改進就是 counting Bloom filter,用一個counter陣列代替位陣列,就可以支援刪除了。 

還有一個比較重要的問題,如 何根據輸入元素個數n,確定位陣列m的大小及hash函式個數。當hash函式個數k=(ln2)*(m/n)時錯誤率最小。在錯誤率不大於E的情況 下,m至少要等於n*lg(1/E)才能表示任意n個元素的集合。但m還應該更大些,因為還要保證bit數組裡至少一半為0,則m應 該>=nlg(1/E)*lge 大概就是nlg(1/E)1.44倍(lg表示以2為底的對數)。 

舉個例子我們假設錯誤率為0.01,則此時m應大概是n的13倍。這樣k大概是8個。 

注意這裡m與n的單位不同,m是bit為單位,而n則是以元素個數為單位(準確的說是不同元素的個數)。通常單個元素的長度都是有很多bit的。所以使用bloom filter記憶體上通常都是節省的。 

【擴充套件】 
Bloom filter將集合中的元素對映到位陣列中,用k(k為雜湊函式個數)個對映位是否全1表示元素在不在這個集合中。Counting bloom filter(CBF)將位陣列中的每一位擴充套件為一個counter,從而支援了元素的刪除操作。Spectral Bloom Filter(SBF)將其與集合元素的出現次數關聯。SBF採用counter中的最小值來近似表示元素的出現頻率。 

【問題例項】 
給你A,B兩個檔案,各存放50億條URL,每條URL佔用64位元組,記憶體限制是4G,讓你找出A,B檔案共同的URL。如果是三個乃至n個檔案呢? 

根據這個問題我們來計算下記憶體的佔用,4G=2^32大概是40億*8大概是340億bit,n=50億,如果按出錯率0.01算需要的大概是650億個bit。 現在可用的是340億,相差並不多,這樣可能會使出錯率上升些。另外如果這些urlip是一一對應的,就可以轉換成ip,則大大簡單了。

海量資料處理專題(三)——Hash


【什麼是Hash】 
  Hash,一般翻譯做“雜湊”,也有直接音譯為“雜湊”的,就是把任意長度的輸入(又叫做預對映, pre-image),通過雜湊演算法,變換成固定長度的輸出,該輸出就是雜湊值。這種轉換是一種壓縮對映,也就是,雜湊值的空間通常遠小於輸入的空間,不同的輸入可能會雜湊成相同的輸出,而不可能從雜湊值來唯一的確定輸入值。簡單的說就是一種將任意長度的訊息壓縮到某一固定長度的訊息摘要的函式。 
HASH主要用於資訊保安領域中加密演算法,它把一些不同長度的資訊轉化成雜亂的128位的編碼,這些編碼值叫做HASH值. 也可以說,hash就是找到一種資料內容和資料存放地址之間的對映關係。 
  陣列的特點是:定址容易,插入和刪除困難;而連結串列的特點是:定址困難,插入和刪除容易。那麼我們能不能綜合兩者的特性,做出一種定址容易,插入刪除也容易的資料結構?答案是肯定的,這就是我們要提起的雜湊表,雜湊表有多種不同的實現方法,我接下來解釋的是最常用的一種方法——拉鍊法,我們可以理解為“連結串列的陣列”,如圖: 


 
左邊很明顯是個陣列,陣列的每個成員包括一個指標,指向一個連結串列的頭,當然這個連結串列可能為空,也可能元素很多。我們根據元素的一些特徵把元素分配到不同的連結串列中去,也是根據這些特徵,找到正確的連結串列,再從連結串列中找出這個元素。 
元素特徵轉變為陣列下標的方法就是雜湊法。雜湊法當然不止一種,下面列出三種比較常用的。 
1,除法雜湊法 
最直觀的一種,上圖使用的就是這種雜湊法,公式: 
index = value % 16 
學過彙編的都知道,求模數其實是通過一個除法運算得到的,所以叫“除法雜湊法”。 
2,平方雜湊法 
求index是非常頻繁的操作,而乘法的運算要比除法來得省時(對現在的CPU來說,估計我們感覺不出來),所以我們考慮把除法換成乘法和一個位移操作。公式: 
index = (value * value) >> 28 
如果數值分配比較均勻的話這種方法能得到不錯的結果,但我上面畫的那個圖的各個元素的值算出來的index都是0——非常失敗。也許你還有個問題,value如果很大,value * value不會溢位嗎?答案是會的,但我們這個乘法不關心溢位,因為我們根本不是為了獲取相乘結果,而是為了獲取index。 
3,斐波那契(Fibonacci)雜湊法 
平方雜湊法的缺點是顯而易見的,所以我們能不能找出一個理想的乘數,而不是拿value本身當作乘數呢?答案是肯定的。 
1,對於16位整數而言,這個乘數是40503 
2,對於32位整數而言,這個乘數是2654435769 
3,對於64位整數而言,這個乘數是11400714819323198485 
這幾個“理想乘數”是如何得出來的呢?這跟一個法則有關,叫黃金分割法則,而描述黃金分割法則的最經典表示式無疑就是著名的斐波那契數列,如果你還有興趣,就到網上查詢一下“斐波那契數列”等關鍵字,我數學水平有限,不知道怎麼描述清楚為什麼,另外斐波那契數列的值居然和太陽系八大行星的軌道半徑的比例出奇吻合,很神奇,對麼?
對我們常見的32位整數而言,公式: 
i ndex = (value * 2654435769) >> 28 
如果用這種斐波那契雜湊法的話,那我上面的圖就變成這樣了: 

 


很明顯,用斐波那契雜湊法調整之後要比原來的取摸雜湊法好很多。 
【適用範圍】 
快速查詢,刪除的基本資料結構,通常需要總資料量可以放入記憶體。 
【基本原理及要點】 
hash函式選擇,針對字串,整數,排列,具體相應的hash方法。 
碰撞處理,一種是open hashing,也稱為拉鍊法;另一種就是closed hashing,也稱開地址法,opened addressing。 
【擴充套件】 
d-left hashing中的d是多個的意思,我們先簡化這個問題,看一看2-left hashing。2-left hashing指的是將一個雜湊表分成長度相等的兩半,分別叫做T1和T2,給T1和T2分別配備一個雜湊函式,h1和h2。在儲存一個新的key時,同 時用兩個雜湊函式進行計算,得出兩個地址h1[key]和h2[key]。這時需要檢查T1中的h1[key]位置和T2中的h2[key]位置,哪一個 位置已經儲存的(有碰撞的)key比較多,然後將新key儲存在負載少的位置。如果兩邊一樣多,比如兩個位置都為空或者都儲存了一個key,就把新key 儲存在左邊的T1子表中,2-left也由此而來。在查詢一個key時,必須進行兩次hash,同時查詢兩個位置。 
【問題例項】 
1).海量日誌資料,提取出某日訪問百度次數最多的那個IP。 
IP的數目還是有限的,最多2^32個,所以可以考慮使用hash將ip直接存入記憶體,然後進行統計。

海量資料處理專題(四)——Bit-map

【什麼是Bit-map】 
所謂的Bit-map就是用一個bit位來標記某個元素對應的Value, 而Key即是該元素。由於採用了Bit為單位來儲存資料,因此在儲存空間方面,可以大大節省。 
如果說了這麼多還沒明白什麼是Bit-map,那麼我們來看一個具體的例子,假設我們要對0-7內的5個元素(4,7,2,5,3)排序(這裡假設這些元素沒有重複)。那麼我們就可以採用Bit-map的方法來達到排序的目的。要表示8個數,我們就只需要8個Bit(1Bytes),首先我們開闢1Byte的空間,將這些空間的所有Bit位都置為0(如下圖:) 


 
然後遍歷這5個元素,首先第一個元素是4,那麼就把4對應的位置為1(可以這樣操作 p+(i/8)|(0x01<<(i%8)) 當然了這裡的操作涉及到Big-ending和Little-ending的情況,這裡預設為Big-ending),因為是從零開始的,所以要把第五位置為一(如下圖): 

 


然後再處理第二個元素7,將第八位置為1,,接著再處理第三個元素,一直到最後處理完所有的元素,將相應的位置為1,這時候的記憶體的Bit位的狀態如下: 

 


然後我們現在遍歷一遍Bit區域,將該位是一的位的編號輸出(2,3,4,5,7),這樣就達到了排序的目的。下面的程式碼給出了一個BitMap的用法:排序。 

C程式碼  

複製程式碼
 1     //定義每個Byte中有8個Bit位   2     #include <memory.h>  
3 #define BYTESIZE 8
4 void SetBit(char *p, int posi)
5 {
6 for(int i=0; i < (posi/BYTESIZE); i++)
7 {
8 p++;
9 }
10
11 *p = *p|(0x01<<(posi%BYTESIZE));//將該Bit位賦值1 12 return;
13 }
14
15 void BitMapSortDemo()
16 {
17 //為了簡單起見,我們不考慮負數 18 int num[] = {3,5,2,10,6,12,8,14,9};
19
20 //BufferLen這個值是根據待排序的資料中最大值確定的
21 //待排序中的最大值是14,因此只需要2個Bytes(16個Bit)
22 //就可以了。 23 const int BufferLen = 2;
24 char *pBuffer = new char[BufferLen];
25
26 //要將所有的Bit位置為0,否則結果不可預知。 27 memset(pBuffer,0,BufferLen);
28 for(int i=0;i<9;i++)
29 {
30 //首先將相應Bit位上置為1 31 SetBit(pBuffer,num[i]);
32 }
33
34 //輸出排序結果 35 for(int i=0;i<BufferLen;i++)//每次處理一個位元組(Byte) 36 {
37 for(int j=0;j<BYTESIZE;j++)//處理該位元組中的每個Bit位 38 {
39 //判斷該位上是否是1,進行輸出,這裡的判斷比較笨。
40 //首先得到該第j位的掩碼(0x01<<j),將記憶體區中的
41 //位和此掩碼作與操作。最後判斷掩碼是否和處理後的
42 //結果相同 43 if((*pBuffer&(0x01<<j)) == (0x01<<j))
44 {
45 printf("%d ",i*BYTESIZE + j);
46 }
47 }
48 pBuffer++;
49 }
50 }
51
52 int _tmain(int argc, _TCHAR* argv[])
53 {
54 BitMapSortDemo();
55 return 0;
56 }
複製程式碼

【適用範圍】 

可進行資料的快速查詢,判重,刪除,一般來說資料範圍是int的10倍以下 

【基本原理及要點】 

使用bit陣列來表示某些元素是否存在,比如8位電話號碼 

【擴充套件】 

Bloom filter可以看做是對bit-map的擴充套件 

【問題例項】 

1)已知某個檔案內包含一些電話號碼,每個號碼為8位數字,統計不同號碼的個數。 

8位最多99 999 999,大概需要99m個bit,大概10幾m位元組的記憶體即可。 (可以理解為從0-99 999 999的數字,每個數字對應一個Bit位,所以只需要99M個Bit==1.2MBytes,這樣,就用了小小的1.2M左右的記憶體表示了所有的8位數的電話) 

2)2.5億個整數中找出不重複的整數的個數,記憶體空間不足以容納這2.5億個整數。 

將bit-map擴充套件一下,用2bit表示一個數即可,0表示未出現,1表示出現一次,2表示出現2次及以上,在遍歷這些數的時候,如果對應位置的值是0,則將其置為1;如果是1,將其置為2;如果是2,則保持不變。或者我們不用2bit來進行表示,我們用兩個bit-map即可模擬實現這個2bit-map,都是一樣的道理。 

海量資料處理專題(五)——堆

【什麼是堆】
概念:堆是一種特殊的二叉樹,具備以下兩種性質
1)每個節點的值都大於(或者都小於,稱為最小堆)其子節點的值
2)樹是完全平衡的,並且最後一層的樹葉都在最左邊
這樣就定義了一個最大堆。如下圖用一個數組來表示堆:

那麼下面介紹二叉堆:二叉堆是一種完全二叉樹,其任意子樹的左右節點(如果有的話)的鍵值一定比根節點大,上圖其實就是一個二叉堆。

你一定發覺了,最小的一個元素就是陣列第一個元素,那麼二叉堆這種有序佇列如何入隊呢?看圖:

假設要在這個二叉堆裡入隊一個單元,鍵值為2,那隻需在陣列末尾加入這個元素,然後儘可能把這個元素往上挪,直到挪不動,經過了這種複雜度為Ο(logn)的操作,二叉堆還是二叉堆。

那如何出隊呢?也不難,看圖:


出隊一定是出陣列的第一個元素,這麼來第一個元素以前的位置就成了空位,我們需要把這個空位挪至葉子節點,然後把陣列最後一個元素插入這個空位,把這個“空位”儘量往上挪。這種操作的複雜度也是Ο(logn)。

【適用範圍】
海量資料前n大,並且n比較小,堆可以放入記憶體

【基本原理及要點】
最大堆求前n小,最小堆求前n大。方法,比如求前n小,我們比較當前元素與最大堆裡的最大元素,如果它小於最大元素,則應該替換那個最大元 素。這樣最後得到的n個元素就是最小的n個。適合大資料量,求前n小,n的大小比較小的情況,這樣可以掃描一遍即可得到所有的前n元素,效率很高。

【擴充套件】
雙堆,一個最大堆與一個最小堆結合,可以用來維護中位數。

【問題例項】
1)100w個數中找最大的前100個數。
用一個100個元素大小的最小堆即可。

海量資料處理專題(六)

【什麼是雙層桶】  
事實上,與其說雙層桶劃分是一種資料結構,不如說它是一種演算法設計思想。面對一堆大量的資料我們無法處理的時候,我們可以將其分成一個個小的單元,然後根據一定的策略來處理這些小單元,從而達到目的。

【適用範圍】 
第k大,中位數,不重複或重複的數字

【基本原理及要點】 
因為元素範圍很大,不能利用直接定址表,所以通過多次劃分,逐步確定範圍,然後最後在一個可以接受的範圍內進行。可以通過多次縮小,雙層只是一個例子,分治才是其根本(只是“只分不治”)。

【擴充套件】 
當有時候需要用一個小範圍的資料來構造一個大資料,也是可以利用這種思想,相比之下不同的,只是其中的逆過程。

【問題例項】 
1).2.5億個整數中找出不重複的整數的個數,記憶體空間不足以容納這2.5億個整數。

有 點像鴿巢原理,整數個數為2^32,也就是,我們可以將這2^32個數,劃分為2^8個區域(比如用單個檔案代表一個區域),然後將資料分離到不同的區 域,然後不同的區域在利用bitmap就可以直接解決了。也就是說只要有足夠的磁碟空間,就可以很方便的解決。 當然這個題也可以用我們前面講過的BitMap方法解決,正所謂條條大道通羅馬~~~

2).5億個int找它們的中位數。

這個例子比上面那個更明顯。首先我們將int劃分為2^16個區域,然後讀取資料統計落到各個區域裡的數的個數,之後我們根據統計結果就可以判斷中位數落到那個區域,同時知道這個區域中的第幾大數剛好是中位數。然後第二次掃描我們只統計落在這個區域中的那些數就可以了。

實 際上,如果不是int是int64,我們可以經過3次這樣的劃分即可降低到可以接受的程度。即可以先將int64分成2^24個區域,然後確定區域的第幾 大數,在將該區域分成2^20個子區域,然後確定是子區域的第幾大數,然後子區域裡的數的個數只有2^20,就可以直接利用direct addr table進行統計了。

3).現在有一個0-30000的隨機數生成器。請根據這個隨機數生成器,設計一個抽獎範圍是0-350000彩票中獎號碼列表,其中要包含20000箇中獎號碼。

這個題剛好和上面兩個思想相反,一個0到3萬的隨機數生成器要生成一個0到35萬的隨機數。那麼我們完全可以將0-35萬的區間分成35/3=12個區 間,然後每個區間的長度都小於等於3萬,這樣我們就可以用題目給的隨機數生成器來生成了,然後再加上該區間的基數。那麼要每個區間生成多少個隨機數呢?計 算公式就是:區間長度*隨機數密度,在本題目中就是30000*(20000/350000)。最後要注意一點,該題目是有隱含條件的:彩票,這意味著你 生成的隨機數裡面不能有重複,這也是我為什麼用雙層桶劃分思想的另外一個原因。

海量資料處理專題(七)——資料庫索引及優化

索引是對資料庫表中一列或多列的值進行排序的一種結構,使用索引可快速訪問資料庫表中的特定資訊。

資料庫索引

什麼是索引

  資料庫索引好比是一本書前面的目錄,能加快資料庫的查詢速度。
  例如這樣一個查詢:select * from table1 where id=44。如果沒有索引,必須遍歷整個表,直到ID等於44的這一行被找到為止;有了索引之後(必須是在ID這一列上建立的索引),直接在索引裡面找44(也就是在ID這一列找),就可以得知這一行的位置,也就是找到了這一行。可見,索引是用來定位的。
  索引分為聚簇索引和非聚簇索引兩種,聚簇索引 是按照資料存放的物理位置為順序的,而非聚簇索引就不一樣了;聚簇索引能提高多行檢索的速度,而非聚簇索引對於單行的檢索很快。

概述

  建立索引的目的是加快對錶中記錄的查詢或排序。
  為表設定索引要付出代價的:一是增加了資料庫的儲存空間,二是在插入和修改資料時要花費較多的時間(因為索引也要隨之變動)。

B樹索引-Sql Server索引方式

為什麼要建立索引

  建立索引可以大大提高系統的效能。
    第一,通過建立唯一性索引,可以保證資料庫表中每一行資料的唯一性。
    第二,可以大大加快資料的檢索速度,這也是建立索引的最主要的原因。
    第三,可以加速表和表之間的連線,特別是在實現資料的參考完整性方面特別有意義。
    第四,在使用分組和排序子句進行資料檢索時,同樣可以顯著減少查詢中分組和排序的時間。
    第五,通過使用索引,可以在查詢的過程中,使用優化隱藏器,提高系統的效能。
  也許會有人要問:增加索引有如此多的優點,為什麼不對錶中的每一個列建立一個索引呢?因為,增加索引也有許多不利的方面。
    第一,建立索引和維護索引要耗費時間,這種時間隨著資料量的增加而增加。
    第二,索引需要佔物理空間,除了資料表佔資料空間之外,每一個索引還要佔一定的物理空間,如果要建立聚簇索引,那麼需要的空間就會更大。
    第三,當對錶中的資料進行增加、刪除和修改的時候,索引也要動態的維護,這樣就降低了資料的維護速度。

在哪建索引

  索引是建立在資料庫表中的某些列的上面。在建立索引的時候,應該考慮在哪些列上可以建立索引,在哪些列上不能建立索引。一般來說,應該在這些列上建立索引:
  在經常需要搜尋的列上,可以加快搜索的速度;
  在作為主鍵的列上,強制該列的唯一性和組織表中資料的排列結構;
  在經常用在連線的列上,這些列主要是一些外來鍵,可以加快連線的速度;在經常需要根據範圍進行搜尋的列上建立索引,因為索引已經排序,其指定的範圍是連續的;
  在經常需要排序的列上建立索引,因為索引已經排序,這樣查詢可以利用索引的排序,加快排序查詢時間;
  在經常使用在WHERE子句中的列上面建立索引,加快條件的判斷速度。
  同樣,對於有些列不應該建立索引。一般來說,不應該建立索引的的這些列具有下列特點:
  第一,對於那些在查詢中很少使用或者參考的列不應該建立索引。這是因為,既然這些列很少使用到,因此有索引或者無索引,並不能提高查詢速度。相反,由於增加了索引,反而降低了系統的維護速度和增大了空間需求。
  第二,對於那些只有很少資料值的列也不應該增加索引。這是因為,由於這些列的取值很少,例如人事表的性別列,在查詢的結果中,結果集的資料行佔了表中資料行的很大比例,即需要在表中搜索的資料行的比例很大。增加索引,並不能明顯加快檢索速度。
  第三,對於那些定義為text, image和bit資料型別的列不應該增加索引。這是因為,這些列的資料量要麼相當大,要麼取值很少,不利於使用索引。
  第四,當修改效能遠遠大於檢索效能時,不應該建立索引。這是因為,修改效能和檢索效能是互相矛盾的。當增加索引時,會提高檢索效能,但是會降低修改效能。當減少索引時,會提高修改效能,降低檢索效能。因此,當修改操作遠遠多於檢索操作時,不應該建立索引。

資料庫優化

  此外,除了資料庫索引之外,在LAMP結果如此流行的今天,資料庫(尤其是MySQL)效能優化也是海量資料處理的一個熱點。下面就結合自己的經驗,聊一聊MySQL資料庫優化的幾個方面。
  首先,在資料庫設計的時候,要能夠充分的利用索引帶來的效能提升,至於如何建立索引,建立什麼樣的索引,在哪些欄位上建立索引,上面已經講的很清楚了,這裡不在贅述。另外就是設計資料庫的原則就是儘可能少的進行資料庫寫操作(插入,更新,刪除等),查詢越簡單越好。如下:

資料庫設計


  其次,配置快取是必不可少的,配置快取可以有效的降低資料庫查詢讀取次數,從而緩解資料庫伺服器壓力,達到優化的目的,一定程度上來講,這算是一個“圍魏救趙”的辦法。可配置的快取包括索引快取(key_buffer),排序快取(sort_buffer),查詢快取(query_buffer),表描述符快取(table_cache),如下圖:

配置快取

  第三,切表,切表也是一種比較流行的資料庫優化法。分表包括兩種方式:橫向分表和縱向分表,其中,橫向分表比較有使用意義,故名思議,橫向切表就是指把記錄分到不同的表中,而每條記錄仍舊是完整的(縱向切表後每條記錄是不完整的),例如原始表中有100條記錄,我要切成2個表,那麼最簡單也是最常用的方法就是ID取摸切表法,本例中,就把ID為1,3,5,7。。。的記錄存在一個表中,ID為2,4,6,8,。。。的記錄存在另一張表中。雖然橫向切表可以減少查詢強度,但是它也破壞了原始表的完整性,如果該表的統計操作比較多,那麼就不適合橫向切表。橫向切表有個非常典型的用法,就是使用者資料:每個使用者的使用者資料一般都比較龐大,但是每個使用者資料之間的關係不大,因此這裡很適合橫向切表。最後,要記住一句話就是:分表會造成查詢的負擔,因此在資料庫設計之初,要想好是否真的適合切表的優化:

分表

第四,日誌分析,在資料庫運行了較長一段時間以後,會積累大量的LOG日誌,其實這裡面的蘊涵的有用的資訊量還是很大的。通過分析日誌,可以找到系統性能的瓶頸,從而進一步尋找優化方案。

效能分析

以上講的都是單機MySQL的效能優化的一些經驗,但是隨著資訊大爆炸,單機的資料庫伺服器已經不能滿足我們的需求,於是,多多節點,分散式資料庫網路出現了,其一般的結構如下:

分散式資料庫結構

這種分散式叢集的技術關鍵就是“同步複製”。。。

海量資料處理專題(八)——倒排索引(搜尋引擎之基石)

引言:

在資訊大爆炸的今天,有了搜尋引擎的幫助,使得我們能夠快速,便捷的找到所求。提到搜尋引擎,就不得不說VSM模型,說到VSM,就不得不聊倒排索引。可以毫不誇張的講,倒排索引是搜尋引擎的基石。

VSM檢索模型

VSM全稱是Vector Space Model(向量空間模型),是IR(Information Retrieval資訊檢索)模型中的一種,由於其簡單,直觀,高效,所以被廣泛的應用到搜尋引擎的架構中。98年的Google就是憑藉這樣的一個模型,開始了它的瘋狂擴張之路。廢話不多說,讓我們來看看到底VSM是一個什麼東東。

在開始之前,我預設大家對線性代數裡面的向量(Vector)有一定了解的。向量是既有大小又有方向的量,通常用有向線段表示,向量有:加、減、倍數、內積、距離、模、夾角的運算。

文件(Document):一個完整的資訊單元,對應的搜尋引擎系統裡,就是指一個個的網頁。

標引項(Term):文件的基本構成單位,例如在英文中可以看做是一個單詞,在中文中可以看作一個詞語。

查詢(Query):一個使用者的輸入,一般由多個Term構成。

那麼用一句話概況搜尋引擎所做的事情就是:對於使用者輸入的Query,找到最相似的Document返回給使用者。而這正是IR模型所解決的問題:

資訊檢索模型是指如何對查詢和文件進行表示,然後對它們進行相似度計算的框架和方法。

舉個簡單的例子:

現在有兩篇文章(Document)分別是 “春風來了,春天的腳步近了” 和 “春風不度玉門關”。然後輸入的Query是“春風”,從直觀上感覺,前者和輸入的查詢更相關一些,因為它包含有2個春,但這只是我們的直觀感覺,如何量化呢,要知道計算機是門嚴謹的學科^_^。這個時候,我們前面講的Term和VSM模型就派上用場了。

首先我們要確定向量的維數,這時候就需要一個字典庫,字典庫的大小,即是向量的維數。在該例中,字典為{春風,來了,春天, 的,腳步,近了,不度,玉門關} ,文件向量,查詢向量如下圖:

VSM模型示例

PS:為了簡單起見,這裡分詞的粒度很大。

將Query和Document都量化為向量以後,那麼就可以計算使用者的查詢和哪個文件相似性更大了。簡單的計算結果是D1和D2同Query的內積都是1,囧。當然了,如果分詞粒度再細一些,查詢的結果就是另外一個樣子了,因此分詞的粒度也是會對查詢結果(主要是召回率和準確率)造成影響的。

上述的例子是用一個很簡單的例子來說明VSM模型的,計算文件相似度的時候也是採用最原始的內積的方法,並且只考慮了詞頻(TF)影響因子,而沒有考慮反詞頻(IDF),而現在比較常用的是cos夾角法,影響因子也非常多,據傳Google的影響因子有100+之多。
大名鼎鼎的Lucene專案就是採用VSM模型構建的,VSM的核心公式如下(由cos夾角法演變,此處省去推導過程)

VSM模型公式

從上面的例子不難看出,如果向量的維度(對漢語來將,這個值一般在30w-45w)變大,而且文件數量(通常都是海量的)變多,那麼計算一次相關性,開銷是非常大的,如何解決這個問題呢?不要忘記了我們這節的主題就是 倒排索引,主角終於粉墨登場了!!!

倒排索引

倒排索引非常類似我們前面提到的Hash結構。以下內容來自維基百科:

倒排索引(英語:Inverted index),也常被稱為反向索引置入檔案反向檔案,是一種索引方法,被用來儲存在全文搜尋下某個單詞在一個文件或者一組文件中的儲存位置的對映。它是文件檢索系統中最常用的資料結構。

有兩種不同的反向索引形式:

  • 一條記錄的水平反向索引(或者反向檔案索引)包含每個引用單詞的文件的列表。
  • 一個單詞的水平反向索引(或者完全反向索引)又包含每個單詞在一個文件中的位置。

後者的形式提供了更多的相容性(比如短語搜尋),但是需要更多的時間和空間來建立。

由上面的定義可以知道,一個倒排索引包含一個字典的索引和所有詞的列表。其中字典索引中包含了所有的Term(通俗理解為文件中的詞),索引後面跟的列表則儲存該詞的資訊(出現的文件號,甚至包含在每個文件中的位置資訊)。下面我們還採用上面的方法舉一個簡單的例子來說明倒排索引。

例如現在我們要對三篇文件建立索引(實際應用中,文件的數量是海量的):

文件1(D1):中國移動網際網路發展迅速

文件2(D2):移動網際網路未來的潛力巨大

文件3(D3):中華民族是個勤勞的民族

那麼文件中的詞典集合為:{中國,移動,網際網路,發展,迅速,未來,的,潛力,巨大,中華,民族,是,個,勤勞}

建好的索引如下圖:

倒排索引

在上面的索引中,儲存了兩個資訊,文件號和出現的次數。建立好索引以後,我們就可以開始查詢了。例如現在有一個Query是”中國移動”。首先分詞得到Term集合{中國,移動},查倒排索引,分別計算query和d1,d2,d3的距離。有沒有發現,倒排表建立好以後,就不需要在檢索整個文件庫,而是直接從字典集合中找到“中國”和“移動”,然後遍歷後面的列表直接計算。

對倒排索引結構我們已經有了初步的瞭解,但在實際應用中還有些需要解決的問題(主要是由海量資料引起的)。筆者列舉一些問題,並給出相應的解決方案,拋磚以引玉,希望大家可以展開討論:

1.左側的索引表如何建立?怎麼做才能最高效?

可能有人不假思索回答:左側的索引當然要採取hash結構啊,這樣可以快速的定位到字典項。但是這樣問題又來了,hash函式如何選取呢?而且hash是有碰撞的,但是倒排表似乎又是不允許碰撞的存在的。事實上,雖然倒排表和hash異常的相思,但是兩者還是有很大區別的,其實在這裡我們可以採用前面提到的Bitmap的思想,每個Term(單詞)對應一個位置(當然了,這裡不是一個位元位),而且是一一對應的。如何能夠做到呢,一般在文書處理中,有很多的編碼,漢字中的GBK編碼基本上就可以包含所有用到的漢字,每個漢字的GBK編碼是確定的,因此一個Term的”ID”也就確定了,從而可以做到快速定位。注:得到一個漢字的GBK號是非常快的過程,可以理解為O(1)的時間複雜度。

2.如何快速的新增刪除更新索引?

有經驗的碼農都知道,一般在系統的“做加法”的代價比“做減法”的代價要低很多,在搜尋引擎中中也不例外。因此,在倒排表中,遇到要刪除一個文件,其實不是真正的刪除,而是將其標記刪除。這樣一個減法操作的代價就比較小了。

3.那麼多的海量文件,如果儲存呢?有麼有什麼備份策略呢?

當然了,一臺機器是儲存不下的,分散式儲存是採取的。一般的備份儲存3份就足夠了。

好了,倒排索引終於完工了,不足的地方請指正。謝謝