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

三 ElasticSearch的搜索功能以及esengine搜索的实现

在上期博客中,我们用elasticsearch为我们的博客建立了索引,为我们的框架提供了数据基础。这期博客中,我们将介绍elasticsearch中的核心功能——搜索,并实现框架中的单字段搜索以及多字段搜索功能。

在实现了框架中的搜索功能后,我们可以得到以下结果:

页面右上角出现了一个带有复选框的搜索表单,我们可以对多个字段进行搜索,图中是同时对标题和正文两个字段进行搜索。我们的搜索结果已分好页,且已被高亮显示。

elasticsearch提供两种搜索方式:URI搜索以及Request Body搜索。在URI搜索中,我们的搜索参数通过URI字符串传递给es。在这种搜索方式中,所有的参数都会暴露在外(类似于get方式),比较适合用cURL进行搜索测试;而Request Body方式适用于传递复杂的参数,使用ES提供的Query DSL语言进行各种复杂的搜索。在Request Body方式中,DSL语言以json的格式组织,并传递给ES,从而得到搜索结果。

我们先以ES文档中的一个Query DSL的范例来看一下这种json形式的搜索语言:

"query": {
    "match" : {
        "message" : "this is a test"
    }
}

每个Query DSL都以query关键字作为开始,表示它是一个query表达式。第二层表明了搜索方式,在这里为match,而第三层的message为欲搜索的字段,"this is a test"则为搜索的内容。注意,以上所有的关键字以及搜索内容都要用双引号包起来。这句DSL可以理解为如下含义:查找message字段中含有this is a test内容的数据。

match只是搜索方式的一种。在我们的框架中,我们使用到了以下搜索方式:

match:在指定字段中搜索指定内容;

match_phrase_prefix:以指定的内容作为前缀对指定字段进行搜索。在我们的框架中,作为当match或multi_match方式搜索不到时的一种补充搜索方式;

multi_match:在多个字段中搜索同一个指定内容;

而ES会返回给我们以下内容,我们需要在其中解析出我们想要的数据:

{
    "took": 1,
    "timed_out": false,
    "_shards":{
        "total" : 1,
        "successful" : 1,
        "skipped" : 0,
        "failed" : 0
    },
    "hits":{
        "total" : 1,
        "max_score": 1.3862944,
        "hits" : [
            {
                "_index" : "twitter",
                "_type" : "_doc",
                "_id" : "0",
                "_score": 1.3862944,
                "_source" : {
                    "user" : "kimchy",
                    "message": "this is a test",
                    "date" : "2009-11-15T14:12:12",
                    "likes" : 0
                }
            }
        ]
    }
}

看着返回的参数很多,其实我们只需要关注第一层hits中的以下参数即可:

1. total:此参数会告诉我们ES搜到了多少符合条件的数据;

2. hits中的参数:

_index:此次搜索的index名称;

_type:此次搜索的docType;

_source:搜索出来的对象的各个字段以及值。

有了上面的基础,我们就可以开始实现我们自己的搜索功能了。在我们的搜索框架中,主要实现两个搜索功能:单字段搜索和多字段搜索。顾名思义,单字段搜索将只对一个字段根据指定的关键字进行搜索,并返回高亮的搜索结果;而多字段搜索将基于框架提供的多选表单实现,在选定的字段范围内进行搜索。

我们选用Request Body方式来实现我们的搜索,因此我们先来组成单字段和多字段的Query DSL的json字符串:

# esengine/esenginecore.py
class esengine:
    # ....
    def __buildSingleQueryBody(self,searchfield,keyword,matchmethod='match'):
        querystr = ''
        if type(searchfield) == str:
            querystr = '{"query":' \
                       '{"%s":{"%s":"%s"}' \
                       '}' \
                       '}' % (matchmethod,searchfield,keyword)
        body = json.loads(querystr)
        return body

    def __buildMultiQueryBody(self,searchfield,keyword):
        if type(searchfield) == list:
            fieldlist = []
            for singlefield in searchfield:
                singlefield = '"' + singlefield + '"'
                fieldlist.append(singlefield)
            fieldstr = ','.join(fieldlist)
            fieldstr = '['+fieldstr +']'
            querystr = '{"query":' \
                       '{"multi_match":' \
                       '{"query": "%s",' \
                       '"fields":%s}' \
                       '}' \
                       '}' % (keyword,fieldstr)
            body = json.loads(querystr)
            return body
    # ....

