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

立泉

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

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

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

索引

生成索引需要先安装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