文章482
标签257
分类63

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

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


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

前言

对于一些使用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见官方文档:

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

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张小凯

内容和博客同步更新~



本文作者:Jasonkay
本文链接:https://jasonkayzk.github.io/2020/04/21/为你的博客添加站内搜索吧/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可