使用 fusejs 给 hugo 博客增加搜索功能
搜索方案概述
搜索方案大体上分为前端搜索和后端搜索。
后端搜索通常使用 elastic search,这是一个很成熟的解决方案。另外,使用 rust 编写的 meili search 也是一个很好的选择。最后,不同的语言通常会有一些小众化的解决方案,例如 php 中的 tnt search,以及 迅搜。
后端搜索还可以使用第三方的服务,algolia 就是一个不错的选择,有免费额度,个人用户可以考虑。再者就是使用各大云厂商提供的搜索服务。
前端搜索也可以叫做 browser search 或者 offline search,总的来说就是搜索的时候不需要发请求到服务器。关于前端搜索,我所了解的,有这么几个可供选择:
- lunrjs - js 写的一个搜索库
- elasticlunr - 基于 lunrjs 开发的另一个方案
- fusejs - js 写的一个搜索库,官方口号是 Fuse.js is a powerful, lightweight fuzzy-search library, with zero dependencies.
这个网站 还有列举了另外几个不是很流行的库。
总体上说,前端方案是轻量级的,而后端方案成熟、大而全。下面介绍 fusejs 方案。
基本用法
结合 hugo 使用
官方罗列了好几个 解决方案,其中 这个 使用的是 fusejs。
大体的流程如下:
- hugo 生成 json 数据。
- 搜索的时候,用 js 发请求,拿到数据。
- 实例化 fuse,搜索。
- 得到搜索结果后,把结果展示在页面上。
下面以官方给的代码为例,阐述实现步骤。
hugo 生成 json 数据
hugo 支持自定义输出类型,参考: https://gohugo.io/templates/output-formats#output-format-definitions 。
需要在 Config.toml 中增加 json 输出:
定义 json 格式
layout/_default/index.json
做好上面两步之后,可以通过 http(s)://youdomain.com/index.json
拿到数据了。执行 hugo
构建站点,在 public 文件夹中也可以看到 index.json 文件。
引入 fuse.js
通常来说,主题文件里有一个 themes/xxx/layouts/_default/baseof.html
的文件,把 <script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
放到这个 html 的 header 中。
搜索框
同样找到 themes/xxx/layouts/_default/baseof.html
文件,在 body 中放入:
搜索框样式
新建文件 static/css/search.css
,填入以下内容:
记得在
themes/xxx/layouts/_default/baseof.html
中引入:<link rel="stylesheet" href='{{ "css/search.css" | relURL }}'>
。
执行搜索的 js 代码
新建文件 static/js/search.js
,填入以下内容:
记得在
themes/xxx/layouts/_default/baseof.html
中引入:<script src='{{ "js/search.js" | relURL }}'></script>
。
官方的这个例子可以使用快捷键打开搜索框,在 MacOS 上按 Command + / 或者在 Windows 上按 win + / 即可开启搜索。
官方给的例子,只使用了 fusejs 最基本的功能。本站使用了一些 fusejs 的进阶的功能,下面记录一些我的研究成果。
fusejs 的 option
参考: https://fusejs.io/api/options.html
在初始化 fusejs 对象的时候,可以传入一个 option,调整 fusejs 的行为。我所使用到的,有这么几个:
threshold
如果设定为 0.0
,则会精确的匹配关键字中的每个字符是否相同以及位置是否相同。
如果设定为 1.0
,则会把任何内容都搜索出来。
我的实际经验:
- 设定为
0.1
,输入搜索python
进行搜索,匹配到的内容基本都是包含python
这个单词的。 - 设定为
0.2
,输入错误的单词pythno
,包含python
这个单词的内容也能被匹配到,其原因是threshold
越大,fusejs 对于字符的位置的限制也变得越“宽松”。
isCaseSensitive
是否希望大小写敏感,默认是不敏感,大多数情况,保持默认即可。
includeScore
是否希望搜索结果中包含分数,默认是不包含的。如果开启,搜索结果中就会包含一个分数。
需要注意的是,分数越高,意味着结果越不匹配;分数越低,意味着结果越匹配。分数低于 threshold
的结果将被丢弃。
可以通过分数过滤结果集。例如可以把分数小于 2 的,标注为精确匹配项,分数大于等于 2 的标注为非精确匹配项。
includeMatches
是否希望搜索结果中包含 Matches 项,开启之后,fusejs 会把原始内容返回,可以利用原始内容做高亮显示。
minMatchCharLength
默认值是 1,意味着只输入一个字符,fusejs 就开始工作。如果设定为 2,当输入一个字符时,fusejs 会忽略。同理,如果设定为 3,当输入一个字符或者两个字符时,fusejs 会忽略。
shouldSort
默认是开启的,fusejs 会把结果集按照分数排列,分数最低(最匹配)的排在最前面。
findAllMatches
这一项类似于正则匹配中的贪婪与非贪婪,默认是关闭的,开启之后,fusejs 会在搜索到结果之后,继续往字符串后面进行搜索。暂时不知道有啥用。
keys
定义需要被搜索的内容,支持数组和对象,可以在官方提供的 playground 做实验。
举个例子,被搜索的内容是一个字符串数组,就可以使用数组的方式,此时不需要设置 keys 的值。代码如下:
如果被搜索的内容是文章,需要搜索标题和正文,就可以使用对象的方式,此时需要设置 keys 的值。代码如下:
如果要设定权重,keys
可以这样设置:
在设定 keys
的时候,还支持 嵌套搜索,但是我没有深入研究,也没有做更多的测试。
接下来的选项和模糊搜索有关,分别是 location
、threshold
、distance
、ignoreLocation
,这三个需要搭配使用。
location、threshold、distance
参考: https://www.fusejs.io/concepts/scoring-theory.html
只有在 ignoreLocation
被设置为 false
的时候,才需要考虑这三个参数。
这三个参数共同决定了可搜索的字符串范围,下面通过几个实验进一步了解。
对照实验一
结论:
- 第一个实验中,threshold • distance = 2,意味着可以搜索
朝辞白
- 第二个实验中,threshold • distance = 4,意味着可以搜索
朝辞白帝彩
,比第一个实验多了 2 个字符
对照实验二
结论:
- 两个实验的 location 都是 3,说明是以
帝
字为中心点进行计算的 - 第一个实验,threshold • distance = 1,意味着可以搜索
白帝彩
- 第二个实验,threshold • distance = 2,意味着可以搜索
辞白帝彩云
通过以上两组对照实验,可以看出 location 可比喻为“中心点”,而 threshold • distance 则是基于“中心点”向两侧延伸的“长度”。
ignoreLocation
当开启这个参数之后,location、distance 这两个参数则不再起作用。fusejs 将会搜索整个字符串。
值得注意的是 threshold 依然有效,threshold 的作用之一就是参与了可搜索的字符串范围的计算,之二则是用于过滤搜索结果集(和前面提到的 includeScore 有关),例如把 threshold 设置为 0.5,则 score 大于 0.5 的搜索结果会被丢弃。
除了上面讨论的这些参数,fusejs 还支持更高级的参数,useExtendedSearch
、getFn
、sortFn
、ignoreFieldNorm
,这几个参数先略过吧。等啥时候回来补充。 todo 补充内容