Elasticsearch搜索与排序经验小记

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

最近维护公司的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原理解析自己经验小记下来抛砖引玉一得之见。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

“Elasticsearch搜索与排序经验小记” 的相关文章