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

五 esengine的视图表单类

在上一篇博客中,我们实现了高级搜索的后台部分。高级搜索可以使我们组合各种条件来进行搜索,从而得到更精确的结果。在这篇博客中,我将为大家介绍esengine的视图表单类,以及一个自定义django标签highlightresult。通过自定义标签,用户可以在前端对搜索结果进行自定义。

esengine的表单与之前的whoosh版框架表单大同小异,这里主要介绍它的高级搜索表单esadvancesearchForm的实现。

图中就是高级表单。从图中可知,该表单包含两个文本框,用于输入需包含的关键字以及要排除的关键字;一组单选框表示以何种方式包含关键字;两组复选框,用于选择搜索字段;以及两个以选择形式出现的日期框。

因此,我们的表单代码如下:

# esengine/searchForm.py
# ...
class esadvancesearchForm(forms.Form):
    def __init__(self,*args,**kwargs):
        self.searchfields = {}
        includelist = kwargs.pop('includelist',[])
        excludelist = kwargs.pop('excludelist',[])
        startyear = kwargs.pop('startyear','1900')
        endyear = kwargs.pop('endyear','2050')
        yearrange = range(int(startyear),int(endyear))
        super(esadvancesearchForm, self).__init__(*args, **kwargs)
        self.includeChoice = self.__buildSearchrange(includelist)
        self.excludeChoice = self.__buildSearchrange(excludelist)
        self.fields['includeKeyword'] = forms.CharField(label=u'包含以下关键词,以逗号分割',max_length=40,required=False)
        self.fields['includerange'] = forms.MultipleChoiceField(label='', widget=forms.CheckboxSelectMultiple,
                                                               choices=self.includeChoice, initial=1,required=False)
        self.fields['includemethod'] = forms.ChoiceField(label='',widget=forms.RadioSelect,choices=[['1',u'与'],['2',u'或']],initial='1')
        self.fields['excludeKeyword'] = forms.CharField(label=u'排除以下关键词,以逗号分割',max_length=40,required=False)
        self.fields['excluderange'] = forms.MultipleChoiceField(label='',widget=forms.CheckboxSelectMultiple,
                                                                choices=self.excludeChoice,initial=1,required=False)
        self.fields['startdate'] = forms.DateField(label=u'起始时间',widget=forms.SelectDateWidget(years=yearrange))
        self.fields['enddate'] = forms.DateField(label=u'终止时间',widget=forms.SelectDateWidget(years=yearrange))


    def __buildSearchrange(self,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
# ...

这里仍然用表格来介绍一下这个表单的各个字段。

esadvancesearchForm的各个字段
字段名含义类型
includeKeyword输入包含关键字的文本框文本框
excludeKeyword输入排除关键字的文本框文本框
startdate

起始日期

下拉选择框
enddate终止日期下拉选择框
includerange包含关键字的搜索字段复选框
includemethod用与还是或来搜索包含关键字单选框
excluderange排除关键字的搜索字段复选框

在django中,我们使用widget这一属性来实现不同类型的表单显示形式。通过设置不同的widget,我们可以得到不同形式的表单,如下拉、单选以及多选。常用的widget有以下几种:

Django表单中的widget
Widget含义
CheckboxSelectMultiple多选框
RadioSelect单选框
SelectDateWidget选择日期框,格式为月-日-年
PasswordInput密码输入框

在设计好表单后,我们就可以来设计视图类了。 我们的视图类需要达到以下要求:1、在第一次进入页面时,要显示我们的高级搜索表单;2、在用户执行了搜索后,如果有结果,则分页显示搜索结果,且在右上角显示一个普通搜索表单;若用户的搜索没有结果,则继续显示高级搜索表单,且右上角不显示普通搜索表单;3、若有搜索结果,则根据用户在前端的要求对结果进行高亮显示。

下面就让我们来看一下我们的视图类是如何实现的:

# esengine/views.py
# ...
class esAdvanceSearchView(View):

    def __init__(self,indexname,doctype,model,templatename,includelist,excludelist,datefield,resultsperpage = 10):
        self.indexname = indexname
        self.doctype = doctype
        self.model = model
        self.templatename = templatename
        self.keyword = ''
        self.includelist = includelist
        self.excludelist = excludelist
        self.datefield = datefield
        self.resultsperpage = resultsperpage

    def buildform(self,request):
        kwargs = {}
        #kwargs['includelist'] = [{'title':u'标题'},{'content':u'正文'}]
        #kwargs['excludelist'] = [{'title': u'标题'}, {'content': u'正文'}]
        kwargs['includelist'] = self.includelist
        kwargs['excludelist'] = self.excludelist
        self.form = esadvancesearchForm(request.GET,**kwargs)
        self.simpleform = esbasesearchForm(request.GET)

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

    def search(self,request):
        if self.form.is_valid():
            engine = esengine(self.indexname, self.doctype, self.model)
            includekeywords = self.form.cleaned_data['includeKeyword']
            excludekeywords = self.form.cleaned_data['excludeKeyword']
            startdate = self.form.cleaned_data['startdate']
            enddate = self.form.cleaned_data['enddate']
            includemethod = self.form.cleaned_data['includemethod']
            includefields = []
            excludefields = []
            for searchrange in self.form.cleaned_data['includerange']:
                includefields.append(self.form.searchfields[searchrange])
            for searchrange in self.form.cleaned_data['excluderange']:
                excludefields.append(self.form.searchfields[searchrange])
            totalcount,result = engine.advancesearch(self.indexname,self.doctype,includefields,
                                                     includekeywords,excludefields,excludekeywords,
                                                     self.datefield,startdate,enddate,includemethod)
            #print(result)
            return totalcount,result
        else:
            #print('form is not valid')
            return 0,{}

    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):
        self.buildform(self.request)
        resultcount, searchresults = self.search(self.request)
        if resultcount>0:
            page_result = self.buildpage(self.request, searchresults)
            full_path = self.request.get_full_path()
            pageurl = full_path[full_path.index('?'):]
            content = {
                'simplesearchform':self.simpleform,
                'resultcount': resultcount,
                'searchResult': page_result,
                'pageurl':pageurl
            }

        else:
            page_result = searchresults
            content = {
                'searchform': self.form,
            }
        content.update(**self.extradata())
        return render(self.request,self.templatename,content)

    def extradata(self):
        return {}
