Elasticsearch搜索与排序经验小记

最近维护公司的APP搜索项目,在实际需求中,领导对搜索关心两方面,第一要搜出来,第二排序要符合人的搜索习惯,最近一段时间的搜索经验记录下来分享一下。

‘牛奶木瓜’ 是怎么搜出来的?

先来说说Elasticsearch基本的搜索,一段文字在es中能被搜索出来,抛开复杂的原理,简单理解成一句话: 搜索词的分词结果正好匹配上了内容的分词结果,这段内容就被搜索出来了。

这句话分成两部分来解释, 先从分词说起,对于搜索词来说,它会被分词,根据分词器的不同,会有不同的分词结果。比如 “木瓜牛奶”,如果用 Standard分词,对于中文就比较呆板,一个字一个字被分词成 [“木”,“瓜”,“牛”,“奶”] 四个词,而如果用ik_max_word 分词器,会被分词成 [“木瓜”,“牛奶”]。再看被搜索内容,如果有一个商品叫"好好吃的木瓜牛奶",它被ik_max_word 分词器会被分词为[“好好”,“好吃”,“的”,“木瓜”,“牛奶”]。我们会发现,搜索词跟被搜索的内容被分词拆分之后,是有重合的内容的,[“木瓜”,“牛奶”] 是两个都具有的,这个是下面继续说搜索的基础。

说完分词,再说匹配,在 Elasticsearch 中,有几种不同的查询类型可用于搜索文本数据。以下是 matchPhrase、match 和 term 查询的区别以及与搜索词 “木瓜牛奶” 的示例:

match 查询

match 查询用于在文本字段中查找与搜索词匹配的文档。
该查询会对搜索词进行分词,生成词项,并与文档中的词项进行匹配。默认情况下,match 查询使用 OR 操作符,即匹配任何一个词项。

{
  "query": {
    "match": {
      "channelSkuName": "木瓜牛奶"
    }
  }
}

上述示例将匹配包含短语 “木瓜 or 牛奶” 的文档,如 “我喜欢吃木瓜”、“牛奶是健康的” 等都会被搜索出来。

注意,搜索词会被分词,被搜索的内容同样会被分词,“我喜欢吃木瓜” 被分词为 [“我”,“喜欢吃”,“喜欢”,“吃”,“木瓜”],正因为和搜索词有一样的分词项 [“木瓜”],所以会被搜索出来。

matchPhrase 查询

matchPhrase 查询用于在文本字段中查找包含指定短语的文档。该查询要求文档中的字段与搜索词语完全匹配,包括相对的顺序和位置。什么是相对的顺序和位置?就是我上面对分词结果的排序,它并不是随意排序的,每个分词项都有自己的位置。下面举例说明:

{
  "query": {
    "match_phrase": {
      "channelSkuName": {
        "query": "木瓜牛奶",
        "analyzer": "ik_max_word"
      }
    }
  }
}

这个json跟第一个稍稍不一样, ‘match’替换成了’match_phrase’,然后analyzer可以指定搜索词的分词器,我们知道"木瓜牛奶"的分词结果是[“木瓜”,“牛奶”],然后我们希望搜索 “皇麦世家木瓜牛奶燕麦片 350g*1袋”,我们先看下这个文本的分词结构:

