Blogger Information
Blog 15
fans 0
comment 0
visits 9353
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template
Elasticsearch系列---前缀搜索和模糊搜索
Tina
Original
906 people have browsed it

### 概要


本篇我们介绍一下部分搜索的几种玩法,我们经常使用的浏览器搜索框,输入时会弹出下拉提示,也是基于局部搜索原理实现的。


### 前缀搜索


我们在前面了解的搜索,词条是最小的匹配单位,也是倒排索引中存在的词,现在我们来聊聊部分匹配的话题,只匹配一个词条中的一部分内容,相当于mysql的"where content like '%love%'",在数据库里一眼就能发现这种查询是不走索引的,效率非常低。


Elasticsearch对这种搜索有特殊的拆分处理,支持多种部分搜索格式,这次重点在于not_analyzed精确值字段的前缀匹配。


#### 前缀搜索语法


我们常见的可能有前缀搜需求的有邮编、产品序列号、快递单号、证件号的搜索,这些值的内容本身包含一定的逻辑分类含义,如某个前缀表示地区、年份等信息,我们以邮编为例子:


```java


# 只创建一个postcode字段,类型为keyword

PUT /demo_index

{

"mappings": {

"address": {

"properties": {

"postcode": {

"type": "keyword"

}

}

}

}

}


# 导入一些示例的邮编

POST /demo_index/address/_bulk

{ "index": { "_id": 1 }}

{ "postcode" : "510000"}

{ "index": { "_id": 2 }}

{ "postcode" : "514000"}

{ "index": { "_id": 3 }}

{ "postcode" : "527100"}

{ "index": { "_id": 4 }}

{ "postcode" : "511500"}

{ "index": { "_id": 5 }}

{ "postcode" : "511100"}

```


前缀搜索示例:

```java

GET /demo_index/address/_search

{

"query": {

"prefix": {

"postcode": {

"value": "511"

}

}

}

}

```


搜索结果可以看到两条,符合预期:

```java

{

"took": 3,

"timed_out": false,

"_shards": {

"total": 5,

"successful": 5,

"skipped": 0,

"failed": 0

},

"hits": {

"total": 2,

"max_score": 1,

"hits": [

{

"_index": "demo_index",

"_type": "address",

"_id": "5",

"_score": 1,

"_source": {

"postcode": "511100"

}

},

{

"_index": "demo_index",

"_type": "address",

"_id": "4",

"_score": 1,

"_source": {

"postcode": "511500"

}

}

]

}

}

```


#### 前缀搜索原理

prefix query不计算relevance score,_score固定为1,与prefix filter唯一的区别就是,filter会cache bitset。


我们分析一下示例的搜索过程:

1. 索引文档时,先建立倒排索引,keyword没有分词的操作,直接建立索引,简易示例表如下:


| postcode | doc ids |

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

| 510000 | 1 |

| 514000 | 2 |

| 527100 | 3 |

| 511500 | 4 |

| 511100 | 5 |


2. 如果是全文搜索,搜索字符串"511",没有匹配的结果,直接返回为空。

3. 如果是前缀搜索,扫描到一个"511500",继续搜索,又得到一个"511100",继续搜,直到整个倒排索引全部搜索完,返回结果结束。


从这个过程我们可以发现:match的搜索性能还是非常高的,前缀搜索由于要遍历索引,性能相对低一些,但有些场景,却是只有前缀搜索才能胜任。如果前缀的长度越长,那么能匹配的文档相对越少,性能会好一些,如果前缀太短,只有一个字符,那么匹配数据量太多,会影响性能,这点要注意。


#### 通配符和正则搜索


通配符搜索和正则表达式搜索跟前缀搜索类型,只是功能更丰富一些。


##### 通配符


常规的符号:?任意一个字符,0个或任意多个字符,示例:

```java

GET /demo_index/address/_search

{

"query": {

"wildcard": {

"postcode": {

"value": "*110*"

}

}

}

}

```


##### 正则搜索


