前兩講我們討論了搜尋的基本原理和斷詞,帶著這些基本知識,這一講便來聊聊在我們的 Astro 專案中搜尋功能的主要流程。
現存的套件有蠻多種的,像是 Astro 官方主題用的 Pagefind、老牌的 Lunr.js、主打高性能的 Flex Search 和彈性較高的 MiniSearch 等等。
由於個人部落格的文章不會太多,各家套件的效能在這個量級比較起來其實差不多,那麼挑選的著重點就在於功能了。
剛開始有考慮過使用 Pagefind,因為套用在我們的專案中還算簡單,能夠分片(Sharding)且被 Astro 官方使用,但是在中文斷詞這塊就不太好客製。
因此最終選擇了 MiniSearch + Jieba 斷詞,基本上就能將所有流程都涵蓋在內,自己讀取文章、斷詞、Highlight 等等。
search-data.json
文件先來看看要在前端搜尋,我們需要提供怎麼樣的資訊?
這是一份叫做 search-data.json
的 JSON 檔案,包含 index
及 docs
兩個欄位,index
放的是序列化的 MiniSearch 索引字串,而 docs
則包含文章 id
、 title
、url
等等。
實際的 JSON 看起來像是:
{
"index": "{\"documentCount\":33,\"nextId\":33,\"documentIds\":{\"0\":\"astro-basic\",\"1\":\"astro-components-layouts\",...",
"docs": [
{
"id": "astro-basic",
"title": "Astro(一):輕又快的靜態網站產生器,淺談島嶼架構",
"url": "",
"tags": ["frontend", "javascript", "css"]
},
{
"id": "astro-components-layouts",
"title": "Astro(二):初始化專案,理解 Astro Components 及 Layouts 基礎",
"url": "",
"tags": ["frontend", "javascript"]
}
]
其中的 docs
比較好理解,就是所有文章的基本資訊;而 index
則包含 documentIds
、fieldIds
、storedFields
等,能讓 MiniSearch 在前端迅速復原索引和權重等重要資訊。
有了 search-data.json
後,我們可以在前端頁面載入時就順便下載這個檔案,透過 MiniSearch.loadJSON(index)
讀回所需資訊。
接著實作一個搜尋視窗,在 Input Element 中輸入任何文字時呼叫 mini.search(value)
取得每一筆資料的結果,包含 id
、score
、 match
、terms
等等。
例如搜尋「執行」這個關鍵字,我們可以找到包含這個關鍵字的文章,不論他在標題或內文。
*搜尋「執行」示意圖
所謂的 terms
是實際被索引到的字詞(經過我們斷詞後的),在這個例子中就是「執行」二字; score
則是這筆文章的相關度分數,我們可以依據這個分數來排序搜尋結果。
而 match
則是一個物件,key
是被命中的詞,value
為這個詞出現在哪些欄位。以圖中第一筆文章為例,是 { '執行': ['tokenizedTitle', 'tokenizedContent'] }
,代表同時命中標題和內文。
完整的搜尋結果是:
{
"id": "docker-cmd-entrypoint-multiple-processes",
"score": 7.20,
"terms": ["執行"],
"queryTerms": ["執行"],
"match": { "執行": ["tokenizedTitle", "tokenizedContent"] },
"title": "Docker CMD 及 ENTRYPOINT,以及如何在 Docker 同時執行多個程序",
"tags": ["docker"]
}
值得一提的是,如果我們換一種方式來搜尋,像是剛剛這標題中有「同時執行多個程序」的字樣,我們僅僅擷取「行多」這個沒有什麼意義的組合字試試。
*搜尋失敗示意圖
就會發現找不到相關文章,這是由於我們並非全文字串完全的一一比對,而是根據常用中文的字詞作為索引,藉此提升搜尋效率。
而在實際能搜尋之前,這份 search-data.json
的文件就需要在建置的流程中產出,如此一來才能提供 MiniSearch 所需的資訊給前端使用。
我們首先可以建立一個 build-search-docs.ts
檔案,先把所有 Markdown 文章抓出來,以此專案為例,從 src/content/blog
裡面擷取內容,並抽出標題、網址、標籤等等。
再來就需要透過 nodejieba
來做中文斷詞了,將標題、內文、標籤拆成一串字詞,然後用空白串起來。例如「Docker 同時執行」拆成含有空白的「Docker 同時 執行」,這就是 MiniSearch 建立索引的基礎。
最後將每篇文章變成一個搜尋物件,包含 title
、url
、tags
等資訊,打包成一個 search-data.json
檔,就如同上一章節的範例所示。
這樣一來,就能在部落格中對中文做算是不錯的站內搜尋功能了!