# ...

在这个视图类中,比较重要的是buildform、search和create_response这三个函数。buildform函数用于根据构造函数传入的参数来构造高级搜索表单,以及生成一个需要显示在结果页面上的普通搜索表单;而search函数用于处理从网页中传过来的值,并将其传入advancesearch函数中,得到最后的搜索结果。而这里需要额外处理的就是includerange和excluderange这两个复选框,这两个复选框将会以选中的index列表传给django,如['1','2'],这表示第1项和第2项都被选中了。我们需要做的就是从我们的searchfields(形式如{'1':'content','2':'title'})中获取字段名,再将其存入includefields和excludefields中;而当表单没有通过验证时,我们直接返回0和空字典作为搜索结果个数以及搜索结果集,表明这是一次无效的搜索;最后的create_response函数则是用来渲染最后的页面。当搜索结果个数大于0时,返回分好页的搜索结果,并显示普通搜索表单。此外,由于我们的搜索比较复杂,因此在前端来拼接下一页或前一页的url行不通了。在这里,django提供了request.get_full_path()方法来获得当前页面的url,这个url会包含我们搜索的所有参数,因此我们只需将其处理一下(拿到?之后的值)再以pageurl的名称丢给前端即可;而当搜索结果为0时(无效表单输入或是真没搜到),继续返回高级搜索表单以便用户继续搜索。

好,看来我们已经实现了我们上面三个需求中的前两个。那如何实现根据用户在前端的要求来高亮显示搜索结果呢?这就需要用到django提供的自定义tag功能了。

自定义tag功能是django模板语言提供的一个强大功能。通过自定义tag,我们可以在前端层面实现各种复杂灵活的显示效果,包括过滤指定字符、统一时间格式、实现特殊格式显示等功能,是个强大的前端工具。在这里我们可以设计一套自己的tag来支持用户用自定义的格式来对显示结果进行高亮,从而实现我们的第三条需求。

