文章524
标签277
分类67

为你的博客添加站内搜索吧

一直想给自己的博客增加一个全文搜索, 今天花了一个下午使用本地的搜索引擎实现了全文搜索;


为你的博客添加站内搜索吧

前言

对于一些使用Hexo的NexT主题的人来说, 其主题默认提供了两个站内搜索解决方案:swiftype 和 tinysou; 尤其是Swiftype, 网上有大量的教程这里不再介绍;

有人不想使用swiftype 和 tinysou方法也可能是基于某些其他原因:

  • 不愿意受限于第三方服务:第三方服务随时可能完蛋,风险大受限多。原本建独立博客目的就是为了不受限,如此这般, swiftype 就是如此原因;
  • 第三方服务样式单一;
  • 第三方服务,需要应用第三方JS,存在安全问题;

而Hexo还给出了其他的解决方案: hexo-generator-search;


基本思路

在博客中添加搜索功能主要有以下几步:

  • Step1: 生成索引文件;
  • Step2: 有效的算法:根据用户输入的检索词,返回包含检索词的文章列表;
  • Step3 :使用 JavaScript 和 jQuery 来实现在浏览器中执行算法;
  • Step4 :在页面的合适位置展示搜索框和输出结果;
  • Step5 :美化。

下面来逐步解决上面的问题;


生成索引文件

对于swiftype这类第三方服务是通过url域名爬虫后生成索引; 而对于我们本地处理则需要自己生成索引文件; 而Hexo提供生成器: hexo-generator-search;

安装:

1
npm install --save hexo-generator-search

安装hexo-generator-search之后, 在使用hexo g命令时会按照配置生成一个 XML 文件,用于保存全站的文档数据。但是这个 XML 文件只是简单地做了数据的结构化存储,而完全没有考虑分词、倒排等问题!

在站点配置文件 _config.yml 当中写入如下配置,即可为全站的文档生成索引文件:

1
2
3
search:
path: search.xml
field: post

配置说明:

  • path,生成的路径,上述配置后可以通过 /search.xml 访问到文件
  • field,用来配置全局检索的区间,可以是 post/page/all

hexo-generator-search 会为全站所有 post 类型的页面生成结构化的数据,并保存在本站的 /search.xml 当中

若想让 page 类型的页面也纳入索引,则可以将 field 的值修改为 all


更多关于hexo-generator-search见官方文档:

https://www.npmjs.com/package/hexo-generator-search

配置完成后在使用hexo g时就会在生成的静态资源中创建search.xml了!


注: search.xml文件大小

生成的search.xml还是不小的,比如我博客中有200多篇文章,生成的文件有3M多!

而search.xml会在首次请求时以异步的方式由服务器传送给用户!所以search.xml文件的大小决定了整个博客加载的时长!

这也是使用本地搜索引擎的弊端:完整的search.xml需要被传输


检索算法

在github上已经有检索算法的实现了, 直接拿来用就好了:

search.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
var searchFunc = function (path, search_id, content_id) {
// 0x00. environment initialization
'use strict';
var BTN = "<i id='local-search-close'>×</i>";
var $input = document.getElementById(search_id);
var $resultContent = document.getElementById(content_id);
$resultContent.innerHTML = BTN + "<ul><span class='local-search-empty'>首次搜索,正在载入索引文件,请稍后……<span></ul>";
$.ajax({
// 0x01. load xml file
url: path,
dataType: "xml",
success: function (xmlResponse) {
// 0x02. parse xml file
var datas = $("entry", xmlResponse).map(function () {
return {
title: $("title", this).text(),
content: $("content", this).text(),
url: $("url", this).text()
};
}).get();
$resultContent.innerHTML = "";

$input.addEventListener('input', function () {
// 0x03. parse query to keywords list
var str = '<ul class=\"search-result-list\">';
var keywords = this.value.trim().toLowerCase().split(/[\s\-]+/);
$resultContent.innerHTML = "";
if (this.value.trim().length <= 0) {
return;
}
// 0x04. perform local searching
datas.forEach(function (data) {
var isMatch = true;
var content_index = [];
if (!data.title || data.title.trim() === '') {
data.title = "Untitled";
}
var orig_data_title = data.title.trim();
var data_title = orig_data_title.toLowerCase();
var orig_data_content = data.content.trim().replace(/<[^>]+>/g, "");
var data_content = orig_data_content.toLowerCase();
var data_url = data.url;
var index_title = -1;
var index_content = -1;
var first_occur = -1;
// only match artiles with not empty contents
if (data_content !== '') {
keywords.forEach(function (keyword, i) {
index_title = data_title.indexOf(keyword);
index_content = data_content.indexOf(keyword);

if (index_title < 0 && index_content < 0) {
isMatch = false;
} else {
if (index_content < 0) {
index_content = 0;
}
if (i == 0) {
first_occur = index_content;
}
// content_index.push({index_content:index_content, keyword_len:keyword_len});
}
});
} else {
isMatch = false;
}
// 0x05. show search results
if (isMatch) {
str += "<li><a href='" + data_url + "' class='search-result-title' target='_blank'>" + orig_data_title + "</a>";
var content = orig_data_content;
if (first_occur >= 0) {
// cut out 100 characters
var start = first_occur - 20;
var end = first_occur + 80;

if (start < 0) {
start = 0;
}

if (start == 0) {
end = 100;
}

if (end > content.length) {
end = content.length;
}

var match_content = content.substr(start, end);

// highlight all keywords
keywords.forEach(function (keyword) {
var regS = new RegExp(keyword, "gi");
match_content = match_content.replace(regS, "<em class=\"search-keyword\">" + keyword + "</em>");
});

str += "<p class=\"search-result\">" + match_content + "...</p>"
}
str += "</li>";
}
});
str += "</ul>";
if (str.indexOf('<li>') === -1) {
return $resultContent.innerHTML = BTN + "<ul><span class='local-search-empty'>没有找到内容,请尝试更换检索词。<span></ul>";
}
$resultContent.innerHTML = BTN + str;
});
}
});
$(document).on('click', '#local-search-close', function() {
$('#local-search-input').val('');
$('#local-search-result').html('');
});
}

