技术笔记外传——用whoosh搭建自己的搜索框架(四)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/BetrayArmy/article/details/84677706

在上一篇博客中,我对这个搜索框架的搜索功能进行了扩展,使其能搜索模型中外键字段的内容,大幅增强了搜索能力。这篇博文中,我将继续介绍这个搜索框架中最后两部分:表单和视图类。

五 blogsearchengine的表单与视图类

如haystack一样,我们的blogsearchengine框架也提供了基本的表单类和视图类,以供用户快速实现他们自己的搜索功能。blogsearchengine框架提供两种表单:1、只包含一个搜索框的搜索表单;2、带有单选框的搜索表单,且支持用户自定义单选框序列。对应这两种表单,我们也提供其对应的视图类,用于直接使用该表单生成搜索结果。

首先来让我们看看表单类的实现。在框架目录下建立searchForm.py文件,开始构建表单类:

# blogsearchengine/searchForm.py
# -*- coding=utf-8 -*-
from django import forms

class enginebasesearchForm(forms.Form):
    searchKeyword = forms.CharField(label=u'搜索',max_length=40)
    def __init__(self,*args,**kwargs):
        super(enginebasesearchForm,self).__init__(*args,**kwargs)

class enginechoicesearchForm(forms.Form):
    def __init__(self,*args,**kwargs):
        self.searchfields = {}
        searchlist = kwargs.pop('searchlist', False)
        self.CHOICES = self.__buildSearchrange(searchlist)
        super(enginechoicesearchForm, self).__init__(*args, **kwargs)
        self.fields['searchrange'] = forms.ChoiceField(label='',widget=forms.RadioSelect,choices=self.CHOICES,initial=1)
        self.fields['searchKeyword'] = forms.CharField(label=u'搜索',max_length=40)
        # searchlist should be below style:
        # [{'title':u'标题'},{'content':u'全文'},{'username':u'用户'},...,{'xx':u'yy']
        # [u'aa',u'bb',u'cc']


    def __buildSearchrange(self,searchlist):
        print(searchlist)
        searchrange = []
        i = 1
        for choice in searchlist:
            if type(choice) == str:
                subrange = []
                subrange.append(str(i))
                subrange.append(choice)
                i = i + 1
                searchrange.append(subrange)
            elif type(choice) == dict:
                for key in choice:
                    subrange = []
                    subrange.append(str(i))
                    subrange.append(choice[key])
                    self.searchfields[str(i)] = key
                    i = i + 1
                    searchrange.append(subrange)
        return searchrange

这两个表单均继承于django的forms类,这也是在django中实现自定义表单的常规做法。enginebasesearchForm是我们提供的基础表单,只包含一个名为searchKeyword的文本框作为搜索框;而enginechoicesearchForm为带若干单选框的搜索表单,它不仅允许用户自定义选择项的值,甚至可以让用户指定每个选择项对应的字段,以便在框架提供的视图中直接生成搜索结果!

choicesearchForm包括以下成员变量:searchfields字典用于存储选择项与搜索字段的对应关系,用于最后生成结果;searchlist为欲搜索的项的list;CHOICES为根据searchlist生成的单选框的选择范围;以及剩下的单选框searchrange和文本框searchKeyword。

这个表单的核心部分在于下面的__buildSearchrange函数。该函数会根据searchlist的形态生成不同的CHOICES,并填充searchfields,以供视图类使用。从code的注释部分可知,searchlist支持两种形态:1、形如[{'title':u'标题'},{'content':u'正文'},{'username':u'作者'}]这样以字典为元素的list,每个元素的key为该标题所对应的搜索字段;2、普通的list,如[u'标题',u'正文',u'作者'],这种searchlist只包括标题,而所对应的搜索字段需要用户在自己的视图中额外指定。可以说,第一种形式是专用于框架所提供的表单类所用,而第二种由于只有标题而不包含搜索字段,适用于用户的自定义视图。

当searchlist为第一种形态时,__buildSearchrange函数会将其拆分并填充searchfields,返回django标准的单选框结构:[['1',u'标题'],['2',u'正文'],['3',u'作者']],用于生成单选框序列;而searchfields会将选择值和字段对应起来:{'1':u'title','2',u'content','3',u'username'},这样就实现了将选项和字段对应的功能;而当searchlist为第二种形态时,则只会返回django标准的单选框结构,不会填充searchfields。

注意,在这个表单的__init__函数中,使用的是kwargs.pop('searchlist',False)方法来从kwargs中得到searchlist,而不是直接作为构造函数的参数。这是因为我们在构造表单时,通常要传入request.GET作为构造参数之一,而这个参数会作为关键字参数,导致我们真正的searchlist不会被识别,因此我们需要通过pop的方式从关键字参数中返回,更详细的原因请看这里。

