用Golang寫一個搜索引擎(0x07)--- 正排索引

【轉載】:https://segmentfault.com/a/1190000004998397

最近各種技術盛會太多,朋友圈各種刷屏,有廠商發的各種廣告,有講師發的各種自拍,各種參會的朋友們各種自拍,好不熱鬧,不知道你的朋友圈是不是也是這樣啊,去年還沒這麼多技術會議,今年感覺爆發了,呵呵,真是一個互聯網技術的好時代,而且還有各種撕B可看,真想八一八,怕得罪人,我們這種碼農還是專注技術專注寫代碼吧。

你有什麼想了解的也可以給我留言哈,歡迎交流,我的工作之前主要做的是搜索的,也做推薦和廣告,這部分的東西可能寫得多點,對了,嵌入式領域也行(跨得有點大,這個嵌入式不是iOS和Android,是真的嵌入式),沒什麼高端背景,也不是BAT這種大廠的,就是一小公司寫代碼的,所以有很多東西還是不懂,你要是和我交流了發現我答不上來很正常啊,人艱不拆啊。。

本篇也比較長,但是乾貨不多,建議上廁所的時候看,或者在地鐵一邊聽歌一邊看。

前面幾篇,基本上把倒排索引的數據結構給講完了,並且簡單的說了一下排序,然後說了一下倒排索引的構建。這一篇主要寫一下正排索引以及倒排和正排怎麼配合起來形成一個完整的字段索引。

正排索引

正排索引,也叫前向索引,和倒排索引(也叫反向索引)是相對的,正排索引相對倒排來說簡單多了,第二篇文章的時候有下面兩個表格(表1和表2)

這個是表1

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

這個是表2

關鍵詞文檔編號
Go1
語言1,2,3
實現1,3
搜索引擎1,4
PHP2
世界2,4
最好2,4
彙編3
公司4

我們之前一直在說作爲倒排索引的表2,對於表1,我們認爲是數據的詳情(detail)信息,最後用來做數據內容展示的,如果是放在一個只支持全文搜索的搜索引擎中的話,那確實表1只是用來做最後的數據展示,但是如果我們的搜索引擎還想要一些複雜的功能,那麼表1就是一個正排索引,如果我們的搜索引擎同時支持倒排索引和正排索引,我們可以簡單的認爲這是一個數據庫系統(當然,和真正的數據庫還差得遠啊)。

首先,我們看什麼情況下要使用正排索引

很明顯,如果倒排索引滿足不了搜索要求的時候,就需要引入正排索引,比如一個電商的搜索引擎,那麼正排索引就是必須的了,假如我們有以下幾個商品需要上架:

商品編號商品標題發佈時間價格品牌
10001錘子手機T92026-06-065000錘子
10002小米手機102020-02-021999小米
10003華爲手機P202022-12-123999華爲

搜索的時候我們可能需要搜索價格在一個區間的手機,那麼僅僅用全文倒排索引就比較難完成任務了,而且我們在使用電商的搜索引擎的時候,經常會在搜索結果的上方看到一些彙總的信息【比如品牌,型號,價格彙總】,這一部分的東西也是通過正排索引來實現的,像下面這個圖

圖片描述

所以說,如果我們的搜索需求不僅僅是進行關鍵詞的匹配,還需要進行一些過濾操作(比如價格區間的過濾),彙總操作(比如結果集中每種品牌數量的統計),那麼就必須引入正排索引了。

第二,我們看看如何實現一個正排索引

實現正排索引有兩種方式:

一種還是基於倒排索引,之前的倒排索引不是通過B+樹構建的麼,B+樹天然的帶排序功能,所以是可以進行範圍查找的,比如上面那個表格,我們要搜索的關鍵詞爲手機,價格區間在1500–4000之間

  • 我們把價格字段和商品標題字段分別建立一個倒排。

  • 首先,通過標題的倒排索引,檢索出所有的帶手機這個關鍵詞的商品的結果集,他們是【1,2,3】

  • 然後進行價格區間的檢索,因爲B+樹最下面的葉子節點是通過指針連在一起的,我們只需要通過指針遍歷葉子節點,就可以遍歷出價格區間中所有價格的倒排鏈,然後把這些鏈求並集,得到的結果集是【2,3】,就是滿足這個價格區間的所以商品了。

  • 最後再和關鍵詞查出來的商品求交集,就是最後的結果了。

