前言
书接上文,我们为电商项目做了个性化的索引配置之后,加下来就是正式的使用了。再ES的检索方面,也有一些值得注意的小技巧。本篇将会着重讲解笔者在使用ElasticSearch(下面简称ES)进行检索时的一些心得体会。
如果没有看过配置篇,可以移步这里: 电商项目中使用ES–配置篇
检索的前一步
其实在电商项目的中,真正走到ES检索之前,还是需要对用户的检索条件进行处理的,我们称这一步为预处理操作,这里的预处理并没有统一的范式,都是根据各自项目的需要对用户检索信息进行再加工。
拿笔者所经历的电商项目来说,预处理就包含了检索信息的纠错、敏感词过滤、同义词转换、意图识别、中文/英文/拼音识别等等,这些不在本篇所涉及的话题范围内,有兴趣的同学可以自行谷歌学习。
检索
到这里,我们需要调用SQL在ES中检索信息了,其实说白了就是CURD中的R(Retrieve
)了,那么这一步,有哪些值得注意的点呢。
这里对ES的检索语法不做过多的介绍,但是作为使用ES的基本技能,如果小伙伴们还不熟悉的话,需要自行谷歌学习。
分数
相较于“精确查询“的Mysql而言,ES几乎90%的时间执行的都是匹配度查询,它除了我们需要的查询结果之外,每条文档还会返回一个_score
,这个_score
是ES对检索结果的打分,分数越高,我们就可以认为这条文档与检索条件的相关性更高。很显然,分数越高的,再返回结果中就越靠前。
sort
值得一提的是,当我们显式的制定排序字段时候,_score
将会失效(返回值为null
)。其实这很容易理解,人为干预的排序与ES所给出的匹配度分数是互相冲突的。这时候_score
有值反而容易让人产生迷惑,返回null
就显得比较合理。
operator
我们在查询字段时,ES给我们提供了基于match
查询的API operator
。比如下面这样
{
"query": {
"bool": {
"must": [
{
"match": {
"title": "超级索尼子",
"operator": "or"
}
}
]
}
},
"from": 0,
"size": 10
}
当不配置operator
是ES默认是or
,另外一个选项是and
。那么or
和and
有什么区别呢。
假设ES的对“超级索尼子“的分词结果是:“超级“,“索尼子“,“索尼“,“子“
当operator
为or
时,文档title
中包含上述分词中任何一个分词,该条文档就会被检索到。
当operator
为and
时,文档title
中必须包含上述分词中所有分词,这条文档才能够被检索到。
从字面解释上可以看出,and
的检索精度是比or
要高的。那么在电商项目中,我们是怎么运用其在不同的检索环节当中的呢。
二次召回
这里就不得不提到我们检索的两个环节:首次召回,二次召回。
当我们进行首次召回时,目标是力求精准匹配用户的检索条件。此时在operator
中,我们理所当然的使用and
进行召回。
但是当首次召回没有返回结果时,我们就需要考虑进行二次召回,此时的目标就从精准匹配变为尽可能匹配多的结果,所以我们需要使用or
(这里其实比较复杂,还涉及到同义词,意图词,扩展词,拼音等,这些都属于预处理的范畴,与ES无关,这里省略掉)
改变权重
有时候我们对搜索结果的打分有一些额外的需求,这很合理,比如说我们在搜索“超级索尼子“的时候,再匹配到的若干文档中,有的再ipname
中包含"超级索尼子“字符,有的则在title
中包含。此时我们希望title
中包含该字符的文档优先被检索并展示出来。
如果什么都不做的话,ES总是会对每个字段按照相同的权重进行打分。此时匹配度更高,但是匹配字段在ipname
中的文档将会排在前面,这显然不是我们想看到的。
通常我们使用 boost API来改变匹配字段的权重,像这样
{
"query": {
"match" : {
"title": {
"query": "超级索尼子",
"boost": 2
},
"ipname": {
"query": "超级索尼子"
}
}
}
}
'
上述查询语句在执行过程中,源自title
字段的单词将具有比源自ipname
字段的单词更高的分数(可以简单的理解为title
的权重系数相比ipname
提升了{boost}
倍)。
boost
不设置时默认为1
组合查询
我们在上篇中提到过full_name
,在实际的查询过程中,查询DSL可能长这样
{
"query": {
"function_score": {
"boost_mode": "sum",
"score_mode": "sum",
"functions": [
{
"filter": {
"bool": {
"should": {
"prefix": {
"title": "超级索尼子"
}
}
}
},
"weight": 640
},
{
"filter": {
"bool": {
"should": {
"prefix": {
"ipname": "超级索尼子"
}
}
}
},
"weight": 320
},
{
"filter": {
"bool": {
"should": {
"prefix": {
"keyword.pinyin": "suonizi"
}
}
}
},
"weight": 160
}
],
"query": {
"bool": {
"minimum_should_match": "1",
"must": {
"term": {
"brandname": "世嘉"
}
},
"should": [
{
"prefix": {
"title": "超级索尼子"
}
},
{
"prefix": {
"ipname": "超级索尼子"
}
},
{
"prefix": {
"keyword.pinyin": "超级索尼子"
}
},
{
"match": {
"full_name": {
"analyzer": "ik_smart_t2s",
"operator": "and",
"query": "超级索尼子"
}
}
}
]
}
}
}
}
}
我们来解析一下上面的语句的含义,这依旧是一个查询“超级索尼子“的语句,它分位以下几部分:
- 包含一个
must
查询,brandname
必须为“世嘉“ - 包含一个
should
查询以及minimum_should_match
,表示当至少匹配{minimum_should_match}
个should
中的查询条件,该文档可以被检索到(minimum_should_match
至少为1) should
查询中包含3个前缀查询prefix
以及一个匹配查询match
- 包含一个
functions
权重条件 functions
条件中包含对3个前缀查询的加权系数(wieght
为加权因子)boost_mode
和score_mode
均为sum
,表示加权方式和分数计算方式均为求和
我们先看看3个前缀查询prefix
以及一个匹配查询match
,前缀查询,顾名思义,当检索关键词匹配对应Field
的前缀时(包含以“超级索尼子“开头的文档)即算作命中查询条件
match
查询中则用到了full_name
,我们在上篇中提到过,这是一个copy_to
字段,忘记了的话,可以倒回去回忆一下。由于有众多字段被拷贝到了full_name
,这样可以避免我们的检索返回的结果过少的问题(前面的查询条件相对比较严格一些,可能导致返回结果少甚至没有)
归因问题(functionScore)
上篇我们提到了使用full_name
查询会导致一个问题,这个问题就是归因,通俗点说,就是我们无法判断用户的检索信息命中的是哪一个Field
(已本文为例,到底是哪个字段上查到了“超级索尼子“呢,title
,ipname
或者是其他某个字段?)
笔者的项目中解决这个问题的方式依赖两点,除了full_name
之外,我们要对需要进行归因的字段进行查询,就像上文的查询语句那样;
其次,就是使用functionScore
来改变命中文档的分数权重(参见示例语句中的functions
和weight
),在本文示例中具体表现为,如果查询在match
之前命中了某个prefix
查询(比如说命中了title
),那么针对命中的文档,ES会根据function
在该文档score
的基础上加上一个较大的wieght
分数(title
的话是640,可以翻上去查看示例DSL进行对照)
我们在拿到这个分数之后通过位运算进行处理,就可以知道用户检索信息命中了title
字段(其他Fields
同理),这样我们就实现了对查询结果的归因操作。
关于functionScore
的用法有很多,限于篇幅原因不可能完全展开所有细节,感兴趣的小伙伴可以自行谷歌学习。
聚合
ES也支持聚合,复杂的聚合本身不是ES的强项,在电商搜索中运用范围有限,本文就不详细描述了 : P,同样,需要小伙伴们自行查阅学习。
结语
关于ES在电商中的应用,根据场景,使用方式也会有较大的差异。ES本身提供了非常灵活的查询机制,我们可以自由组合,配置出符合我们需要的功能。本文目的更多在于通过抛砖引玉的方式让小伙伴了解,并发掘更多ES在电商项目中的使用技巧。从而解决问题,更好实现并完善搜索功能。
明天就要上班了,让我们打起精神,迎接春节后第一个工作日吧~(还在休假的小伙伴自动忽略这句话~)