優化了破網站的搜尋功能
> 使用 ES + 雲開發實戰優化網站搜尋
大家好,我是魚皮,今天搞一場技術實戰,需求分析 => 技術選型 => 設計實現,從 0 到 1,帶大家優化網站搜尋的靈活性。
## ES + 雲開發搜尋優化實戰
本文大綱:
![魚皮 - 網站搜尋優化](https://qiniuyun.code-nav.cn/%E9%B1%BC%E7%9A%AE%20-%20%E7%BD%91%E7%AB%99%E6%90%9C%E7%B4%A2%E4%BC%98%E5%8C%96.png)
### 背景
我開發的 [程式設計導航網站](https://www.code-nav.cn) 已經上線 6 個月了,但是從上線之初,網站一直存在一個很嚴重的問題,就是搜尋功能並不好用。
此前,為了追求快速上線,搜尋功能就簡單地使用了資料庫模糊查詢(包含)來實現,開發是方便了,但這種方式很不靈活。
舉個例子,網站上有個資源叫 “Java 設計模式”,而使用者搜尋 “Java設計模式” 就啥都搜不出來,原因是資源名中包含了空格,而使用者搜尋時輸入的關鍵詞並不包含空格。
空格只是一種特例,類似的情況還有很多,比如網站上有個資源叫 “Java 併發程式設計實戰”,但使用者搜尋 “Java 實戰” 時,明明前者包含 “Java” 和 “實戰” 這兩個詞,但卻是什麼都搜不出來的。
要知道,搜尋功能對於一個資訊聚合類站點是至關重要的,直接影響使用者的體驗。在你的網站上搜不到資源,誰還會用?
所以我也收到了一些小夥伴的禮貌建議,比如這位禿頭 Tom:
![](https://qiniuyun.code-nav.cn/image-20210728135414226.png)
之前沒有優化搜尋,主要是兩個原因:窮 + 怕麻煩。但隨著網站使用者量的增大,是時候填坑了!
### 技術選型
想要提高網站搜尋靈活性,可以使用 **全文搜尋** 技術,在前端和後端都可以實現。
#### 前端全文搜尋
有時,我們要檢索的資料是有限的,且所有資料都是 **儲存在客戶端** 的。
比如個人部落格網站,我們通常會把每篇文章作為一個檔案存放在某目錄下,而不是存在後臺數據庫中,這種情況下,不需要再從伺服器上去請求動態資料,那麼可以直接在前端搜尋資料。
有一些現成的搜尋庫,比如 `Lunr.js`(GitHub 7k+ star),先新增要檢索的內容:
```javascript
var idx = lunr(function () {
this.field('title')
this.field('body')
// 內容
this.add({
"title": "yupi",
"body": "wx搜程式設計師魚皮,閱讀我的原創文章",
"id": "1"
})
})
```
然後搜尋就可以了:
```javascript
idx.search("魚皮")
```
純前端全文搜尋的好處是無需後端、簡單方便,可以節省伺服器的壓力;無需連網,也沒有額外的網路開銷,檢索更快速。
#### 後端全文搜尋
區別於前端,後端全文搜尋在伺服器上完成,從遠端資料庫中搜索符合要求的資料,再直接返回給前端。
目前主流的後端全文搜尋技術是 Elasticsearch,一個分散式、RESTful 風格的搜尋和資料分析引擎。
![](https://qiniuyun.code-nav.cn/image-20210729131646597.png)
它的功能強大且靈活,但是需要自己搭建、定義資料、管理詞典、上傳和維護資料等,可操作性很強,需要一些水平,新手和大佬設計出的 ES 搜尋系統那是天差地別。
所以,對於不熟悉 Elasticsearch 的同學,也可以直接使用現成的全文檢索服務。比如 Algolia,直接通過它提供的 API 上傳需要檢索的資料,再用它提供的 API 檢索就行了。它提供了一定的免費空間,對於小型網站和學習使用完全足夠了。
![Algolia 檢索服務](https://qiniuyun.code-nav.cn/image-20210729130939206.png)
#### 選擇
那麼我的程式設計導航網站選擇哪種實現方式呢?
首先,該網站的資源數是不固定的、無規律動態更新的,因此不適合前端全文檢索。
其次,考慮到日後網站的資料量會比較大,而且可能要根據使用者的搜尋動態地去優化檢索系統(比如自定義程式設計詞典),因此考慮使用 **Elasticsearch 技術** 自行搭建搜尋引擎,而不用現成的全文檢索服務,這樣今後自己想怎麼定製系統都可以。此外,不用向其他平臺傳送網站資料,能保證資料的安全。
![](https://qiniuyun.code-nav.cn/b7f5df950b9d8488e931f8a884990dd1.jpg)
### ES 安裝
確定使用 Elasticsearch 後,要先搭建環境。
可以自己購買伺服器,再按照官方文件一步步手動安裝。對於有一定規模的個人網站來說,雖然搭建過程不難,但後期的維護成本卻是巨大的,比如效能分析、監控、告警、安全等等,都需要自己來配置。尤其是後期網站資料量更大了,還要考慮搭建叢集、水平擴容等等。
因此,我選擇直接使用雲服務商提供的 Elasticsearch 服務,這裡選擇騰訊雲,自動為你搭建了現成的 ES 叢集服務,還提供了視覺化架構管理、叢集監控、日誌、高階外掛、智慧巡檢等功能。
![雲 ES 叢集架構圖](https://qiniuyun.code-nav.cn/image-20210729135950668.png)
雖然 ES 服務的價格貴,但節省下大量時間成本,對我來說是值得的。
> 還有個很方便的定製化搜尋服務 Elastic App Search,大家感興趣可以試試。
### ES 公共服務
我們的目標是優化網站資源的搜尋功能,但接下來要做的不是直接編寫具體的業務邏輯,而是先開發一個 **公共的 ES 服務** 。
其實對 ES 的操作比較簡單,可以先簡單地把它理解為一個數據庫,那麼公共的 ES 服務應具有基本的增刪改查功能,供其他函式呼叫。
#### 實現
由於程式設計導航的後端使用的是騰訊雲開發技術,用 Node.js 來編寫服務,所以選用官方推薦的 `@elastic/elasticsearch` 庫來操作 ES。
> 沒用過雲開發也沒事,可以先把它理解為一個後端,歡迎閱讀我之前的文章:[瞭解雲開發](https://mp.weixin.qq.com/s/m4ok3OW29TberTtounLdcw) 。
程式碼很簡單,先是建立和 ES 的連線,此處為了保證資料安全,使用內網地址:
```javascript
const client = new Client({
// 內網地址
node: 'http://10.0.61.1:9200',
// 使用者名稱和密碼
auth: {
username: esConfig.username,
password: esConfig.password,
},
});
```
然後是編寫增刪改查。這裡做一步 **抽象**,通過 `switch` 等分支語句,根據請求引數來區分操作、要操作的資料等,這樣就不用把每個操作都獨立寫成一個介面了。
```javascript
// 接受請求引數
const { op, index, id, params } = event;
// 根據操作執行增刪改查
switch (op) {
case 'add':
return doAdd(index, id, params);
case 'delete':
return doDelete(index, id);
case 'search':
return doSearch(index, params);
case 'update':
return doUpdate(index, id, params);
}
```
> 在雲開發中,假如某個函式太久沒被呼叫,就會釋放資源。下次請求時,會進行冷啟動,重新建立資源,導致介面返回較慢。因此,把多個操作封裝到同一個函式中,也可以減少冷啟動的機率。
具體的增刪改查程式碼就不贅述了,對著 ES Node 的官方文件看一遍就行了,後面會把程式碼開源到程式設計導航倉庫中(https://github.com/liyupi/code-nav)。
#### 本地除錯
編寫好程式碼後,可以用雲開發自帶的 `tcb` 命令列工具在本地執行該函式。
記得先把 ES 的連線地址改成公網,然後輸入一行命令就行了。比如我們要向 ES 插入一條資料,傳入要執行的函式名、請求引數、程式碼路徑:
```shell
tcb fn run
--name <functionName>
--params "{\"op\": \"add\"}"
--path <functionPath>
```
執行成功後,就能在 ES 中看到新插入的資料了(通過 Kibana 面板或 curl 檢視):
![](https://qiniuyun.code-nav.cn/image-20210729143420613.png)
#### 遠端測試
本地測試好公共服務程式碼後,把 ES 連線地址改成內網 IP,然後釋出到雲端。
接下來試著編寫一個其他的函式來訪問公共 ES 服務,比如插入資源到 ES,通過 `callFunction` 請求:
```javascript
// 新增資源到 ES
function addData() {
// 請求公共服務
app.callFunction({
name: 'esService',
data: {
op: 'add',
index: 'resource',
id,
params: data,
}
});
}
```
但是,資料並沒有被成功插入,而是返回了介面超時,Why?
#### 內網配置
通過日誌得知是 ES 連線不上,會不會是因為釋出上線的 ES 公共服務所在的機器和 ES 不在同一個內網呢?
所以需要在雲開發控制檯更改 ES 公共服務的私有網路配置,選擇和購買 ES 時同樣的子網就行了:
![配置 ES 雲函式私有網路](https://qiniuyun.code-nav.cn/image-20210729144253445.png)
修改之後,再次遠端請求 ES 公共服務,資料就插入成功了~
### 資料索引
開發好 ES 公共服務後,就可以編寫具體的業務邏輯了。
首先要在 ES 中建立一個索引(類似資料庫的表),來約定資料的型別、分詞等資訊,而不是允許隨意插入資料。
比如為了更靈活搜尋,資源名應該指定為 "text" 型別,以開啟分詞,並指定 `ik` 中文分詞器:
```json
"name": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
```
而點贊數應設定為 "long" 型別,只允許傳入數字:
```json
"likeNum": {
"type": "long"
}
```
最好還要為索引指定一個別名,便於後續修改欄位時重建索引:
```json
"aliases" : { "resource": {}}
```
編寫好建立索引的 json 配置後,通過 curl 或 Kibana 去呼叫 ES 新建索引介面就行了。
### 資料同步
之前,程式設計導航網站的資源資料都是存在資料庫中的,使用者從資料庫中查詢。而現在要改為從 ES 中查詢,ES 空空如也可不行,得想辦法把資料庫中的資源資料同步到 ES 中。
這裡有幾種同步策略。
#### 雙寫
以前,使用者推薦的資源只會插入到資料庫,雙寫是指在資源插入資料庫的時候,同時插入到 ES 就好了。
聽上去挺簡單的,但這種方式存在一些問題:
1. 會改動以前的程式碼,每個寫資料庫的地方都要補充寫入 ES。
2. 會存在一邊兒寫入失敗、另一邊兒成功的情況,導致資料庫和 ES 的資料不一致。
![](https://qiniuyun.code-nav.cn/image-20210729161243585.png)
那有沒有對現有程式碼 **侵入更小** 的方法呢?
#### 定時同步
如果對資料實時性的要求不高,可以選擇定時同步,每隔一段時間將最新插入或修改的資料從資料庫複製到 ES 上。
實現方式有很多種,比如用 `Logstash` 資料傳輸管道,或者自己編寫定時任務程式,這樣就完全不用改現有的程式碼。
#### 實時同步
如果對資料實時性要求很高,剛剛插入資料庫的資料就要能立刻就能被搜尋到,那麼就要實時同步。除了雙寫外,還可以監聽資料庫的 binlog,在資料庫發生任何變更時,我們都能感知到。
阿里有個開源專案叫 `Canal` ,能夠實時監聽 MySQL 資料庫,並推送通知給下游,感興趣的朋友可以看看。
![Canal 專案](https://qiniuyun.code-nav.cn/2179781-20201017213719639-864183204.jpg)
#### 實現
由於程式設計資源的搜尋對實時性要求不高,所以定時同步就 ok。
雲開發預設提供了定時函式功能,我就直接寫一個雲函式,每 1 分鐘執行一次,每次讀取資料庫中近 5 分鐘內發生了變更的資料,以防止上次執行失敗的情況。此外,還要配置超時時間,防止函式執行時間過長導致的執行失敗。
在雲開發 - 雲函式控制檯就能視覺化配置了,需要為定時任務指定一個 crontab 表示式:
![配置雲函式定時和超時](https://qiniuyun.code-nav.cn/image-20210729151519010.png)
開啟定時同步後,不要忘了再編寫並執行一個 **首次** 同步函式,用於將歷史的全量資料同步到 ES。
### 資料檢索
現在 ES 上已經有資料了,只剩最後一步,就是怎麼把資料搜出來呢?
首先我們要學習 ES 的搜尋 DSL(語法),包括如何取列、搜尋、過濾、分頁、排序等,對新手來講,還是有點麻煩的,尤其是查詢條件中布林表示式的組合,稍微不注意就查不出資料。所以建議大家先在 Kibana 提供的除錯工具中編寫查詢語法:
![Kibana 除錯](https://qiniuyun.code-nav.cn/image-20210729154232910.png)
查出預期的資料後,再編寫後端的搜尋函式,接受的請求引數最好和原介面保持一致,減少改動。
可以根據前端傳來的請求動態拼接查詢語法,比如要按照資源名搜尋:
```javascript
// 傳了資源名if (name) { // 拼接查詢語句 query.bool.should = [ { match: { name } } ];}
```
由此,整個網站的搜尋優化完畢。
再去試一下效果,現在哪怕我輸入一些多 “魚” 的詞,也能搜到了!
![](https://qiniuyun.code-nav.cn/image-20210729160001838.png)
> ES 是怎麼實現靈活搜尋的呢?歡迎閱讀 [這篇文章](https://mp.weixin.qq.com/s/TuA95KshbRSXVkFvV-elTQ) 。
新 ES 搜尋介面的釋出並不意味著老的資料庫查詢介面淘汰,可以同時保留。按名稱搜尋資源時用新介面,更靈活;而根據稽核狀態、搜尋某使用者釋出過的資源時,可以用老介面,從資料庫查。從而分攤負載,職責分離,讓對的技術做對的事情!
---
以上就是本期分享,有幫助的話點個贊吧 ❤️
我是魚皮,最後再送大家一些 **幫助我拿到大廠 offer 的學習資料**:
[跑了,留下 6T 的資源!](https://t.1yb.co/qOJG)
歡迎閱讀 **我從 0 自學進入騰訊的程式設計學習、求職、考證、寫書經歷,不再迷茫!**
[我學計算機的四年,共勉!](https://t.1yb.co/w66s)
![](https://qiniuyun.code-nav.cn/image-20210613143744811.png)