```c
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": 4,
      "end_offset": 6,
      "type": "CN_WORD",
      "position": 3
    },
    {
      "token": "牛奶",
      "start_offset": 6,
      "end_offset": 8,
      "type": "CN_WORD",
      "position": 4
    },
    {
      "token": "燕麦片",
      "start_offset": 8,
      "end_offset": 11,
      "type": "CN_WORD",
      "position": 5
    },
    {
      "token": "燕麦",
      "start_offset": 8,
      "end_offset": 10,
      "type": "CN_WORD",
      "position": 6
    },
    {
      "token": "麦片",
      "start_offset": 9,
      "end_offset": 11,
      "type": "CN_WORD",
      "position": 7
    },
    {
      "token": "350g",
      "start_offset": 12,
      "end_offset": 16,
      "type": "LETTER",
      "position": 8
    },
    {
      "token": "350",
      "start_offset": 12,
      "end_offset": 15,
      "type": "ARABIC",
      "position": 9
    },
    {
      "token": "g",
      "start_offset": 15,
      "end_offset": 16,
      "type": "ENGLISH",
      "position": 10
    },
    {
      "token": "袋",
      "start_offset": 18,
      "end_offset": 19,
      "type": "COUNT",
      "position": 11
    }
  ]
}

我们看到"木瓜"和"牛奶"的position是3和4,这就是上面我们说的位置,不过这里是绝对位置。我们再看看搜索词"木瓜牛奶"的位置。

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能匹配上的要求有两个:

  1. 要求文档中的分词字段与搜索词分词完全匹配。
  2. 相对的顺序和位置符合要求

这两个都能满足,所以 “皇麦世家木瓜牛奶燕麦片 350g1袋" 能被搜索出来。说到这里,matchPhrase比match更能符合人类的搜索预期,matchPhrase相当于全文搜索,match相当于模糊搜索,但是我们再举一个相对顺序不一致的情况,比如搜索词是"皇麦木瓜燕麦片 350g1袋”,人的搜索习惯看起来跟商品名差不多,但是对搜索引擎来说,“皇麦"到"木瓜"到"燕麦片"之间,没有了"世家”,“牛奶”两个分词,在相对顺序上,它们已经匹配不上搜索内容分词的相对顺序了,所以无法搜索到,这个容错对于搜索引擎来说应该是要可以兼容的,没错,在使用matchPhrase的情况下,的确有个参数可以兼容顺序不一致的情况,非常实用。

slop 参数

slop 是一个参数,用于指定 matchPhrase 查询中允许的最大位置偏移量。它用于控制短语查询中单词的相对位置。默认情况下,slop 的值为 0,表示单词必须按照给定的顺序连续出现。如果设置了一个正整数的 slop 值,那么在指定范围内,单词可以以任意顺序出现,且允许有一些其他单词插入其中。

比如我们的搜索词是 "皇麦木瓜燕麦片 350g1袋",通过分词分析,相比 "皇麦世家木瓜牛奶燕麦片 350g1袋"的分词,少了[“世家”,“牛奶”],我们设置slop为2,表示允许的中间不匹配位置的最大数目为2,这时候,"皇麦世家木瓜牛奶燕麦片 350g*1袋"就可以被搜出来。

{
  "query": {
    "match_phrase": {
      "channelSkuName": {
        "query": "皇木瓜牛奶燕麦片 350g*1袋",
        "slop": 2,
        "analyzer": "ik_max_word"
      }
    }
  }
}

term 查询

matchPhrase 和 match 都建立在分词再查找的基础上,而 term 查询不会对查询词进行分词,而是直接与文档中的词项进行精确匹配。适合精确的编码查询等场景。

但是需要注意的是,term 适合查询keyword类型的字段,一般文本类型分为 text和keyword,前文说到搜索的匹配原则: 搜索词的分词结果需要匹配被搜索内容的分词内容,被搜索内容分不分词就取决于字段类型,text会分词,keyword不会分词。试想,如果使用term 查询 text内容,term 不会对搜索词分词,但是text会对被搜索内容分词,根据搜索的匹配原则,即使搜索词跟被搜索内容一模一样,但是拿不分词的搜索词去匹配分词的内容,是无论如何匹配不上的。所以term 适合查询keyword类型的字段.

{
  "query": {
    "term": {
      "channelSkuName": "世家"
    }
  }
}

上面用"世家"是能搜索到 "皇麦世家木瓜牛奶燕麦片 350g1袋"的内容的,因为"世家"不分词直接匹配上了"皇麦世家木瓜牛奶燕麦片 350g1袋"的分词结果

搜出来了再怎么排序?

搜出来之后,因为是个列表,我们需要根据人的搜索预期进行排序,产品给了如下需求:

  1. 搜索短语完全匹配的在前面
  2. 搜索短语也要支持模糊匹配
  3. 其它业务上的排序(依照其它字段排序)

其实前两个需求用matchPhrase 和 match搜索就行了,两者用should相连,不管是精确匹配还是模糊匹配都能满足要求,至于排序我们需要了解score机制。

score

在Elasticsearch中,每个搜索结果都会有一个分数(score),用于表示与查询的匹配程度。分数越高表示与查询的匹配度越高。es默认用score进行排序,看起来似乎满足我们的需求,因为完全匹配的score分数肯定更高,但是我们的排序规则还带上业务上规则的时候,比如同样是完全匹配的商品,自营的会排序更靠前,要实现这样的排序,你至少要保证完全匹配的商品score分数要一致,才能实现自营的会排序更靠前。但是es复杂的score计算机制,完全匹配的商品,score分数几乎都不可能相等(es有自己的匹配度计算),这时候你会想,如果能自己定义score分数就好了。

Constant 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原理解析,自己经验小记下来,抛砖引玉,一得之见。

你可能感兴趣的:(elasticsearch,elasticsearch,搜索引擎,constant_score,score,match,match_phrase,java)