這是第一種實現方式,彙總操作大家可以自己想想怎麼做,也能做,就是麻煩點。這種實現方式有下面幾個特點

  • 沒有單獨的正排文件,和倒排文件合在一起了,同時也不佔用額外的空間。

  • 但是它限制了倒排索引的實現方式只能是B+樹這種帶排序的字典,如果倒排文件使用哈希表來實現的話,就不能這麼幹了。

  • 檢索的時候如果是區間搜索的話,需要進行多次求並集操作,效率上需要進行優化。

  • 由於只有倒排文件,那麼最後用來做數據展示的時候還需要一個輔助的Detail文件或者和數據庫綁定在一起才能進行最終的結果展示。

除了上面那個,還有一種實現方式,就是通過一個數組來實現,數組的下表就是文檔編號(docid,不是商品編號,商品編號是主鍵),由於在搜索引擎中,docid是自增的,而且不會進行刪除,所以也是唯一的,正好可以和一個一維數組的下標對上,所以可以用一個數組來存儲正排索引,就像下面這個表格,分別表示價格和品牌建立的正排索引,其實就是把表1的數據拆開來進行存儲了而已。(爲了節省空間,我把兩個寫在一起了)

DOCID價格DOCID品牌
050000錘子
119991小米
239992華爲

這麼存的話,檢索的時候怎麼做呢?如果還是上面那個檢索條件要搜索的關鍵詞爲手機,價格區間在1500–4000之間

  • 只把標題建立倒排,價格字段建立一個一維數組的正排

  • 首先,通過標題的倒排索引,檢索出所有的帶手機這個關鍵詞的商品的結果集,他們的DOCID是【1,2,3】

  • 遍歷結果集,每遍歷一個docid,直接通過那個一維數組和對應的正排文件進行比對,看是否滿足條件,滿足的留下,不滿足的丟棄。

  • 遍歷完成以後,得到最終的結果集【2,3】

如果是彙總操作的話,和上述類似,在第二步遍歷結果集的時候順便就可以進行統計了,遍歷完了也就統計完了。

條條大路通羅馬,通過兩種不同的數據結構,最後得到了一樣的結果,第二方式有以下幾個特點

  • 要爲需要進行範圍查找的字段單獨建立正排索引,不能和倒排的數據結構合併。

  • 通過倒排獲取到結果集以後需要對結果集進行一次遍歷,然後得到一個新的結果集作爲最後的結果,如果結果集特別巨大,那麼也需要時間進行遍歷。

  • 因爲是一維數組來實現的正排,如果文檔數非常多的話,內存中是裝不下這麼多正排文件的,需要在磁盤上來實現這個一維數組。

  • 如果我們將每一個字段都建立一個正排索引的話,那就不需要單獨的detail文件或者和數據庫對接了,直接正排文件合起來就是一個完整的文檔信息,少了外部依賴。

上面就是正排索引的兩種實現方式,使用哪一種要看具體的業務需求,比如像百度這種全文搜索引擎,主要的需求其實就是查找關鍵字,很少用到過濾,彙總操作,那麼不用單獨來實現正排索引,用第一種方式就行了,而如果是電商類型的搜索引擎的話,有大量的過濾啊,彙總操作,那麼通過第二種方式來實現正排索引還是比較必要的。

我的代碼裏面就是用的第二種方式,並且實現的時候是用mmap的方式在磁盤上實現的,如果內存夠大,可以全載入到內存提高檢索速度。

索引設計管理

正排索引和倒排索引終於都說完了,這要是搜索引擎最關鍵的數據結構了,其他所有的東西都是在這個基礎上發展起來的,我們已經有了正排和倒排索引的結構,那麼如果來構建一個索引系統的,我是這麼來做的。

