當前位置:
首頁 > 最新 > 使用Docker和Elasticsearch搭建全文本搜索引擎應用

使用Docker和Elasticsearch搭建全文本搜索引擎應用

給應用添加快速、靈活的全文本搜索對誰都不是一件容易的事情。許多主流資料庫,如PostgreSQL和MongoDB,受限於查詢和索引結構,只提供基礎文本搜索能力。為了提供高效全文本搜索一般都需要一個獨立的資料庫。Elasticsearch正是這樣一個能夠提供靈活性和快速全文本搜索能力的開源資料庫。

本文採用Docker來設置依賴環境。Docker是目前最常見的容器化引擎,Uber、Spotify、ADP和Paypal都是用這個技術,它的優勢在於與操作系統無關,可以運行在Windows、macOS和Linux之上——寫操作指南很容易。如果從來沒有用過Docker也沒問題,本文會詳細提供配置文件。

本文也分別採用Node.js采(用Koa框架)和Vue.js創建搜索API和前端Web應用。

1. 什麼是Elasticsearch

現代應用中全文本檢索是高請求負載的應用。搜索功能也是比較困難完成的功能(許多大眾網站都有subpar功能,但不是返回很慢就是返回結果不準確),大部分原因是因為底層資料庫:許多標準關係型資料庫只能提供基本字元串匹配功能,而對CONTAINS或者LIKE SQL查詢只能提供有限支持。

而本文提供的搜索應用能夠提供:

快速:查詢結果應該實時返回,提高用戶體驗。

靈活:根據不同數據和使用場景,可以調整搜索過程。

最佳建議:對於輸入錯誤,返回最可能的結果。

全文本:除了搜索關鍵詞和標籤之外,希望能夠搜索到所有匹配文本。

實現以上要求的搜索應用,最好採用一個為全文本檢索優化的資料庫,這也是本文採用Elasticsearch的原因。Elasticsearch是一個用Java開發的,開源的內存資料庫,最開始是包含在Apache Lucene庫中。以下是一些官方給出的Elasticsearch使用場景:

Wikipedia使用Elasticsearch提供全文檢索,提供高亮顯示、search-as-you-type和did-you-mean建議等功能。

Guardian使用Elasticsearch將訪問者社交數據整合反饋給作者。

Stack Overflow將位置信息和more-like-this功能與全文本檢索整合提供相關問題和答案。

GitHub使用Elasticsearch在一千三百億行代碼中進行搜索。

Elasticsearch有什麼獨特之處

本質上,Elasticsearch通過使用反向索引提供快速和靈活的全文本搜索。

「索引」是一種在資料庫中提供快速查詢和返回的數據結構。資料庫一般將數據域和相應表位置生成索引信息。將索引信息存放在一個可搜索的數據結構中(一般是B-Tree),資料庫可以為優化數據請求獲得線性搜索響應(例如「Find the row with ID=5」)。

可以把資料庫索引看做學校圖書館卡片分類系統,只要知道書名和作者,就可以準確告訴查找內容的入口。資料庫表一般都有多個索引表,可以加速查詢(例如,對name列的索引可以極大加速對特定name的查詢)。

而反向索引工作原理與此完全不同。每行(或者每個文檔)的內容被分拆,每個入口(本案例中是每個單詞)反向指向包含它的文檔。

反向索引數據結構對查詢「football」位於哪個文檔這種查詢非常迅速。Elasticsearch使用內存優化反向索引,可以實現強大和客制化全文本檢索任務。

2. 項目安裝

2.0 Docker

本文使用Docker作為項目開發環境。Docker是一個容器化引擎,應用可以運行在隔離環境中,不依賴於本地操作系統和開發環境。因為可以帶來巨大靈活性和客制化,許多互聯網公司應用都已經運行在容器中。

