優(yōu)化了破網(wǎng)站的搜索功能
使用 ES + 云開(kāi)發(fā)實(shí)戰(zhàn)優(yōu)化網(wǎng)站搜索
大家好,我是魚(yú)皮,今天搞一場(chǎng)技術(shù)實(shí)戰(zhàn),需求分析 => 技術(shù)選型 => 設(shè)計(jì)實(shí)現(xiàn),從 0 到 1,帶大家優(yōu)化網(wǎng)站搜索的靈活性。
ES + 云開(kāi)發(fā)搜索優(yōu)化實(shí)戰(zhàn)
本文大綱:

背景
我開(kāi)發(fā)的 編程導(dǎo)航網(wǎng)站 已經(jīng)上線 6 個(gè)月了,但是從上線之初,網(wǎng)站一直存在一個(gè)很?chē)?yán)重的問(wèn)題,就是搜索功能并不好用。
此前,為了追求快速上線,搜索功能就簡(jiǎn)單地使用了數(shù)據(jù)庫(kù)模糊查詢(包含)來(lái)實(shí)現(xiàn),開(kāi)發(fā)是方便了,但這種方式很不靈活。
舉個(gè)例子,網(wǎng)站上有個(gè)資源叫 “Java 設(shè)計(jì)模式”,而用戶搜索 “Java設(shè)計(jì)模式” 就啥都搜不出來(lái),原因是資源名中包含了空格,而用戶搜索時(shí)輸入的關(guān)鍵詞并不包含空格。
空格只是一種特例,類似的情況還有很多,比如網(wǎng)站上有個(gè)資源叫 “Java 并發(fā)編程實(shí)戰(zhàn)”,但用戶搜索 “Java 實(shí)戰(zhàn)” 時(shí),明明前者包含 “Java” 和 “實(shí)戰(zhàn)” 這兩個(gè)詞,但卻是什么都搜不出來(lái)的。
要知道,搜索功能對(duì)于一個(gè)信息聚合類站點(diǎn)是至關(guān)重要的,直接影響用戶的體驗(yàn)。在你的網(wǎng)站上搜不到資源,誰(shuí)還會(huì)用?
所以我也收到了一些小伙伴的禮貌建議,比如這位禿頭 Tom:

之前沒(méi)有優(yōu)化搜索,主要是兩個(gè)原因:窮 + 怕麻煩。但隨著網(wǎng)站用戶量的增大,是時(shí)候填坑了!
技術(shù)選型
想要提高網(wǎng)站搜索靈活性,可以使用 全文搜索 技術(shù),在前端和后端都可以實(shí)現(xiàn)。
前端全文搜索
有時(shí),我們要檢索的數(shù)據(jù)是有限的,且所有數(shù)據(jù)都是 存儲(chǔ)在客戶端 的。
比如個(gè)人博客網(wǎng)站,我們通常會(huì)把每篇文章作為一個(gè)文件存放在某目錄下,而不是存在后臺(tái)數(shù)據(jù)庫(kù)中,這種情況下,不需要再?gòu)姆?wù)器上去請(qǐng)求動(dòng)態(tài)數(shù)據(jù),那么可以直接在前端搜索數(shù)據(jù)。
有一些現(xiàn)成的搜索庫(kù),比如 (GitHub 7k+ star),先添加要檢索的內(nèi)容:
然后搜索就可以了:
純前端全文搜索的好處是無(wú)需后端、簡(jiǎn)單方便,可以節(jié)省服務(wù)器的壓力;無(wú)需連網(wǎng),也沒(méi)有額外的網(wǎng)絡(luò)開(kāi)銷(xiāo),檢索更快速。
后端全文搜索
區(qū)別于前端,后端全文搜索在服務(wù)器上完成,從遠(yuǎn)程數(shù)據(jù)庫(kù)中搜索符合要求的數(shù)據(jù),再直接返回給前端。
目前主流的后端全文搜索技術(shù)是 Elasticsearch,一個(gè)分布式、RESTful 風(fēng)格的搜索和數(shù)據(jù)分析引擎。

它的功能強(qiáng)大且靈活,但是需要自己搭建、定義數(shù)據(jù)、管理詞典、上傳和維護(hù)數(shù)據(jù)等,可操作性很強(qiáng),需要一些水平,新手和大佬設(shè)計(jì)出的 ES 搜索系統(tǒng)那是天差地別。
所以,對(duì)于不熟悉 Elasticsearch 的同學(xué),也可以直接使用現(xiàn)成的全文檢索服務(wù)。比如 Algolia,直接通過(guò)它提供的 API 上傳需要檢索的數(shù)據(jù),再用它提供的 API 檢索就行了。它提供了一定的免費(fèi)空間,對(duì)于小型網(wǎng)站和學(xué)習(xí)使用完全足夠了。