在我们实现完表单后,该构造我们的视图类了。我们在views.py中实现我们的表单类,首先来看基础搜索表单:

# blogsearchengine/views.py
from django.shortcuts import render
from django.views import View
from blogsearchengine.searchForm import enginebasesearchForm,enginechoicesearchForm
from blogsearchengine.engine import searchengine
from django.core.paginator import Paginator,EmptyPage,PageNotAnInteger
# Create your views here.


class baseSearchView(View):

    def __init__(self,modelname,searchfield,updatefield,indexname,templatename,resultsperpage = 10):
        self.modelname = modelname
        self.searchfield = searchfield
        self.updatefield = updatefield
        self.templatename = templatename
        self.indexname = indexname
        self.keyword = ''
        self.resultsperpage = resultsperpage

    def search(self,request):
        searchresults = []
        correct_dict = {}
        self.form = enginebasesearchForm(request.GET)
        if self.form.is_valid():
            self.keyword = self.form.cleaned_data['searchKeyword']
            print('keyword data is' + self.keyword)
            engine = searchengine(self.modelname, self.updatefield, indexname=self.indexname)
            searchresults, correct_dict = engine.search(self.searchfield, self.keyword)
        return searchresults, correct_dict

    def __call__(self, request):
        self.request = request
        return self.create_response()


    def buildpage(self,request,results):
        # 引入分页机制
        paginator = Paginator(results, self.resultsperpage)
        page = request.GET.get('page')
        try:
            searchresult = paginator.page(page)
        except PageNotAnInteger:
            searchresult = paginator.page(1)
        except EmptyPage:
            searchresult = paginator.page(paginator.num_pages)
        return searchresult

    def create_response(self):
        searchresults,correct_dict = self.search(self.request)
        page_result = self.buildpage(self.request,searchresults)
        content = {'correct':correct_dict,
                   'searchResult':page_result,
                   'searchform':self.form,
                   'searchKeyword':self.keyword
                   }
        content.update(**self.extradata())
        return render(self.request,self.templatename,content)

    def extradata(self):
        return {}

这个视图类接受以下参数作为构造参数:modelname是搜索的模型,searchfield,updatefield和indexname分别为构造engine所需的几个参数;templatename为视图欲使用的模板文件;而resultsperpage为每页显示的搜索结果,默认为10。在几个成员函数中,search函数顾名思义,用于根据表单的内容进行搜索;buildpage函数则可以对搜索结果进行分页,并根据构造参数来设定每页显示多少结果;create_response用于返回最后的显示结果。

这里重点介绍一下python中__call__函数的用法。在python中,当在一个类中实现了__call__方法时,该类就可以作为一个可调用对象看待,即这个类实例会被当做函数来看待。通常,若一个视图类继承了View类,可以通过django提供的各种函数来实现自己的需求,然而这种实现有比较大的局限性。因此,在这里我参考了haystack的实现方式,将所有要做的事情统一写在__call__方法中,这样就可以处理更灵活的需求。

下面再来看另一个视图类的实现:

# blogsearchengine/views.py
# ...
class choiceSearchView(View):
    def __init__(self,modelname,searchfield,updatefield,indexname,templatename,resultsperpage = 10):
        self.modelname = modelname
        self.searchfield = searchfield
        self.updatefield = updatefield
        self.templatename = templatename
        self.indexname = indexname
        self.keyword = ''
        self.searchrange = ''
        self.resultsperpage = resultsperpage
        #self.searchfield = searchfield

    # ...

    def search(self,request):
        print(self.searchfield)
        searchresults = []
        correct_dict = {}
        kwargs = {}
        kwargs['searchlist'] = self.searchfield
        # [{'content': u'全文'}, {'title': u'标题'}]
        self.form = enginechoicesearchForm(request.GET,**kwargs)
        if self.form.is_valid():
            self.keyword = self.form.cleaned_data['searchKeyword']
            print('keyword data is' + self.keyword)
            engine = searchengine(self.modelname, self.updatefield, indexname=self.indexname)
            #print('form searchfield'+self.form.searchfields)
            for key in self.form.searchfields:
                if key == self.form.cleaned_data['searchrange']:
                    searchresults, correct_dict = engine.search(self.form.searchfields[key], self.keyword)
                    self.searchrange = key
                    break
        return searchresults, correct_dict

    def create_response(self):
        searchresults,correct_dict = self.search(self.request)
        page_result = self.buildpage(self.request,searchresults)
        content = {'correct':correct_dict,
                   'searchResult':page_result,
                   'searchform':self.form,
                   'searchKeyword':self.keyword,
                   'searchRange':self.searchrange
                   }
        content.update(**self.extradata())
        return render(self.request,self.templatename,content)
    # ...

