当我们在一些主流的搜索引擎(Google,Baidu)或者电商网站(JD,TaoBao,并夕夕)首页的输入框输入单词搜索的时候,有时候我们仅仅输入一半的单词,输入框就可以快速推荐出我们可能要搜索的内容,这个功能还是十分提升用户体验度量的,那么假如你现在接到了产品的需求,要优化你们系统的搜索框,也是可以支持单词或者语句搜索推荐的功能,你要怎么做呢?
其实 ElasticSearch 作为一个强大的搜索引擎中间件,就已经提供了搜索推荐相关的功能,也就是今天我们要介绍的 Suggesters API。
那么 ElasticSearch 又是怎么支持这个功能的呢?其实 Suggesters 会将用户输入的文本进行分解,然后在索引里查找相似的 Term ,根据匹配度进行打分,最终返回打分高的Top文档。 ElasticSearch 提供了 Suggesters 的四种使用场景对应的API。
- Term Suggester:基于单词的纠错补全。
- Phrase Suggester:基于短语的纠错补全。
- Completion Suggester:自动补全单词,输入词语的前半部分,自动补全单词。
- Context Suggester:基于上下文的补全提示,可以实现上下文感知推荐。
在 _search请求中,推荐请求部分和查询部分一起定义,如果省略了查询部分,则只返回建议。
下面是本篇文章所举案例的索引与数据。
PUT /topic
{
"mappings": {
"properties": {
"id": {
"type": "keyword"
},
"name": {
"type": "keyword"
},
"description": {
"type": "text"
}
}
}
}
PUT /topic/_doc/1
{
"id":"1",
"name":"cache_sync",
"description":" cache sync topic"
}
PUT /topic/_doc/2
{
"id":"2",
"name":"binlog_sync",
"description":"topic for mysql binlog sync"
}
PUT /topic/_doc/3
{
"id":"3",
"name":"order_event_notify",
"description":"when order status change,push a message to every consumer group"
}
1. Term Suggester
Term Suggester 是基于单词的纠错,补全,它其实是基于编辑距离(edit instance)来工作的,编辑距离的主旨就是一个单词需要改变多少个字符才可以和另一个单词一致。 所以如果一个单词转化成另一个单词的改变越小,也就是两个单词的相似度越高,被匹配上的几率就越大。
Term Suggester 执行的时候首先会将输入的文本拆分成一个个单词,然后根据每一个单词提供建议,所以不会考虑输入文本中各单词的关系。
For Example:
GET topic/_search
{
"query": {
"match": {
"description": "topic synb"
}
},
"suggest": {
"my_suggest": {
"text": "topic synb",
"term": {
"suggest_mode": "missing",
"field": "description"
}
}
}
}
上面的SQL,我们搜索了 "topic synb" ,其中 "synb" 是拼写错误的。Suggester API 需要在 "suggest" 块中指定使用的参数。类似于聚合查询,"my_suggest" 是这次推荐的名字,"term" 指的是我们使用的是 Term Suggester。
下面介绍 Term Suggester 在开发中可能会用到的参数。
| 字段 | 说明 |
|---|---|
| text | 需要Es帮助推荐的文本内容,一般是用户的输入内容,例子中是:"topic synb"。 |
| field | 指定从文档的哪个字段中获取推荐。 |
| analyzer | 指定分词器来对输入文本进行分词,默认与 field 指定的字段设置的分词器一致。 |
| size | 为每个单词提供的最大建议数量。 |
| sort | 建议结果排序的方式,有以下两个选项: |
score:先按相似性得分排序,然后按文档频率排序,最后按词项本身(字母顺序的等)排序。 | |
frequency:先按文档频率排序,然后按相似性得分排序,最后按词项本身排序。 | |
| suggest_mode | 设置建议的模式。其值有以下几个选项: |
missing:如果索引中存在就不进行建议,默认的选项。上例中使用的是此选项,所以可以看到返回的结果中 "topic" 这个词是没有建议的。 | |
popular:推荐出现频率更高的词。 | |
always:不管是否存在,都进行建议。 |
{
...
"hits" : {
...
"hits" : [
{
"_index" : "topic",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.5989681,
"_source" : {
"id" : "1",
"name" : "cache_sync",
"description" : " cache sync topic"
}
},
{
"_index" : "topic",
"_type" : "_doc",
"_id" : "2",
"_score" : 0.5142972,
"_source" : {
"id" : "2",
"name" : "binlog_sync",
"description" : "topic for mysql binlog sync"
}
}
]
},
"suggest" : {
"my_suggest" : [
{
"text" : "topic",
"offset" : 0,
"length" : 5,
"options" : [ ]
},
{
"text" : "synb",
"offset" : 6,
"length" : 4,
"options" : [
{
"text" : "sync",
"score" : 0.75,
"freq" : 2
}
]
}
]
}
}
了解了 Suggester API 的常用参数,我们再回头来看上面SQL的返回结果。返回结果中对于每个词语的建议放在了 "options" 数组内。如果一个词语有多个建议,这多个推荐会按照 sort 参数指定的方式进行推荐。由于 "topic" 这个单词是存在的,并且 suggest_mode=missing ,所以不进行推荐。
为了避免建议文本的重复,可以定义一个全局文本。在下面的SQL中,建议文本是全局定义的,适用于 "my-suggest-1"和 "my-suggest-2" 建议。
GET topic/_search
{
"suggest": {
"text" : "cache sync",
"my-suggest-1" : {
"term" : {
"field" : "description"
}
},
"my-suggest-2" : {
"term" : {
"field" : "name"
}
}
}
}
如果存在全局的建议文本和针对某一个字段的建议文本,那么针对某一个字段的建议文本会在这个字段的建议上覆盖全局的文本。
2. Phrase Suggester
Term Suggester 产生的推荐是基于单词的,如果想要针对整个短语或者一句话做推荐,Term Suggester 就做不到了,这个时候可以使用 Phrase Suggester。
Phrase Suggester 在 Term Suggester 的基础上增加了补充逻辑分析多个 term 之间的关系,比如相邻的程度,词频等等
For Example:
GET topic/_search
{
"suggest": {
"my_suggest": {
"text": "topic for bimlog mysql sync",
"phrase": {
"field": "description",
"highlight": {
"pre_tag": "<em>",
"post_tag": "</em>"
}
}
}
}
}
## 搜索结果
{
...
"suggest" : {
"my_suggest" : [
{
"text" : "topic for bimlog mysql sync",
"offset" : 0,
"length" : 27,
"options" : [
{
"text" : "topic for binlog mysql sync",
"highlighted" : "topic for <em>binlog</em> mysql sync",
"score" : 0.0022598275
}
]
}
]
}
}
上面的SQL "phrase" 指定使用 Phrase Suggester。我们在查询的时候指定了 "highlight" 参数,所以返回结果中被替换的单词会高亮显示。
开发中 Phrase Suggester 经常用到的参数:
| 参数 | 说明 |
|---|---|
| field | 指定基于哪一个字段进行建议。 |
| real_word_error_likelihood | 即词典中存在某个Term,该Term拼写错误的可能性。默认值为0.95,这意味着5%的真实单词拼写错误。 |
| confidence | 控制返回结果的条数,如果输入的短语得分为 N,name返回结果的得分需要大于 N * confidence 。默认值为1.0。 |
| max_errors | 指定最多可以拼写错误的单词的个数。 |
| separator | 设置两个Term的分隔符。如果未设置,则使用空白字符作为分隔符。 |
| size | 为每个单独的查询项生成的候选项的数量。像3或5这样的低数字通常会产生良好的结果。提高这一点可能会带来编辑距离更高的术语。默认值为5。 |
| analyzer | 设置需要建议的文本的分词器,默认使用指定的建议字段的分词器。 |
| shard_size | 设置要从每个单独的分片中检索的建议术语的最大数量,在reduce阶段,基于size设置的值,返回Top N的建议,默认是5。 |
| text | 需要被建议的文本。 |
| highlight | 高亮字段。 |
3. Completion Suggester
Completion Suggester 支持自动补全功能,其应用场景是用户每输入一个字符就需要返回匹配的结果给用户。在并发量大、用户输入速度快的时候,对服务的吞吐量来说压力比较大。所以 Completion Suggester 不能像上面的 Suggester API 那样简单通过倒排索引来实现,必须通过某些更高效的数据结构和算法才能满足需求。
Completion Suggester 在实现的时候会将 analyze(将文本分词,并且去除没用的词语,例如 is、at这样的词语) 后的数据进行编码,构建为 FST 并且和索引存放在一起。FST是一种高效的前缀查询索引。由于 FST 天生为前缀查询而生,所以其非常适合实现自动补全的功能。ES 会将整个 FST 加载到内存中,所以在使用 FST 进行前缀查询的时候效率是非常高效的。
在使用 Completion Suggester 前需要重新定义 Mapping,对应的字段需要使用 completion type。
准备新的测试数据:
DELETE /topic
PUT /topic
{
"mappings": {
"properties": {
"id": {
"type": "keyword"
},
"name": {
"type": "keyword"
},
"description": {
"type": "text"
},
"test_completion":{
"type": "completion"
}
}
}
}
PUT /topic/_doc/1
{
"id":"1",
"name":"cache_sync",
"description":"cache sync topic",
"test_completion": "cache sync topic"
}
PUT /topic/_doc/2
{
"id":"2",
"name":"binlog_sync",
"description":"topic for mysql binlog sync",
"test_completion": "topic for mysql binlog sync"
}
PUT /topic/_doc/3
{
"id":"3",
"name":"order_event_notify",
"description":"when order status change,push a message to every consumer group",
"test_completion": "when order status change,push a message to every consumer group"
}
For Example:
GET topic/_search
{
"suggest": {
"my_suggest": {
"prefix": "when order statu",
"completion": {
"field": "test_completion"
}
}
}
}
## 返回结果
{
...
"suggest" : {
"my_suggest" : [
{
"text" : "when order statu",
"offset" : 0,
"length" : 16,
"options" : [
{
"text" : "when order status change,push a message to every c",
"_index" : "topic",
"_type" : "_doc",
"_id" : "3",
"_score" : 1.0,
"_source" : {
"id" : "3",
"name" : "order_event_notify",
"description" : "when order status change,push a message to every consumer group",
"test_completion" : "when order status change,push a message to every consumer group"
}
}
]
}
]
}
}
上面的SQL在 "my_suggest" 中,"prefix" 指定了用户输入的内容(需要匹配到前缀数据),"completion" 的 "field" 字段中指定了按照文档的哪一个字段进行匹配。
Completion Suggester在索引数据的时候经过了 analyze 的阶段,所以使用不同的分词器会造成构建 FST 的数据不同,比如某些词被去掉,大小写发生转换。由于构建的数据不同,所以查询的结果就可能不同。
4. Context Suggester
Context Suggester 是 Completion Suggester 的扩展,可以实现上下文感知推荐。例如当我们在缓存同步的topic中查询 "sync" 的时候,可以返回 "cache sync" 相关的topic,但在日志同步的topic中,将会返回 "binlog sync"。 要实现这个功能,可以在文档中加入分类信息,帮助我们做精准推荐。
ES 支持两种类型的上下文:
- Category:任意字符串的分类。
- Geo:地理位置信息。
在索引和查询启用 contexts 的
completion字段时,必须提供 contexts。
在使用 Context Suggester 前,首先要修改 Mapping,然后在数据中加入相关的 Context 信息。准备数据:
PUT /topic
{
"mappings": {
"properties": {
"id": {
"type": "keyword"
},
"name": {
"type": "keyword"
},
"description": {
"type": "text"
},
"test_completion":{
"type": "completion",
"contexts":[
{
"name":"topic_type",
"type":"category"
}
]
}
}
}
}
PUT /topic/_doc/1
{
"id":"1",
"name":"cache_sync",
"description":"cache sync topic",
"test_completion": {
"input":["cache sync topic"],
"contexts":{
"topic_type":"cachesync"
}
}
}
PUT /topic/_doc/2
{
"id":"2",
"name":"binlog_sync",
"description":"topic for mysql binlog sync",
"test_completion": {
"input":["topic for mysql binlog sync"],
"contexts":{
"topic_type":"logsync"
}
}
}
上面的 Mapping,其中 "test_completion" 的类型还是为 "completion",在 "contexts" 中有两个字段,其中 "type" 为上下文的类型,就是上面提到的 Category 和 Geo,我们使用了 Category。而 "name" 则为 上下文的名称(即哪个分类),本例子为 "topic_type"。
导入的数据中,"test_completion" 中的 "input" 字段用于内容匹配。"topic_type" 的值有多个,"cachesync" 是缓存同步类的,"logsync" 是Log同步类的。
GET topic/_search
{
"suggest": {
"my_suggest": {
"prefix": "cac",
"completion": {
"field": "test_completion",
"contexts": {
"topic_type": "cachesync"
}
}
}
}
}
上面的SQL,还是使用 "prefix" 字段来指定需要匹配的前缀数据,它将会与input内的字段进行匹配。而 "contexts" 中指定了 "topic_type" 为 "cachesync"。这条SQL表示:在topic类型为 "cachesync" 的数据里面,推荐 "cac" 开头的书本。
5.在查询结果中返回Suggester类型
有时我们需要知道 suggester 的请求类型是什么,好决定我们怎么解析响应结果。通过 typed_keys 参数可以让我们自定义的建议名中拼接上 suggester 的类型。
For Example:
GET topic/_search?typed_keys
{
"suggest": {
"text" : "some test mssage",
"my-first-suggester" : {
"term" : {
"field" : "description"
}
},
"my-second-suggester" : {
"phrase" : {
"field" : "description"
}
}
}
}
上面这条SQL的返回结果,可以看到,在自定义的建议名中已经拼接上了前缀 suggester类型。
{
...
"suggest" : {
"term#my-first-suggester" : [
{
"text" : "some",
"offset" : 0,
"length" : 4,
"options" : [ ]
},
{
"text" : "test",
"offset" : 5,
"length" : 4,
"options" : [ ]
},
{
"text" : "mssage",
"offset" : 10,
"length" : 6,
"options" : [ ]
}
],
"phrase#my-second-suggester" : [
{
"text" : "some test mssage",
"offset" : 0,
"length" : 16,
"options" : [ ]
}
]
}
}