var getSearchFile = function(){
var path = "/search.xml";
searchFunc(path, 'local-search-input', 'local-search-result');
}

代码地址:

https://github.com/Liam0205/hexo-search-plugin-snippets/blob/master/snippets/search.js

经过本人测试, 效率还是相当高的;

上述检索算法的主要处理流程是:

  • 载入 search.xml;
  • 解析 search.xml;
  • 解析检索词;
  • 在索引中进行字符串匹配;
  • 展现结果;

注: search.js 依赖 jQuery

因此,你需要在引入 search.js 之前,引入 jQuery 的脚本。

比如,你可以在 head 部分这样做:<script src="//cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>

经过以上两步, 就完成了索引的创建和检索算法的实现;

下面就要在博客中引入search.js并创建form表单;


创建搜索框和输出结果

我选择的是在左侧drawer中加入搜索框,而这个搜索框是在主题的配置文件_config.yml被解析后在_widget/目录下寻找对应的ejs模块动态生成的, 所以:

修改配置文件

首先配置_config.yml加入search模块:

1
2
3
4
5
6
7
widgets:
- social
- search
- tagcloud
- category
- archive
- recent_posts

注意模块的摆放顺序

同时还需要修改国际化配置文件在languages/zh-CN中加入:

1
2
3
4
5
6
archive: 文章归档
recent_posts: 最新文章
categories: 文章分类
tagcloud: 标签云
social: 社交按钮
search: 站内搜索

此后search就会被解析为站内搜索


创建ejs模块

在配置文件中加入search之后, 在动态创建页面时会在layout目录下寻找search.ejs文件, 所以在此目录下创建此文件:

search.ejs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div class="nexmoe-widget-wrap">
<h3 class="nexmoe-widget-title"><%= __('search') %></h3>
<div class="nexmoe-widget nexmoe-search">
<form class="site-search-form">
<input type="text" id="local-search-input" class="st-search-input" />
</form>
<div id="local-search-result" class="local-search-result-cls">
</div>
</div>
</div>

<!-- 执行本地搜索脚本 -->
<script type="text/javascript">
var path = "/search.xml";
searchFunc(path, 'local-search-input', 'local-search-result');
</script>

上述的div创建了一个form表单, 在表单中创建了一个输入框;

而下面的div用于显示结果;

这样在页面的drawer中就加入了站内搜索模块;


注: 调用search.js中的函数

现在,已经有了索引文件、搜索算法和搜索框。

但是,现在而言,搜索框是孤立的:用户在搜索框中输入内容,并不会触发搜索算法。换而言之,搜索函数并没有调用。

因此,我们需要调用定义在 search.js 当中定义的 searchFunc。


引入js脚本

在layout.ejs中加入search.js脚本:

1
2
3
4
5
6
<head>
......
<!-- 本地搜索js -->
<script type="text/javascript" src="/cn/js/search.js"></script>
......
</head>

美化

我加入了少量的css, 主要是优化搜索条样式和搜索结果的排列等:

style.styl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 搜索
.nexmoe-search {
padding: 5px;
a {
width: 36px;
height: 36px;
line-height: 36px;
margin: 5px;
border-radius: 100%;
display: inline-block;
text-align: center;
color: #34bfff !important;
}
}

search.ejs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<style type="text/css">
/* 搜索条 */
.site-search-form {
text-align: center;
}
#local-search-input {
width: 100%;
}
/* 搜索结果关闭 */
#local-search-close {
font-size: 22.5px;
cursor: pointer;
color: #ff4e6a;
}
.search-result-list > li {
line-height: 0.75;
margin-top: -5px;
}
</style>

样式可以根据自己需要配置;


总结

使用本地搜索引擎虽然避免了第三方的不稳定性, 把所有工作都自己完成了; 但是并不是完美的解决方案:

本地搜索引擎需要传输一个比较大的search.xml文件, 此时需要消耗一定的带宽(尽管这个过程是异步完成的)

为了给访问者比较好的体验, 可以加入CDN对网站加速, 毕竟Github Pages是部署在美国的!

除此之外, 可以通过添加网页载入的加载条给用户比较好的体验, 如使用NProgress加载进度条等;

最后如果大家有什么疑问可以在下方评论留言, have fun~❤


附录

文章参考:

如果觉得文章写的不错, 可以关注微信公众号: Coder张小凯

内容和博客同步更新~



本文作者:Jasonkay
本文链接:https://jasonkayzk.github.io/cn/2020/04/21/%E4%B8%BA%E4%BD%A0%E7%9A%84%E5%8D%9A%E5%AE%A2%E6%B7%BB%E5%8A%A0%E7%AB%99%E5%86%85%E6%90%9C%E7%B4%A2%E5%90%A7/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可