對於作者來說,Docker可以提供平台一致性安裝環境(可以運行在Windows、macOS和Linux系統)。一般Node.js、Elasticsearch和Nginx都需要不同安裝步驟,如果運行在Docker環境中只需要定義好不同配置文件,就可以運行在任何Docker環境。另外,由於應用各自運行在隔離容器中,與本地宿主機關係很小,因此類似於「但是我這可以運行啊」這種排錯問題就很少會出現。

2.1 安裝Docker和Docker-Compose

本項目只需要Docker和Docker-Compose環境。後者是Docker官方工具,在單一應用棧中編排定義多個容器配置。

安裝Docker——https://docs.docker.com/engine/installation/

安裝Docker Compose——https://docs.docker.com/compose/install/

2.2 設置項目安裝目錄

創建一個項目根目錄(例如guttenberg_search),在其下定義兩個子目錄:

/public——為前端 Vue.js webapp存放數據。

/server——伺服器端Node.js 源文件。

2.3 添加Docker-Compose配置文件

下一步,創建docker-compose.yml文件,定義應用棧中每個容器的配置:

gs-api——Node.js 容器後端應用邏輯。

gs-frontend——為前端webapp提供服務的Nginx容器。

gs-search——存儲搜索數據的Elasticsearch容器。

此文件定義應用棧,而不需要在本地宿主機安裝Elasticsearch、Node.js、或者Nginx。每個容器都對宿主機開放相應埠,以便從宿主機訪問和排錯Node API,Elasticsearch實例和前端應用。

2.4 添加Dockerfile

本文使用官方的Nginx和Elasticsearch鏡像,但是需要重新為Node.js創建自己的鏡像。

在應用根目錄定義一個簡單的Dockerfile配置文件。

此Docker配置文件中將應用源碼拷貝進來,安裝了NPM依賴包,形成了自己的鏡像。同樣需要添加一個.dockerignore文件,避免不需要的文件被拷入。

注意:不需要將node_modules拷入,因為我們後續要用npm install來安裝這些進程。如果拷貝node_modules到容器中容易引起兼容性問題。例如在macOS上安裝bcrypt包,如果將此module拷入Ubuntu容器就會引起操作系統不匹配問題。

2.5 添加基礎文件

測試配置文件前,還需要往應用目錄拷入一下佔位文件。在public/index.html中加入如下基礎配置信息:

下一步,在server/app.js中加入Node.js的應用文件。

最後,加入package.json節點配置文件:

此文件定義應用開始命令和Node.js依賴包。

注意:不需要特意運行npm install,容器創建時候會自動安裝依賴包。

2.6 開始測試

都準備好了,接下來可以測試了。從項目根目錄開始,運行docker-compose,會自動創建Node.js容器應用。

運行docker-compose up啟動應用:

注意:這一步可能會運行時間比較長,因為Docker可能需要下載基礎鏡像。以後執行速度會很快,因為本地已經有了基礎鏡像。

訪問localhost:8080,應該看到如下圖輸出「hello world」。

訪問localhost:3000驗證伺服器端返回「hello world」信息。

最後,訪問localhost:9200確認Elasticsearch是否運行,如果正常,應該返回如下輸出:

如果所有URL輸出都正常,恭喜,整個應用框架可以正常工作,下面開始進入真正有趣的部分了。

3. 接入Elasticsearch

第一步是要接入本地Elasticsearch實例。

3.0 加入ES鏈接模塊

在server/connection.js中加入如下初始化代碼:

下面用docker-compose來重建更改過的應用。之後運行docker-compose up -d重新啟動後台進程。

應用啟動後,命令行運行docker exec gs-api "node" "server/connection.js",在容器中運行腳本,應該可以看到如下輸出:

如果一切順利,就可以把最後一行的checkConnection()調用刪掉,因為最終應用會從connection模塊之外調用它。

3.1 給Reset Index添加Helper功能

在server/connection.js文件checkConnection之下添加如下內容, 以便更加方便重置索引。

