### 概要
前面的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架构社区