赞
踩
最近维护公司的APP搜索项目,在实际需求中,领导对搜索关心两方面,第一要搜出来,第二排序要符合人的搜索习惯,最近一段时间的搜索经验记录下来分享一下。
先来说说Elasticsearch基本的搜索,一段文字在es中能被搜索出来,抛开复杂的原理,简单理解成一句话: 搜索词的分词结果正好匹配上了内容的分词结果,这段内容就被搜索出来了。
这句话有两个核心,一个是分词,一个是匹配。
对于第一个核心“分词”来说,它有两个需要分词,一个是搜索词的分词,一个是文档内容的分词(跟倒排索引有关,后面解释)。 先从搜索词说起,对于一个搜索词来说,它会被分词,根据分词器的不同,会有不同的分词结果。比如 “木瓜牛奶”,如果用 standard 分词,对于中文就比较呆板,一个字一个字被分词成 [“木”,“瓜”,“牛”,“奶”] 四个词,而如果用 ik_max_word 分词器,会被分词成 [“木瓜”,“牛奶”]。
下面json就展示了对于搜索词可以指定分词器,当然,只有match这种需要分词的行为才能指定分词器,如果你用term这种精确查询,是不让你用analyzer属性的。
{ "query": { "bool": { "must": [ { "match": { "channelSkuName": { "query": "木瓜牛奶", "analyzer": "ik_max_word" } } } ] } } }
再看文档内容的分词,如果有一个商品名字段叫channelSkuName,值为 “好好吃的木瓜牛奶”,如果这个字段指定了ik_max_word 分词器,会被分词为[“好好”,“好吃”,“的”,“木瓜”,“牛奶”]。我们会发现,搜索词跟数据库内容被分词拆分之后,是有重合的内容的,[“木瓜”,“牛奶”] 是两个都具有的,这个是能被搜索出来的基础。
这个json截取了索引的mappering结构,展示了channelSkuName字段指定了"ik_max_word"分词器。然后下面还有fields字段,es是允许一个字段分别指定不同的字段类型和分词器的,只要搜索的时候对应好字段后缀就行了,比如"channelSkuName.standard"和"channelSkuName.pinyin"。
{ "channelSkuName":{ "analyzer":"ik_max_word", "type":"text", "fields":{ "standard":{ "analyzer":"standard", "type":"text" }, "pinyin":{ "analyzer":"pinyin", "type":"text" } } } }
说完分词,再说匹配,在 Elasticsearch 中,有几种不同的查询类型可用于搜索文本数据。以下是 matchPhrase、match 和 term 查询的区别。
match 查询用于在文本字段中查找与搜索词匹配的文档。
该查询会对搜索词进行分词,生成词项,并与文档中的词项进行匹配。默认情况下,match 查询使用 OR 操作符,即匹配任何一个词项,这个文档就会被搜索出来。
{ "query": { "bool": { "must": [ { "match": { "channelSkuName": { "query": "吃木瓜", "analyzer": "ik_max_word" } } } ] } } }
上述示例在ik_max_word分词器下被拆分成[“吃”,“木瓜”],所以将匹配包含短语 “吃 or 木瓜” 的文档,如 “牛奶木瓜”会被搜索出来。why? 注意,上文说了,搜索词会被分词,文档内容同样会被分词,如果文档字段仍是使用ik_max_word分词器,“牛奶木瓜” 被分词为 [“牛奶”,“木瓜”],正因为和搜索词有一样的分词项 [“木瓜”],而且match 属于or匹配**,**所以会被搜索出来。
换句话说,如果文档字段使用的是standard分词器,"牛奶木瓜"会被分词成[“牛”,“奶”,“木”,“瓜”]四个词,就无法匹配[“吃”,“木瓜”]中的任何一个,也就没法匹配搜索到。
从这个例子可以看出,一个文档要能被搜索出来,一看分词(搜索词和文档内容),二看匹配规则(比如match),就能理解es搜索的大致方式。
matchPhrase 查询用于在文本字段中查找包含指定短语的文档。该查询要求文档中的字段与搜索词语完全匹配,包括相对的顺序和位置。什么是相对的顺序和位置?就是分词结果的排序,它并不是随意排序的,每个分词项都有自己的位置。下面举例说明:
{
"query":{
"match_phrase":{
"channelSkuName":{
"query":"木瓜牛奶",
"analyzer":"ik_max_word"
}
}
}
}
这个json跟第一个稍稍不一样, ‘match’替换成了’match_phrase’,我们知道"木瓜牛奶"的分词结果是[“木瓜”,“牛奶”],然后我们希望搜索 “皇麦世家木瓜牛奶燕麦片 350g*1袋”,我们先看下这个文本的分词结构:
入参:
GET http://ip:9200/任意index/_analyze
Content-Type: application/json
{
"analyzer": "ik_max_word",
"text": [
"皇麦世家木瓜牛奶燕麦片 350g*1袋"
]
}
出参:
{ "tokens": [ { "token": "皇", "start_offset": 0, "end_offset": 1, "type": "CN_CHAR", "position": 0 }, { "token": "麦", "start_offset": 1, "end_offset": 2, "type": "CN_CHAR", "position": 1 }, { "token": "世家", "start_offset": 2, "end_offset": 4, "type": "CN_WORD", "position": 2 }, { "token": "世", "start_offset": 2, "end_offset": 3, "type": "CN_WORD", "position": 3 }, { "token": "家", "start_offset": 3, "end_offset": 4, "type": "CN_CHAR", "position": 4 }, { "token": "木瓜", "start_offset": 4, "end_offset": 6, "type": "CN_WORD", "position": 5 }, { "token": "牛奶", "start_offset": 6, "end_offset": 8, "type": "CN_WORD", "position": 6 }, { "token": "牛", "start_offset": 6, "end_offset": 7, "type": "CN_WORD", "position": 7 }, { "token": "奶", "start_offset": 7, "end_offset": 8, "type": "CN_CHAR", "position": 8 }, { "token": "燕麦片", "start_offset": 8, "end_offset": 11, "type": "CN_WORD", "position": 9 }, { "token": "燕麦", "start_offset": 8, "end_offset": 10, "type": "CN_WORD", "position": 10 }, { "token": "麦片", "start_offset": 9, "end_offset": 11, "type": "CN_WORD", "position": 11 }, { "token": "350g", "start_offset": 12, "end_offset": 16, "type": "LETTER", "position": 12 }, { "token": "350", "start_offset": 12, "end_offset": 15, "type": "ARABIC", "position": 13 }, { "token": "g", "start_offset": 15, "end_offset": 16, "type": "ENGLISH", "position": 14 }, { "token": "1", "start_offset": 17, "end_offset": 18, "type": "ARABIC", "position": 15 }, { "token": "袋", "start_offset": 18, "end_offset": 19, "type": "COUNT", "position": 16 } ] }
从出参我们看到"木瓜"和"牛奶"的position是5和6,这就是上面我们说的位置,不过这里是绝对位置。我们再看看搜索词"木瓜牛奶"的位置。
入参:
GET http://ip:9200/任意index/_analyze
Content-Type: application/json
入参:
{
"analyzer": "ik_max_word",
"text": [
"木瓜牛奶"
]
}
出参:
{ "tokens":[ { "token":"木瓜", "start_offset":0, "end_offset":2, "type":"CN_WORD", "position":0 }, { "token":"牛奶", "start_offset":2, "end_offset":4, "type":"CN_WORD", "position":1 } ] }
搜索词 "木瓜"和"牛奶"的position是0和1,虽然搜索词的position跟搜索内容的position绝对值不一样,但是他们相对位置是相邻的,matchPhrase能匹配上的要求有两个:
这两个都能满足,所以 “皇麦世家木瓜牛奶燕麦片 350g*1袋” 能被搜索出来。说到这里,我们能看到matchPhrase比match更能符合人类的搜索预期,matchPhrase相当于全文搜索,match相当于模糊搜索,但是我们再举一个相对顺序不一致的情况。
比如搜索词是"皇麦木瓜燕麦片",想搜索的商品名为"皇麦世家木瓜牛奶燕麦片 350g*1袋”, 从人的视觉习惯看起来跟商品名差不多,但是对搜索引擎分词结果来说,“皇麦"到"木瓜"到"燕麦片"之间,没有了"世家”,“牛奶”两个分词,在相对顺序上,它们已经匹配不上搜索内容分词的相对顺序了,所以无法搜索到。但是我们希望有个容错的机制可以容忍一些位置错乱,幸运的是,在使用matchPhrase的情况下,的确有个参数可以兼容顺序不一致的情况,非常实用。
slop 是一个参数,用于指定 matchPhrase 查询中允许的最大位置偏移量。它用于控制短语查询中单词的相对位置。默认情况下,slop 的值为 0,表示单词必须按照给定的顺序连续出现。如果设置了一个正整数的 slop 值,那么在指定范围内,单词可以以任意顺序出现,且允许有一些其他单词插入其中。
还是以上面无法搜索出来的例子来看,比如我们的搜索词是 “皇麦木瓜燕麦片”,通过分词分析,相比 "皇麦世家木瓜牛奶燕麦片 350g*1袋"的分词,少了[“世家”,“世”,“家”,“牛奶”,“牛”,“奶”]6个分词,我们设置slop为6,表示允许的中间不匹配位置的最大数目为6,这时候,"皇麦世家木瓜牛奶燕麦片 350g*1袋"就可以被搜出来。如果设置成5,通过我的实际检验,都没办法搜出来。
{
"query": {
"match_phrase": {
"channelSkuName": {
"query": "皇麦木瓜燕麦片",
"slop": 6,
"analyzer": "ik_max_word"
}
}
}
matchPhrase 和 match 都建立在分词再查找的基础上,而 term 查询不会对查询词进行分词,而是直接与文档中的词项进行精确匹配。所以term不接受analyzer属性,term适合精确的编码查询等场景。
但是需要注意的是,term 适合查询 keyword 类型的字段,一般文本类型分为 text 和 keyword。下面json给了一个keyword示例,channelSkuName本身是text类型,但是channelSkuName.keyword就是keyword类型,keyword 类型不会做分词处理。
{
"channelSkuName":{
"type":"text",
"fields":{
"keyword":{
"ignore_above":256,
"type":"keyword"
}
}
}
}
用"世家"是能搜索到 "皇麦世家木瓜牛奶燕麦片 350g*1袋"的内容的,因为"世家"不分词直接匹配上了"皇麦世家木瓜牛奶燕麦片 350g*1袋"的分词结果(匹配上了“世家”),如果是搜索的channelSkuName.keyword,那就肯定搜索不出来。
{
"query": {
"term": {
"channelSkuName": "世家"
}
}
}
搜出来之后,因为是个列表,我们需要根据人的搜索预期进行排序,产品给了如下需求:
其实前两个需求用 matchPhrase 和 match 搜索就行了,两者用should相连,不管是精确匹配还是模糊匹配都能满足要求,至于排序我们需要了解score机制。
在Elasticsearch中,每个搜索结果都会有一个分数(score),用于表示与查询的匹配程度。分数越高表示与查询的匹配度越高。es默认用score进行排序,看起来似乎满足我们的需求,因为完全匹配的score分数肯定更高,但是我们的排序规则还带上业务上规则的时候,就出现了一些麻烦。比如同样是完全匹配的商品中,自营的商品会排序更靠前,要实现这样的排序,你可能会想到完全匹配的商品作为第一优先级,自营作为第二优先级,很简单的问题。但是你少考虑了一点,es复杂的score计算机制,即使完全匹配的商品,score分数几乎都不可能相等(es有自己的匹配度计算),这样的话就没办法做“第二优先级-自营”的排序了,这时候你会想,如果能自己定义“完全匹配的商品”的score分数就好了。
常量化(Constant Score)是一种将某一搜索条件的分数设置为固定值的方法。有时候我们希望在搜索中不考虑复杂的匹配度,而是将某一搜索条件的分数统一设定为某个固定值。这可以通过使用常量分数查询(Constant Score Query)来实现。
{ "query": { "constant_score": { "filter": { "match_phrase": { "channelSkuName": { "query": "皇木瓜牛奶燕麦片 350g*1袋", "slop": 2, "analyzer": "ik_max_word" } } }, "boost": 5 } } }
通过新增 constant_score 和 boost ,可以指定通过当前条件搜出来的商品score分数会被固化成5分,这样就非常方便我们新增其它的业务排序,更好的符合产品需求
这篇文章更多的是实践经验而非es原理解析,自己经验小记下来,抛砖引玉。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。