即搜索字符串是用正则表达式来写的,也是常规的格式:


```java

GET /demo_index/address/_search

{

"query": {

"regexp": {

"postcode": {

"value": "[0-9]11.+"

}

}

}

}

```


这两种算是一种高级语法介绍,可以让我们编写更灵活的查询请求,但性能都不怎么好,所以用得也不多。


### 即时搜索


我们使用搜索引擎时,会发现搜索框里会有相关性的词语提示,如下我们在google网站上搜索"Elasticsearch"时,会有这样的提示框出现:


![](https://imgkr.cn-bj.ufileos.com/449ebb87-7791-456c-bf16-89d8c058e5ea.png)



浏览器捕捉每一个输入事件,每输入一个字符,向后台发一次请求,将你搜索的内容作为搜索前缀,搜索相关的当前热点的前10条数据,返回给你,用来辅助你完成输入,baidu也有类似的功能。


这种实现原理是基于前缀搜索来完成的,只是google/baidu的后台实现更复杂,我们可以站在Elasticsearch的视角上来模拟即时搜索:


```java

GET /demo_index/website/_search

{

"query": {

"match_phrase_prefix": {

"title": "Elasticsearch q"

}

}

}

```


原理跟match_phrase,只是最后一个term是作前缀来搜索的。

即搜索字符串"Elasticsearch q",Elasticsearch做普通的match查询,而"q"作前缀搜索,会去扫描整个倒排索引,找到所有q开头的文档,然后找到所有文档中,既包含Elasticsearch,又包含以q开头字符的文档。


当然这个查询支持slop参数。


##### max_expansions参数


前缀查询时我们提到了前缀太短会有性能的风险,此时我们可以通过max_expansions参数来降低前缀过短带来的性能问题,建议的值是50,如下示例:


```java

GET /demo_index/website/_search

{

"query": {

"match_phrase_prefix": {

"postcode": {

"query": "Elasticsearch q",

"max_expansions": 50

}

}

}

}

```


max_expansions的作用是控制与前缀匹配的词的数量,它会先查找第一个与前缀"q" 匹配的词,然后依次查找搜集与之匹配的词(按字母顺序),直到没有更多可匹配的词或当数量超过max_expansions时结束。


我们使用google搜索资料时,关键是输一个字符请求一次,这样我们就可以使用max_expansions去控制匹配的文档数量,因为我们会不停的输入,直到想要搜索的内容输入完毕或挑到合适的提示语之后,才会点击搜索按钮进行网页的搜索。


所以使用match_phrase_prefix记得一定要带上max_expansions参数,要不然输入第一个字符的时候,性能实在是太低了。


### ngram的应用


前面我们用的部分查询,没有作索引做过特殊的设置,这种解决方案叫做查询时(query time)实现,这种无侵入性和灵活性通常以牺牲搜索性能为代价,还有一种方案叫索引时(index time),对索引的设置有侵入,提前完成一些搜索的准备工作,对性能提升有非常大的帮助。如果某些功能的实时性要求比较高,由查询时转为索引时是一个非常好的实践。


前缀搜索功能看具体的使用场景,如果是在一级功能的入口处,承担着大部分的流量,建议使用索引时,我们先来了解一下ngram。


#### ngrams是什么


前缀查询是通过挨个匹配来达到查找目的的,整个过程有些盲目,搜索量又大,所以性能比较低,但如果我事先把这些关键词,按照一定的长度拆分出来,就又可以回到match查询这种高效率的方式了。ngrams其实就是拆分关键词的一个滑动窗口,窗口的长度可以设置,我们拿"Elastic"举例,7种长度下的ngram:

- 长度1:[E,l,a,s,t,i,c]

- 长度2:[El,la,as,st,ti,ic]

- 长度3:[Ela,las,ast,sti,tic]

- 长度4:[Elas,last,asti,stic]

- 长度5:[Elast,lasti,astic]

- 长度6:[Elasti,lastic]

- 长度7:[Elastic]


可以看到,长度越长,拆分的词越少。

每个拆分出来的词都会加入到倒排索引中,这样就可以进行match搜索了。


还有一种特殊的edge ngram,拆词时它只留下首字母开头的词,如下:

- 长度1:E

- 长度2:El

- 长度3:Ela

- 长度4:Elas

- 长度5:Elast

- 长度6:Elasti

- 长度7:Elastic


这样的拆分特别符合我们的搜索习惯。


#### 案例

1. 创建一个索引,指定filter

```java

PUT /demo_index

{

"settings": {

"analysis": {

"filter": {

"autocomplete_filter": {

"type": "edge_ngram",

"min_gram": 1,

"max_gram": 20

}

},

"analyzer": {

"autocomplete": {

"type": "custom",

"tokenizer": "standard",

"filter": [

"lowercase",

"autocomplete_filter"

]

}

}

}

}

}

```


filter的意思是对于这个token过滤器接收的任意词项,过滤器会为之生成一个最小固定值为1,最大为20的n-gram。


2. 在自定义分析器autocomplete中使用上面这个token过滤器


```java

PUT /demo_index/_mapping/_doc

{

"properties": {

"title": {

"type": "text",

"analyzer": "autocomplete",

"search_analyzer": "standard"

}

}

}

```


3. 我们可以测试一下效果


```java

GET /demo_index/_analyze

{

"analyzer": "autocomplete",

"text": "love you"

}

```


响应结果:


```java

{

"tokens": [

{

"token": "l",

"start_offset": 0,

"end_offset": 4,

"type": "<ALPHANUM>",

"position": 0

},

{

"token": "lo",

"start_offset": 0,

"end_offset": 4,

"type": "<ALPHANUM>",

"position": 0

},

{

"token": "lov",

"start_offset": 0,

"end_offset": 4,

"type": "<ALPHANUM>",

"position": 0

},

{

"token": "love",

"start_offset": 0,

"end_offset": 4,

"type": "<ALPHANUM>",

"position": 0

},

{

"token": "y",

"start_offset": 5,

"end_offset": 8,

"type": "<ALPHANUM>",

"position": 1

},

{

"token": "yo",

"start_offset": 5,

"end_offset": 8,

"type": "<ALPHANUM>",

"position": 1

},

{

"token": "you",

"start_offset": 5,

"end_offset": 8,

"type": "<ALPHANUM>",

"position": 1

}

]

}

```


测试结果符合预期。


4. 增加一点测试数据


```java

PUT /demo_index/_doc/_bulk

{ "index": { "_id": "1"} }

{ "title" : "love"}

{ "index": { "_id": "2"}}

{"title" : "love me"} }

{ "index": { "_id": "3"}}

{"title" : "love you"} }

{ "index": { "_id": "4"}}

{"title" : "love every one"}

```


5. 使用简单的match查询

```java

GET /demo_index/_doc/_search

{

"query": {

"match": {

"title": "love ev"

}

}

}

```


响应结果:

```java

{

"took": 1,

"timed_out": false,

"_shards": {

"total": 5,

"successful": 5,

"skipped": 0,

"failed": 0

},

"hits": {

"total": 2,

"max_score": 0.83003354,

"hits": [

{

"_index": "demo_index",

"_type": "_doc",

"_id": "4",

"_score": 0.83003354,

"_source": {

"title": "love every one"

}

},

{

"_index": "demo_index",

"_type": "_doc",

"_id": "1",

"_score": 0.41501677,

"_source": {

"title": "love"

}

}

]

}

}

```



如果用match,只有love的也会出来,全文检索,只是分数比较低。




6. 使用match_phrase


推荐使用match_phrase,要求每个term都有,而且position刚好靠着1位,符合我们的期望的。


```java

GET /demo_index/_doc/_search

{

"query": {

"match_phrase": {

"title": "love ev"

}

}

}

```


我们可以发现,大多数工作都是在索引阶段完成的,所有的卖二手手机号码平台查询只需要执行match或match_phrase即可,比前缀查询效率高了很多。


### 搜索提示


Elasticsearch还支持completion suggest类型实现搜索提示,也叫自动完成auto completion。


#### completion suggest原理


建立索引时,要指定field类型为completion,Elasticsearch会为搜索字段生成一个所有可能完成的词列表,然后将它们置入一个有限状态机(finite state transducer 内,这是个经优化的图结构。


执行搜索时,Elasticsearch从图的开始处顺着匹配路径一个字符一个字符地进行匹配,一旦它处于用户输入的末尾,Elasticsearch就会查找所有可能结束的当前路径,然后生成一个建议列表,并且把这个建议列表缓存在内存中。


性能方面completion suggest比任何一种基于词的查询都要快很多。


#### 示例


1. 指定title.fields字段为completion类型

```java

PUT /music

{

"mappings": {

"children" :{

"properties": {

"title": {

"type": "text",

"fields": {

"suggest": {

"type":"completion"

}

}

},

"content": {

"type": "text"

}

}

}

}

}


```


2. 插入一些示例数据


```java

PUT /music/children/_bulk

{ "index": { "_id": "1"} }

{ "title":"children music London Bridge", "content":"London Bridge is falling down"}

{ "index": { "_id": "2"}}

{"title":"children music Twinkle", "content":"twinkle twinkle little star"}

{ "index": { "_id": "3"}}

{"title":"children music sunshine", "content":"you are my sunshine"}

```


3. 搜索请求及响应


```java

GET /music/children/_search

{

"suggest": {

"my-suggest": {

"prefix": "children music",

"completion": {

"field":"title.suggest"

}

}

}

}

```


响应如下,有删节:

```java

{

"took": 26,

"timed_out": false,

"suggest": {

"my-suggest": [

{

"text": "children music",

"offset": 0,

"length": 14,

"options": [

{

"text": "children music London Bridge",

"_index": "music",

"_type": "children",

"_id": "1",

"_score": 1,

"_source": {

"title": "children music London Bridge",

"content": "London Bridge is falling down"

}

},

{

"text": "children music Twinkle",

"_index": "music",

"_type": "children",

"_id": "2",

"_score": 1,

"_source": {

"title": "children music Twinkle",

"content": "twinkle twinkle little star"

}

},

{

"text": "children music sunshine",

"_index": "music",

"_type": "children",

"_id": "3",

"_score": 1,

"_source": {

"title": "children music sunshine",

"content": "you are my sunshine"

}

}

]

}

]

}

}

```


这样返回的值,就可以作为提示语补充到前端页面上,如数据填充到浏览器的下拉框里。


### 模糊搜索


fuzzy搜索可以针对输入拼写错误的单词,有一定的纠错功能,示例:

```java

GET /music/children/_search

{

"query": {

"fuzzy": {

"name": {

"value": "teath",

"fuzziness": 2

}

}

}

}

```


fuzziness:最多纠正的字母个数,默认是2,有限制,设置太大也是无效的,不能无限加大,错误太多了也纠正不了。


常规用法:match内嵌套一个fuzziness,设置为auto。

```java

GET /music/children/_search

{

"query": {

"match": {

"name": {

"query": "teath",

"fuzziness": "AUTO",

"operator": "and"

}

}

}

}

```


了解一下即可。


### 小结


本篇介绍了前缀搜索,通配符搜索和正则搜索的基本玩法,对前缀搜索的性能影响和控制手段做了简单讲解,ngram在索引时局部搜索和搜索提示是非常经典的做法,最后顺带介绍了一下模糊搜索的常规用法,可以了解一下。


Statement of this Website
The copyright of this blog article belongs to the blogger. Please specify the address when reprinting! If there is any infringement or violation of the law, please contact admin@php.cn Report processing!
All comments Speak rationally on civilized internet, please comply with News Comment Service Agreement
0 comments
Author's latest blog post