前言检索的前一步检索
分数
sort operator
二次召回 改变权重
组合查询归因问题(functionScore)聚合 结语 前言
书接上文,我们为电商项目做了个性化的索引配置之后,加下来就是正式的使用了。再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在电商项目中的使用技巧。从而解决问题,更好实现并完善搜索功能。
明天就要上班了,让我们打起精神,迎接春节后第一个工作日吧~(还在休假的小伙伴自动忽略这句话~)