一、自动补全需求说明
当用户在搜索框输入字符时,就应该提示出与该字符有关的搜索项,如图:
二、安装拼音分词器
要实现根据字母做补全,就必须对文档按照拼音分词。在GitHub上恰好有elasticsearch的拼音分词插件。下载地址:https://github.com/medcl/elasticsearch-analysis-pinyin,安装方式与IK分词器一样,分四步:
2.1.下载
这里插件的版本需要和elasticsearch的版本需要保持一致,我的elasticsearch版本是7.17.5,那么拼音分词器的版本应该也是7.17.5,如下:
2.2.解压
下载下来的分词器进行解压,
2.3.上传到虚拟机中,elasticsearch的plugin目录
将上面解压后的整个文件夹上传到/var/lib/docker/volumes/es-plugins/_data/目录,如下:
2.4.重启elasticsearch
这里重启容器即可:
2.5.测试
测试分词查询 :使用ik_max_word:进行分词 :它分的词语比ik_smart:分的词语更多
使用拼音分词器
上面这就是拼音分词器的用法,以后我们在创建拼音分词器的时候可以mapping映射去定义拼音分词器,作为我们的分词器使用了
三、自定义分词器
上面的拼音分词器,还有一些问题,这里把拼音的首字母放到这里,也说明了这句话没有被分词,而是作为一个整体出现的,还把每一个字都形成了一个拼音,这也没什么用,这里只剩下了拼音,我们用拼音搜索的情况占少数的,大多数情况下我们想用中文搜索
3.1.自定义分词器说明
elasticsearch中分词器(analyzer)的组成包含三部分:
- characterfilters:在tokenizer之前对文本进行处理。例如删除字符、替换字符
- tokenizer:将文本按照一定的规则切割成词条(term)。例如keyword,就是不分词;还有ik_smart
- tokenizerfilter:将tokenizer输出的词条做进一步处理。例如大小写转换、同义词处理、拼音处理等
上面的自定义分词器例子就解决了拼音分词器不能分词的问题
3.2.自定义分词器语法
3.2.1.语法
我们可以在创建索引库时,通过settings来配置自定义的analyzer(分词器),语法如下:
上面的参数:
- tokenizer:进行分词
- keep_joined_full pinyin:分词全拼
- Keep_original:要不要保持中文
3.2.2.创建索引库
执行上面创建索引库,自定义分词器:
# 自定义分词器
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.2.3.测试
往之前创建的索引库test中添加两条数据,如下:
POST /test/_doc/1
{
"id":1,
"name":"狮子"
}
POST /test/_doc/2
{
"id":2,
"name":"虱子"
}
然后根据中文进行查询
GET /test/_search
{
"query": {
"match": {
"name": "在动物园能看到狮子"
}
}
}
执行后结果如下:
根据shizi的拼音搜索,发现把同音字也搜到了,这是有问题的,如下
3.4.自定义分词器使用场景
上面根据shizi的拼音搜索,发现把同音字也搜到了,这是有问题的,拼音分词器适合在创建倒排索引的时候使用,但不能在搜索的时候使用。创建倒排索时
这就是问题的所在,在搜索时也用了拼音选择器,拿拼音去搜,就搜出来2条数据,在创建的时候可以用拼育选择器,在搜索的时候不应该用拼音选择器,在搜索的时候用户输入的是中文,用中文去搜,输入的是拼音,才拿拼音去搜。
3.5.创建倒排索引使用自定义分词器
因此字段在创建倒排索引时应该用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"
}
}
}
}
先删除创建的索引库,在创建
DELETE test
然后插入两条数据
POST /test/_doc/1
{
"id":1,
"name":"狮子"
}
POST /test/_doc/2
{
"id":2,
"name":"虱子"
}
然后根据中文进行分词检索如下:
GET /test/_search
{
"query": {
"match": {
"name": "动物园的狮子是体型最大的动物吗?"
}
}
}
执行后发现只出现了一条数据,不会把相同pinyin的文档信息查询出来
四、DSL实现自动补全查询
4.1.completion suggester查询
elasticsearch提供了Completion Suggester查询来实现自动补全功能。这个查询会匹配以用户输入内容开头的词条并返回。为了提高补全查询的效率,对于文档中字段的类型有一些约束:
- 参与补全查询的字段必须是completion类型。
- 字段的内容一般是用来补全的多个词条形成的数组。
语法如下:
# 创建索引库
PUT test
{
"mappings": {
"properties": {
"title":{
"type":"completion"
}
}
}
}
示例数据
# 示例数据
POST test/_doc
{
"title":["Sony","WH-1000XM3"]
}
POST test/_doc
{
"title":["SK-II","PITERA"]
}
POST test/_doc
{
"title":["Nintendo","switch"]4
}
4.2.completion suggester查询
语法如下:
# 自动补全查询
GET /test/_search
{
"suggest": {
"title_suggest": {
"text": "s",//关键字
"completion": {
"field": "title",//补全查询的字段
"skip_duplicates":true,//跳过重复的
"size":10//获取前10条结果
}
}
}
}
测试:删除之前的文档库test,然后执行4.1中创建索引库的操作,在插入三条数据,然后执行上面的DSL代码。搜索s开头的信息,如下:
五、修改酒店索引库结构
拼音搜索功能实现hotel索引库的自动补全,实现思路如下:
- 修改hotel索引库结构,设置自定义拼音分词器
- 修改索引库的name、all字段,使用自定义分词器
- 索引库添加一个新字段suggestion,类型为completion类型,使用自定义的分词器
- 给Hotel Doc类添加suggestion字段,内容包含brand、business
- 重新导入数据到hotel库
注意:name、all是可分词的,自动补全的brand、business是不可分词的,要使用不同的分词器组合
5.1.准备DSL
先把之前的索引库删除,再次创建文档索引库hotel:
# 先删除之前创建的文档库
DELETE 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"
}
}
}
}
5.2.修改实体类HotelDoc
上面添加了一个sugession字段作为自动补全字段使用,所以对应的java代码里面也要加一个sugession字段
使用Arrays.asList()进行集合的使用,然后把brand和business放进这个集合,将来可以根据这个自动补全
@Data
@NoArgsConstructor
public class HotelDoc {
private Long id;
private String name;
private String address;
private Integer price;
private Integer score;
private String brand;
private String city;
private String starName;
private String business;
private String location;
private String pic;
//保存到中心点的距离
private Object distance;
private Boolean isAD;
//completion类型的值一个一个的数组类型的词条,对应在java中用List集合接收
private List<String> suggestion;
public HotelDoc(Hotel hotel) {
this.id = hotel.getId();
this.name = hotel.getName();
this.address = hotel.getAddress();
this.price = hotel.getPrice();
this.score = hotel.getScore();
this.brand = hotel.getBrand();
this.city = hotel.getCity();
this.starName = hotel.getStarName();
this.business = hotel.getBusiness();
this.location = hotel.getLatitude() + ", " + hotel.getLongitude();
this.pic = hotel.getPic();
//内容是品牌和商品,形成集合
this.suggestion = Arrays.asList(this.brand,this.business);
}
}
5.3.运行执行给hotel文档导入数据的单元测试代码
由于之前删除了文档库,重新创建文档库,对于文档库hotel中做了自动补全的处理修改,数据需要重新导入
@Test
void some() throws IOException {
List<Hotel> hotels = service.list();
BulkRequest request=new BulkRequest();
for (Hotel hotel : hotels) {
HotelDoc doc=new HotelDoc(hotel);
request.add(new IndexRequest("hotel").id(hotel.getId().toString()).source(JSON.toJSONString(doc),XContentType.JSON));
}
client.bulk(request,RequestOptions.DEFAULT);
}
查询数据,进行测试,发现可以看到suggestion字段,其值是由酒店品牌和商圈构建出来的
但是像上面的 "江湾、五角商业广场",但是有的使用、有的使用/分割,是由两个值构成,所以需要进行切割,所以改造实体类代码:Collections集合工具类中的addAll()往集合中一次性添加多个元素
@Data
@NoArgsConstructor
public class HotelDoc {
private Long id;
private String name;
private String address;
private Integer price;
private Integer score;
private String brand;
private String city;
private String starName;
private String business;
private String location;
private String pic;
//保存到中心点的距离
private Object distance;
private Boolean isAD;
//completion类型的值一个一个的数组类型的词条,对应在java中用List集合接收
private List<String> suggestion;
public HotelDoc(Hotel hotel) {
this.id = hotel.getId();
this.name = hotel.getName();
this.address = hotel.getAddress();
this.price = hotel.getPrice();
this.score = hotel.getScore();
this.brand = hotel.getBrand();
this.city = hotel.getCity();
this.starName = hotel.getStarName();
this.business = hotel.getBusiness();
this.location = hotel.getLatitude() + ", " + hotel.getLongitude();
this.pic = hotel.getPic();
if(this.business.contains("、")){
//有多个值,切片
String[] arr = this.business.split("、");
//添加元素
this.suggestion = new ArrayList<>();
this.suggestion.add(this.brand);
Collections.addAll(this.suggestion,arr);
}else {
//内容是品牌和商品,形成集合
this.suggestion = Arrays.asList(this.brand,this.business);
}
}
}
再次删除索引库、重新创建,导入数据,然后查询数据,发现已经根据指定符合进行切割如下:
5.4.测试自动补全
测试语法如下:
# 测试查询数据
GET /hotel/_search
{
"suggest": {
"suggestions": {//起个名字
"text": "h",//自动补全查询的字母
"completion": {
"field": "suggestion",//字段名
"skip_duplicates":true,//跳过重复
"size":10//10条数据
}
}
}
}
执行后结果如下:可以实现自动补全查询了
六、Rest API实现自动补全查询
6.1.语法说明如下:
和DSL对照语法如下:
6.2.自动补全单元测试
创建单元测试方法如下:
@Test
void testSuggest() throws IOException {
//1.准备request
SearchRequest request = new SearchRequest("hotel");
//2.准备DSL
request.source().suggest(new SuggestBuilder().addSuggestion("suggestions",
SuggestBuilders.completionSuggestion("suggestion")
.prefix("h")
.skipDuplicates(true)
.size(10)
));
//3.发起请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4.解析结果
System.out.println(response);
}
执行后如下:
6.3.结果解析
语法如下:
编写单元测试方法
@Test
void testSuggest() throws IOException {
//1.准备request
SearchRequest request = new SearchRequest("hotel");
//2.准备DSL
request.source().suggest(new SuggestBuilder().addSuggestion("suggestions",
SuggestBuilders.completionSuggestion("suggestion")
.prefix("h")
.skipDuplicates(true)
.size(10)
));
//3.发起请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4.解析结果
Suggest suggest = response.getSuggest();
//4.1.根据补全查询名称,获取补全结果
CompletionSuggestion suggestions = suggest.getSuggestion("suggestions");
//4.2.获取options
List<CompletionSuggestion.Entry.Option> options = suggestions.getOptions();
//4.3.遍历
for (CompletionSuggestion.Entry.Option option : options) {
String text = option.getText().toString();
System.out.println(text);
}
}
执行后结果如下:
七、实现搜索框自动补全
实现酒店搜索页面输入框的自动补全
查看前端页面,可以发现当我们在输入框键入时,前端会发起ajax请求:
在服务端编写接口,接收该请求,返回补全结果的集合,类型为List<String>
7.1.创建controller
这里就是接受处理前端的ajax请求
/**
* 根据拼音自动补全
* @param prefix 匹配的前缀
* @return 返回list集合
*/
@GetMapping("/suggestion")
public List<String> getSuggestions(@org.springframework.web.bind.annotation.RequestParam("key") String prefix){
return iHotelService.getSuggestions(prefix);
}
7.2.service层
在IHotelService添加接口如下:
/**
* 自动补全
* @param prefix 自动补全的前缀
* @return
*/
List<String> getSuggestions(String prefix);
在HotelService实现类中实现之前的接口,如下:
/**
* 自动补全
* @param prefix 自动补全的前缀
* @return
*/
@Override
public List<String> getSuggestions(String prefix) {
try {
//1.准备request
SearchRequest request = new SearchRequest("hotel");
//2.准备DSL
request.source().suggest(new SuggestBuilder().addSuggestion("suggestions",
SuggestBuilders.completionSuggestion("suggestion")
.prefix(prefix)
.skipDuplicates(true)
.size(10)
));
//3.发起请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4.解析结果
Suggest suggest = response.getSuggest();
//4.1.根据补全查询名称,获取补全结果
CompletionSuggestion suggestions = suggest.getSuggestion("suggestions");
//4.2.获取options
List<CompletionSuggestion.Entry.Option> options = suggestions.getOptions();
//创建集合,报错补全的结果然后返回
List<String> list = new ArrayList<>(options.size());
//4.3.遍历
for (CompletionSuggestion.Entry.Option option : options) {
String text = option.getText().toString();
//添加到集合
list.add(text);
}
return list;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
7.3.测试
重启项目,然后访问,输入x,然后会将自动补全的结果展示在下面,这样就实现了拼音搜索的自动补全功能