四 blogsearchengine的高级模式
在上一篇博文里,我们实现了我们搜索框架的核心功能——建立更新索引,以及搜索功能。在这篇博文里,我们将实现一个高级模式,此模式允许我们对模型的外键建立索引。
在我们之前的代码中,我们建立的索引仅针对模型本身的字段,而不会对该模型涉及的外键模型进行索引。这样就引出了一个问题:我们的blogs模型的auther字段是连接到User模型的外键,在我们现有的索引下无法对作者信息进行搜索。因此,我们需要对建立索引的相关部分进行扩展,以便支持模型中所有外键字段的索引。(注意,我们这个扩展仅针对ForeignKey字段,而对于ManyToMany以及ManyToOne等关系字段不作处理)。
我们将这个功能美其名曰“高级模式”,因此我们修改engine.py中的__init__函数,添加advancemode参数:
# blogsearchengine/searchengine.py
class searchengine:
def __init__(self, model, updatefield, indexpath = None, indexname = None,formatter = None,advancemode=True):
# ...
self.advancemode = advancemode
# ...
当这个advancemode设为True时,我们的索引就会深入到模型中的所有Forgien所指向的模型,将该对象涉及到的外键字段一并加入索引。
因此,我们需要一个递归函数,用于遍历一个模型中的所有外键,将其建立好子schema,最后将其合并到我们的主schema中:
# blosearchengine/searchengine.py
# ...
class searchengine:
# ...
def __buildModelSchema(self,dedicatedmodel):
modelschema = {}
modelfields = dedicatedmodel._meta.get_fields()
for field in modelfields:
if type(field) == CharField:
modelschema[dedicatedmodel.__name__ + '_' + field.__str__().split('.')[-1]] = TEXT(stored=True)
elif type(field) == IntegerField:
modelschema[dedicatedmodel.__name__ + '_' + field.__str__().split('.')[-1]] = NUMERIC(stored=True,numtype=int)
elif type(field) == FloatField:
modelschema[dedicatedmodel.__name__ + '_' + field.__str__().split('.')[-1]] = NUMERIC(stored=True,numtype=float)
elif type(field) == DateField or type(field) == DateTimeField:
modelschema[dedicatedmodel.__name__ + '_' + field.__str__().split('.')[-1]] = DATETIME(stored=True)
elif type(field) == BooleanField:
modelschema[dedicatedmodel.__name__ + '_' + field.__str__().split('.')[-1]] = BOOLEAN(stored=True)
elif type(field) == AutoField:
modelschema[dedicatedmodel.__name__ + '_' + field.__str__().split('.')[-1]] = STORED()
elif type(field) == RichTextUploadingField:
modelschema[dedicatedmodel.__name__ + '_' + field.__str__().split('.')[-1]] = TEXT(stored=True)
elif type(field) == ForeignKey:
subschema = self.__buildModelSchema(field.related_model)
modelschema.update(**subschema)
return modelschema
# ...
这个函数以一个model作为输入参数,随后遍历其所有字段,建立子schema并返回。为了避免子schema的字段名互相冲突,因此我们对子schema的键采用模型名_字段名来区分,避免重复;当我们探测到ForeignKey字段时,我们递归调用这个函数,并将这个schema更新到我们的子schema中,最后一并返回即可。
让我们在之前的__buildSchema函数中调用这个__buildModelSchema,将外键加入索引:
# blogsearchengine/searchengine.py
# ...
class searchengine:
# ..
def __buildSchema(self):
# ...
elif type(field) == ForeignKey and self.advancemode == True:
subschema = self.__buildModelSchema(field.related_model)
self.indexschema.update(**subschema)
有了新的schema,我们还需对__buildIndex函数进行更新,以便根据新的schema建立索引:
# blogsearchengine/searchengine.py
# ...
class searchengine:
# ...
def __buildindex(self):
# ...
for obj in objectlist:
for key in self.indexschema:
if hasattr(obj,key):
# print(key,getattr(obj,key.split('.')[-1]))
document_dic[key] = getattr(obj,key)
# 此else是新增的部分
else:
if self.advancemode:
# 该key属于外键
foreignmodel = key.split('_')[0]
foreignkey = key[key.find('_')+1:]
for field in self.model._meta.get_fields():
if type(field) == ForeignKey:
if field.related_model.__name__ == foreignmodel:
print(field.__str__().split('.')[-1])
foreignobj = getattr(obj,field.__str__().split('.')[-1])
if hasattr(foreignobj,foreignkey):
print(key)
document_dic[key] = getattr(foreignobj,foreignkey)
writer.add_document(**document_dic)
document_dic.clear()
writer.commit()
print('all blog has indexed')
storage.close()
这里我们需要对key做一个解析。根据我们子schema的规定,key中第一个'_'之前的为外键的模型名,其后的都为外键的字段名。由于这里我们取外键的id不是很方便,因此我们直接用getattr函数从主对象中拿到外键对象foreignobj,随后再将其字段值加入文档中。注意,这里加入文档的key名依然为schema中的key,而不是我们解析出的foreignnkey,同样是为了防止覆盖。
接下来,我们还要对__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)
# 此else为新增的部分
else:
if self.advancemode:
# 该key属于外键
foreignmodel = key.split('_')[0]
foreignkey = key[key.find('_') + 1:]
for field in self.model._meta.get_fields():
if type(field) == ForeignKey:
if field.related_model.__name__ == foreignmodel:
print(field.__str__().split('.')[-1])
foreignobj = getattr(obj, field.__str__().split('.')[-1])
if hasattr(foreignobj, foreignkey):
print(key)
document_dic[key] = getattr(foreignobj, foreignkey)
print(document_dic)
writer.add_document(**document_dic)
随后,我们要修改search方法。由于我们在建立外键索引的过程中对存入schema的字段做了处理,而我们不希望用户在使用框架时还要将搜索字段手动拼接成我们存入的形式,因此我们要在search函数中自动将字段处理成我们可以处理的样子。
这里我们用两个小函数来处理这个问题:
# blogsearchengine/searchengine.py
# ...
class searchengine:
# ...
def __get_modelname_fields(self,foreignmodel,sfield):
fields = foreignmodel._meta.get_fields()
found = False
for m_field in fields:
if m_field.__str__().split('.')[-1] == sfield:
return foreignmodel.__name__ + '_' + sfield
for m_field in fields:
if type(m_field) == ForeignKey:
return self.__get_modelname_fields(m_field.related_model,m_field)
if not found:
return sfield
def __recreate_field(self,searchfield):
fields = self.model._meta.get_fields()
for modelfield in fields:
if isinstance(searchfield,str):
if modelfield.__str__().split('.')[-1] == searchfield:
print(modelfield.__str__())
print('Non-foreign')
return searchfield
print(searchfield)
for modelfield in fields:
if isinstance(searchfield,str):
if type(modelfield) == ForeignKey:
print('Foreign')
return self.__get_modelname_fields(modelfield.related_model,searchfield)
return searchfield
# ...
__get_modelname_fields用于搜索外键中的字段名,并将其拼接成模型名_字段名的形式;若没有在任何外键中找到这个字段,则返回原本的值;而__recreate_field函数负责返回最后的结果:若当前字段属于本模型,则直接返回,否则调用__get_modelname_fields去外键中搜索。
然后我们再在search函数中调用__recreate_field函数:
# blogsearchengine/searchengine.py
# ...
class searchengine:
# ...
def search(self,searchfield,searchkeyword,ignoretypo = False):
storage = FileStorage(self.indexpath)
ix = storage.open_index(indexname=self.indexname)
if self.advancemode:
if isinstance(searchfield,str):
searchfield = self.__recreate_field(searchfield)
elif isinstance(searchfield,list):
newsearchfields = []
for s_field in searchfield:
newsearchfields.append(self.__recreate_field(s_field))
searchfield = newsearchfields
# ...
这里也是当使用高级模式时,调用此函数重组字段,根据list和str类型依次重组。
这样,我们就可以搜索用户信息了:
在这篇博文中,我们对这个搜索框架进行了扩展,增强了它的搜索能力,使其可以搜索模型中的任何外键内容。通过在search方法中重组搜索字段,用户可以不必关心我们schema的内部字段,而可以直接使用外键模型中的字段进行搜索,提高了使用的便利性。
由于介绍高级模式花了太长篇幅,因此我在此将表单和视图的部分跳票到下一篇,希望大家继续关注~