用 Golang 寫一個搜尋引擎(0x02)--- 倒排索引技術

吳YH堅發表於2017-01-16

這一篇,我們來說說搜尋引擎最核心的技術,倒排索引技術,倒排索引可能需要分成幾篇文章才說得完,我們先會說說倒排索引的技術原理,然後會講講怎麼用一些資料結構和演算法來實現一個倒排索引,然後會說一個索引器怎麼通過文件來生成一個倒排索引。

倒排索引

什麼是倒排索引呢?索引我們都知道,就是為了能更快的找到文件的資料結構,比如給文件編個號,那麼通過這個號就可以很快的找到某一篇文件,而倒排索引不是根據文件編號,而是通過文件中的某些個詞而找到文件的索引結構。

倒排索引技術簡單,高效,簡直是為搜尋引擎這種東西量身定做的,就是靠這個技術,實現一個搜尋引擎才成為可能,我們也才能在海量的文章中通過一個關鍵詞找到我們想要的內容。

我們看個例子,有下面的幾個文件:

文件編號 文件內容
1 這是一個Go語言實現的搜尋引擎
2 PHP是世界上最好的語言
3 Linux是C語言和組合語言實現的
4 谷歌是一個世界上最好的搜尋引擎公司

直觀的看,我們通過編號1,2,3,4可以很快的找到文件,但是我們需要通過關鍵詞找文件,那麼把上面那個表格稍微變化一下,就是倒排索引了

倒排索引【只列出了部分關鍵詞】

關鍵詞 文件編號
Go 1
語言 1,2,3
實現 1,3
搜尋引擎 1,4
PHP 2
世界 2,4
最好 2,4
彙編 3
公司 4

這樣就非常好理解了吧,實際上倒排索引就是把文件的內容切詞以後重新生成了一個表格,通過這個表格,我們可以很快的找到每個關鍵詞對應的文件,好了,沒有了,到這裡,就是倒排索引的核心原理,也是搜尋引擎最基礎的基石,不管是谷歌還是某度,最核心的東西就是這兩個表格了,呵呵,沒這兩表格,啥都幹不了。

看上去很簡單吧,好吧,我們現在來模擬搜尋引擎進行一次搜尋,比如,我們鍵入關鍵詞搜尋引擎
1.我們在表格2中查到搜尋引擎這個詞出現在第4行
2.找到第4行的第2列,把文件編號找出來,是1和4
3.去第一個表格通過文件編號把每個文件的實際內容找出來
4.將1和4的結果顯示出來
5.搜尋完成

上面就是搜尋引擎的最基礎的技術了,如果來設計一個資料結構和演算法來實現表2就成了搜尋引擎技術的關鍵。

在實現資料結構和演算法之前,我們需要知道搜尋引擎搜尋的是海量的資料,一般的中型電商的資料都是幾十上百G的資料了,所以這個資料結構應該是儲存在本地磁碟的而不是在記憶體中的,基於以上的考慮,為了快速搜尋,要麼自己實現cache來快取熱資料,要麼考慮使用作業系統的底層技術MMAP,鑑於我自己實現的cache不見得(基本上是不太可能)比作業系統做得好,所以我使用的是MMAP

MMAP系統呼叫

mmap是將一個檔案或者其它物件對映進記憶體。檔案被對映到多個頁上,如果檔案的大小不是所有頁的大小之和,最後一個頁不被使用的空間將會清零。實現這樣的對映關係後,程式就可以採用指標的方式讀寫操作這一段記憶體,而系統會自動回寫髒頁面到對應的檔案磁碟上,即完成了對檔案的操作而不必再呼叫read,write等系統呼叫函式。

mmap最大的一個好處是作業系統會自己將磁碟上的檔案對映到記憶體,當記憶體足夠的時候,操作檔案就像操作記憶體一樣快,而當記憶體不足的時候,作業系統又會自己將一些頁從記憶體中去掉,實現了一個類似快取的東西。特別適合於對於巨大檔案的讀操作,而我們的倒排索引檔案就是這種巨大的檔案,而且基本上寫入一次以後就不太修改了,每次查詢都讀操作,所以使用mmap是一個比較好的選擇。

mmap是一個系統呼叫,不同的作業系統實現有所不同,Linux下對應的C的呼叫方法是下面這個,具體的引數含義大家可以man一下:

標頭檔案
函式原型
void mmap(void start,size_t length,int prot,int flags,int fd,off_t offset);

一個巨大的檔案mmap之後,檔案讀寫操作的效能由系統記憶體決定,系統可用記憶體越大,那麼讀寫檔案的效能越好,因為作業系統的記憶體足夠,系統會將更多的檔案載入到記憶體,提高系統吞吐量。

在Go語言中,對應的MMAP呼叫是:(需要引入Syscall包)

func Mmap(fd int, offset int64, length int, prot int, flags int) (data []byte, err error)

引數分別是:檔案描述符,偏移量,需要對映的長度,期望的記憶體保護標誌【是隻讀還是隻寫還是讀寫】,對映方式【是否同步到檔案,還是隻是副本修改等】。

因為mmap是基礎實現,很多地方都需要使用,所以單獨實現了一個mmap的類,在utils.mmap中,提供一些基礎的方法:

func NewMmap(file_name string, mode int) (Mmap, error) 新建一個mmap
func (this
Mmap) ReadInt64(start int64) int64 //從指定位置讀取一個int64的值
func (this Mmap) WriteInt64(start, value int64) error //在指定位置寫入一個int64的值
func (this
Mmap) ReadDocIdsArry(start, len uint64) []DocIdNode //從指定位置讀取一個docid的鏈
......

巨大檔案的讀寫技術方案解決了,實際上主要就是解決了表2的第二列的問題,在一個擁有巨大文件數的資料中,表2的第二列佔用了絕大多數磁碟空間,我們會將表2分成兩個資料結構來儲存,第二列就是一個連續的儲存檔案,叫倒排檔案,在上述例子中,我們會將第二列存成:

1 1,2,3 1,3 1,4 2 2,4 2,4 3 4

而第一列我們將儲存關鍵字和偏移量。這樣,表2就被我們拆分成兩個資料結構了,現在的關鍵是第一列使用什麼資料結構可以保證在查詢的時候迅速找到對應的關鍵字,從而找到偏移量得到第二列的具體資料。

好了,現在有幾位選手要上場,他們都可以實現第一列的結構,他們分別是:順序表雜湊表查詢樹字首樹,下一篇我們分別看看他們的能力。

歡迎大家掃描一下下面的微信公眾號訂閱,首先會在這裡發出來:)

用 Golang 寫一個搜尋引擎(0x02)--- 倒排索引技術

相關文章