在实现tag之前,先让我们设计一下我们的tag。我们的tag支持两种形式的高亮:1、直接使用字体标签(<b>,<i>等)对指定内容进行高亮;2、通过css类对指定内容进行高亮。因此,我们的标签格式有以下两种形式:

{% highlightresult result font "<b>" %}

{% highlightresult result css "classname" %}

其中,highlightresult为tagname,result为要高亮显示的内容;font和css为关键字,分别表示要以字体标签进行高亮和以css形式进行高亮。

在设计好tag语法后,让我们来看一下该如何实现这个tag。

在django中,网页的渲染可以细化为对结点(Node)的渲染,即每个网页都是由很多结点的list组成。因此,自定义tag其实就是对指定tag包围的那些结点进行特殊处理。

和之前建立filter一样,我们需要在esengine目录下新建一个名为templatetags的目录,在其中来编写我们的highlightresult tag。目录名不要修改,这是django默认的自定义tag/filter目录名。

我们在其中新建highlighttemplate.py,开始实现我们的标签:

# esengine/templatetags/highlighttemplate.py
from django import template
register = template.Library()

@register.tag
def highlightresult(parser,token):
    # syntax
    # {% highlightresult result font "<b>" %}
    # {% highlightresult result css "classname" %}
    try:
        format_string_list = token.split_contents()
        tag_name = format_string_list[0]
        highlightcontent = format_string_list[1]
        format_string = format_string_list[2]+' '+format_string_list[3]
    except ValueError:
        raise template.TemplateSyntaxError("%r tag requires a single argument" % tag_name)
    return SearchResultNode(highlightcontent,format_string)

class SearchResultNode(template.Node):
    def __init__(self,highlightcontent,format_string):
        self.format_string = format_string
        self.highlight_content = template.Variable(highlightcontent)

    def render(self, context):
        actual_content = self.highlight_content.resolve(context)
        actual_content = actual_content.replace('<script>','&lt;script&gt;').replace('</script>','&lt;/script&gt;')
        style_tag = self.format_string.split(' ')[0]
        if style_tag == 'font':
            style_tag_element = self.format_string.split(' ')[1][2:-2]
            style_tag = '<%s>' % style_tag_element
            anti_style_tag = '</%s>' % style_tag_element
        elif style_tag == 'css':
            style_tag_element = self.format_string.split(' ')[1]
            style_tag = '<span class=%s>' % style_tag_element
            anti_style_tag = '</span>'
        #return '<b>' + actual_content + '</b>'
        return style_tag+actual_content+anti_style_tag

开头的register=template.Library()是必须的,通过这句将我们的tag注册在系统中。

这个tag由两个函数组成:highlightresult和SearchResultNode。highlightresult负责解析从前端模板中传递过来的字符串,并返回一个SearchResultNode实体;而SearchResultNode则是用于接收前端的内容,并对字符串进行一番修饰后由django调用render函数返回。

让我们看一下这两个函数。首先是highlightresult函数,该函数有两个参数parser和token,都是django提供的系统参数,在这里我们只需关注token即可。关于token,一个很重要的参数就是token.content,它会返回被{% %}包含的所有内容,包括引号在内。如{% highlightresult test font "<b>" %},若使用token.content,我们就可以得到内容为highlightresult test font "<b>"的字符串;而使用split_content,django会根据空格将刚才的字符串进行split,返回一个list,而在这个过程中,引号依然被保留。此外,token.split_content[0]一定为tagname。在这个函数中,我们会将test以及font "<b>"传递给SearchResultNode作为构造参数,从而在Node中返回拼好的html字符串。

在SearchResultNode的构造函数中,我们使用了template.Variable函数来取得highlightcontent的值,而不是直接使用传进来的值,这是因为我们从网页中得到的highlightcontent是一个变量值,需要通过这种方法拿到前端模板中提供的变量值,随后在render函数中,再使用resolve(content)得到其真正的值。在得到了真正的值后,我们开始拼接html字符串。首先就是对<script>进行一个简单的转义,随后再根据用户使用的是font还是css对内容做一个格式拼接。注意这里有个奇怪的点:在拼接字符串时要使用格式化字符串赋值的形式,而不要直接hardcode,否则在前端显示会不正常。

