本文介绍了 Hexo Butterfly 主题下 Algolia 搜索的使用

本文首发在语雀 自动同步更新至CC 的部落格

注册账号

前往 Algolia 官网注册一个账号,新建 应用和 index image.png 数据中心建议选择新加坡或香港,当然根据你自己情况而定 image.png image.png

安装插件

1
2
npm install hexo-algolia --save
npm install hexo-algoliasearch --save

分别是 hexo-algoliahexo-algoliasearch,他们的介绍分别为

Index your hexo website content to Algolia Search.
🔎 A plugin to index posts of your Hexo blog on Algolia

也就很明显了,如果你想要全站搜索可选择前者,如果你只想搜索文章两者兼可。但前者不能将文章内容作为索引上传(其实老版本是支持的,但因为索引大小限制,在新版本取消索引了文章内容),后者目前仍可全文上传。然后就是 HEXO 配置文件中添加以下内容,下文基本以 hexo-algoliasearch 为例,因为我个人认为访客只会搜文章吧(事实上是搜索根本没人用,毕竟也根本没人访问),hexo-algolia 可查看官方文档,注意配置和命令的区别

_config.yml :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
algolia:
appId: "Z7A3XW4R2I"
apiKey: "12db1ad54372045549ef465881c17e743"
adminApiKey: "40321c7c207e7f73b63a19aa24c4761b"
chunkSize: 5000
indexName: "my-hexo-blog"
fields:
- content:strip:truncate,0,500
- excerpt:strip
- gallery
- permalink
- photos
- slug
- tags
- title

为了保险,识别到插件,还可以加入以下内容

1
2
plugins:
- hexo-algoliasearch

去主题配置文件打开 Algolia 搜索,记得关闭本地搜索,二者只能取其一!

_config.butterfly.yml :

1
2
3
4
5
6
7
8
9
# Algolia search
algolia_search:
enable: true
hits:
per_page: 3

# Local search
local_search:
enable: false

然后来看以下具体的参数配置获取方式appIdapiKeyadminApiKey可在 API Keys 页面获取,注意保管好你的 Admin Key,不要让其他人知道,不建议直接写在配置中image.png 对于 Windows 系统,如果你不想每次都进行设定变量操作,可以添加ALGOLIA_ADMIN_API_KEY到系统的环境变量中image.png 而 hexo-algolia 插件环境变量名称为 HEXO_ALGOLIA_INDEXING_KEY 注意根据对应的文档更改,当然也可以使用命令行工具

1
2
3
4
5
6
7
8
9
10
11
12
13
# Windows
## 微软的 powershell)
$env:ALGOLIA_ADMIN_API_KEY = ""

## cmd
建议不用 cmd,正经人不用 cmd

# Linux
## sh/bash
export ALGOLIA_ADMIN_API_KEY=

## fish
set -xg ALGOLIA_ADMIN_API_KEY ""

如果你和我一样使用的自动部署,例如 Github Actions,你可以在工作流中一开始或者对应的步骤添加环境变量,记得 Secrets 中也要添加哦

1
2
3
4
5
6
7
jobs:
deploy:
name: Deploy Hexo Public To Pages
runs-on: ubuntu-latest
env:
TZ: Asia/Shanghai
ALGOLIA_ADMIN_API_KEY: ${{ secrets.ALGOLIA_ADMIN_API_KEY }}

或者使用

1
2
export ALGOLIA_ADMIN_API_KEY=…
export HEXO_ALGOLIA_INDEXING_KEY=…

indexName 即你开始新建的索引名称image.png 其他内容保持默认即可,但对于fields

配置示例

这里选择不截取上传全文,并且删除 html 标签,只留下有用的搜索内容

1
2
3
4
5
6
7
8
9
fields:
- content:strip
- excerpt:strip
- gallery
- permalink
- photos
- slug
- tags
- title

但对于博客来说,没人会按照 tags photos(或者 cover)来搜索吧,所以有些内容不必要上传,并如果你和我一样有多个镜像站,在不改源码(algolia.js)的情况下,不会使用 permalink 而使用 path(改源码可以使用 slug,但没必要),并且只留下必要的内容,如下所示:

1
2
3
4
5
6
7
8
9
algolia:
appId: "947RX7HP3E"
apiKey: "9114b3fa2a3307b2cc8eec7e3ae5a8ea"
chunkSize: 5000
indexName: "ccknbc-blog"
fields:
- path
- title
- content:strip

这样有了标题,全文内容,路径即可在不同镜像站找到对应的页面,而不是跳到主站,当然你选择跳到主站无可厚非。

使用命令

1
2
3
4
5
6
hexo algolia
而在这之前还需要hexo g生成文件
所以具体使用命令就是
hexo cl && hexo g && hexo algolia
或者在未安装 HEXO CLI 的情况下使用以下命令
npm run clean && npm run build && hexo algolia -n && gulp

可选配置

是否删除之前建立好的索引重新建立索引?

1
2
3
hexo algolia -n
或者
hexo algolia --no-clear

