用 Golang 寫一個搜索引擎(0x06)--- 索引構建

不知不覺寫到第七篇了,按這個節奏,估計得寫到15到20篇左右才能寫完,但願本身能堅持下去,以前寫代碼的時候不少東西並無想得那麼細緻,如今每寫一篇文章還要查一些資料,確保文章的準確性,也至關於本身複習了一下吧,呵呵。javascript

先說一下,關於倒排文件,其實還有不少東西沒有講,到後面再統一補充一下吧,主要是倒排文件的壓縮技術,這一部分由於目前的存儲空間不論是硬盤仍是內存都是很大的,因此壓縮技術用得不是不少了。java

今天咱們來說講倒排索引的構建。golang

以前,咱們瞭解到了,倒排索引在系統中是存成下圖這個樣子算法

上面的B+樹是一個文件,下面的倒排鏈是一個文件,那麼,如何來構建這兩個文件呢,本章我會說說通常的常規構建方法,而後說一下我是怎麼構建的。數組

通常狀況下,搜索引擎默認會認爲索引是不會有太大的變化的,因此把索引分爲全量索引和增量索引兩部分,全量索引通常是以天甚至是周,月爲單位構建的,構建完了之後就導入到引擎中進行檢索,而增量索引是實時的進入搜索引擎的,不少就是保存在內存中,搜索的時候分別從全量索引和增量索引中檢索數據,而後把兩部分數據合併起來返回給請求方,因此增量索引不是咱們這一篇的主要內容,在最後個人索引構建部分我會說一下個人增量索引構建方式。如今先看看全量索引。數據結構

全量索引構建通常有如下兩種方式app

一次性構建索引

一種是一次性的構建索引,這種構建方法是全量掃描全部文檔,而後把全部的索引存儲到內存中,直到全部文檔掃描完畢,索引在內存中就構建完了,這時再一次性的寫入硬盤中。大概步驟以下:ui

  • 初始化一個空map ,map的key用來保存term,map的value是一個鏈表,用來保存docid鏈
  • 設置docid的值爲0
  • 讀取一個文檔內容,將文檔編號設置成docid
  • 對文檔進行切詞操做,獲得這個文檔的全部term(t1,t2,t3...)
  • 將全部的 鍵值對的term插入到map的key中,docid追加到map的value中
  • docid加1
  • 若是還有文檔未讀取,返回第三步,不然繼續
  • 遍歷map中的 ,將value寫入倒排文件中,並記錄此value在文件中的偏移offset,而後將 寫入B+樹中
  • 索引構建完畢

用圖來表示就是下面幾個步驟搜索引擎

若是用僞代碼來表示的話就是這樣spa

//初始化ivt的map 和 docid編號
var ivt map[string][]int
var docid int = 0
//依次讀取文件的每一行數據
for content := range DocumentsFileContents{
  terms := segmenter.Cut(content) // 切詞
  for _,term := range terms{
      if _,ok:=ivt[term];!ok{
         ivt[term]=[]int{docid}
      }else{
         ivt[term]=append(ivt[term],docid)
    }
    docid++
}

//初始化一棵B+樹,字典
bt:=InitBTree("./index.dic")
//初始化一個倒排文件
ivtFile := InitFile("./index.ivt")
//依次遍歷字典
for k,v := range ivt{
  //將value追加到倒排文件中,並獲得文件偏移[寫文件]
  offset := ivtFile.Append(v)
  //將term和文件偏移寫入到B+樹中[寫文件]
  bt.Add(term,offset)
}

ivtFile.Close()
bt.Close()

}複製代碼

如此一來,倒排文件就構建好了,這裏我直接使用了map這樣的描述,只是爲了讓你們更加直觀的瞭解到一個倒排文件的構建,在實際中可能不是用這種數據結構。

分批構建,依次合併

一次性構建的方式,因爲是把因此文檔都加載到內存,若是機器的內存空間不夠大的話,會致使構建失敗,因此通常狀況下不採用那種形式,不少索引構建的方式都用這種分批構建,依次合併的方式,這種方式主要按如下方式進行

  • 申請一塊固定大小的內存空間,用來存放字典數據文檔數據
  • 在固定內存中初始化一個可排序的字典(能夠是樹,也能夠是跳躍表,也能夠是鏈表,能排序就行)

  • 設置docid的值爲0

  • 讀取一個文檔內容,將文檔編號設置成docid
  • 對文檔進行切詞操做,獲得這個文檔的全部term(t1,t2,t3...)
  • 將term按順序插入到字典中,而且在內存中生成多個個 的鍵值對 , ,而且將這些鍵值對存入到內存的 文檔數據中,同時保證鍵值對按照term進行排序
  • docid加1
  • 若是內存空間用完了,將文檔數據寫入到磁盤上,清空內存中的文檔數據
  • 若是還有文檔未讀取,返回第三步,不然繼續
  • 因爲各個磁盤文件中的鍵值對是按照term的順序排列的,經過多路歸併算法將各個磁盤文件進行合併操做,合併的過程當中生成每個term的倒排鏈,追加的寫一次倒排文件,並配合詞典生成這個term的文件偏移,直到全部文件合併完成,詞典也跟着構建完成了。
  • 索引構建完畢