choiceSearchView的函数与baseSearchView大同小异,因此这里只贴上有变化的三个函数。在search里,我们用到了前文choice表单中的searchfields,使View可以自行根据选项搜索;而create_response中要多传一个searchRange的参数,以便翻页后使用。

最后,让我们看一下这个视图的前端模板实现:

{% extends "parentTemplate.html" %}
{% load blogfilter %}
{% block othernavitem %}
<form method="get" action="{% url 'blogSearch' %}">
搜索:{{ searchform.searchKeyword }}
{% for choice in searchform.searchrange %}
<span>{{ choice }}</span>
{% endfor %}
<input type="submit" value="搜索">
</form>
{% endblock %}
{% block content %}
<div class="content-wrap">
<b>{{ correct.corrected }}</b>
{% for blog in searchResult %}
    <div>
        <h3><a href="{% url 'blogs:content' blog.id %}">
            {{ blog.title }}
        </a></h3>
        {% for highresult in blog.highlight %}
            {{ highresult.content|script|safe }}
        {% endfor %}
        
    </div>
{% endfor %}
{% if searchResult.has_previous %}
    {% if searchRange %}
	    <a href="?searchKeyword={{ searchKeyword }}&amp;searchrange={{ searchRange }}&amp;page={{ searchResult.previous_page_number }}">前一页</a>
	{% else %}
	    <a href="?searchKeyword={{ searchKeyword }}&amp;page={{ searchResult.previous_page_number }}">前一页</a>
	{% endif %}
{% endif %}
第{{ searchResult.number }}页
{% if searchResult.has_next %}
    {% if searchRange %}
        <a href="?searchKeyword={{ searchKeyword }}&amp;searchrange={{ searchRange }}&amp;page={{ searchResult.next_page_number }}">下一页</a>
    {% else %}
	    <a href="?searchKeyword={{ searchKeyword }}&amp;page={{ searchResult.next_page_number }}">下一页</a>
	{% endif %}
{% endif %}

</div>
{% endblock %}

这个模板支持普通搜索框和带选项的搜索框。对于带选项的搜索框,会将每个单选框放到<span>标签中;而对于搜索结果,会显示博客的标题,以及高亮的博客正文内容;最后一部分则是对分页的处理,在这里用has_previous和has_next来判断是否有前一页和后一页,并根据是否传入searchRange来拼接对应的链接。注意,我们每页都要从GET方法中得到我们的searchKeyword,以及对应的searchRange,因此在拼接链接时也要将这两部分一并拼入。

万事具备,只欠修改urls.py。我们在myblogs/urls.py中加入一行,来调用我们的视图类:

# myblogs/urls.py
# ...
urlpatterns = [
    # ...
    url(r'^search/$',choiceSearchView(modelname=Blog,
                                      searchfield=[{'content':u'全文'},{'title':u'标题'}],
                                      updatefield='content',
                                      templatename='myblog/normalsearchengine.html',
                                      indexname='myblogindex',
                                      resultsperpage=3),name='blogSearch')

这里我们让它搜索标题和全文。在主页中搜索关键字后,我们会看到以下页面:

 右上角显示的表单就是我们在指定的searchfield,而从地址栏中我们可以看到包含searchKeyword,searchrange和page信息的url,表明它现在是在第二页。

好了,我们的blogsearchengine框架功能已经全部实现了。总结一下,我们的blogsearchengine框架具备以下特性:

1、支持对django模型字段的搜索,包括对外键字段的搜索;

2、索引会随着指定字段的更新而更新;

3、当模型数据发生增或删时,索引会增量更新;

4、提供灵活的表单和视图类,尤其是单选框表单及其搜索视图,大幅简化了用户的开发;

5、框架所提供的函数及工具类可方便地支持用户的自定义视图开发(主要是engine的search方法以及自定义表单)。

在写这个框架的过程中,借鉴了一些haystack的思路,也明白了haystack在某些地方为何那样设计,更让我对python的动态特性有了更深入的了解。这个外传系列就到此为止了。在之后的日子里,我们会继续探索python的web开发的相关内容,也许是redis,也许是elasticsearch,总之希望大家继续关注我的博客~

猜你喜欢

转载自blog.csdn.net/BetrayArmy/article/details/84677706
今日推荐