Go/Golang 是我最喜歡的語言之一;我喜歡極簡主義和它的干淨,它的語法非常緊湊,並且努力讓事情變得簡單(我是KISS 原則的忠實粉絲)。
我最近面臨的主要挑戰之一是建立一個快速的搜尋引擎。當然還有 SOLR 和 ElasticSearch 等選項;兩者都工作得很好並且具有高度可擴展性,但是,我需要簡化搜索,使其更快、更容易部署,幾乎沒有依賴項。
我需要進行足夠的最佳化才能快速返回結果,以便可以對它們進行重新排名。雖然 C/Rust 可能很適合這個,但我更重視開發速度和生產力。我認為 Golang 是兩全其美的。
在本文中,我將透過一個簡單的範例來介紹如何使用 Go 建立自己的搜尋引擎,你會驚訝地發現:它並沒有您想像的那麼複雜。
我不知道為什麼,但 Golang 在某種程度上感覺像 Python。文法很容易掌握,也許是因為到處都沒有分號和括號,或是沒有醜陋的 try-catch 語句。也許這是很棒的 Go 格式化程序,我不知道。
無論如何,由於 Golang 產生一個獨立的二進位文件,因此部署到任何生產伺服器都非常容易。您只需“建置”並交換可執行檔即可。
這正是我所需要的。
不,這不是打字錯誤嗎? Bleve 是一個功能強大、易於使用且非常靈活的 Golang 搜尋庫。
身為 Go 開發人員,您通常會像躲避瘟疫一樣避免使用 3rd 方包;有時使用第三方軟體包是有意義的。 Bleve 速度快、設計精良,並提供足夠的價值來證明使用它的合理性。
此外,這就是我「Bleve」的原因:
獨立,Golang 的一大優點是單一二進位文件,所以我想保持這種感覺,不需要外部資料庫或服務來儲存和查詢文件。 Bleve 與 Sqlite 類似,在記憶體中運行並寫入磁碟。
易於擴充。由於它只是 Go 程式碼,因此我可以根據需要輕鬆調整庫或在我的程式碼庫中擴展它。
快速:1000 萬個文件的搜尋結果只需 50-100 毫秒,其中包括過濾。
分面:如果沒有一定程度的分面支持,您就無法建立現代搜尋引擎。 Bleve 完全支援常見的構面類型:例如範圍或簡單類別計數。
快速索引:Bleve 比 SOLR 稍慢。 SOLR 可以在 30 分鐘內索引 1000 萬個文檔,而 Bleve 需要一個多小時,但一個小時左右仍然相當不錯,速度足以滿足我的需求。
良好的品質結果。 Bleve 在關鍵字結果方面表現出色,而有些語意類型搜尋在 Bleve 中也非常有效。
快速啟動:如果您需要重新啟動或部署更新,只需幾毫秒即可重新啟動 Bleve。在記憶體中重建索引時不會阻塞讀取,因此重新啟動後幾毫秒內即可搜尋索引,不會出現中斷。
在 Bleve 中,「索引」可以被視為資料庫表或集合(NoSQL)。與常規 SQL 表不同,您不需要指定每一列,基本上可以在大多數用例中使用預設架構。
要初始化 Bleve 索引,您可以執行下列操作:
mappings := bleve.NewIndexMapping() index, err = bleve.NewUsing("/some/path/index.bleve", mappings, "scorch", "scorch", nil) if err != nil { log.Fatal(err) }
Bleve 支援幾種不同的索引類型,但經過多次擺弄後我發現「scorch」索引類型可以為您提供最佳效能。如果您不傳入最後 3 個參數,Bleve 將預設為 BoltDB。
為 Bleve 新增文件非常簡單。您基本上可以在索引中儲存任何類型的結構:
type Book struct { ID int `json:"id"` Name string `json:"name"` Genre string `json:"genre"` } b := Book{ ID: 1234, Name: "Some creative title", Genre: "Young Adult", } idStr := fmt.Sprintf("%d", b.ID) // index(string, interface{}) index.index(idStr, b)
如果您要索引大量文檔,最好使用批次:
// You would also want to check if the batch exists already // - so that you don't recreate it. batch := index.NewBatch() if batch.Size() >= 1000 { err := index.Batch(batch) if err != nil { // failed, try again or log etc... } batch = index.NewBatch() } else { batch.index(idStr, b) }
正如您所注意到的,使用「index.NewBatch」可以簡化諸如批次記錄並將其寫入索引之類的複雜任務,該任務會建立一個容器來暫時索引文件。
此後,您只需在循環時檢查大小,並在達到批量大小限制後刷新索引。
Bleve 公開了多個不同的搜尋查詢解析器,您可以根據您的搜尋需求進行選擇。為了使本文簡短而有趣,我將使用標準查詢字串解析器。
searchParser := bleve.NewQueryStringQuery("chicken reciepe books") maxPerPage := 50 ofsset := 0 searchRequest := bleve.NewSearchRequestOptions(searchParser, maxPerPage, offset, false) // By default bleve returns just the ID, here we specify // - all the other fields we would like to return. searchRequest.Fields = []string{"id", "name", "genre"} searchResults, err := index.Search(searchResult)
只需這幾行,您現在就擁有了一個強大的搜尋引擎,可以以較低的記憶體和資源佔用提供良好的結果。
這是搜尋結果的 JSON 表示,「hits」將包含符合的文件:
{ "status": { "total": 5, "failed": 0, "successful": 5 }, "request": {}, "hits": [], "total_hits": 19749, "max_score": 2.221337297308545, "took": 99039137, "facets": null }
如前所述,Bleve 提供開箱即用的全面分面支持,而無需在您的架構中進行設定。以《流派》一書為例,您可以執行以下操作:
//... build searchRequest -- see previous section. // Add facets genreFacet := bleve.NewFacetRequest("genre", 50) searchRequest.AddFacet("genre", genreFacet) searchResults, err := index.Search(searchResult)
我們只用 2 行程式碼擴充了之前的 searchRequest。 「NewFacetRequest」接受 2 個參數:
欄位:索引中要分面的欄位(字串)。
大小:要計數的條目數(整數)。因此,在我們的範例中,它只會計算前 50 個流派。
以上內容現在將填入我們搜尋結果中的「面向」。
接下來,我們只需將我們的方面添加到搜尋請求中即可。它接受“方面名稱”和實際方面。 「Facet name」是您將在我們的搜尋結果中找到此結果集的「鍵」。
雖然“QueryStringQuery”解析器可以為您帶來相當多的幫助;有時您需要更複雜的查詢,例如“一個必須匹配”,您希望將搜尋字詞與多個欄位進行匹配,並傳回結果,只要至少有一個字段匹配即可。
您可以使用「Disjunction」和「Conjunction」查詢類型來完成此操作。
聯合查詢:基本上,它允許您將多個查詢連結在一起形成一個巨大的查詢。所有子查詢必須至少符合一個文件。
析取查詢:這將允許您執行上面提到的「一個必須匹配」查詢。您可以傳入 x 數量的查詢,並設定必須符合至少一個文件的子查詢數量。
析取查詢範例:
mappings := bleve.NewIndexMapping() index, err = bleve.NewUsing("/some/path/index.bleve", mappings, "scorch", "scorch", nil) if err != nil { log.Fatal(err) }
與我們之前使用「searchParser」的方式類似,我們現在可以將「析取查詢」傳遞到「searchRequest」的建構子中。
雖然不完全相同,但類似以下 SQL:
type Book struct { ID int `json:"id"` Name string `json:"name"` Genre string `json:"genre"` } b := Book{ ID: 1234, Name: "Some creative title", Genre: "Young Adult", } idStr := fmt.Sprintf("%d", b.ID) // index(string, interface{}) index.index(idStr, b)
您也可以透過設定「query.Fuzziness=[0 or 1 or 2]」來調整搜尋的模糊程度
連線查詢範例:
// You would also want to check if the batch exists already // - so that you don't recreate it. batch := index.NewBatch() if batch.Size() >= 1000 { err := index.Batch(batch) if err != nil { // failed, try again or log etc... } batch = index.NewBatch() } else { batch.index(idStr, b) }
您會注意到語法非常相似,您基本上可以互換使用“Conjunction”和“Disjunction”查詢。
這將類似於 SQL 中的以下內容:
searchParser := bleve.NewQueryStringQuery("chicken reciepe books") maxPerPage := 50 ofsset := 0 searchRequest := bleve.NewSearchRequestOptions(searchParser, maxPerPage, offset, false) // By default bleve returns just the ID, here we specify // - all the other fields we would like to return. searchRequest.Fields = []string{"id", "name", "genre"} searchResults, err := index.Search(searchResult)
總結一下;當您希望所有子查詢符合至少一個文件時,請使用「聯合查詢」;當您希望符合至少一個子查詢但不一定符合所有子查詢時,請使用「析取查詢」。
如果您遇到速度問題,Bleve 也可以將資料分佈在多個索引分片上,然後在一個請求中查詢這些分片,例如:
{ "status": { "total": 5, "failed": 0, "successful": 5 }, "request": {}, "hits": [], "total_hits": 19749, "max_score": 2.221337297308545, "took": 99039137, "facets": null }
分片可能會變得相當複雜,但正如您在上面看到的,Bleve 消除了很多痛苦,因為它會自動「合併」所有索引並在它們之間進行搜索,然後在一個結果集中返回結果,就像您搜尋一樣單一索引。
我一直在使用分片來搜尋 100 個分片。整個搜尋過程平均只需 100-200 毫秒即可完成。
您可以如下建立分片:
//... build searchRequest -- see previous section. // Add facets genreFacet := bleve.NewFacetRequest("genre", 50) searchRequest.AddFacet("genre", genreFacet) searchResults, err := index.Search(searchResult)
只要確保為每個文檔建立唯一的 ID,或採用某種可預測的方式新增和更新文檔,而不會弄亂索引。
執行此操作的簡單方法是將包含分片名稱的前綴儲存在來源資料庫中或從您取得文件的任何位置。這樣,每次您嘗試插入或更新時,您都會尋找“前綴”,它會告訴您在哪個分片上呼叫“.index”。
說到更新,只需呼叫「index.index(idstr, struct)」即可更新現有文件。
僅使用上面的這種基本搜尋技術並將其置於 GIN 或標準 Go HTTP 伺服器後面,您就可以建立非常強大的搜尋 API 並服務數百萬個請求,而無需推出複雜的基礎設施。
但有一點需要注意;但是,Bleve 不支援複製,因為您可以將其包裝在 API 中。只需有一個 cron 作業,從您的來源讀取數據,然後使用 goroutine 將更新「爆炸」到您的所有 Bleve 伺服器。
或者,您可以將寫入磁碟鎖定幾秒鐘,然後將資料「rsync」到從屬索引,儘管我不建議這樣做,因為您可能還需要每次重新啟動 go 二進位檔案.
以上是Bleve:如何建立一個快速的搜尋引擎?的詳細內容。更多資訊請關注PHP中文網其他相關文章!