清茶豆奶 · 2020年03月22日

Elasticsearch系列---近似匹配

概要

前面的match查询只能告诉我们,搜索的文档里有这些关键词,但无法告知词语之间的顺序,而不同的词语顺序表达的意思可能完全相反。我们想要的,是跟我们期望搜索的语义要相似,这就需要短语匹配和近似匹配来控制了。

短语搜索

短语搜索即把一小段话完完整整地进行搜索,必须保证被搜索的文档内有一模一样的才行,如下:

GET /music/children/_search
{
  "query": {
    "match_phrase": {
      "content": "in the morning"
    }
  }
}

Elasticsearch对短语搜索必须要满足如下要求:

  1. in the morning 三个单词必须要全部出现
  2. the的位置比in大1
  3. morning的位置比the大1

任何一个不成立,则搜索不到匹配的结果。意思上是说,短语搜索除了关注关键词是否出现,还关心被搜索文档中这几个关键词的位置,我们可以用调度命令看一下词条的位置:

GET /_analyze
{
  "analyzer":"standard",
  "text": "in the morning"
}

响应结果:

{
  "tokens": [
    {
      "token": "in",
      "start_offset": 0,
      "end_offset": 2,
      "type": "<ALPHANUM>",
      "position": 0
    },
    {
      "token": "the",
      "start_offset": 3,
      "end_offset": 6,
      "type": "<ALPHANUM>",
      "position": 1
    },
    {
      "token": "morning",
      "start_offset": 7,
      "end_offset": 14,
      "type": "<ALPHANUM>",
      "position": 2
    }
  ]
}

留意一下tokens显示的position信息,position连续说明这三个词紧靠在一起,搜索文档时,也只有命中这三个词的文档,position按次序连接才能匹配得上。

近似匹配

近似匹配是在短语匹配的基础上,短语匹配有些严格,要求位置必须按照搜索字符串来,近似匹配则有一些变通,允许短语之间的位置有变化,变化的程度由参数slop决定。

例如:

GET /music/children/_search
{
  "query": {
    "match_phrase": {
      "content": {
        "query": "you me",
        "slop": 1
      }
    }
  }
}

slop含义

  • query string搜索文本中的几个term,要经过n次移动才能与一个document匹配,这个移动的次数,就是slop。
  • slop表示移动的最大次数,离得越近的,分数就会越高。
  • term之间交换位置也行的,但是slop得大一些。

我们以字符串"you make me happy"举例,画个移动表格:

pos 1pos 2pos 3pos 4
DOCyoumakemehappy
queryyoume
slop 1you->me

me只需要移动一步,就能匹配上,所以slop 1能查询到结果。

演示样例有限,我们把搜索串改成"me you",模拟颠倒次序的slop,但slop至少要是3才行:

GET /music/children/_search
{
  "query": {
    "match_phrase": {
      "content": {
        "query": "me you",
        "slop": 3
      }
    }
  }
}

为什么是3,我们以字符串"you make me happy"再画个移动表格

pos 1pos 2pos 3pos 4
DOCyoumakemehappy
queryyoume
slop 1me/you<-
slop 2you ->me
slop 3you->me

注意slop 1时,me和you共占用同一个位置,二者交换一下顺序,就需要slop为2。

近似匹配,就是使用了slop参数的短语匹配。

数组类型的slop

我们music索引中的tags字段,设计时是数组类型的,如果我们对这个字段进行近似匹配,结果会是怎么样:

_id为1的文档数据,tags是这样的:
"tags": ["enlighten","gymbo","friend"]

按照slop的偏移量,slop为1应该是可以匹配上

GET /music/children/_search
{
  "query": {
    "match_phrase": {
      "tags": {
        "query": "enlighten friend",
        "slop": 1
      }
    }
  }
}

结果竟然是空,怎么回事呢?我们分析一下该field的tokens信息:

GET /music/_analyze
{
  "field": "tags",
  "text": ["enlighten","gymbo","friend"]
}

响应

{
  "tokens": [
    {
      "token": "enlighten",
      "start_offset": 0,
      "end_offset": 9,
      "type": "<ALPHANUM>",
      "position": 0
    },
    {
      "token": "gymbo",
      "start_offset": 10,
      "end_offset": 15,
      "type": "<ALPHANUM>",
      "position": 101
    },
    {
      "token": "friend",
      "start_offset": 16,
      "end_offset": 22,
      "type": "<ALPHANUM>",
      "position": 202
    }
  ]
}

注意一下position的值,数组元素之间,position间隔都是100。

6.x的版本,position_increment_gap参数值默认是100,表示元素之间,步长为100,毕竟没有人近似查询时会关系slop大于100的结果。之前老版本这个值默认是1,出现了很多意外的问题,6.x后算是对此问题的修复。

召回率与精准度的平衡

召回率:假设有100个doc,你搜索一段文本,能返回多个doc,与总doc的比例,就是召回率,recall。

精准度:你搜索一段文本love me,能不能尽可能让包含这两个关键字的doc,或者离得近的doc先返回,排在前面,就是精准度,precision。

这二者看似有些矛盾,想要召回率高,精准度可能就低,反过来也是如此,精准度越是高的,召回率就越低,如何找一个平衡点?

我们一般的原则是优先满足召回率,同时兼顾精准度。比如match和match_phrase同时使用:

GET /music/children/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "content": "you gymbo"
          }
        }
      ],
      "should": [
        {
          "match_phrase": {
            "content": {
              "query": "loves gymbo",
              "slop": 2
            }
          }
        }
      ]
    }
  }
}

我们可以看到加了match_phrase条件后,_id为3的_score由0.39556286上升到0.65997654。

rescoring优化性能

match查询和短语搜索(近似搜索)区别

match查询:只要简单的匹配到了一个term,就可以理解将term对应的doc作为结果返回,扫描倒排索引,扫描到了就表示有结果匹配了。

短语搜索(phrase match):先扫描所有term的doc list,找到包含所有term的doc list,然后对每个doc都计算每个term的position,是否符合指定的范围。

近似搜索(proxmity match):slop需要进行复杂的运算,来判断能否通过slop移动,匹配一个doc

match query性能要高一些,比phrase match高10倍,比proximity match高20倍。不过Elasticsearch内搜索的效率基本控制在几毫秒内,10、20倍不过也百十来毫秒,哪怕是繁忙的ES集群,也不过一两百毫秒,实际上完全可用。

如何优化proximity match?

一个查询可能会匹配成千上万的结果,但我们的用户很可能只对结果的前几页感兴趣,所以优化的思路就是proximity只要符合match条件的前几十个文档进行评分,而不是全部数据,速度自然能大大加快。

resocre重打分: proximity match,前20个doc进行rescore即可。

语法示例:

GET /music/children/_search
{
  "query": {
    "match": {
      "content": "gymbo you"
    }
  },
  "rescore": {
    "window_size": 20,
    "query": {
      "rescore_query": {
        "match_phrase": {
          "content": {
            "query": "gymbo you",
            "slop": 1
          }
        }
      }
    }
  }
}
  1. match 查询决定哪些文档将包含在最终结果集中,并通过TF/IDF排序。
  2. window_size 是每一分片进行重新评分的顶部文档数量,例子中取20个。

小结

本篇主要介绍近似匹配的常规玩法,以及rescoring优化性能的思路。

专注Java高并发、分布式架构,更多技术干货分享与心得,请关注公众号:Java架构社区
可以扫左边二维码添加好友,邀请你加入Java架构社区微信群共同探讨技术
Java架构社区

推荐阅读
关注数
1
内容数
14
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息