注意查看命令行输出信息,然后去官网检查索引是否生成image.png 事实上到这里已经可以获得下图所示的搜索效果(这是冰老师博客的效果,它使用的是 hexo-algolia,毕竟有关于我界面) image.png

Algolia 配置

这里不细节讲,你可以查阅官方文档,虽然有些过时的参数,但结合 Upgrade from v2 to v3 还是勉强能用,蝴蝶已经做好了高亮标题,虽然会查询文章内容,但并不会高亮文章内容节选,我们要做的就是修改部分 js 内容,并对应的设置好 Algolia,以便按照我们要求的优先顺序展示搜索结果,而不是默认的很奇怪的排序,毕竟针对中文分词他是一个一个分不能按照英语那样,针对英文我们可以开启分词查询,驼峰查找而不是盲目的匹配整个单词,并且允许拼错字母或汉字的情况存在,这些都是一个搜索系统要考虑的问题。然后针对搜索速度,我们可以对文章内容进行切片或者属性的刻画,但 V2 所支持的功能实在太少,派的上用场的大概就是 匹配的字词内容,匹配度,匹配内容的摘录(默认 10 个字词),还有高级搜索用法的启用。 image.png

image.png image.png image.png image.png image.pngimage.pngimage.pngimage.pngimage.png

JS 修改

主题 4.0.0 以下版本

