增强typecho的搜索功能
By 苏剑林 | 2018-01-09 | 26747位读者 |科学空间是使用typecho程序搭建的博客,侧边栏提供了搜索功能,然而typecho内置搜索功能仅仅是基于字符串的全匹配查找,因此导致很多合理的查询都没法得到结果,比如“2018天象”、“新词算法”都没法给出结果,原因就是文章中都不包含这些字符串。
于是就萌生了加强搜索功能的想法,之前也有读者建议过这个事情。这两天搜索了一下,本来计划用Python下的Whoosh库来建立一个全文检索引擎,但感觉整合和后期维护的工作量太大,还是放弃了。后来想到在typecho自身的搜索上加强,在公司同事(大佬)的帮助下,完成了这个改进。
由于是直接修改typecho源文件实现的改进,因此如果typecho升级后就可能被覆盖,因此在这里做个备忘。
探索 #
通过在Github检索我发现,typecho的搜索功能是在var/Widget/Archive.php
中实现的,具体代码大概在1185~1192行:
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');
}
可见,搜索结果是通过在SQL中匹配keywords返回的,其中%是SQL中的通配符。因此我们还发现,如果我们输入查询语句是自带空格的话,那么空格也会被替换成通配符,这样搜索起来就灵活一点。
因此很自然的一个想法是,不管查询语句有没有空格,我们人工对查询语句进行分词,然后用通配符连接分词结果,从而实现在没有空格的情况下也更灵活搜索。这确实是我实践的第一个思路。然而这样做存在的问题是:尽管进行了分词,然而还是要匹配完所有的词才出结果,如果有一个词在博客中从未出现过,那么就匹配不到了。于是要想更好,那么需要考虑每个词都只是候选词而不是必选词的做法。
实践 #
为了实现上述目的,我用Python写了个http接口,放到服务器上,这个http接口负责分词并生成SQL语句,然后将$keywords = $this->request->filter('url', 'search')->keywords;
替换为$keywords = $this->request->keywords;
,并改写上述代码为
if (!$hasPushed) {
$url = 'http://127.0.0.1:7777/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);
}
其中接口http://127.0.0.1:7777/token?text=
是Python程序:
#! -*- coding:utf-8 -*-
import bottle
import jieba
jieba.initialize()
def convert(s):
ws = jieba.cut(s)
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=7777, server='gunicorn')
这个接口返回的是SQL语句的算分部分,具体算法是:先分词,如果文章标题中包含一个词,那么加2分,如果文章内容包含一个词,加1分,最后算个总分,用到的函数SIGN、INSTR等,大家百度一下就知道了。这里推荐一下,用bottle这个轻量级的库写http接口是非常方便的~
还有要修改的是:因为我们修改的php部分,用了order($searchQuery, Typecho_Db::SORT_DESC);
来希望按分数降序排列。然而这不会直接生效,因为typecho中默认全部按时间降序排列,因此我们还要修改同一个文件的1396~1397行,将原来是
$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);
}
大概意思是判断一下是不是搜索语句,如果是的话,那么就不按时间排列;如果不是的话,就按时间排列。直接去掉按时间排列是不行的,因为这一句也包含了首页的输出,首页的输出必须按照时间排序。
结语 #
为什么要用这种Python和PHP结合的方案,而不纯写成PHP版?没错,写成纯PHP也可以,结巴分词的确也有PHP版,然而最重要的问题是我不会PHP!而且PHP版的结巴也需要额外配置,略麻烦。像这样用Python对我来说就简单多了,如果有什么要改进的,修改Python脚本即可。
最后,也许有使用者会担心这么粗暴的解决方法会不会存在效率问题,事实上,如果文章多达几十万的话,那么上述做法肯定有很严重的效率问题,然而对于一个只有几百篇文章的博客来说,这个问题并不需要考虑了。
终于可以用更自由地搜索了~欢迎大家更多的建议。
转载到请包括本文地址:https://kexue.fm/archives/4797
更详细的转载事宜请参考:《科学空间FAQ》
如果您还有什么疑惑或建议,欢迎在下方评论区继续讨论。
如果您觉得本文还不错,欢迎分享/打赏本文。打赏并非要从中获得收益,而是希望知道科学空间获得了多少读者的真心关注。当然,如果你无视它,也不会影响你的阅读。再次表示欢迎和感谢!
如果您需要引用本文,请参考:
苏剑林. (Jan. 09, 2018). 《增强typecho的搜索功能 》[Blog post]. Retrieved from https://kexue.fm/archives/4797
January 18th, 2018
请尝试更新Typecho-AMP插件,测试是否解决问题。
April 21st, 2018
麻烦问下,出现了这样的问题
Traceback (most recent call last):
File "soso.py", line 4, in
import jieba
File "/www/wwwroot/xxxxx.com/jieba.py", line 5, in
AttributeError: 'module' object has no attribute 'initialize'
是什么原因呢?
jieba安装好了,py2.7
你写的脚本不要命名为jieba.py
November 19th, 2018
\$keywords这个关键词你是怎么获取的啊,用这个\$keywords = \$this->request->filter('url', 'search')->keywords;貌似会自动过滤空格和特殊字符
文章已经详细说了呀
June 6th, 2020
我觉得可以另辟蹊径,也就是调用外部搜索的结果,然后包一下替换自带的搜索页。比如用Google的站内搜索
谢谢建议,不过这样做终究可能会有遗漏,取决于搜索引擎的收录程度。
August 31st, 2020
现在好像还不能对评论进行搜索?
而且只能看到最近几条评论,导致想找自己之前的评论根本找不到(捂脸)
不能,内置没有这个功能,我也不懂php开发。
November 14th, 2020
然后将$keywords = $this->request->filter('url', 'search')->keywords;替换为$keywords = $this->request->keywords;
============》 会不会导致漏洞呢?
不大了解php,请赐教,谢谢。