MENU

使用分词增强 Typecho 的搜索功能

July 30, 2021 • Read: 3640 • 杂谈阅读设置

本博客是使用 Typecho 搭建的,侧边提供了搜索功能,然而 Typecho 内置的搜索功能仅仅只是基于字符串的全匹配查找,功能非常鸡肋,很多合理的查询都没法得到结果,比如 “Transformer 的文章”、“BERT 的相关内容” 都没有查询结果,因为文章中都不包含这些字符串

之前看到过苏剑林大佬的文章增强 typecho 的搜索功能,一开始觉得太麻烦,于是并没有考虑采用它的方法,转而在网上找一些增强 Typecho 搜索功能的插件。兜兜转转找了很多,发现效果都不是太好,最后还是决定考虑采用苏剑林大佬的方法

首先 Typecho 的搜索功能是在 var/Widget/Archive.php 中实现的,具体代码大概在 1184~1191 行(注意,我的 Typecho 版本是 1.1)

  • if (!$hasPushed) {
  • $searchQuery = '%' . str_replace(' ', '%', $keywords) . '%';
  • /**搜索无法进入隐私项保护归档 */
  • $select->where('table.contents.password IS NULL')
  • ->where('table.contents.title LIKE ? OR table.contents.text LIKE ?', $searchQuery, $searchQuery)
  • ->where('table.contents.type = ?', 'post');
  • }

从这个代码可以看出,搜索框内的字符会给到变量 keywords,并且空格会被替换为通配符,关键词检索的范围包括 title(标题)和 text(正文)。那么很自然的一个想法是,首先通过分词工具对查询语句进行分词,然后对所有的文章进行一个排序,排序的规则是:文章的标题每包含一个词,加 2 分;文章的正文每包含一个词,加 1 分,最后算总分然后排序输出即可

为了实现上述目的,我们需要一个接口,输入句子,输出分词后的结果。说到分词,自然会想到 python 的很多分词库,但实际上 php 也有,不过我对 php 并不熟悉所以就不考虑了。分词很容易解决,但是如何将分词后的结果输出到网页上,或者说利用 python 写一个 http 接口,这其实是比较麻烦的,如果写的复杂就用 flask,简单一点用 bottle 这个轻量级的库写 http 接口即可(下面的代码在 python2 环境下测试通过,python3 应该也没问题。运行前先分别安装 jiebabottlegunicorn 三个库)

  • #! -*- coding:utf-8 -*-
  • import bottle
  • import jieba
  • import jieba.analyse
  • jieba.initialize()
  • def convert(s):
  • ws = jieba.analyse.extract_tags(sentence=s, topK=5)
  • search = []
  • for i in ws:
  • search.append('2*SIGN(INSTR(table.contents.title, "%s"))'%i)
  • search.append('SIGN(INSTR(table.contents.text, "%s"))'%i)
  • return '(%s)'%(' + '.join(search))
  • @bottle.route('/token', method='GET')
  • def token_home():
  • text = bottle.request.GET.get('text')
  • if not text:
  • text = ''
  • return convert(text)
  • if __name__ == '__main__':
  • bottle.run(host='0.0.0.0', port=7778, server='gunicorn')

程序写好之后只需要在 linux 服务器中用 python xxx.py 运行即可,为了测试功能是否正常,可以通过访问 http://域名:7778/token?text= 进行测试

接下来是对 Typecho 源码的修改,具体来说是对 Archive.php 文件的修改,建议大家先保存一份副本。首先是将 $keywords = $this->request->filter('url', 'search')->keywords; 替换为 $keywords = $this->request->keywords;,并将最前面提到的代码改为

  • if (!$hasPushed) {
  • $url = 'http://127.0.0.1:7778/token?text=' . $keywords;
  • $url = str_replace(' ', '%20', $url);
  • $searchQuery = file_get_contents($url);
  • /**当接口失效时使用简单全匹配 */
  • if (!$searchQuery) {
  • $searchQuery = 'SIGN(INSTR(table.contents.title, "' . $keywords . '"))';
  • $searchQuery = $searchQuery . ' + SIGN(INSTR(table.contents.text, "' . $keywords . '"))';
  • }
  • /**搜索无法进入隐私项保护归档 */
  • $select->where('table.contents.password IS NULL')
  • ->where($searchQuery . ' > 0')
  • ->where('table.contents.type = ?', 'post')
  • ->order($searchQuery, Typecho_Db::SORT_DESC);
  • }

还有要修改的是:因为我们修改的部分 order($searchQuery, Typecho_Db::SORT_DESC) 按分数进行降序排序,然而这并不会直接生效,因为 Typecho 中默认全部按时间降序排列,因此我们还需要修改同一个文件的第 1390~1391 行,将原来的

  • $select->order('table.contents.created', Typecho_Db::SORT_DESC)
  • ->page($this->_currentPage, $this->parameter->pageSize);

改为

  • if (strpos($select, 'INSTR') === false) {
  • $select->page($this->_currentPage, $this->parameter->pageSize)
  • ->order('table.contents.created', Typecho_Db::SORT_DESC);
  • } else {
  • $select->page($this->_currentPage, $this->parameter->pageSize);
  • }

修改的这部分大概意思是先判断一下是不是搜索语句,如果是的话,就不按照时间排列;如果不是的话,就按照时间排列。直接去掉按时间排列是不行的,因为 这一句也包含了首页的输出,而首页的输出必须按照时间排序

PS:大家不要想着通过调用我的接口来偷懒,因为我并没有对外开放 7778 端口

Reference

Last Modified: September 3, 2021
Archives Tip
QR Code for this page
Tipping QR Code