Elasticsearch系列---近似匹配

2020-06-01 00:00:00 文档 匹配 召回 短语 近似

### 概要


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


### 短语搜索


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

```java

GET /music/children/_search

{

"query": {

"match_phrase": {

"content": "in the morning"

}

}

}

```


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

1. in the morning 三个单词必须要全部出现

2. the的位置比in大1

3. morning的位置比the大1


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

```java

GET /_analyze

{

"analyzer":"standard",

"text": "in the morning"

}

```


响应结果:

```java

{

"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决定。


例如:

```java

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 1 | pos 2 | pos 3 | pos 4 |

| :---- | :--: | :--: | :--: | -----: |

| DOC | you | make | me | happy |

| query | you | me | | |

| slop 1 | you | -> | me | |


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


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

```java

GET /music/children/_search

{

"query": {

"match_phrase": {

"content": {

"query": "me you",

"slop": 3

}

}

}

}

```


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


| | pos 1 | pos 2 | pos 3 | pos 4 |

| :---- | :--: | :--: | :--: | -----: |

| DOC | you | make | me | happy |

| query | you | me | | |

| slop 1 | me/you | <- | | |

| slop 2 | you ->| me | | |

| slop 3 | you | -> | me | |


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


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


#### 数组类型的slop


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


_id为1的文档数据,tags是这样的:

`"tags": ["enlighten","gymbo","friend"]`


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

```java

GET /music/children/_search

{

"query": {

"match_phrase": {

"tags": {

"query": "enlighten friend",

"slop": 1

}

}

}

}

```


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

```java

GET /music/_analyze

{

"field": "tags",

"text": ["enlighten","gymbo","friend"]

}


```


响应

```java

{

"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同时使用:

```java

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即可。


语法示例:

```java

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架构社区

相关文章