技术笔记外传二——用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类或字体标签对搜索结果进行高亮,希望大家继续关注~