一樣,咱們用一個圖來表示就是下面這個樣子

若是用僞代碼表示的話,就是下面這個樣子,代碼流程也很簡單,結合上面的步驟和圖仔細看看就能明白

//初始化固定的內存空間,存放字典和數據
dic := new DicMemory()
data := new DataMemory()
var docid int = 0
//依次讀取文件的每一行數據
for content := range DocumentsFileContents{
  terms := segmenter.Cut(content) // 切詞
  for _,term := range terms{
      //插入字典中
      dic.Add(term)
      //插入到數據文件中
      data.Add(term,docid)
      //若是data滿了,寫入磁盤並清空內存
      if data.IsFull() {
          data.WriteToDisk()
          data.Empty()
    }
    docid++
}


//初始化一個文件描述符數組
idxFiles := make([]*Fd,0)

//依次讀取每個磁盤文件
for idxFile := range ReadFromDisk {
    //獲取每個磁盤文件的文件描述符,存到一個數組中
    idxFiles.Append(idxFile)
}

//配合詞典進行多路歸併,並將結果寫入到一個新文件中
ivtFile:=InitFile("./index.ivt")
dic.SetFilename("./index.dic")
//多路歸併
KWayMerge(idxFiles,ivtFile,dic)

//構建完成
ivtFile.Close()
dic.Close()

}複製代碼

上面就是兩種構建全量索引的方法,對於第二種方法,還有一種特殊狀況,就是當內存中的詞典也很巨大,將內存撐爆了怎麼辦,這是能夠將詞典也分步的寫到磁盤,而後在進行詞典的合併,這裏就不說了,感興趣的能夠本身去查一查。

我上面說的這些和一些搜索引擎的書可能說的不太同樣,可是基本思想應該差很少,爲了讓你們更直觀的抓到本質,不少特殊一點的狀況我並無詳細說明,畢竟這不是一篇純理論的文章,若是你們真的感興趣確定能夠找到不少辦法來更深刻的瞭解搜索引擎的。

關於上面提到的多路歸併,是一個標準的外排序的方法,處處都能找到資料,這裏就不詳細展開了。

另外,在索引的構建過程當中還有一些細節的東西,好比通常的索引構建都是兩次掃描文檔,第一次用來生成一些統計信息,也就是上一篇說的詞的信息,好比TF,DF之類的,第二次掃描纔開始真正的構建,這樣的話,能夠把term的相關性的計算放到構建索引的時候來進行,那麼在檢索的時候只須要進行排序而不用計算相關性了,能夠極大提升檢索的效率。

個人構建方法

最後,我來講說我是怎麼構建索引的,因爲我寫的這個搜索引擎,是沒有明確的區分全量和增量索引概念的,把這個決定權交到了上層的引擎層來決定,因此在底層構建索引的時候不存在全量增量的概念,因此採用了第一種和第二種方法結合的方式進行索引的構建。

  • 首先設定一個閾值,好比10000篇文檔,在這10000篇文檔的範圍內,按照第一種方式構建索引,生成一個字典文件和一個倒排文件,這一組文件叫作一個段(segment)
  • 每10000篇文檔生成一個段(segment),直到全部文檔構建完成,從而生成了多個段,而且在搜索引擎啓動之後,增量數據也按這個方法進行構建,因此段會愈來愈多
  • 每個段就是索引的一部分,他有倒排索引的所有東西(詞典,倒排表),能夠進行一次正常的檢索操做,每次檢索的時候依次搜索各個段,而後把結果合併起來就是最終結果了
  • 若是段的數量過多,按照第二種方式的思想,對多個段的詞典和倒排文件進行多路合併操做,因爲詞典是有序的,因此能夠按照term的順序進行歸併操做,每次歸併的時候把倒排全拉出來,而後生成一個新的詞典和新的倒排文件,當合並完了之後把老的都刪掉。

上面的合併操做策略徹底交給上層的引擎層甚至業務層來完成,有些場景下增量索引少,那麼第一次構建完索引之後就能夠把各個段合併到一塊兒,增量索引每隔必定的時間合併一次,有些場景下數據一直不停的進入系統中,那麼能夠經過一些策略,不停的在系統空閒時合併一部分索引,來保證檢索的效率。

OK,上面就是索引構建的方法,到這一篇完成,倒排索引的數據結構,構建方式都說完了,可是仍是有不少零碎的東西沒有說,後面會統一的把一些沒說起到的地方整理一篇文章說一下,接下來,我會用一到兩篇的文章說一下正排索引,而後就能夠跨到檢索層去了。

最後,歡迎掃一掃關注個人公衆號哈:)

相關文章
相關標籤/搜索