我以爲這個標題應該改改了,我寫下來實際上是告訴你們怎麼寫一個搜索引擎,並無涉及太多的Golang的東西,我以爲這樣也挺好,熟悉了原理,用什麼實現其實並不重要了,並且說說原理比說代碼更實在。前端
以前已經說了底層的數據結構了,包括倒排和正排索引。今天咱們上一層,來講說索引的字段和段。數據庫
字段這個上一篇已經介紹過了,字段的概念其實是搜索引擎索引中咱們能看到的最底層的東西,也是對外暴露的最底層的概念,在字段之下是倒排和正排索引,這兩項其實對用戶是封裝起來了,咱們能夠認爲每一個字段對應一個正排和一個倒排,而實際上也確實是這樣的。數組
在字段之上就是咱們這一篇主要說的段了,段這個概念並非搜索引擎特有的,也不是必須的,是我這個項目新增出來的,固然,也不是我原創,不少搜索引擎的引擎系統都有這個概念。數據結構
所謂段,就是最基本的檢索系統,一個段包含全部字段,包含一部分連續的文檔集合,可以進行完整的檢索,能夠把它當成一個檢索系統最基本單位。分佈式
這麼說可能仍是有點抽象,咱們打個比方,在數據庫中,一行數據是最基本的單位,對應搜索引擎中的是一個文檔,而表是全部文檔的集合,對應搜索引擎中是一份索引,而段就是一部分表,它包含一部分文檔的內容,能夠對這一部分文檔進行檢索,多個段合併起來就是一份完整的索引。搜索引擎
若是一個搜索引擎的數據再建好索引之後並不變化,那麼徹底沒有必要使用段,直接在創建全量索引的時候把數據都建好就好了spa
若是有增量數據,而且增量數據是不斷進入系統的話,那麼段的概念就有必要了,新增的數據首先在內存中進行保存,而後週期性的生成一個段,持久化到磁盤中提供檢索操做。日誌
段還有一個好處就是當系統是一個分佈式的系統的時候,進行索引同步的時候,由於各個段持久化之後就不會變化了,只須要把段拷貝到各個機器,就能夠提供檢索服務了,不須要在各個機器上重建索引。code
一個段損壞了,並不影響其餘段的檢索,只須要從其餘機器上將這個段拷貝過來就能正常檢索了,若是隻有一個索引的話,一旦索引壞了,就沒法提供檢索服務了,須要等把正確索引拷貝過來才行。排序
一個段包含幾個文件
indexname_{segementNumber}.meta 這裏是段的元信息,包括段中字段的名稱,類型,也包括段的文檔的起始和終止編號。
indexname_{segementNumber}.bt 這裏是段的倒排索引的字典文件
indexname_{segementNumber}.idx 這裏是段的全部字段的倒排文件
indexname_{segementNumber}.pfl 這裏是段的全部數字正排文件的數據,同時也包含字符串類型數據的位置信息
indexname_{segementNumber}.dtl 這裏是段的字符串類型數據的詳情數據
上面的indexname是這個索引的名稱,至關於數據庫中的表名,segmentNumber是段編號,這個編號是系統生成的。
多個段合在一塊兒就是一個完整的索引,檢索的時候其實是每一個段單獨檢索,而後把數據合併起來就是最後的結果集了。
下面咱們一個一個來講說這些個文件,看看一堆正排和一堆倒排如何構成一個段的。
一個真正意義上的段的構建由如下幾個步驟來構建,咱們以一個實際的例子來講明一下段的構建,好比咱們如今索引結構是這樣,這個索引包括三個字段,分別是姓名(字符串),年齡(數字),自我介紹(帶分詞的字符串),那麼構建段和索引的時候步驟是這樣的
首先新建一個段須要先初始化一個段,在初始化段的時候咱們實際上已經知道這個段包含哪些字段,每一個字段的類型。
初始化一個段信息,包含段所包含的字段信息和類型,在這裏就是包含姓名(字符串【正排和倒排】),年齡(數字【正排】),自我介紹(帶分詞的字符串【正排和倒排】)。
給段一個編號,好比1000。
準備開始接收數據。
內存中的段是構建段的第一步,以上述的字段信息爲例,咱們會在內存中創建如下幾個數據結構,在這裏我都是使用語言自動的原始數據結構
姓名須要創建倒排索引,因此創建一個map<string,list>,key是姓名,value是docid,姓名也要創建正排索引,因此創建一個StringArray[],保存每條數據的姓名的詳情。
年齡須要創建正排索引,因此創建一個IntegerArray[],保存每條數據的年齡的詳情。
自我介紹須要創建倒排索引,因此創建一個map<string,list>,key是自我介紹的分詞的term,value是docid,自我介紹也要創建正排索引,因此創建一個StringArray[],保存每條數據的自我介紹的詳情。
當新增一條數據的時候{"name":"張三","age":18,"introduce":"我喜歡跑步"}
,首先咱們給他一個docid【假如是0】,而後咱們把數據分別存放到上面的5個數據結構中,若是再來一條數據{"name":"李四","age":28,"introduce":"我喜歡唱歌"}
,咱們給他一個docid【假如是1】,那麼數據就變成了下圖的樣子
這樣,隨着數據的不停導入,內存中的數據結構不斷變化,內存段的數據也愈來愈大,當達到必定閾值的時候(這部分策略之後會說,我把這部分策略放到了引擎層,由引擎來決定何時進行段的持久化),咱們將把數據持久化到磁盤中。
進行持久化的過程當中
若是是map的數據結構,咱們將遍歷整個map,首先將value追加寫到.idx文件中,而後把key創建B+樹,value是剛剛寫入的idx文件的偏移位置。
若是是IntegerArray,咱們遍歷整個數組,而後把數據寫入到pfl文件中,每一個數據佔用8個字節。
若是是StringArray,咱們遍歷整個數據,首先把value追加寫入到dtl文件中,而後把文件偏移量寫入到pfl文件中
完成上面的三個步驟,咱們的持久化工做就完成了,完成之後數據結構就變成下面的樣子了,你們能夠本身腦子裏實現一遍。
段構建完成後,這個段就算徹底持久化磁盤中了,不會再進行更改了,至關於提交到索引系統了,能夠進行檢索了。這時候,咱們再新建一個段,接着接收新的文檔數據,而後繼續把後續的段持久化到磁盤中。
當檢索的時候,依次檢索每一個段,而後將結果集合並起來返回給前端。
段創建好了之後,可能須要對段進行合併操做,段的合併方式也不少,最簡單的就是新建一個段,而後遍歷以前的全部數據,重新創建一個段便可,這比較適合於數據量少的狀況,由於新建一個段是在內存中的,若是以前的數據太多的話,內存會撐不住。
還有一種方式是分別將倒排,正排依次合併,這種方式不耗費內存,可是比較耗費磁盤的IO,兩種方式你們能夠根據本身的業務場景進行選擇,第一種的方法和以前段的構建是同樣的,這裏咱們說說第二種方式。
咱們使用的B+樹對倒排索引的字典文件進行存儲,B+樹自然帶排序,那麼合併段的時候實際上就是合併多個B+樹,咱們只要使用歸併排序的方式就能合併多個B+樹了。歸併排序不清楚的能夠本身去查查,每一個B+樹的Key就是待歸併的元素,一邊掃描B+樹一邊構建一個新的B+樹,而後把倒排文件合併起來造成一個新的idx文件,倒排文件就合併完了。
合併正排文件更加簡單,只須要按照字段依次遍歷每一個段的正排文件,而後一邊遍歷一邊就造成了一個新的正排文件,遍歷完正排文件也就合併完了。
合併的方法在FalconIndex/segment/segment.go
的 MergeSegments
中有詳細代碼,你們能夠參考一下這種最簡單的合併方式。
段的策略比較自由,通常也不建議固化到索引中。通常有如下幾種策略可供選擇,具體須要根據本身的業務邏輯來選擇一個合適的段的持久化策略。
若是你的系統是一個一旦創建了索引就不怎麼變化的系統,那麼在作全量索引的時候創建一個段就好了,全量索引構建完了,而後把段持久化到磁盤就好了,若是全量索引量很大,怕內存扛不住,那麼能夠每10萬條創建一個段,當全量索引完成了之後再將全部的段合併成一個段就好了,段的合併後面會說,合併段基本不佔用什麼內存,能夠隨時合併,若是有增量數據,每隔一段時間序列化一下段,而後再每隔一段時間將全部非全量數據的段合併一下,那麼系統中就基本上只有一個全量的段和一個增量的段,檢索起來仍是很是快的。
若是你的系統是一個實時變化比較大的系統,好比日誌系統,那麼全量索引實際就沒什麼意義了,因爲日誌系統的檢索其實實時性要求沒有那麼高,那麼段的策略能夠是每新增10萬條數據持久化一個段,沒到10個段將全部段合併成一個段。或者按照時間戳來合併段,方便剔除老的數據。
若是你的系統是一個實時性要求很高的系統,那麼能夠按照時間(好比10秒)持久化一次段,每當系統空閒的時候將小的段合併成一個大的段。
總之,段的策略比較自由,徹底由引擎層來實現,根據本身的業務場景來選擇重寫一個段合併的策略都是能夠的。
段是索引的一部分,也是一個微型的索引,下面一篇咱們將會介紹索引層了,索引層介紹玩之後搜索引擎的數據層就徹底結束了,上面就是各類引擎的策略了,有了索引層之後,其實對上你要變成一個搜索引擎仍是要變成一個數據庫,或者變成一個KVDB的數據庫都是能夠的,反正基礎的東西不會有太多變化。
好了,若是你想看以前的文章,能夠關注個人公衆號哈:)