選擇
那么我的編程導(dǎo)航網(wǎng)站選擇哪種實(shí)現(xiàn)方式呢?
首先,該網(wǎng)站的資源數(shù)是不固定的、無(wú)規(guī)律動(dòng)態(tài)更新的,因此不適合前端全文檢索。
其次,考慮到日后網(wǎng)站的數(shù)據(jù)量會(huì)比較大,而且可能要根據(jù)用戶的搜索動(dòng)態(tài)地去優(yōu)化檢索系統(tǒng)(比如自定義編程詞典),因此考慮使用 Elasticsearch 技術(shù) 自行搭建搜索引擎,而不用現(xiàn)成的全文檢索服務(wù),這樣今后自己想怎么定制系統(tǒng)都可以。此外,不用向其他平臺(tái)發(fā)送網(wǎng)站數(shù)據(jù),能保證數(shù)據(jù)的安全。

ES 安裝
確定使用 Elasticsearch 后,要先搭建環(huán)境。
可以自己購(gòu)買(mǎi)服務(wù)器,再按照官方文檔一步步手動(dòng)安裝。對(duì)于有一定規(guī)模的個(gè)人網(wǎng)站來(lái)說(shuō),雖然搭建過(guò)程不難,但后期的維護(hù)成本卻是巨大的,比如性能分析、監(jiān)控、告警、安全等等,都需要自己來(lái)配置。尤其是后期網(wǎng)站數(shù)據(jù)量更大了,還要考慮搭建集群、水平擴(kuò)容等等。
因此,我選擇直接使用云服務(wù)商提供的 Elasticsearch 服務(wù),這里選擇騰訊云,自動(dòng)為你搭建了現(xiàn)成的 ES 集群服務(wù),還提供了可視化架構(gòu)管理、集群監(jiān)控、日志、高級(jí)插件、智能巡檢等功能。

雖然 ES 服務(wù)的價(jià)格貴,但節(jié)省下大量時(shí)間成本,對(duì)我來(lái)說(shuō)是值得的。
還有個(gè)很方便的定制化搜索服務(wù) Elastic app Search,大家感興趣可以試試。
ES 公共服務(wù)
我們的目標(biāo)是優(yōu)化網(wǎng)站資源的搜索功能,但接下來(lái)要做的不是直接編寫(xiě)具體的業(yè)務(wù)邏輯,而是先開(kāi)發(fā)一個(gè) 公共的 ES 服務(wù) 。
其實(shí)對(duì) ES 的操作比較簡(jiǎn)單,可以先簡(jiǎn)單地把它理解為一個(gè)數(shù)據(jù)庫(kù),那么公共的 ES 服務(wù)應(yīng)具有基本的增刪改查功能,供其他函數(shù)調(diào)用。
實(shí)現(xiàn)
由于編程導(dǎo)航的后端使用的是騰訊云開(kāi)發(fā)技術(shù),用 Node.js 來(lái)編寫(xiě)服務(wù),所以選用官方推薦的 庫(kù)來(lái)操作 ES。
沒(méi)用過(guò)云開(kāi)發(fā)也沒(méi)事,可以先把它理解為一個(gè)后端,歡迎閱讀我之前的文章:了解云開(kāi)發(fā) 。
代碼很簡(jiǎn)單,先是建立和 ES 的連接,此處為了保證數(shù)據(jù)安全,使用內(nèi)網(wǎng)地址:
然后是編寫(xiě)增刪改查。這里做一步 抽象,通過(guò) 等分支語(yǔ)句,根據(jù)請(qǐng)求參數(shù)來(lái)區(qū)分操作、要操作的數(shù)據(jù)等,這樣就不用把每個(gè)操作都獨(dú)立寫(xiě)成一個(gè)接口了。
在云開(kāi)發(fā)中,假如某個(gè)函數(shù)太久沒(méi)被調(diào)用,就會(huì)釋放資源。下次請(qǐng)求時(shí),會(huì)進(jìn)行冷啟動(dòng),重新創(chuàng)建資源,導(dǎo)致接口返回較慢。因此,把多個(gè)操作封裝到同一個(gè)函數(shù)中,也可以減少冷啟動(dòng)的幾率。
具體的增刪改查代碼就不贅述了,對(duì)著 ES Node 的官方文檔看一遍就行了,后面會(huì)把代碼開(kāi)源到編程導(dǎo)航倉(cāng)庫(kù)中(https://github.com/liyupi/code-nav)。
本地調(diào)試
編寫(xiě)好代碼后,可以用云開(kāi)發(fā)自帶的 命令行工具在本地執(zhí)行該函數(shù)。
記得先把 ES 的連接地址改成公網(wǎng),然后輸入一行命令就行了。比如我們要向 ES 插入一條數(shù)據(jù),傳入要執(zhí)行的函數(shù)名、請(qǐng)求參數(shù)、代碼路徑:
執(zhí)行成功后,就能在 ES 中看到新插入的數(shù)據(jù)了(通過(guò) Kibana 面板或 curl 查看):

遠(yuǎn)程測(cè)試
本地測(cè)試好公共服務(wù)代碼后,把 ES 連接地址改成內(nèi)網(wǎng) IP,然后發(fā)布到云端。
接下來(lái)試著編寫(xiě)一個(gè)其他的函數(shù)來(lái)訪問(wèn)公共 ES 服務(wù),比如插入資源到 ES,通過(guò) 請(qǐng)求:
但是,數(shù)據(jù)并沒(méi)有被成功插入,而是返回了接口超時(shí),Why?
內(nèi)網(wǎng)配置
通過(guò)日志得知是 ES 連接不上,會(huì)不會(huì)是因?yàn)榘l(fā)布上線的 ES 公共服務(wù)所在的機(jī)器和 ES 不在同一個(gè)內(nèi)網(wǎng)呢?
所以需要在云開(kāi)發(fā)控制臺(tái)更改 ES 公共服務(wù)的私有網(wǎng)絡(luò)配置,選擇和購(gòu)買(mǎi) ES 時(shí)同樣的子網(wǎng)就行了:

