一直想给自己的博客增加一个全文搜索, 今天花了一个下午使用本地的搜索引擎实现了全文搜索;
为你的博客添加站内搜索吧
前言
对于一些使用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
;
安装:
npm install --save hexo-generator-search
安装hexo-generator-search
之后, 在使用hexo g
命令时会按照配置生成一个 XML 文件,用于保存全站的文档数据。但是这个 XML 文件只是简单地做了数据的结构化存储,而完全没有考虑分词、倒排等问题!
在站点配置文件 _config.yml 当中写入如下配置,即可为全站的文档生成索引文件:
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
见官方文档:
配置完成后在使用hexo g
时就会在生成的静态资源中创建search.xml
了!
注: search.xml文件大小
生成的
search.xml
还是不小的,比如我博客中有200多篇文章,生成的文件有3M多!而
search.xml
会在首次请求时以异步的方式由服务器传送给用户!所以search.xml
文件的大小决定了整个博客加载的时长!这也是使用本地搜索引擎的弊端:完整的
search.xml
需要被传输
检索算法
在github上已经有检索算法的实现了, 直接拿来用就好了:
search.js
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模块:
widgets:
- social
- search
- tagcloud
- category
- archive
- recent_posts
注意模块的摆放顺序
同时还需要修改国际化配置文件在languages/zh-CN
中加入:
archive: 文章归档
recent_posts: 最新文章
categories: 文章分类
tagcloud: 标签云
social: 社交按钮
search: 站内搜索
此后search就会被解析为站内搜索
创建ejs模块
在配置文件中加入search之后, 在动态创建页面时会在layout
目录下寻找search.ejs
文件, 所以在此目录下创建此文件:
search.ejs
<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脚本:
<head>
......
<!-- 本地搜索js -->
<script type="text/javascript" src="/js/search.js"></script>
......
</head>
美化
我加入了少量的css, 主要是优化搜索条样式和搜索结果的排列等:
style.styl
// 搜索
.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
<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张小凯
内容和博客同步更新~