如何一天做出搜索引擎(2)——搜索与匹配

@

写在前面

大家好!这一章主要介绍搜索引擎的搜索与匹配部分的思路与实现。在上一章中,我们实现了新浪新闻的搜集和数据库的建立。这为我们这一章的搜索打下了基础。我们在这一章要实现搜索引擎的最为重要的部分——将用户输入的文字与数据库中的新闻进行匹配,从而为用户推荐与他的搜索最为相关的、且时效性较好的几条新闻。

这篇文章只是讲解思路,代码的展示也是为了配合讲解。如果大家要查看源码,请移步我的github,这篇文章所讲内容在Search.py中。

开启我们的旅程

我们的目标是对于用户输入的语句,搜索引擎能够从数据库中找到最相关的新闻。当然,相关性不是我们唯一的目标,因为如果一篇新闻的相关性很高,但它是几年前发布的,那它就失去了时效性,这种新闻一般也是没有价值的。所以我们程序在给新闻打分的时候,要兼顾相关性和时效性。

让我们开启旅程吧😄

1. 处理搜索语句

首先我们对用户输入的语句进行处理。和处理新闻的思路一样,我们先用jieba库对用户输入的语句分词。再将这些词中的停用词和标点符号去除(停用词的概念请参考我的上一篇博客)。剩下的词就可以作为我们搜索的关键词啦。

举个栗子,如果我输入“中国和美国之间的贸易战”,在处理完以后,剩下的关键词就是“中国”、“美国”、“之间”、“贸易战”,而停用词“和”、“的”就被去除了。

提取出关键词后,我们还要对关键词的词频进行统计。试想,如果在搜索语句中“中国”出现了3次,而“美国”只出现了1次,那么说明用户更关心“中国”,所以与“中国”有关的新闻就应该排在与“美国”有关新闻之前。于是我们还应该统计关键词的词频,并用一个字典记录下来,留着之后推荐度打分用。

输入“中国与美国之间的贸易战,中国将如何应对”,程序的关键词提取和词频统计结果:
在这里插入图片描述
代码如下:

def search(sentence, N, avg_l):
    build_StopWords()	#建立停用词文档
    #对输入的词进行相关度评价
    searchTerms1 = jieba.lcut(sentence, cut_all = False)
    #清除停用词
    searchTerms = {}
    for p in searchTerms1:
        p=p.strip()
        if len(p)>0 and p not in stop_words and not p.isdigit():
            if p not in searchTerms:
                searchTerms[p] = 1
            else:
                searchTerms[p] += 1

2. 从数据库中取出新闻词频统计

分析完输入语句后,我们接下来将我们上一篇博客中保存在数据库里的数据(包括新闻的日期,关键词频,标题等)提取出来。
这一步比较简单,就是一个对sqlite3数据库的一个简单的提取操作。(如果对数据库操作不熟悉,请自行百度sqlite3的基本用法)代码如下:
(这段代码接上一段,也在search函数中)

	db = sqlite3.connect('news.sqlite')
    #从数据库sqlite中读出表
    df2 = pandas.read_sql_query('SELECT * FROM TermDict', con = db)
    df3=df2.T
    Dict1 = df3.to_dict()
    Dict = {}
    for i in Dict1:
        Dict[Dict1[i]['index']] = [Dict1[i]['0'], Dict1[i]['1']]

3. 对新闻的相关性和时效性进行综合评估

经过前两步的准备,可谓是万事俱备,只欠打分这一股东风了。我们接下来就是要依据我们第一步中提取出的搜索关键词词频,与第二步中的数据中的关键词词频比对,再结合新闻发布的时间,给出推荐度打分。

因为我们的搜索引擎要追求极致的体验(😂),所以搜索的速度必须快,且数据库的容量必须相对较大。所以我们只对新闻的标题和关键字进行匹配。因为如果匹配正文的话,就会多消耗几倍甚至几十倍的时间。

在这里,我们利用BM25算法进行打分。(有关BM25算法的介绍请参考这篇博客,本文和此篇博客做了相同的化简)
代码如下:
(这段代码接上一段,也在search函数中)

    #采用基于概率的BM25模型计算相关度
    RelaScore = {}  #相关度分数,和allPages中的index相对应

    #参数
    b = 0.75
    k1 = 1.2

    RelaWeight = 0.7
    TimeWeight = 0.3

    #文档平均长度avg_l, 文档总数N 在前面已经求出
    for word in searchTerms:
        #将qtf近似为1
        #qtf = searchTerms[word]  #查询中的词频(query's term frequency)
        if word in Dict:
            df = Dict[word][0]   #文档频率(document frequency),即该词在所有新闻中出现的总次数
            docs = Dict[word][1].split('\n')
            IDF = math.log2( (N + 0.5 + df) / (df+0.5))  #将分子上的df去掉了
            for x in docs:  #对每一个新闻处理,加上与这个词的相关度
                doc = x.split('\t')
                doc_id = int(doc[0])
                doc_time = doc[1]   #新闻时间
                tf = int(doc[2])    #文档中的词频(term frequency)
                ld = int(doc[3])    #文档长度(length of document)


                K = k1 * ( 1 - b + b*ld/avg_l )
                #计算w(word, doc)
                RelaW = IDF * (tf*(k1+1)) / (tf+K) 

                #w = qtf*tf/ld

                #计算时间因子
                newsTime = datetime.strptime(doc_time,'%Y-%m-%d %H:%M')
                nowTime = datetime.now()
                timeDis = (nowTime.day-newsTime.day)*24 + nowTime.hour-newsTime.hour +(nowTime.minute-newsTime.minute)/60   #以小时为单位

                w = RelaW * RelaWeight + TimeWeight/timeDis

                #print("%f %f"%(RelaW * RelaWeight,TimeWeight/timeDis ))
                if doc_id in RelaScore:
                    RelaScore[doc_id] += w
                else:
                    RelaScore[doc_id] = w 

打分打完了,最后在排一波序就ok啦:
(这段代码接上一段,也在search函数中)

minheap = MinHeap()
    for i in RelaScore:
        minheap.add((RelaScore[i],i))
                 
    return minheap

其中MinHeap是我写的一个最小堆,代码就不贴出来了,好奇的朋友(😂)请参考我的github中的MinHeap.py

写在后面

这一章总体来说并不是很复杂,主要的难点在于理解BM25算法。其实说实话,即使不理解BM25算法,只要会用也可以做出来。当然啦,身为积极向上的有志青年,我们当然是不能满足只会用的!(是吧...)

源码请移步我的github,这篇文章所讲内容在Search.py中。

希望大家能有所收获!

posted @ 2019-02-28 21:22  沙河小渔民  阅读(1538)  评论(0编辑  收藏  举报