基于Pagefind实现静态博客站内搜索

立泉

Jekyll生成的静态博客并不支持站内搜索,以往实现要么使用搜索引擎的site:mudan.me限定站点,要么搭配服务器自建全文搜索服务,但是前者索引更新缓慢,后者则太“重”,总之对于倾向免费托管的静态博客并没有特别好的方案。

直到2022年Pagefind出现,它是一个基于RustWebAssembly的轻量级静态搜索工具。先使用其Terminal命令扫描站点生成索引文件和对应的检索API,所谓“API”其实就是Wasm程序提供的Javascript函数接口,搜索时在Web页面加载调用它们即可自动访问索引文件从中生成结果。

这种模式只是从浏览器访问云端的静态资源,并不需要通常意义上的“搜索服务器”,而且它的搜索算法也会尽量降低检索文件时传输的数据量来节省带宽。以本博客为例,每次搜索下载的数据甚至低于分段字体,所以不必担心高频访问的问题。

索引

生成索引需要先安装Pagefind,由于主要应用在Web端,所以官方提供npm包,可以通过npx直接使用:

# 扫描`--site`指定的目录,将索引输出到`--output-path`指定的目录下
# 生成索引耗时数秒,不建议使用某些方式与Jekyll的生成命令绑定
# 只在文章内容变化时手动执行
npx -y pagefind --site _site --output-path npm/pagefind
# 注意此命令并不会清除已有数据,而是覆盖
# 所以每次执行前需要手动删除上次的索引,否则可能会越积越多

也可以下载预编译的二进制文件安装到本机,注意如果要处理中文,必须安装pagefind_extended而不是默认的pagefind,否则搜索出的结果可能会很奇怪。上面npx使用的就是extended版本。

# 解压后需要把`pagefind_extended`可执行文件添加到环境变量中,以方便在Terminal中使用
# 对macOS,只需将其软链接到`/usr/local/bin`中
ln -s /Users/apqx/Documents/MyApp/pagefind_extended-v1.1.1-x86_64-apple-darwin/pagefind_extended /usr/local/bin/pagefind 
# 生成索引,由于已经安装到本机,所以省去了`npx`检测网络的步骤,执行时更快
pagefind --site _site --output-path npm/pagefind

执行后会在./npm/pagefind目录中生成以下内容:

# 索引分页
|-fragment/
|-index/
# 搜索API
|-pagefind.js
# 默认UI
|-pagefind-ui.js
|-pagefind-ui.css
# 模块化UI
|-pagefind-modular-ui.js
|-pagefind-modular-ui.css
# 其它索引项和Wasm程序文件
...

注意到里面有名为*-uiJSCSS,它们是Pagefind预置的搜索界面,简单几步就能嵌入站点:

<!-- 引入UI资源 -->
<link href="/pagefind/pagefind-ui.css" rel="stylesheet">
<script src="/pagefind/pagefind-ui.js"></script>
<div id="search"></div>
<script>
    // 创建UI插入点
    window.addEventListener('DOMContentLoaded', (event) => {
        new PagefindUI({ element: "#search", showSubResults: true });
    });
</script>

pagefind-ui大概长这样,搜索结果包含下级标题和摘要,效果很不错。而且关键样式都可以通过CSS全局变量自定义,比如背景色对应的是--pagefind-ui-background

Pagefind

至于pagefind-modular-ui则是模块化的组件UI,搜索框和搜索结果是分开的,可以自由插入到指定位置,配置更加灵活。详细信息参考这里

一般情况下,这些文件应该和博客主站托管到一起,但是对于考虑国内访问的海外站点,也可把它们单独放到大陆的服务器或OSS上,以达到更快的访问速度。

搜索

如果对预置UI不满意,可以使用Pagefind提供的JS函数接口手动调用搜索API,自行处理搜索逻辑和结果展示。

// pagefind.js文件URL,我在部署时把它放到了国内OSS上,与主站分离
const pagefindUrl: string = isDebug() ? "/npm/pagefind/pagefind.js" : "https://apqx.***.com/blog/pagefind/pagefind.js"
// 动态加载
const pagefind = await import(pagefindUrl)
// 初始化
pagefind.init()
// 执行搜索,获取结果
const pagefindResult = await this.pagefind.search(key)

配合Material Design做出的搜索Dialog:

Pagefind Material Design

注意图中TipsPagefind为实现轻量化是对单词建立索引,而它的中文分词并不完善,一些人名之类的低频词并不能被正确识别,这个时候可以尝试在单字之间加空格,它会搜索同时包含这些的结果,匹配准确度更好。

Webpack

如上所述调用搜索API很简单,只需要import对应的JS文件而已,但是在Webpack中,importJS必须是本地文件且会被直接打包而非运行时动态从指定位置加载,导致Pagefind搜索函数总是不能正确访问到云端索引…这个莫名其妙的问题困扰我很久,毕竟不是Web开发者,明明按照文档一步步做,结果却一直卡住走不通…之后不断试错,Google一长串Error翻好多页才终于找到与之相关的一篇文章。

正确做法仅仅是添加一个webpackIgnore注释,告诉Webpack不要打包import的这个pagefind.js文件,留到运行时再加载。

const pagefind = await import(/*webpackIgnore: true*/ pagefindUrl)

就是这么简单🙄。

未完待续

对于博客,Pagefind提供的基础搜索已经足够,此外它也支持定制索引范围、指定索引元数据、搜索过滤、搜索排序、搜索多个源等高阶功能,如果配置得当甚至可以替代JekyllTag标签和Pagination分页。

这也是我最近考虑要做的事,分页,用它还是Jekyll的原生功能呢。

arrow_upward