3.2 添加Book Schema

緊接resetIndex之後,添加如下功能:

此處為書目索引定義了mapping(映射)。Elasticsearch索引類似於SQL的表或者MongoDB的connection。通過mapping我們可以定義文檔每個域和數據類型。Elasticsearch是schema-less,因此技術上說不需要添加mapping,但是通過mapping可以更好控制數據處理方式。

例如,有兩個關鍵詞域,分別是「titile」和「author」,文本定為「text」域。這樣定義搜索引擎會有完全不同的動作:搜索中,引擎會在text域中查找可能匹配項,而在關鍵詞域則是精確匹配。看起來差別不大,但卻對搜索行為和搜索速度有很大影響。

在文件最後輸出功能和屬性,可以被其它模塊訪問。

4. 載入源數據

本文使用從Gutenberg項目(一個在線提供免費電子書的應用)提供的數據。包括100本經典書目,例如《80天環繞地球》、《羅密歐與朱麗葉》以及《奧德賽》等。

4.1 下載書籍數據

本文的數據可以從以下網站下載:

https://cdn.patricktriest.com/data/books.zip,之後解壓到項目根目錄下的books/ 子目錄下。

也可以用命令行實現以上操作:

4.2 預覽書籍

打開一本書,例如219-0.txt。書籍以公開訪問license開始,跟著是書名、作者、發行日期、語言以及字元編碼。

隨後是聲明信息:*** START OF THIS PROJECT GUTENBERG EBOOK HEART OF DARKNESS ***,緊接著就是書的實際內容。

書的最後會發現書籍結束聲明:*** END OF THIS PROJECT GUTENBERG EBOOK HEART OF DARKNESS ***,緊跟著是更加詳細的書籍license。

下一步將用編程方法從書中提取元數據,並且從* * *之間將書籍內容抽取出來。

4.3 讀取數據目錄

本節寫一段腳本讀取書籍內容添加到Elasticsearch中,腳本存放在server/load_data.js 中。

首先,獲得books目錄下所有文件列表。

運行docker-compose -d --build重建鏡像更新應用。

運行docker exec gs-api "node" "server/load_data.js"調用包含load_data腳本應用,應該看到Elasticsearch輸出如下。隨後,腳本會因為錯誤退出,原因是調用了一本目前還不存在的helper函數(parseBookFile)。

4.4 讀取數據文件

創建server/load_data.js文件,讀取每本書元數據和內容:

此函數執行以下功能:

從文件系統中讀入文件

使用正則表達式抽取書名和作者

通過定位***,來抽取書中內容

解析出段落

清洗數據,移除空行

最後返回一個包含書名、作者和段落列表的對象。

運行docker-compose up -d --build和docker exec gs-api "node" "server/load_data.js" ,輸出如下:

到這步,腳本順利分理出書名和作者,腳本還會因為同樣問題出錯(調用還未定義的函數)。

4.5 在ES中索引數據文件

最後一步在load_data.js中添加insertBookData函數,將上一節中提取數據插入Elasticsearch索引中。

此函數索引書籍段落,包括作者、書名和段落元數據信息。使用bulk操作插入段落,比分別索引段落效率高很多。

批量bulk索引這些段落可以使本應用運行在低配電腦上(我只有1.7G內存),如果你有高配電腦(大於4G內容),也許不用考慮批量bulk操作。

運行docker-compose up -d --build 和 docker exec gs-api "node" "server/load_data.js" 輸出如下:

5. 搜索

Elasticsearch已經灌入100本書籍數據(大約230000段落),本節做一些搜索操作。

5.0 簡單http查詢

首先,使用http://localhost:9200/library/_search?q=text:Java&pretty , 這裡使用全文本查詢關鍵字「Java」,輸入應該如下:

Elasticsearch HTTP介面對於測試數據是否正常插入很有用,但是如果直接暴露給web應用就很危險。不應該將操作性API功能(例如直接添加和刪除文檔)直接暴露給應用,而應該寫一段簡單Node.js API接收客戶端請求,(通過私網)轉發給Elasticsearch進行查詢。

5.1 請求腳本

這一節介紹如何從Node.js應用中向Elasticsearch中發送請求。首先創建新文件:server/search.js。

本模塊定義了一個簡單的search功能,使用輸入信息進行匹配查詢。詳細欄位解釋如下:

from:為結果標出頁碼。每次查詢默認返回10個結果;因此指定from為10,可以直接顯示10-20的查詢結果。

query:具體查詢關鍵詞。

operator:具體查詢操作;本例中採用「and」操作符,優先顯示包含所有查詢關鍵詞的結果。

fuzziness:錯誤拼寫修正級別(或者是模糊查詢級別),默認是2。數值越高,允許模糊度越高;例如數值1,會對Patricc的查詢返回Patrick結果。

highlights:返回額外信息,其中包含HTML格式顯示匹配文本信息。

可以調整這些參數看看具體的顯示信息,可以查看Elastic Full-Text Query DSL[1]獲得更多信息。

6. API

本節提供前端代碼訪問的HTTP API。

6.0 API Server

修改server/app.js內容如下:

這段代碼導入服務依賴環境,為Koa.js Node API Server設置簡單日誌和錯誤處理機制。

6.1 將服務端點與查詢鏈接起來

這一節為Server端添加服務端點,以便暴露給Elasticsearch查詢服務。

在server/app.js中//ADD ENDPOINTS HERE 之後插入如下代碼:

用docker-compose up -d --build重啟服務端。在瀏覽器中,調用此服務。例如:http://localhost:3000/search?term=java。

返回結果看起來應該如下:

6.2 輸入驗證

此時服務端還是很脆弱,下面對輸入參數進行檢查,對無效或者缺失的輸入進行甄別,並返回錯誤。

我們使用Joi和Koa-Joi-Validate庫進行這種類型的驗證:

現在如果重啟服務端,並做一個缺失參數查詢(http://localhost:3000/search),將會返回HTTP 400錯誤,例如:Invalid URL Query - child "term" fails because ["term" is required]。

可以用docker-compose logs -f api 查看日誌。

7. 前端應用

/search服務端硬體可以了,本節寫一段簡單前端web應用測試API。

7.0 Vue.js

本節使用Vue.js來開發前端。創建一個新文件/public/app.js:

應用特別簡單,只是定義一些共享數據屬性,添加一個接收方法以及為結果分頁的功能;搜索間隔設置為100ms,以防API被頻繁調用。

解釋Vue.js如何工作超出本文的範圍,如果想了解相關內容,可以查看Vue.js官方文檔[2]。

7.1 HTML

將/public/index.html用如下內容代替:

7.2 CSS

添加一個新文件:/public/styles.css:

7.3 測試

打開localhost:8080,應該能夠看到一個簡單分頁返回結果。此時可以鍵入一些關鍵詞進行查詢測試。

這一步不需要重新運行docker-compose up命令使修改生效。本地public目錄直接掛載在Ngnix伺服器容器中,因此前端本地系統數據改變直接反應在容器化應用中。

如果點任一個輸出,沒什麼效果,意味著還有一些功能需要添加進應用中。

8. 頁面檢查

最好點擊任何一個輸出,可以查出上下文來自哪本書。

8.0 添加Elasticsearch查詢

首先,需要定義一個從給定書中獲得段落的簡單查詢。在server/search.js下的module.exports中加入如下內容:

此功能將返回給定書排序後的段落。

8.1 添加API服務埠

本節將把上節功能鏈接到API服務埠。在server/app.js中原來的/search服務埠下添加如下內容:

8.2 添加UI界面

本節添加前端查詢功能,並顯示書中包含查詢內容的整頁信息。在/public/app.js methods功能塊中添加如下內容:

以上五個功能塊提供在書中下載和分頁(每頁顯示10段)邏輯操作。

在/public/index.html 中的分界符下加入顯示書頁的UI代碼如下:

重啟應用伺服器(docker-compose up -d --build),打開localhost:8080。此時如果點擊搜索結果,就可以查詢段落上下文。如果對查到結果感興趣,甚至可以從查詢處一直讀下去。

恭喜!!到這一步主體框架已經搭建完畢。以上所有代碼都可以從這裡[3]獲得。

9. Elasticsearch的不足

9.0 資源消耗

Elasticsearch是計算資源消耗的應用。官方建議至少運行在64G以上內存的設備上,不建議少於8GB內存。Elasticsearch是一個內存資料庫,因此查詢速度會很快,但是也會消耗大量內存。生產中,強烈推薦運行Elasticsearch集群提供高可用性、自動分片和數據冗餘功能。

9.1 資料庫之間的同步

對許多應用,將數據存放在Elasticsearch中並不是理想的選擇。建議將ES作為交易型資料庫,但是因為ES不兼容ACID標準(當擴展系統導入數據時,可能造成寫入操作丟失的問題),所以也不推薦。很多場景下,ES承擔著很特殊的角色,例如全文本查詢,這種場景下需要某些數據從主資料庫複製到Elasticsearch資料庫中。

例如,假設我們需要將用戶存放到PostgreSQL表中,但是使用ES承擔用戶查詢功能。如果一個用戶,「Albert」,決定修改名字為「Al」,就需要在主PostgreSQL庫和ES集群中同時進行修改。

這個操作有些複雜,依賴現有的軟體棧。有許多開源資源可選,既有監控MongoDB操作日誌並自動同步刪除數據到ES的進程,到創建客制化基於PSQL索引自動與ES通訊的PostgreSQL插件。

如果之前提到的選項都無效,可以在服務端代碼中根據資料庫變化手動更新Elasticsearch索引。但是我認為這種選擇並不是最佳的,因為使用客制化商業邏輯保持ES同步很複雜,而且有可能會引入很多bugs。

Elasticsearch與主資料庫同步需求,與其說是ES的弱點,不如說是架構複雜造成的;給應用添加一個專用搜索引擎是一件值得考慮的事情,但是要折衷考慮帶來的問題。

結論

全文本搜索對現代應用來說是一個很重要的功能,同時也是很難完成的功能。Elasticsearch則提供了實現快速和客制化搜索的實現方式,但是也有其它替代選項。Apache Solr是另外一個基於Apache Lucene(Elasticsearch核心也採用同樣的庫)實現的開源類似實現。Algolia則是最近很活躍的search-as-a-service模式web平台,對初學者來說更加容易上手(缺點是客制化不強,而且後期投入可能很大)。

「search-bar」模式功能遠不僅是Elasticsearch的唯一使用場景。ES也是一個日誌存儲和分析常用工具,一般用於ELK架構(Elasticsearch,Logstash,Kibana)。ES實現的靈活全文本搜索對數據科學家任務也很有用,例如修改、規範化數據集拼寫或者搜索數據集。

如下是有關本項目的考慮:

在應用中添加更多喜愛的書,創建自己私有庫搜索引擎。

通過索引Google Scholar論文,創建一個防抄襲引擎。

通過索引字典中單詞到ES中,建立拼寫檢查應用。

通過載入Common Crawl Corpus到ES(注意,有50億頁內容,是一個非常巨大數據集),建立自己的與谷歌競爭的互聯網搜索引擎。

在新聞業中使用Elasticsearch:在例如Panama論文和Paradise論文集中搜索特點名稱和詞條。

本文所有代碼都是開源的,可以在GitHub庫中找到,具體下載地址[4]。希望本文對大家有所幫助。


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Docker 的精彩文章:

Kubernetes存儲系統介紹及機制實現

TAG:Docker |