__buildSingleQueryBody根据指定的参数返回match的json消息,而__buildMultiQueryBody返回multi_match的json消息。如前文所说,关键字和搜索内容都要用双引号包含,所以这里要用"%s",而不能直接用%s来占位。

这里给出一个multi_match的json范例:

{
  "query": {
    "multi_match" : {
      "query":    "this is a test", 
      "fields": [ "subject", "message" ] 
    }
  }
}

在fields里要将搜索的字段用中括号包起来,且字段名仍然要用双引号包含。

然后,我们用__parseresult函数将我们的搜索结果存在结果字典中的highlight键中,因为我们仍然需要其他字段值来在页面上显示:

# esengine/esenginecore.py
class esengine:
    # ....
    def __parseresult(self, res,searchfield):
        parsebody = res['hits']['hits']
        resultcount = res['hits']['total']
        result_list = []
        keyresult = {}
        for hitbody in parsebody:
            keyresult = hitbody['_source']
        # 设置highlight
            if type(searchfield) == str:
                keyresult['highlight'] = keyresult[searchfield]
            elif type(searchfield) == list:
                highlightlist = []
                for _field in searchfield:
                    highlightlist.append(keyresult[_field])
                keyresult['highlight'] = highlightlist
            result_list.append(keyresult)
        return (resultcount,result_list)
    # ...

和whoosh篇中的类似,我们会将搜索字段的值存在highlight中,若为多字段搜索,我们会将每个字段与其值组成一个字典,再放入列表中,最后将这个列表存储在highlight中,并与结果个数一并返回。

最后,让我们来看单字段搜索函数basicsearch以及多字段搜索函数multifieldsearch:

# esengine/esenginecore.py
class esengine:
    # ....
    def basicsearch(self,indexname,doctype,searchfield,keyword):
        if type(searchfield) == str:
            querybody = self.__buildSingleQueryBody(searchfield,keyword)
            res = self.es.search(indexname,doctype,body=querybody)
            totalcount,result = self.__parseresult(res,searchfield)
            if totalcount == 0:
                querybody = self.__buildSingleQueryBody(searchfield,keyword,matchmethod='match_phrase_prefix')
                res = self.es.search(indexname,doctype,body=querybody)
                totalcount,result = self.__parseresult(res,searchfield)
            return totalcount,result

在单字段函数中,我们将搜索内容在指定字段中进行match匹配,若没有找到的话,我们会继续将这个内容进行match_phrase_prefix匹配,以找到以它为前缀的内容,最后同样是返回搜索个数以及有highlight字段的搜索结果。

# esengine/esenginecore.py
class esengine
    # ....
    def multifieldsearch(self,indexname,doctype,searchfield,keyword):
        if type(searchfield) == list:
            querybody = self.__buildMultiQueryBody(searchfield, keyword)
            res = self.es.search(indexname, doctype, body=querybody)
            totalcount, result = self.__parseresult(res, searchfield)
            if totalcount == 0:
                for field in searchfield:
                    querybody = self.__buildSingleQueryBody(field, keyword,matchmethod='match_phrase_prefix')
                    res = self.es.search(indexname, doctype, body=querybody)
                    eachcount,eachresult = self.__parseresult(res, field)
                    totalcount += eachcount
                    result += eachresult
                # 去重
                tmpresult = []
                result_ids = set()
                for singleresult in result:
                    result_ids.add(singleresult['id'])

                for resultId in result_ids:
                    for singleresult in result:
                        if singleresult['id'] == resultId:
                            tmpresult.append(singleresult)
                            break
                result = tmpresult
                totalcount = len(result)
        elif type(searchfield) == str:
            totalcount,result = self.basicsearch(indexname,doctype,searchfield,keyword)
        return totalcount, result

这个函数其实和basicsearch函数很像,只不过match_phrase_prefix没有多字段版本,因此我们在没有找到搜索结果时,需要分别对每个字段进行前缀搜索,并对结果进行去重后得到最终的结果。

总结一下:在这期博客中,我们使用elasticsearch的Query DSL语言实现了我们的单字段及多字段的搜索功能,并采用前缀匹配的方式作为普通匹配的补充,提高了搜索命中率。

这篇博客中只是实现了搜索功能的后端部分,在下篇博客中,我们将继续实现搜索页面的前端实现,包括多选表单以及一个用户自定义标签,使用自定义标签,用户可在前端方便地使用不同的css类或字体标签对搜索结果进行高亮,希望大家继续关注~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值