whoosh自定义scoring和collectors

自定义scoring

scoring模块是whoosh控制搜索结果得分的。

使用whoosh自带的scoring就可以实现特别好的搜索结果,但架不住业务上的要求,就比如我们要将搜索结果内在售的排在前面, 而且还要将最近的年份的显示在前面,并且不能简单的靠是否在售和时间来排序,还要根据搜索关键词的相关性综合考虑。其实就比较蛋疼, 要控制好这几个维度的度,也就是各个维度的权重。

重写BM25FScorer

from whoosh import scoring


class MyBM25FScorer(scoring.BM25FScorer):
    def __init__(self, searcher, fieldname, text, B, K1, qf=1):
        super().__init__(searcher, fieldname, text, B, K1, qf=qf)
        self.searcher = searcher

    def score(self, matcher):
        s = self._score(matcher.weight(), self.dfl(matcher.id()))
        # customize
        d = self.searcher.stored_fields(matcher.id())
        return self.customize_add_score(d, s)

    @staticmethod
    def customize_add_score(d, s):
        if d['saleStatus'] == "在售":
            # 如果在售,加一定的分数
        # 其他条件...
        if ...
            ...
        return s

自定义collectors

自定义scoring后发现确实搜索结果都按照预期的来了,但有出现一个问题,有的关键词搜索结果特别多就会特别慢,原因就是每条记录都会根据自定义逻辑就行一次取值判断加分, 匹配到的结果太多时间自然就变长了。实际测试匹配结果达到2万5千条的时候需要将近8s的时间。

其实搜索到的排在后面的数据基本都是不相关的,而且也不可能有用户去查看2w多条记录

于是可以减少匹配的数量来加快检索。

from whoosh import collectors

MIN_SCORE = 6

...
class MyBM25FScorer(scoring.BM25FScorer):
    ...

    def score(self, matcher):
        s = self._score(matcher.weight(), self.dfl(matcher.id()))
        # customize
        if s < MIN_SCORE:
            return s
        d = self.searcher.stored_fields(matcher.id())
        return self.customize_add_score(d, s)


class MyUnlimitedCollector(collectors.UnlimitedCollector):
    def _collect(self, global_docnum, score):
        if score < MIN_SCORE:
            return 0
        self.items.append((score, global_docnum))
        self.docset.add(global_docnum)
        # Negate score to act as sort key so higher scores appear first
        return 0 - score

使用自定义的scoring和collectors搜索

...
with ix.searcher(weighting=MyBM25F()) as s:
    c = MyUnlimitedCollector()
    s.search_with_collector(query, c)
    results = c.results()
...