首先,我們需要定一個規矩,所謂規矩就是我們的這個搜索引擎哪些操作我支持,哪些操作我不支持,比如,我爲了簡單,我就支持全文檢索,其他都不支持,那麼只需要好好的實現一個倒排索引結構,那數據結構部分就設計的差不多了。而我在做這個搜索引擎的時候,想實現的是下面這些個功能。

  • 支持關鍵詞的倒排,也支持完全匹配類型的倒排。

  • 支持過濾操作,但是隻支持整數類型(如果是浮點數根據保留的小數位數轉成整數)和日期類型的過濾,對於字符串只提供檢索操作,不提供過濾操作。

  • 對於過濾操作,支持大於,小於,等於,不等於,區間的過濾。

  • 支持字段的彙總。

  • 不要外接數據庫系統進行數據詳情的展示。

既然是這麼來實現,那對於每個字段,他可能的類型就是

字段類型行爲備註舉例
完整匹配的字符串建立倒排,正排(正排只展示,不進行過濾操作)主鍵,型號
關鍵詞字符串建立倒排,正排(正排只展示,不進行過濾操作)標題,描述
數字只建立正排價格,庫存
日期只建立正排上架
僅展示只建立正排(正排只展示,不進行過濾操作)商品詳情描述

這樣,我們實現的時候,首先實現一個倒排索引(src/FalconIndex/segment/invert.go),然後實現一個正排索引(src/FalconIndex/segment/profile.go),然後實現一個字段類(src/FalconIndex/segment/field.go)用來管理倒排和正排,那麼搜索引擎最最基本的數據結構就OK了,對外來說倒排和正排是隱藏的,只有Field類對外暴露,對檢索操作來說主要提供幾個接口方法:

  • addDocument 添加文檔(建立正排或者倒排)

  • query 通過倒排檢索文檔

  • filter 通過正排過濾文檔

  • getValue 通過正排文件獲取這個字段的值

文章中我儘量少列或者不列代碼,主要是對搜索引擎的原理有了解,原理了解了可以自己來實現代碼,實在不會可以自己去參考參考我的代碼,畢竟編程這東西只要知道了原理和算法,怎麼實現並不是麻煩事。

寫在後面的話

之前我一直做C++開發的,寫的搜索代碼也是C++的,現在用Golang,也沒啥特別的難度,當然因爲我對Golang的特性並不是很熟悉,所以基本沒有用Golang的高級功能,寫出來的代碼當然不夠Golang範,但這也不影響我的實現。

OK,字段部分介紹完了,搜索引擎的核心數據結構也介紹完了,後面接下來會繼續往上走,先到段層,然後到索引層,然後會說一下檢索邏輯實現,合併邏輯之類的,索引之上會繼續說一下搜索引擎的引擎部分,後面還會遇到一些數據結構,比如bitmap,哦,還會單獨寫一到兩篇來介紹分詞,至於排序和索引結構優化也會單獨拿出來說。

另外,我的代碼基本完成了,包括分佈式的部分,會在最近提交到github上去,所以後面也會有幾篇來說搜索引擎的分佈式實現,還是本着原生的原則,沒用第三方庫,所以分佈式部分沒有PAXOS這種高端的理論,也沒有ZooKeeper這種高端玩意,到時候大家看吧。

目前我的代碼初步測試,8G,24核的機器中,1000萬條數據(微博數據,每條不超過140個字,我不是微博的人哈,不存在數據泄密,數據是某號稱亞二爬的博士爬來的,我只是下下來用而已),單個term的平均檢索時間在5ms,用AB進行單個URL測試,QPS大概在7000,如果是隨機關鍵詞測試,QPS大約在2000,基本達到我之前自己定的目標了,而且還有優化空間。下次測測ElasticSearch,目前感覺比它報出來的數據要快,但是環境不一樣,下次部一個比較一下,而且功能上還完全達不到ElasticSearch的水平,不過它那一套要實現出來也是沒什麼問題的,需要的是堅持,我會把這個項目維護下去,不過最近實在是太忙了,苦逼啊。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章