最后,使用@register.tag的修饰器修饰highlightresult,完成我们的tag编写。

下面来让我们看一下前端页面的编写。在myblog/templates/myblog下新建advancesearch.html页面,写入以下内容:

advancesearch.html
{% extends "parentTemplate.html" %}
{% load blogfilter %}
{% load highlighttemplate %}
{% block othernavitem %}
{% if simplesearchform %}
<form method="get" action="{% url 'simpleSearch' %}">
搜索:{{ simplesearchform.searchKeyword }}
<input type="submit" value="搜索">
<a href="{% url 'advanceSearch' %}">高级搜索</a>
</form>
{% endif %}
{% endblock %}
{% block content %}
{% if searchform %}
<form method="get" action="{% url 'advanceSearch' %}">
<p>包含以下关键字,以逗号分割:{{ searchform.includeKeyword }}</p>
<p>{% for choice in searchform.includemethod %}
<span> {{ choice }} </span>
{% endfor %}</p>
{% for choice in searchform.includerange %}
<span>{{ choice }}</span>
{% endfor %}
<p>排除以下关键字,以逗号分割:{{ searchform.excludeKeyword }}</p>
{% for choice in searchform.excluderange %}
<span>{{ choice }}</span>
{% endfor %}
<p>从{{ searchform.startdate }}</p>
<p>到{{ searchform.enddate }}</p>
<input type="submit" value="搜索">
</form>
{% endif %}
<div class="content-wrap">
共搜索到{{ resultcount }}条记录
{% for blog in searchResult %}
    <div>
        <h3><a href="{% url 'blogs:content' blog.id %}">
            {{ blog.title }}
        </a></h3>
        {% for highresult in blog.highlight %}
            {% highlightresult highresult font "<b>" %}
        {% endfor %}
    </div>
{% endfor %}
{% if searchResult.has_previous %}
    {% if pageurl %}
	    <a href="{{ pageurl }}&amp;page={{ searchResult.previous_page_number }}">前一页</a>
	{% endif %}
{% endif %}
第{{ searchResult.number }}页
{% if searchResult.has_next %}
    {% if pageurl %}
	    <a href="{{ pageurl }}&amp;page={{ searchResult.next_page_number }}">下一页</a>
	{% endif %}
{% endif %}

</div>
{% endblock %}

这里要注意的是,要想使用我们的highlightresult tag,我们要在页面中写入{% load highlighttemplate %}。注意,这里load的是自定义tag的py文件名,py文件名,py文件名(重要事情说三遍),而不是我们定义的tag类名。在引入这个py文件后,我们就可以在模板中使用我们的tag了,即

{% for highresult in blog.highlight %}
    {% highlightresult highresult font "<b>" %}
{% endfor %}

这里我选用了<b>的形式,因为实在不想设计css类。

最后,我们就得到了有用户自定义的高亮搜索结果:

六 结语

我们的esengine框架到此就全部完成了。与whoosh篇一样,依然包括以下四个部分:建立更新索引、普通搜索、高级搜索以及前端表单设计。与whoosh相比,elasticsearch的搜索性能更强,由于其分布式的特性,更加适合大公司使用;在搜索方面,es提供的json形式的Query DSL虽然比whoosh直接提供的搜索函数复杂的多,然而也灵活的多,可以充分将不同搜索条件嵌套组合;而从占用资源的角度来说,whoosh是普通的一个python库,不存在额外的资源占用,而es则需要启动一个server或多个server,占用系统资源较多,如果再配上完整的工具栈则消耗资源更多。总体来说,这两种搜索工具各有长短:企业级别一般会使用es来进行大量搜索,充分利用其分布式特性;而个人学习的话还是建议从whoosh入手,比较简单易于上手(我猜流量不大的话用whoosh也可以,仅是猜测,因为我并不是做互联网的)。

这个专题就结束了,在今后的博客中,我将继续带来django的相关内容,希望大家继续关注~

 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值