mkdocs如何支持中文搜索

mkdocs是一个很方便的文档网站生成器,文档使用Markdown格式来编写,luat的wiki就是用mkdocs生成的。
mkdocs自带搜索功能,可惜的是不支持中文搜索,同事参考这个方案https://my.oschina.net/u/3465063/blog/895142 实现了中文搜索的功能,但是大家用了发现搜索很慢(搜索时间在8秒左右)。

下面记录一下解决这个问题的过程:

分析问题

1.用chrome的开发者工具分析了一下,发现大部分时间都在执行加载文档(search.js: index.add(doc))的接口上,粗看了一下这部分js代码没看出什么异常,只好先了解mkdocs如何实现搜索的

2.分析mkdocs如何实现全文搜索的:

  • 在mkdocs build构建站点时,生成全部文档的JSON格式的数据(search_index.json),再使用lunr.js提供文本搜索功能,在生成的站点目录下会有如下数据:
    site/mkdocs/search_index.json: 全部文档的文本JSON数据
    site/mkdocs/js: lunr.js实现搜索功能需要的js脚本,这些js脚本是由mkdocs工具自带的
  • lunr.js介绍:Lunr.js是一个用于浏览器的小型全文搜索库。 它对JSON文档进行索引,并提供一个简单的搜索接口,用于检索最符合文本查询的文档

3.分析lunr的搜索的实现方式,可以简单总结为下面两个步骤:

  • 分词:在调用lunr的add接口添加文档数据时会在lunr内部由tokenizer接口对文档实现分词生成词库,举例说明一下什么是分词:
    比如有句话:我来到北京清华大学
    分词以后可以得到这些词: 我,来到,北京,清华,华大,大学
  • 匹配:将要搜索的文本与每个文档的分词库匹配,如果匹配成功,就将该文档输出到搜索结果中

4.此时再结合第一步的情况,可以发现之前的中文搜索方案分词速度很慢,所以执行时间很长

解决方案

经过上面的分析以后,现在知道解决这个问题的关键就是如何提高分词的效率,也可以说是如何快速得到文档的词库,那么有几个方案:

  1. 优化js代码,提高分词效率
  2. 直接在构建文档站点时候生成词库,浏览器直接加载词库,不再进行浏览器执行js分词

由于对分词算法不了解、js水平一般般,所以选择了第2个方案,那么就要找一个合适的分词库来完成中文分词,最后选择了结巴分词

方案确定以后,现在开始改造mkdocs,如果之前对mkdocs做过修改,请重新安装mkdocs将mkdocs恢复成原始文件:

  1. python安装jieba库
pip install jieba
  1. 修改search.py(lib\site-packages\mkdocs): 在generate_search_index生成index数据时调用jieba分词生成索引,jieba.cut接口第二个参数为true为全模式分词,没有采用搜索模式分词是觉得那个切词切的太碎了,感觉没有必要
    def generate_search_index(self):
        """python to json conversion"""
        page_dicts = {
            'docs': self._entries,
        }
        for doc in page_dicts['docs']:
            # 调用jieba的cut接口生成分词库,过滤重复词,过滤空格
            tokens = list(set([token.lower() for token in jieba.cut_for_search(doc['title'].replace('\n', ''), True)]))
            if '' in tokens:
                tokens.remove('')
            doc['title_tokens'] = tokens

            tokens = list(set([token.lower() for token in jieba.cut_for_search(doc['text'].replace('\n', ''), True)]))
            if '' in tokens:
                tokens.remove('')
            doc['text_tokens'] = tokens
  1. 修改lunr.js(lib\site-packages\mkdocs\assets\search\mkdocs\js\lunr.min.js, 压缩过的lunr.js文件 lunr.min.js)有两个地方要修改:
  • lunr.Index.prototype.add: 获取分词数据的方式,不在从内部的分词接口计算分词,直接从文档的词库加载
  • lunr.trimmer: 过滤空白字符的接口,修改匹配方式,把原来只匹配字母、数字改成匹配所有非空字符
lunr.Index.prototype.add = function (doc, emitEvent) {
  var docTokens = {},
      allDocumentTokens = new lunr.SortedSet,
      docRef = doc[this._ref],
      emitEvent = emitEvent === undefined ? true : emitEvent

  this._fields.forEach(function (field) {
    // 删掉内部接口计算分词
    // var fieldTokens = this.pipeline.run(this.tokenizerFn(doc[field.name]))
    // 直接从文档词库加载
    var fieldTokens = doc[field.name + '_tokens']

    docTokens[field.name] = fieldTokens

    for (var i = 0; i < fieldTokens.length; i++) {
      var token = fieldTokens[i]
      allDocumentTokens.add(token)
      this.corpusTokens.add(token)
    }
  }, this)
lunr.trimmer = function (token) {
  var result = token.replace(/^\s+/, '')
                    .replace(/\s+$/, '')  //  \W -> \s
  return result === '' ? undefined : result
}

现在重新构建发布站点(mkdocs build),可以发现搜索速度大大加快了。

注意:修改后的search_index.json会比之前大不少,我们从之前的300多K,变成了900多K,如果采用nginx作为http服务器的话,可以开启gzip压缩提高加载速度,gzip的压缩配置如下:

gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_comp_level 2;
gzip_types text/plain application/x-javascript text/css application/xml text/javascript application/x-httpd-php application/json;
gzip_vary off;
gzip_disable "MSIE [1-6]\.";

推荐阅读更多精彩内容