且听疯吟 如此生活三十年
Whoosh 全文搜索

Whoosh 是一个纯 Python 实现的全文搜索组件。基础架构和 Lucene 比较像。使用试了试,记录一些东西。

中文分词
Whoosh 本身只有英文分词,因此需要添加中文分词组件。
最后选择了 Jieba 这个 Python 中文分词组件,初步测试分词效果还不错。有时间会把几个中文分词组件对比一下看看。
Jieba 已经封装好了 ChineseAnalyzer for Whoosh,只需要引用 from jieba.analyse import ChineseAnalyzer 来替换 Whoosh 的 Analyzer 即可。

HTML 抽取
对于纯文本直接分析建立索引即可。
而对于 HTML 文本,我们需要先将其中的文本抽取出来再进行运行分析程序。否则其中的 HTML 标签将会被当作文本来分析,比如搜索 “span” 将会得到所有包含 <span></span> 的内容。举个例子,用 HTMLParser 来提取文本,其他类似功能的模块也有不少。

def html_strip(html):
    from HTMLParser import HTMLParser
    html = html.strip()
    html = html.strip("\n")
    result = []
    parse = HTMLParser()
    parse.handle_data = result.append
    parse.feed(html)
    parse.close()
    return "".join(result)

关键词 Highlight
默认的高亮结果只会包含结果命中的部分碎片,需要不同展示可以使用不同的 Fragmenters 。比如展示全文需要 whoosh.highlight.WholeFragmenter
然而 HTML 的高亮有一个问题。简单的基于匹配的替换带来的问题就是 HTML 标签的属性内容也被替换了,比如 a 标签的 href 属性,导致结构发生错乱。对此除了自己写 HTMLFragmenter 之外似乎没有现成的解决办法。
考虑到服务端解析的效率问题,放弃 Whoosh 和服务端的高亮,使用 js 在客户端高亮(其原理也是通过判断关键词前后的标签匹配,并经过一系列的正则替换最终实现只替换文本关键词而忽略标签)。试过效果比较好的高亮方案,https://github.com/jbr/jQuery.highlightRegex
只需要在 results = searcher.search(q, terms=True) 时设置 terms=True 即可从 results 或 results 的 hit 中取得关键词 terms = results.matched_terms(),然后将关键词传递给前端用 highlightRegex 来高亮。

结果分页
对于结果的分页,whoosh 提供了 search_page 方法。但是这个方法可以说是个半成品。首先,search_page 方法支持的参数设置较少,很多功能没法在 search_page 中完成。其次,search_page 方法返回的结果为 ResultsPage 类型,而 search 方法返回 Results 类型,且这两者之前并无继承关系,Results 中包含的属性比 ResultsPage 丰富得多。
最重要的是,到目前为止,使用 search_page 方法从所有结果中获取中间页时,其性能与使用 search 获取所有结果然后手动分页是一样的,从源代码可以看到 search_page 仅仅是对 search 的一次封装。search_page 仅仅是出于方便使用的功能(虽然我也没看出 search_page 存在的意义和方便在哪…… )
因此,还是使用 search 的 limit 参数来满足分页需求。limit 参数限制了返回的结果数目。可以使用

results = searcher.search(q, limit=page * pagesize)

来控制返回的结果,然后使用

results[(page - 1) * pagesize:page * pagesize]

获取指定的分页。

词典选择
中文分词的效果有很大一部分取决于词典,但并不是词典越大越全越好。分析词典 Build Trie 是一个比较消耗 CPU 的过程(虽然只是在第一次需要进行这个过程,之后会读取 Cache 中的 Model),越大的词典分析时消耗的资源也越大。因此根据实际情况选择词典比较好。
此外,如果需要分析的文本包含许多专业性词汇,也可以考虑设置自定义词典来增强歧义分析能力。
词典的设置很简单,使用 jieba.set_dictionary(dict_path) 即可。

其他

虽然 Whoosh 的性能不尽如人意,相关资料和扩展也缺乏。
但总体来说,对于小规模的使用,whoosh 开发简单,基本可以满足需求,如果使用过 Lucene 也可以很容易上手。而且纯 Python 实现,看源代码也方便。