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

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

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

这种模式只是从浏览器访问云端静态资源,搜索算法在本地执行,并不需要通常意义上的“搜索服务器”,而且它会尽量降低检索时传输的数据量来节省带宽。官方描述是10000个页面的全文搜索包括下载Wasm程序在内传输的数据量不超过400KB,大部分网站会在100KB左右。

索引

生成索引需要先安装Pagefind,官方提供npm包,可以通过npx直接使用:

# 扫描 --site 指定的目录,将索引输出到 --output-path 指定的目录下
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