[Go] Bleve - 自帶全文搜尋

Reading time ~4 minutes

最近在把之前弄的新聞萬事通做優化, 話說, 好久沒來宣傳一下新聞萬事通了(毆飛~~)

加入新聞萬事通請按 :

加入好友 加入好友

好, 回歸正題, 之前新聞萬事通檢查假新聞的邏輯是:

  1. 載入新聞小幫手資料庫到記憶體(server啟動時), 四千多筆資料存放在一個map的資料結構
  2. 使用者輸入的訊息如果含有url去map裡面找到對應的url
  3. 但常常有類似內容的新聞有不同來源, 因此會用Gojieba取出標題的關鍵字去四千多筆資料的標題對應關鍵字出現的次數

聽起來很沒效率(四千筆在記憶體內, 其實也不算慢啦, 但就吃記憶體), 那就要優化囉? 那需要一個搜尋引擎囉?

第一個先想到的是Elastic search, 但我還不想搞那麼大, 為了四千多筆資料多拉一台search server,我只想 一台respberry pi就能搞定

Gojieba知道, 有Bleve Search這東西, Bleve Search也是一個full text index engine, 但跟Elastic search不同的是, 它是內嵌在你的程式內, 而不是獨立的server, 而且它是用Go寫的(開心), 而不是Java

Bleve

Bleve Search的來頭也不小,它是來自於著名的NoSQL DB : Couchbase, 這篇有介紹一下他的功能 : Bleve:来自Couchbase、基于Go语言的全文索引与检索库

使用Bleve Search也很簡單, 就建立index, 然後search:

import "github.com/blevesearch/bleve"

func main() {
    // open a new index
    mapping := bleve.NewIndexMapping()
    index, err := bleve.New("example.bleve", mapping)

    // index some data
    err = index.Index(identifier, your_data)

    // search for some text
    query := bleve.NewMatchQuery("text")
    search := bleve.NewSearchRequest(query)
    searchResults, err := index.Search(search)
}

Bleve Search會把indexes放到資料庫內, 預設是使用Bolt DB, Bolt跟leveldbRocksDb 類似, 都是一種Key-Value database, 只是Bolt是純粹以Go開發的

如果你不想要用Bolt, Bleve也是支援使用leveldb和Rocksdb的, 純粹是作者想做成全Go的方案才預設Bolt db, 我自己有實測幾次, 使用這三種DB, 搜尋的速度差不多, 但建立index時bolt較快, 需求的磁碟空間則是Rocksdb優(比Bolt好很多)

Bleve Search的架構做成很有彈性, 除了可以使用不同的DB(我自己也有實作以Redis當KV database的plugin, 不過實在也沒好多少就作罷了), 文字分析(Text Analysis)比如說斷詞, 也是可以用plugin擴增的

官方支援的語言為: Danish, Dutch, English, Finnish, French, German, Hungarian, Italian, Norwegian, Persian, Portuguese, Romanian, Russian, Sorani, Spanish, Swedish, Thai, Turkish

就是沒中文!

還好Gojieba後來也加入了bleve的analyzer和tokenizer, 這一部分可以獲得解決

使用Gojieba斷詞:

	indexMapping := bleve.NewIndexMapping()
    os.RemoveAll(INDEX_DIR)
    // clean index when example finished
    defer os.RemoveAll(INDEX_DIR)

    err := indexMapping.AddCustomTokenizer("gojieba",
        map[string]interface{}{
            "dictpath":     gojieba.DICT_PATH,
            "hmmpath":      gojieba.HMM_PATH,
            "userdictpath": gojieba.USER_DICT_PATH,
            "type":         "gojieba",
        },
    )
    if err != nil {
        panic(err)
    }
    err = indexMapping.AddCustomAnalyzer("gojieba",
        map[string]interface{}{
            "type":      "gojieba",
            "tokenizer": "gojieba",
        },
    )
    if err != nil {
        panic(err)
    }
    indexMapping.DefaultAnalyzer = "gojieba"

Wukong

除了Bleve外, 還有一個悟空 Wukong, 這孫猴子也好像蠻識字的嘛

這個Wukong是由阿里巴巴的陳輝所開發的, 一樣是內嵌的全文檢索引擎

package main

import (
    "github.com/huichen/wukong/engine"
    "github.com/huichen/wukong/types"
    "log"
)

var (
    // searcher是协程安全的
    searcher = engine.Engine{}
)

func main() {
    // 初始化
    searcher.Init(types.EngineInitOptions{
        SegmenterDictionaries: "github.com/huichen/wukong/data/dictionary.txt"})
    defer searcher.Close()

    // 将文档加入索引,docId 从1开始
    searcher.IndexDocument(1, types.DocumentIndexData{Content: "此次百度收购将成中国互联网最大并购"}, false)
    searcher.IndexDocument(2, types.DocumentIndexData{Content: "百度宣布拟全资收购91无线业务"}, false)
    searcher.IndexDocument(3, types.DocumentIndexData{Content: "百度是中国最大的搜索引擎"}, false)

    // 等待索引刷新完毕
    searcher.FlushIndex()

    // 搜索输出格式见types.SearchResponse结构体
    log.Print(searcher.Search(types.SearchRequest{Text:"百度中国"}))
}

架構上跟Bleve有點接近, 寫法也差不多, 但效率上來說, Bleve根本不能比, index的效率快上許多, 它的docId不像是Bleve用string而是uint64

比較快的原因目前我也還沒深究, 不過它存儲並沒用到BoltDB或LevelDB之類的, 而是自己的格式, 它也像Bleve一樣支援換資料庫引擎, 我嘗試想用BoltDB來取代它原生的, 想試看看是不是這原因, 但一直沒換成功過(後來也懶得追了)

另一點的差別在於是, 剛剛Bleve我用的斷詞器是Gojieba, 而Wukong用的是陳輝自己寫的sego, 這時我就好奇這會不會有影響?

BleveSego

好, 既然要確認斷詞器對index效率有沒影響, 我就得自己實作一個基於Sego的Bleve text analyzer和Tokenizer, 因此仿Gojieba的做了:

blevesego

使用blevesego跟使用Gojieba的有點類似:

	indexMapping := bleve.NewIndexMapping()
	err := indexMapping.AddCustomTokenizer("sego",
		map[string]interface{}{
			"dictpath": "dictionary.txt",
			"type":     "sego",
		},
	)

	if err != nil {
		getLogger().Fatal(err)
		return nil
	}

	err = indexMapping.AddCustomAnalyzer("sego",
		map[string]interface{}{
			"type":      "sego",
			"tokenizer": "sego",
		},
	)

	if err != nil {
		getLogger().Fatal(err)
		return nil
	}

	newsHelperIndexMapping.DefaultAnalyzer = "sego"

實驗的結果, 同樣Bleve, 用不同的斷詞, 似乎用sego有稍微快一點(在raspberry pi下, 約五千多筆資料大約快個一秒鐘), 但似乎不是Wukong效率高過於Bleve的主因

到最後, 我是選擇了Bleve, 原因是, 它目前看起來比較活躍, 而Wukong就比較沒啥更新