到这里还没有结束,如果你这样操作就会有一个问题,假设你的访问流量很大,有很多人用搜索功能,那么免费的 1 万次搜索额度可能不够一个月的使用,需要按下ENTER键再执行搜索而不是实时搜索,因此可以稍作修改(blog\themes\butterfly\source\js\search\algolia.js) js 的部分内容,不想动源码的可以保存到其他与主题不冲突的路径,然后更换 CDN 地址即可主要修改以下内容,然后就是排版问题改了改位置,不喜欢的可以不改,很直白就不用过多解释了,这样就可获得和本博客一样的搜索效果了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  search.addWidget(
instantsearch.widgets.searchBox({
container: '#algolia-search-input',
reset: false,
magnifier: false,
+ searchOnEnterKeyPressOnly: true,
placeholder: GLOBAL_CONFIG.algolia.languages.input_placeholder
})
)
search.addWidget(
instantsearch.widgets.hits({
container: '#algolia-hits',
templates: {
item: function (data) {
const link = data.permalink ? data.permalink : (GLOBAL_CONFIG.root + data.path)
return (
'<a href="' + link + '" class="algolia-hit-item-link"><b>' +
data._highlightResult.title.value + '</b><br>'
+ + data._snippetResult.contentStrip.value + '<br>( 匹配字词 : '
+ + data._highlightResult.contentStrip.matchedWords + ' ) | ( 匹配等级 : '
+ + data._highlightResult.contentStrip.matchLevel + ' )</a>'
)
},

主题 4.0.0 以上版本

已经升级到 V4 版本,那么一些特性就可以使用了,修改内容其实差不多,只是建议对于第 87 行的页数限制,主要是为了手机上排版美观,不会转到下一行,但是这样会有一个问题,如果结果超过 5 页,那么将无法显示,最后一页代表第 5 页,所以我个人会选择删掉这个参数限制,同时合并删除了部分代码,以及使用 widget 的 powerby 组件而不是官方的 svg 代码解决方案。另外因为新版官方的每次访问网站都会有一次全局请求,这在消耗免费额度的同时,也影响网站加载的速度,所以修改默认行为为按下回车后再请求同样的部分参数发生了改变(L55-59),可以自行比对或查看官方文档,也可以直接引用我的

特别提一嘴 官方切片方式对我来说 140 个字太长了,所以如果可以接受就用官方的,不用做其他更改,但想要和我的显示方式一样,就注意配置好切片,默认是 10 个词符,毕竟有现成的切片高亮处理可以用,就没必要再来一次了

image.png 另外,新版浏览器支持搜索并定位高亮处理,所以对跳转链接也做了处理,算是弥补了一点不能精准定位的缺陷,比如点击如下链接它会跳转到搜索结果对应的位置,至于前后匹配多少字符你们自行修改,但也是处于不太好用的状态,毕竟是分片还是有点奇怪,对英文来说应该是单词识别,只是中文恰巧是另一标准,单字变成了词

#:~:text=勿滥用-,表情,-符号和

image.png

1
2
3
CDN:
# search
algolia_js: https://cdn.jsdelivr.net/gh/CCKNBC/ccknbc.github.io/js/search/algolia.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
window.addEventListener("load", () => {
const openSearch = () => {
const bodyStyle = document.body.style;
bodyStyle.width = "100%";
bodyStyle.overflow = "hidden";
btf.animateIn(document.getElementById("search-mask"), "to_show 0.5s");
btf.animateIn(
document.querySelector("#algolia-search .search-dialog"),
"titleScale 0.5s"
);
setTimeout(() => {
document.querySelector("#algolia-search .ais-SearchBox-input").focus();
}, 100);

// shortcut: ESC
document.addEventListener("keydown", function f(event) {
if (event.code === "Escape") {
closeSearch();
document.removeEventListener("keydown", f);
}
});
};

const closeSearch = () => {
const bodyStyle = document.body.style;
bodyStyle.width = "";
bodyStyle.overflow = "";
btf.animateOut(
document.querySelector("#algolia-search .search-dialog"),
"search_close .5s"
);
btf.animateOut(document.getElementById("search-mask"), "to_hide 0.5s");
};

const searchClickFn = () => {
document
.querySelector("#search-button > .search")
.addEventListener("click", openSearch);
};

const searchClickFnOnce = () => {
document
.getElementById("search-mask")
.addEventListener("click", closeSearch);
document
.querySelector("#algolia-search .search-close-button")
.addEventListener("click", closeSearch);
};

const algolia = GLOBAL_CONFIG.algolia;
const isAlgoliaValid = algolia.appId && algolia.apiKey && algolia.indexName;
if (!isAlgoliaValid) {
return console.error("Algolia setting is invalid!");
}

const search = instantsearch({
indexName: algolia.indexName,
searchClient: algoliasearch(algolia.appId, algolia.apiKey),
searchFunction(helper) {
helper.state.query && helper.search();
},
});

const configure = instantsearch.widgets.configure({
hitsPerPage: algolia.per_page || 5,
});

const searchBox = instantsearch.widgets.searchBox({
container: "#algolia-search-input",
showReset: false,
showSubmit: false,
searchAsYouType: false,
placeholder: GLOBAL_CONFIG.algolia.languages.input_placeholder,
showLoadingIndicator: true,
});

const hits = instantsearch.widgets.hits({
container: "#algolia-hits",
templates: {
item(data) {
const link = data.permalink
? data.permalink
: GLOBAL_CONFIG.root + data.path;
const content = data._snippetResult.contentStrip.value;
return `
<a href="${link}#:~:text=${content.substring(
content.indexOf("<mark>") - 3,
content.indexOf("<mark>")
)}-,${content.substring(
content.indexOf("<mark>") + 6,
content.indexOf("</mark>")
)},-${content.substring(
content.indexOf("</mark>") + 7,
content.indexOf("</mark>") + 10
)}" class="algolia-hit-item-link">
<b>${data._highlightResult.title.value || "no-title"}</b>
<br>${content}</br>
匹配字词: <em><mark>${
data._highlightResult.contentStrip.matchedWords
}</mark></em> | 匹配等级: <em><mark>${
data._highlightResult.contentStrip.matchLevel
}</mark></em>
</a>`;
},
empty: function (data) {
return (
'<div id="algolia-hits-empty">' +
GLOBAL_CONFIG.algolia.languages.hits_empty.replace(
/\$\{query}/,
data.query
) +
"</div>"
);
},
},
});

const stats = instantsearch.widgets.stats({
container: "#algolia-info > .algolia-stats",
templates: {
text: function (data) {
const stats = GLOBAL_CONFIG.algolia.languages.hits_stats
.replace(/\$\{hits}/, data.nbHits)
.replace(/\$\{time}/, data.processingTimeMS);
return `<hr>${stats}`;
},
},
});

const powerBy = instantsearch.widgets.poweredBy({
container: "#algolia-info > .algolia-poweredBy",
});

const pagination = instantsearch.widgets.pagination({
container: "#algolia-pagination",
totalPages: algolia.totalPages,
templates: {
first: '<i class="fa-solid fa-angle-double-left" title="第一页"></i>',
last: '<i class="fa-solid fa-angle-double-right" title="最后一页"></i>',
previous: '<i class="fa-solid fa-angle-left" title="上一页"></i>',
next: '<i class="fa-solid fa-angle-right" title="下一页"></i>',
},
});

search.addWidgets([configure, searchBox, hits, stats, powerBy, pagination]); // add the widgets to the instantsearch instance

search.start();

searchClickFn();
searchClickFnOnce();

window.addEventListener("pjax:complete", () => {
getComputedStyle(document.querySelector("#algolia-search .search-dialog"))
.display === "block" && closeSearch();
searchClickFn();
});

window.pjax &&
search.on("render", () => {
window.pjax.refresh(document.getElementById("algolia-hits"));
});
});

效果预览

image.png (这个是因为我修改了源码,实际上也能通过修改 JS 实现,但大多数人不会关心这些搜索小贴士)image.png 对于中文它当作单字匹配image.png 允许拼写错误 image.png

特别说明

因两个月前已申请通过,本博客已切换至不限搜索次数的DocSearch!同时也加入了开源计划,但因为 10DSN 太香了,虽然 instantsearch 可玩性更好,但我也只申请了 200k/月的额度(虽然可以增加),所以为了即时搜索我还是选择了白嫖,而且设定为每天自动爬取的话,省去了生成索引上传的这一步骤,节省了自动部署的时间。而且爬取到数据后,前端我并非一定要使用 docsearch 方案,用 instantsearch 配合其他插件也不是不可以。 image.png