修改之后,再次遠(yuǎn)程請(qǐng)求 ES 公共服務(wù),數(shù)據(jù)就插入成功了~
數(shù)據(jù)索引
開(kāi)發(fā)好 ES 公共服務(wù)后,就可以編寫(xiě)具體的業(yè)務(wù)邏輯了。
首先要在 ES 中建立一個(gè)索引(類似數(shù)據(jù)庫(kù)的表),來(lái)約定數(shù)據(jù)的類型、分詞等信息,而不是允許隨意插入數(shù)據(jù)。
比如為了更靈活搜索,資源名應(yīng)該指定為 "text" 類型,以開(kāi)啟分詞,并指定 中文分詞器:
而點(diǎn)贊數(shù)應(yīng)設(shè)置為 "long" 類型,只允許傳入數(shù)字:
最好還要為索引指定一個(gè)別名,便于后續(xù)修改字段時(shí)重建索引:
編寫(xiě)好建立索引的 json 配置后,通過(guò) curl 或 Kibana 去調(diào)用 ES 新建索引接口就行了。
數(shù)據(jù)同步
之前,編程導(dǎo)航網(wǎng)站的資源數(shù)據(jù)都是存在數(shù)據(jù)庫(kù)中的,用戶從數(shù)據(jù)庫(kù)中查詢。而現(xiàn)在要改為從 ES 中查詢,ES 空空如也可不行,得想辦法把數(shù)據(jù)庫(kù)中的資源數(shù)據(jù)同步到 ES 中。
這里有幾種同步策略。
雙寫(xiě)
以前,用戶推薦的資源只會(huì)插入到數(shù)據(jù)庫(kù),雙寫(xiě)是指在資源插入數(shù)據(jù)庫(kù)的時(shí)候,同時(shí)插入到 ES 就好了。
聽(tīng)上去挺簡(jiǎn)單的,但這種方式存在一些問(wèn)題:
會(huì)改動(dòng)以前的代碼,每個(gè)寫(xiě)數(shù)據(jù)庫(kù)的地方都要補(bǔ)充寫(xiě)入 ES。
會(huì)存在一邊兒寫(xiě)入失敗、另一邊兒成功的情況,導(dǎo)致數(shù)據(jù)庫(kù)和 ES 的數(shù)據(jù)不一致。

