在上一篇博客中,我们为我们的搜索框架实现了建立索引schema以及初始化索引的工作。对于一个搜索引擎来说,应该可以跟踪搜索对象的更新,确保永远将最新的内容保存在索引中,因此我们需要一个更新算法以确保我们搜索的内容永远是最新的。
我们向engine.py文件中添加如下代码,实现对搜索内容的增量更新:
# blogsearchengine/searchengine.py
class searchengine:
# ...
def updateindex(self):
print('updateindex')
storage = FileStorage(self.indexpath)
ix = storage.open_index(indexname=self.indexname)
index_id = set()
to_index_id = set()
objlist = self.model.objects.all()
with ix.searcher() as searcher:
writer = AsyncWriter(ix)
for indexfield in searcher.all_stored_fields():
if len(indexfield) > 0:
indexId = indexfield['id']
print(indexId)
index_id.add(indexId)
# 数据库未找到此篇,则可能已被删除,故从index中删除此篇
if not self.model.objects.filter(id=indexId):
writer.delete_by_term('id', indexId)
for key in indexfield:
# 根据updatefield进行更新
if key == self.updatefield:
objfromdb = self.model.objects.get(id=indexId)
contentofobj = getattr(objfromdb,self.updatefield)
if contentofobj != indexfield[key]:
writer.delete_by_term('id',indexId)
to_index_id.add(indexId)
print('update id is %s, title is %s' % (indexId,objfromdb.title))
for obj in objlist:
if obj.id in to_index_id or obj.id not in index_id:
self.__addonedoc(writer,obj.id)
print('add id is %s, title is %s' % (obj.id,obj.title))
writer.commit()
storage.close()
这里使用了whoosh官方文档提供的一种增量索引更新算法,其思路是用两个set维护当前索引的对象id以及要加入索引的对象id,然后根据用户设定的updatefield来判断是否对对象进行更新。
在这里,index_id和to_index_id分别存储当前索引的对象id和欲加入索引的对象id。在开始时将索引中的id依次加入index_id中,随后再判断此id对应的对象在数据库中是否存在:若该对象在数据库中已不存在,则调用delete_by_term方法将对象从索引中删除;接下来对索引对象判断是否需要更新:从索引的对象中取出updatefield的值,再将其与当前数据库中的值相比,若不同的话则将该索引删除,并将其id加入到to_index_id中;最后,对数据库中所有对象进行遍历,对那些在to_index_id中或不在index_id中的对象,调用__addonedoc将其加入索引。__addonedoc函数代码如下:
# blogsearchengine/searchengine.py
class searchengine:
# ...
def __addonedoc(self,writer,docId):
print('docId is %s' % docId)
obj = self.model.objects.get(id=docId)
document_dic = {}
print('enter __addonedoc')
for key in self.indexschema:
print('key in __addonedoc is %s' % key)
print(key)
if hasattr(obj,key):
document_dic[key] = getattr(obj,key)
print(document_dic)
writer.add_document(**document_dic)
这个函数很简单,将一个对象按照field-value的形式组成字典,再调用add_document加入索引即可。
现在,我们有了更新索引的方法,下面就是要找到一个合适的时机来调用这个方法了。毫无疑问,最好的时机是每当有新博客发表或任意一篇博客有改动时都自动调用这个函数。因此,我们需要使用在技术笔记——Django+Nginx+uwsgi搭建自己的博客(十二)中提到django信号机制来触发这个函数。
我们在blogs app中的views.py中为post_save信号注册updateIndex函数,借此实现索引的自动更新。当然,用户可以用同样的手法来实现对其他model的更新。
# blogs/views.py
# ...
from blogsearchengine.engine import searchengine
from django.db.models.signals import post_save
from django.dispatch.dispatcher import receiver
@receiver(post_save,sender=Blog)
def updateIndex(sender,**kwargs):
engine = searchengine(Blog,'content',indexname='myblogindex')
engine.updateindex()
# ...
这样,每当blogs model触发save事件后,我们的索引都会自动更新。
三 使用blogsearchengine框架进行搜索
我们已经实现了索引的建立以及自动更新功能,下面让我们来看如何使用blogsearchengine框架对索引好的内容进行搜索。我们的搜索功能支持以下特性:1、对搜索结果中的关键字进行高亮显示;2、支持用户对高亮效果的自定义;3、支持对多个关键字的或条件搜索;4、发现搜索关键字的拼写错误,并提供正确拼写的关键字的搜索结果。
我们通过实现search函数来达到以上4个目的:
# blogsearchengine/searchengine.py
# ...
from whoosh.qparser import QueryParser
from django.utils.html import strip_tags
from whoosh.qparser import MultifieldParser
from whoosh.qparser import OrGroup
# ...
class searchengine:
# ...
def search(self,searchfield,searchkeyword,ignoretypo = False):
storage = FileStorage(self.indexpath)
ix = storage.open_index(indexname=self.indexname)
if isinstance(searchfield,str):
qp = QueryParser(searchfield, schema=self.indexschema, group=OrGroup)
elif isinstance(searchfield,list):
qp = MultifieldParser(searchfield, schema=self.indexschema)
q = qp.parse(searchkeyword)
resultobjlist = []
corrected_dict = {}
with ix.searcher() as searcher:
corrected = searcher.correct_query(q,searchkeyword)
if corrected.query != q and ignoretypo == False:
q = qp.parse(corrected.string)
corrected_dict = {'corrected': u'您要找的是不是' + corrected.string}
results = searcher.search(q,limit=None)
#results.formatter = BlogFormatter()
results.formatter = self.formatter()
for result in results:
obj_dict = {}
highlightresults = []
for key in result:
obj_dict[key] = result[key]
if isinstance(searchfield,str):
highlightresults.append({searchfield:'<'+result.highlights(searchfield) + '>'})
elif isinstance(searchfield,list):
for _field in searchfield:
highlightresults.append({_field:'<' + result.highlights(_field) + '>'})
obj_dict['highlight'] = highlightresults
extradata_dic = self.extradata()
if len(extradata_dic) > 0:
obj_dict.update(**extradata_dic)
resultobjlist.append(obj_dict)
storage.close()
return resultobjlist,corrected_dict
whoosh提供了一种名为QueryParser的对象来处理对索引的查询工作。QueryPaser的主要任务是将我们的搜索关键字和搜索字段组合成query对象,(可以理解为sql中的where语句)以供whoosh的search方法调用。在这里我们使用两种QueryParser——使用OrGroup参数的QueryParser和MultifieldParser。前者为普通的QueryParser,用于对单个field进行关键字搜索。若不指定group的话,对于多个关键字,whoosh会使用AND条件进行搜索,而在我们指定了OrGroup后,whoosh就会使用OR来连接多个关键字,达到我们的多关键字搜索目的。MultifieldParser顾名思义,支持对多个搜索字段进行搜索,只需将搜索字段组成的list传入即可。
对于拼写修正,whoosh提供了correct_query方法来获得可能正确的搜索,并且通过返回对象的string属性可以得到whoosh猜测的正确关键字拼写。在这里,我们采取的方法是通过参数ignoretypo来控制是否要检查拼写。若ignoretypo为True,则不会对任何拼写错误进行检查,反之则会提供正确拼写的搜索结果,并在页面上提示用户是否搜索的为正确搜索结果。
通过以上两步,我们已经建立了正确的query对象,可以将其传入searcher的search方法拿到结果集。search返回的结果集是个字典的list,为了能让用户自由显示搜索对象的每个字段以及高亮结果还有用户自定义的数据,我们需要对结果集进行重组,利用字典的特性实现一个object的list,以便将高亮结果一并存入。
对于高亮结果,whoosh提供了highlights函数来生成高亮结果。该函数的效果由highlight.Formatter类或其子类来决定,我们在这里使用了results.formatter = self.formaatter()来实现高亮效果的自定义。我们在这里提供一个简单的Formatter类,代码如下:
# blogsearchengine/searchengine.py
# ...
import whoosh.highlight as highlight
class BlogFormatter(highlight.Formatter):
def format_token(self, text, token, replace=False):
tokentext = highlight.get_text(text,token,replace)
return '<b>%s</b>' % strip_tags(tokentext)
该函数返回加粗的搜索结果,即用<b>标签包围的去掉html标签的文本。
其实这里有一个问题,就是由于我们的博客内容采用富文本格式存储,在源数据中包含着各种html标签,然而在使用highlights方法时,返回的高亮结果总是会”吃掉“最左侧和最右侧的<和>,导致显示的搜索结果格式出现问题。我到现在也没有搞明白这个问题,也没有搜到相关资料。
下面来让我们看一下这个search方法的效果:
这里我们输入的搜索关键字是tes,而search方法自动为我们搜索了包含test的博客,并且还在搜索页面顶部给出了提示;且正文中的test字样均被加粗。
在这篇博客中,我们完成了blogsearchengine框架核心部分的实现——自动更新和搜索。在之后的博客中,将为大家带来该框架剩下的两部分:自带的搜索表单和view的实现,希望大家继续关注~