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

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

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

本篇也比較長,可是乾貨很少,建議上廁所的時候看,或者在地鐵一邊聽歌一邊看。算法

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

正排索引

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

這個是表1數組

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

這個是表2數據結構

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

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

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

很明顯,若是倒排索引知足不了搜索要求的時候,就須要引入正排索引,好比一個電商的搜索引擎,那麼正排索引就是必須的了,假如咱們有如下幾個商品須要上架:測試

商品編號 商品標題 發佈時間 價格 品牌
10001 錘子手機T9 2026-06-06 5000 錘子
10002 小米手機10 2020-02-02 1999 小米
10003 華爲手機P20 2022-12-12 3999 華爲

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

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

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

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

一種仍是基於倒排索引,以前的倒排索引不是經過B+樹構建的麼,B+樹自然的帶排序功能,因此是能夠進行範圍查找的,好比上面那個表格,咱們要搜索的關鍵詞爲手機,價格區間在1500–4000之間

  • 咱們把價格字段和商品標題字段分別創建一個倒排。
  • 首先,經過標題的倒排索引,檢索出全部的帶手機這個關鍵詞的商品的結果集,他們是【1,2,3】
  • 而後進行價格區間的檢索,由於B+樹最下面的葉子節點是經過指針連在一塊兒的,咱們只須要經過指針遍歷葉子節點,就能夠遍歷出價格區間中全部價格的倒排鏈,而後把這些鏈求並集,獲得的結果集是【2,3】,就是知足這個價格區間的因此商品了。
  • 最後再和關鍵詞查出來的商品求交集,就是最後的結果了。

這是第一種實現方式,彙總操做你們能夠本身想一想怎麼作,也能作,就是麻煩點。這種實現方式有下面幾個特色

  • 沒有單獨的正排文件,和倒排文件合在一塊兒了,同時也不佔用額外的空間。
  • 可是它限制了倒排索引的實現方式只能是B+樹這種帶排序的字典,若是倒排文件使用哈希表來實現的話,就不能這麼幹了。
  • 檢索的時候若是是區間搜索的話,須要進行屢次求並集操做,效率上須要進行優化。
  • 因爲只有倒排文件,那麼最後用來作數據展現的時候還須要一個輔助的Detail文件或者和數據庫綁定在一塊兒才能進行最終的結果展現。

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

DOCID 價格 DOCID 品牌
0 5000 0 錘子
1 1999 1 小米
2 3999 2 華爲

這麼存的話,檢索的時候怎麼作呢?若是仍是上面那個檢索條件要搜索的關鍵詞爲手機,價格區間在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的水平,不過它那一套要實現出來也是沒什麼問題的,須要的是堅持,我會把這個項目維護下去,不過最近實在是太忙了,苦逼啊。。。

最後,繼續發個二維碼,你懂的,關注一下唄:)

相關文章
相關標籤/搜索