那有沒(méi)有對(duì)現(xiàn)有代碼 侵入更小 的方法呢?
定時(shí)同步
如果對(duì)數(shù)據(jù)實(shí)時(shí)性的要求不高,可以選擇定時(shí)同步,每隔一段時(shí)間將最新插入或修改的數(shù)據(jù)從數(shù)據(jù)庫(kù)復(fù)制到 ES 上。
實(shí)現(xiàn)方式有很多種,比如用 數(shù)據(jù)傳輸管道,或者自己編寫(xiě)定時(shí)任務(wù)程序,這樣就完全不用改現(xiàn)有的代碼。
實(shí)時(shí)同步
如果對(duì)數(shù)據(jù)實(shí)時(shí)性要求很高,剛剛插入數(shù)據(jù)庫(kù)的數(shù)據(jù)就要能立刻就能被搜索到,那么就要實(shí)時(shí)同步。除了雙寫(xiě)外,還可以監(jiān)聽(tīng)數(shù)據(jù)庫(kù)的 binlog,在數(shù)據(jù)庫(kù)發(fā)生任何變更時(shí),我們都能感知到。
阿里有個(gè)開(kāi)源項(xiàng)目叫 ,能夠?qū)崟r(shí)監(jiān)聽(tīng) MySQL 數(shù)據(jù)庫(kù),并推送通知給下游,感興趣的朋友可以看看。

實(shí)現(xiàn)
由于編程資源的搜索對(duì)實(shí)時(shí)性要求不高,所以定時(shí)同步就 ok。
云開(kāi)發(fā)默認(rèn)提供了定時(shí)函數(shù)功能,我就直接寫(xiě)一個(gè)云函數(shù),每 1 分鐘執(zhí)行一次,每次讀取數(shù)據(jù)庫(kù)中近 5 分鐘內(nèi)發(fā)生了變更的數(shù)據(jù),以防止上次執(zhí)行失敗的情況。此外,還要配置超時(shí)時(shí)間,防止函數(shù)執(zhí)行時(shí)間過(guò)長(zhǎng)導(dǎo)致的執(zhí)行失敗。
在云開(kāi)發(fā) - 云函數(shù)控制臺(tái)就能可視化配置了,需要為定時(shí)任務(wù)指定一個(gè) crontab 表達(dá)式:

開(kāi)啟定時(shí)同步后,不要忘了再編寫(xiě)并執(zhí)行一個(gè) 首次 同步函數(shù),用于將歷史的全量數(shù)據(jù)同步到 ES。
數(shù)據(jù)檢索
現(xiàn)在 ES 上已經(jīng)有數(shù)據(jù)了,只剩最后一步,就是怎么把數(shù)據(jù)搜出來(lái)呢?
首先我們要學(xué)習(xí) ES 的搜索 DSL(語(yǔ)法),包括如何取列、搜索、過(guò)濾、分頁(yè)、排序等,對(duì)新手來(lái)講,還是有點(diǎn)麻煩的,尤其是查詢條件中布爾表達(dá)式的組合,稍微不注意就查不出數(shù)據(jù)。所以建議大家先在 Kibana 提供的調(diào)試工具中編寫(xiě)查詢語(yǔ)法:

查出預(yù)期的數(shù)據(jù)后,再編寫(xiě)后端的搜索函數(shù),接受的請(qǐng)求參數(shù)最好和原接口保持一致,減少改動(dòng)。
可以根據(jù)前端傳來(lái)的請(qǐng)求動(dòng)態(tài)拼接查詢語(yǔ)法,比如要按照資源名搜索:
由此,整個(gè)網(wǎng)站的搜索優(yōu)化完畢。
再去試一下效果,現(xiàn)在哪怕我輸入一些多 “魚(yú)” 的詞,也能搜到了!

ES 是怎么實(shí)現(xiàn)靈活搜索的呢?歡迎閱讀 這篇文章 。
新 ES 搜索接口的發(fā)布并不意味著老的數(shù)據(jù)庫(kù)查詢接口淘汰,可以同時(shí)保留。按名稱搜索資源時(shí)用新接口,更靈活;而根據(jù)審核狀態(tài)、搜索某用戶發(fā)布過(guò)的資源時(shí),可以用老接口,從數(shù)據(jù)庫(kù)查。從而分?jǐn)傌?fù)載,職責(zé)分離,讓對(duì)的技術(shù)做對(duì)的事情!
以上就是本期分享,有幫助的話點(diǎn)個(gè)贊吧 ??
我是魚(yú)皮,最后再送大家一些 幫助我拿到大廠 offer 的學(xué)習(xí)資料:
指路:https://t.1yb.co/qOJG
歡迎閱讀 我從 0 自學(xué)進(jìn)入騰訊的編程學(xué)習(xí)、實(shí)習(xí)、求職、考證、寫(xiě)書(shū)經(jīng)歷,不再迷茫!
指路:https://t.1yb.co/w66s
