文章目录
- 1. 什么是自动补全
- 2. 拼音分词器
- 2.1 初识拼音分词器
- 2.2 下载拼音分词器
- 2.3 安装拼音分词器
- 2.4 测试拼音分词器
- 3. 自定义分词器
- 3.1 拼音分词器存在的问题
- 3.2 分词器(analyzer)的组成
- 3.3 如何自定义分词器
- 3.4 拼音分词器的可选参数
- 3.5 配置自定义分词器的tokenizer和filter
- 3.6 如何使用自定义分词器
- 3.7 测试自定义分词器
- 3.7.1 直接测试
- 3.7.2 插入文档测试
- 3.8 使用自定义分词器要注意的事项
- 4. DSL实现自动补全查询
- 4.1 字段的类型的约束
- 4.2 查询语法
- 5. 自动补全案例
- 5.1 准备工作
- 5.1.1 创建hotel索引库
- 5.1.2 导入测试工程
- 5.1.3 导入酒店数据到数据库中
- 5.1.4 将数据库中的数据导入到ElasticSearch
- 5.2 测试自动补全功能
- 6. RestAPI实现自动补全查询
- 7. 综合案例:实现搜索框自动补全
视频教程:SpringCloud+RabbitMQ+Docker+Redis+搜索+分布式,系统详解springcloud微服务技术栈课程|黑马程序员Java微服务
阅读本文前可以先阅读以下文章:
- ElasticSearch快速入门——上篇(认识ElasticSearch、安装ElasticSearch、安装kibana、IK分词器、ElasticSearch中的基本概念、索引库操作、文档操作)
- ElasticSearch快速入门——下篇(在Java代码中操作ElasticSearch、JavaRestClient、操作索引库、操作文档、DSL查询、JavaRestClient查询、数据聚合)
- 通过docker启动ElasticSearch后为ElasticSearch设置用户和密码
1. 什么是自动补全
ElasticSearch 中的自动补全跟我们理解的自动补全不太一样,为了大家理解,我们来看一个案例
当我们在搜索框输入 sj 时,搜索框下方会显示以 sj 拼音首字母开头的词条(如手机、湿巾、数据线、史记、书架等),这个功能被称为自动补全
自动补全功能可以让用户尽可能地搜索到想要的东西,而不需要打出完整的内容
2. 拼音分词器
要想实现自动补全功能,我们需要先学习一下拼音分词器,因为自动补全功能是基于拼音分词器实现的
2.1 初识拼音分词器
拼音分词器的官网:analysis-pinyin
拼音分词器跟我们学过的 IK 分词器相似,都是 ElasticSearch 的一个插件
2.2 下载拼音分词器
下载地址:v7.17.18
本次演示使用的 ElasticSearch 版本为 7.17.18
其它 ElasticSearch 版本对应的拼音分词器的下载地址:Tags
2.3 安装拼音分词器
解压完成之后,将拼音分词器上传到 ElasticSearch 的 plugin 目录下(本次演示是通过 docker 安装 ElasticSearch 的)
先将拼音分词器上传到服务器,一般是当前用户的目录
cd ~
接着将拼音分词器复制到 ElasticSearch 的 plugin 的目录下
sudo cp elasticsearch-analysis-pinyin-7.17.18 -r /var/lib/docker/volumes/elasticsearch-plugins/_data
最后重启 ElasticSearch 容器
sudo docker restart elasticsearch
2.4 测试拼音分词器
我们在 Kibana 提供的控制台中测试拼音分词器是否生效
在浏览器打开 Kibana 提供的控制台
http://127.0.0.1:5601/app/dev_tools#/console
输入以下内容测试拼音分词器是否生效
POST /_analyze
{
"text": [
"练习时长两年半"
],
"analyzer": "pinyin"
}
测试结果如下,主要包含两部分内容:
- 每个字的完整拼音
- 每个字的拼音首字母的合并
{
"tokens" : [
{
"token" : "lian",
"start_offset" : 0,
"end_offset" : 0,
"type" : "word",
"position" : 0
},
{
"token" : "lxsclnb",
"start_offset" : 0,
"end_offset" : 0,
"type" : "word",
"position" : 0
},
{
"token" : "xi",
"start_offset" : 0,
"end_offset" : 0,
"type" : "word",
"position" : 1
},
{
"token" : "shi",
"start_offset" : 0,
"end_offset" : 0,
"type" : "word",
"position" : 2
},
{
"token" : "chang",
"start_offset" : 0,
"end_offset" : 0,
"type" : "word",
"position" : 3
},
{
"token" : "liang",
"start_offset" : 0,
"end_offset" : 0,
"type" : "word",
"position" : 4
},
{
"token" : "nian",
"start_offset" : 0,
"end_offset" : 0,
"type" : "word",
"position" : 5
},
{
"token" : "ban",
"start_offset" : 0,
"end_offset" : 0,
"type" : "word",
"position" : 6
}
]
}
3. 自定义分词器
3.1 拼音分词器存在的问题
拼音分词器还无法正常用于生产环境,因为拼音分词器存在一些问题
以 “练习时长两年半” 这句话为例,拼音分词器存在以下问题:
- “练习时长两年半” 这句话没有被分词,而是作为一个整体出现
- 把 “练习时长两年半” 这句话中的每一个字都形成了一个拼音(用处不大)
- 分词后的结果只剩下拼音,没有汉字
其实我们很少使用拼音搜索,大多数情况下我们都是使用中文去搜索的,分词后有拼音只是锦上添花,分词后的结果中汉字是必须保留的,所以我们需要对拼音分词器做一些配置,也就是自定义分词器
3.2 分词器(analyzer)的组成
ElasticSearch 中分词器(analyzer)的组成有三部分:
- character filters:在 tokenizer 之前对文本进行处理,例如删除字符、替换字符
- tokenizer:将文本按照一定的规则切割成词条(term),例如 keyword(不分词)、ik_smart 等
- tokenizer filter:将 tokenizer 输出的词条做进一步处理,例如大小写转换、同义词处理、拼音处理等
3.3 如何自定义分词器
要想自定义分词器,一定要在创建索引库的时候去设置
我们可以在创建索引库时,通过 settings 来配置自定义的 analyzer(分词器)
自定义分词器时可以只设置分词器(analyzer)的某个部分
PUT /test
{
"settings": {
"analysis": {
"analyzer": {
"my_analyzer": {
"tokenizer": "ik_max_word",
"filter": "pinyin"
}
}
}
}
}
tokenizer 我们使用 ik_max_word,先分词,分好词后再将词条交给拼音分词器处理,这样做可以解决拼音分词器没有分词的问题
但是拼音分词器还存在两个问题:分词后的每一个字都形成了一个拼音、分词后的结果只剩下拼音,没有汉字
3.4 拼音分词器的可选参数
我们需要对拼音分词器做进一步的定制
在拼音分词器的官网上,给出了很多的可选参数(Optional Parameters)
参数名称 | 含义 |
---|---|
keep_first_letter | 启用后,只保留每个汉字的第一个字母。例如,刘德华变为ldh。默认:true。 |
keep_separate_first_letter | 启用后,保留每个汉字的第一个字母,并分别显示。例如,刘德华变为l,d,h。默认:false。注意:这可能会因词频增加查询的模糊度。 |
limit_first_letter_length | 设置第一个字母结果的最大长度。默认:16。 |
keep_full_pinyin | 启用后,保留每个汉字的完整拼音。例如,刘德华变为[liu,de,hua]。默认:true。 |
keep_joined_full_pinyin | 启用后,将每个汉字的完整拼音连接起来。例如,刘德华变为[liudehua]。默认:false。 |
keep_none_chinese | 保留结果中的非汉字字母或数字。默认:true。 |
keep_none_chinese_together | 保留非汉字字母在一起。默认:true。例如,DJ音乐家变为DJ,yin,yue,jia。当设置为false时,DJ音乐家变为D,J,yin,yue,jia。注意:需要先启用keep_none_chinese。 |
keep_none_chinese_in_first_letter | 在首字母中保留非汉字字母。例如,刘德华AT2016变为ldhat2016。默认:true。 |
keep_none_chinese_in_joined_full_pinyin | 在连接的完整拼音中保留非汉字字母。例如,刘德华2016变为liudehua2016。默认:false。 |
none_chinese_pinyin_tokenize | 如果非汉字字母是拼音,将其拆分为单独的拼音词。默认:true。例如,liudehuaalibaba13zhuanghan变为liu,de,hua,a,li,ba,ba,13,zhuang,han。注意:需要先启用keep_none_chinese和keep_none_chinese_together。 |
keep_original | 启用后,保留原始输入。默认:false。 |
lowercase | 将非汉字字母转换为小写。默认:true。 |
trim_whitespace | 默认:true。 |
remove_duplicated_term | 启用后,移除重复的词以节省索引空间。例如,de的变为de。默认:false。注意:可能与位置相关的查询受到影响。 |
ignore_pinyin_offset | 在6.0版本之后,偏移量受到严格限制,不允许重叠的词。通过此参数,将允许重叠的词,忽略偏移量。请注意,所有与位置相关的查询或高亮将变得不正确。如果需要偏移量,请设置为false。默认:true。 |
3.5 配置自定义分词器的tokenizer和filter
PUT /test
{
"settings": {
"analysis": {
"analyzer": {
"my_analyzer": {
"tokenizer": "ik_max_word",
"filter": "py"
}
},
"filter": {
"py": {
"type": "pinyin",
"keep_full_pinyin": false,
"keep_joined_full_pinyin": true,
"keep_original": true,
"limit_first_letter_length": 16,
"remove_duplicated_term": true,
"none_chinese_pinyin_tokenize": false
}
}
}
}
}
创建一个自定义的分词器my_analyzer
,使用ik_max_word
分词器进行中文分词,并通过pinyin
过滤器将中文词条转换为拼音,保留了原始中文词条和连接起来的全拼,同时限制了首字母长度并移除重复的词条
3.6 如何使用自定义分词器
自定义分词器创建好了之后,该怎么使用呢
要使用自定义分词器,我们需要在定义索引库字段(Mapping)的时候使用
PUT /test
{
"settings": {
"analysis": {
"analyzer": {
"my_analyzer": {
"tokenizer": "ik_max_word",
"filter": "py"
}
},
"filter": {
"py": {
"type": "pinyin",
"keep_full_pinyin": false,
"keep_joined_full_pinyin": true,
"keep_original": true,
"limit_first_letter_length": 16,
"remove_duplicated_term": true,
"none_chinese_pinyin_tokenize": false
}
}
}
},
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "my_analyzer"
}
}
}
}
3.7 测试自定义分词器
3.7.1 直接测试
POST /test/_analyze
{
"text": [
"练习时长两年半"
],
"analyzer": "my_analyzer"
}
测试结果
{
"tokens" : [
{
"token" : "练习",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "lianxi",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "lx",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "时长",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "shichang",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "sc",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "两年",
"start_offset" : 4,
"end_offset" : 6,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "liangnian",
"start_offset" : 4,
"end_offset" : 6,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "ln",
"start_offset" : 4,
"end_offset" : 6,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "两",
"start_offset" : 4,
"end_offset" : 5,
"type" : "COUNT",
"position" : 3
},
{
"token" : "liang",
"start_offset" : 4,
"end_offset" : 5,
"type" : "COUNT",
"position" : 3
},
{
"token" : "l",
"start_offset" : 4,
"end_offset" : 5,
"type" : "COUNT",
"position" : 3
},
{
"token" : "年半",
"start_offset" : 5,
"end_offset" : 7,
"type" : "CN_WORD",
"position" : 4
},
{
"token" : "nianban",
"start_offset" : 5,
"end_offset" : 7,
"type" : "CN_WORD",
"position" : 4
},
{
"token" : "nb",
"start_offset" : 5,
"end_offset" : 7,
"type" : "CN_WORD",
"position" : 4
}
]
}
3.7.2 插入文档测试
测试数据如下(狮子和虱子的拼音是一样的)
POST /test/_doc/1
{
"id": 1,
"name": "狮子"
}
POST /test/_doc/2
{
"id": 2,
"name": "虱子"
}
我们先通过拼音 shizi 来搜索
GET /test/_search
{
"query": {
"match": {
"name": "shizi"
}
}
}
成功搜索出狮子和虱子
但如果我们搜索的内容是掉入狮子笼怎么办呢
GET /test/_search
{
"query": {
"match": {
"name": "掉入狮子笼怎么办"
}
}
}
从搜索结果中我们可以发现,我们明明搜索的是狮子,怎么虱子也搜索出来了?
这说明我们自定义的分词器有问题,在用拼音搜索时确实没问题,但是在用中文搜索时却搜出了同音词
3.8 使用自定义分词器要注意的事项
拼音分词器适合在创建倒排索引的时候使用,但不能在搜索的时候使用
创建倒排索引时
用户搜索狮子,搜索结果中居然出现了虱子
所以,我们在创建倒排索引时使用的分词器要和搜索时使用的分词器分开
怎么分开呢,在创建倒排索引时使用 my_analyzer 分词器,搜索时使用 ik_smart 分词器
PUT /test
{
"settings": {
"analysis": {
"analyzer": {
"my_analyzer": {
"tokenizer": "ik_max_word",
"filter": "py"
}
},
"filter": {
"py": {
"type": "pinyin",
"keep_full_pinyin": false,
"keep_joined_full_pinyin": true,
"keep_original": true,
"limit_first_letter_length": 16,
"remove_duplicated_term": true,
"none_chinese_pinyin_tokenize": false
}
}
}
},
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "my_analyzer",
"search_analyzer": "ik_smart"
}
}
}
}
我们删除 test 索引库之后,重写创建 test 索引库进行测试
DELETE /test
GET /test/_search
{
"query": {
"match": {
"name": "掉入狮子笼怎么办"
}
}
}
测试结果如下(可以看到,搜索结果中没有虱子了)
4. DSL实现自动补全查询
ElasticSearch 提供了 Completion suggester 查询来实现自动补全功能,这个查询会匹配以用户输入内容开头的词条并
返回
4.1 字段的类型的约束
为了提高补全查询的效率,对于文档中字段的类型有一些约束:
- 参与补全查询的字段必须是 completion 类型
- 字段的内容一般是用来补全的多个词条形成的数组
4.2 查询语法
索引库
PUT test2
{
"mappings": {
"properties": {
"title":{
"type": "completion"
}
}
}
}
测试数据
POST test2/_doc
{
"title": ["Sony", "WH-1000XM3"]
}
POST test2/_doc
{
"title": ["SK-II", "PITERA"]
}
POST test2/_doc
{
"title": ["Nintendo", "switch"]
}
执行查询操作
POST /test2/_search
{
"suggest": {
"title_suggest": {
"text": "s",
"completion": {
"field": "title",
"skip_duplicates": true,
"size": 10
}
}
}
}
查询结果(查询结果中包含了文档的原始信息)
5. 自动补全案例
我们来做一个关于酒店数据的自动补全案例
5.1 准备工作
5.1.1 创建hotel索引库
PUT /hotel
{
"settings": {
"analysis": {
"analyzer": {
"text_anlyzer": {
"tokenizer": "ik_max_word",
"filter": "py"
},
"completion_analyzer": {
"tokenizer": "keyword",
"filter": "py"
}
},
"filter": {
"py": {
"type": "pinyin",
"keep_full_pinyin": false,
"keep_joined_full_pinyin": true,
"keep_original": true,
"limit_first_letter_length": 16,
"remove_duplicated_term": true,
"none_chinese_pinyin_tokenize": false
}
}
}
},
"mappings": {
"properties": {
"id": {
"type": "keyword"
},
"name": {
"type": "text",
"analyzer": "text_anlyzer",
"search_analyzer": "ik_smart",
"copy_to": "all"
},
"address": {
"type": "keyword",
"index": false
},
"price": {
"type": "integer"
},
"score": {
"type": "integer"
},
"brand": {
"type": "keyword",
"copy_to": "all"
},
"city": {
"type": "keyword"
},
"starName": {
"type": "keyword"
},
"business": {
"type": "keyword",
"copy_to": "all"
},
"location": {
"type": "geo_point"
},
"pic": {
"type": "keyword",
"index": false
},
"all": {
"type": "text",
"analyzer": "text_anlyzer",
"search_analyzer": "ik_smart"
},
"suggestion": {
"type": "completion",
"analyzer": "completion_analyzer",
"search_analyzer": "ik_smart"
}
}
}
}
5.1.2 导入测试工程
测试工程的 Gitee 地址:hotel-demo
5.1.3 导入酒店数据到数据库中
SQL 脚本在测试工程的 doc 目录下
5.1.4 将数据库中的数据导入到ElasticSearch
导入数据前,更改与连接 ElasticSearch 相关的信息(如果 ElasticSearch 没有设置密码,可以去除 setHttpClientConfigCallback 代码)
运行 HotelDocumentTest
测试类中的 testBulkRequest 方法,将数据库中的数据导入到 ElasticSearch
在 Kibana 提供的控制台检查数据是否导入成功
GET /hotel/_search
{
"query": {
"match_all": {}
}
}
5.2 测试自动补全功能
在 Kibana 提供的控制台测试自动补全功能
GET /hotel/_search
{
"suggest": {
"suggestions": {
"text": "s",
"completion": {
"field": "suggestion",
"skip_duplicates": true,
"size": 10
}
}
}
}
测试结果
6. RestAPI实现自动补全查询
构建请求参数的 API
结果解析
import cn.itcast.hotel.service.IHotelService;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.search.suggest.Suggest;
import org.elasticsearch.search.suggest.SuggestBuilder;
import org.elasticsearch.search.suggest.SuggestBuilders;
import org.elasticsearch.search.suggest.completion.CompletionSuggestion;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.IOException;
import java.util.List;
@SpringBootTest
class HotelSuggestionTest {
private RestHighLevelClient restHighLevelClient;
@Autowired
private IHotelService hotelService;
@Test
void testSuggestion() throws IOException {
// 1.准备SearchRequest
SearchRequest searchRequest = new SearchRequest("hotel");
// 2.准备DSL
searchRequest.source().suggest(new SuggestBuilder().addSuggestion(
"suggestions",
SuggestBuilders.completionSuggestion("suggestion")
.prefix("h")
.skipDuplicates(true)
.size(10)
));
// 3.发送请求
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 4.解析结果
// 4.1.获取suggest对象
Suggest suggest = searchResponse.getSuggest();
// 4.2.根据名称获取suggestion对象
CompletionSuggestion suggestion = suggest.getSuggestion("suggestions");
// 4.3.获取options
List<CompletionSuggestion.Entry.Option> options = suggestion.getOptions();
// 4.4.遍历
for (CompletionSuggestion.Entry.Option option : options) {
System.out.println("option.getText().string() = " + option.getText().string());
}
}
@BeforeEach
void setUp() {
// 用户名和密码
String username = "elastic";
String password = "tF8RGg2vd0FAzgkK";
final BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password));
RestClientBuilder restClientBuilder = RestClient
.builder(new HttpHost("127.0.0.1", 9200, "http"))
.setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider));
restHighLevelClient = new RestHighLevelClient(restClientBuilder);
}
@AfterEach
void tearDown() throws IOException {
restHighLevelClient.close();
}
}
7. 综合案例:实现搜索框自动补全
测试工程已实现搜索框自动补全,启动测试工程后,在浏览器中查看搜索框的自动补全效果
http://localhost:8089/
前端源代码的 Gitee 地址:auto-complete