自动补全
一、自动补全需求说明
当用户在搜索框输入字符时,就应该提示出与该字符有关的搜索项,如图:

二、安装拼音分词器
要实现根据字母做补全,就必须对文档按照拼音分词。在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,然后会将自动补全的结果展示在下面,这样就实现了拼音搜索的自动补全功能


浙公网安备 33010602011771号