目录
b)给 HotelDoc 类添加 suggestion 字段
当用户在搜索框中输入字符时,我们应该提示出与该字符有关的搜索项.
例如百度中,输入关键词 "byby",他的效果如下:
要实现根据字母补全,就需要对文档按照拼英分词. 在GitHub 上有一个 es 的拼英分词插件.
这里的安装方式和 IK 分词器一样,分四步:
1. 安装解压.
2. 上传到云服务器中,es 的 plugin 目录.
3. 重启 es.
4. 测试.
这里可以看到,拼音分词器不光对每个字用拼音进行分词,还对每个字的首字母进行分词.
根据上述测试,可以看出.
1. 拼音分词器是将一句话中的每一个字都分成了拼音,这没什么实际的用处.
2. 这里并没有分出汉字,只有拼英. 实际的使用中,用户更多的是使用汉字去搜,有拼音只是锦上添花,但是也不能只用拼音分词器,把汉字丢了.
因此这里我们需要对拼音分词器进行一些自定义的配置.
想要自定义分词器,首先要先了解 es 中分词器的构成.
分词器主要由以下三个部分组成:
- 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
- }
- }
- }
- }
- }
例如,创建一个 test 索引库,来测试自定义分词器.
- 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"
- }
- }
- }
- }
使用此索引库的分词器进行测试
从上图中可以看出:
1.不光有拼音,还有中文分词.
2.还有中文分词后的英文全拼,以及分词首字母.
上面实现的拼音分词器还不能应用到实际的生产环境中~
可以想象这样一个场景:
如果词库中有这两个词:“狮子” 和 “虱子”,那么也就意味着,创建倒排索引时,通过上述自定义的 拼音分词器 ,就会把这两个词归为一个文档,因为他们在分词的时候,会分出共同的拼音 "shizi" 和 "sz",这就导致他两的文档编号对应同一个词条,导致将来用户在搜索框里输入 “狮子” ,点击搜索之后,会同时搜索出 "狮子" 和 “虱子” ,这并不是我们想看到的.
因此字段在创建倒排索引时因该使用 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" //创建倒排索引使用 my_analyzer 分词器.
- "search_analyzer": "ik_smart" //搜索时使用 ik_smart 分词器.
- }
- }
- }
- }
es 中提供了 completion suggester 查询来实现自动补全功能. 这个查询会匹配用户输入内容开头的词条并返回.
为了提高补全查询的效率,对于文档中的字段类型有一些约束,如下:
- POST /test2/_search
- {
- "suggest": {
- "title_suggest": { //自定义补全名
- "text": "s", //用户在搜索框中输入的关键字
- "completion": { // completion 是自动补全中的一种类型(最常用的)
- "field": "补全时需要查询的字段名", //这里的字段名指向的是一个数组(字段必须是 completion 类型),就是要根据数组中的字段进行查询,然后自动补全
- "skip_duplicates": true, //如果查询时有重复的词条,是否自动跳过(true 为跳过)
- "size": 10 // 获取前 10 条结果.
- }
- }
- }
- }
这里我用一个示例来演示 completion suggester 的用法.
首先创建索引库(参与自动补全的字段类型必须是 completion).
- 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"]
- }
这里我们设置关键字为 "s",来自动补全查询,如下:
- POST /test2/_search
- {
- "suggest": {
- "title_suggest": {
- "text": "s",
- "completion": {
- "field": "title",
- "skip_duplicates": true,
- "size": 10
- }
- }
- }
- }
首先创建索引库,如下参与自动补全的字段为 suggestion(通过 copy title 得到).
- PUT /test
- {
- "mappings": {
- "properties": {
- "title": {
- "type": "text",
- "analyzer": "ik_smart",
- "copy_to": "suggestion"
- },
- "suggestion": {
- "type": "completion"
- }
- }
- }
- }
插入示例数据
- POST test/_doc/1
- {
- "title": "今天天气真好"
- }
-
- POST test/_doc/2
- {
- "title": "我想出去玩"
- }
-
-
- POST test/_doc/3
- {
- "title": "我要去找小伙伴"
- }
设置关键字为 "我"
- POST /test/_search
- {
- "suggest": {
- "title_suggest": {
- "text": "我",
- "completion": {
- "field": "suggestion",
- "skip_duplicates": true,
- "size": 10
- }
- }
- }
- }
自动补全效果如下
这里我们基于之前实现的黑马旅游案例来做栗子,实现步骤如下:
1.设置自定义分词器.
2. 修改索引库的 name、all 字段(建立倒排索引使用 拼音分词器,搜索时使用 ik 分词器).
3. 给索引库添加一个新字段 suggestion,类型为 completion 类型,使用自定义分词器.
- 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"
- }
- }
- }
- }
suggestion 字段(包含多个字段的数组,这里可以使用 List 表示),内容包含 brand、business.
Ps:name、all 是可以分词的,自动补全的 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;
- private List
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 = new ArrayList<>();
- suggestion.add(brand);
- suggestion.add(business);
- }
- }
将 hotel 索引库删了,然后重建(a 中的 DSL). 通过单元测试将所有信息从数据库同步到 es 上.
- @Test
- public void testBulkDocument() throws IOException {
- //1.获取酒店所有数据
- List
hotelList = hotelService.list(); - //2.构造请求
- BulkRequest request = new BulkRequest();
- //3.准备参数
- for(Hotel hotel : hotelList) {
- //转化为文档(主要是地理位置)
- HotelDoc hotelDoc = new HotelDoc(hotel);
- String json = objectMapper.writeValueAsString(hotelDoc);
- request.add(new IndexRequest("hotel").id(hotel.getId().toString()).source(json, XContentType.JSON));
- }
- //4.发送请求
- client.bulk(request, RequestOptions.DEFAULT);
- }
例如自动补全关键为 "h" 的内容.
- @Test
- public void testSuggestion() throws IOException {
- //1.创建请求
- SearchRequest request = new SearchRequest("hotel");
- //2.准备参数
- request.source().suggest(new SuggestBuilder().addSuggestion(
- "testSuggestion",
- SuggestBuilders
- .completionSuggestion("suggestion")
- .prefix("h")
- .skipDuplicates(true)
- .size(10)
- ));
- //3.发送请求,接收响应
- SearchResponse search = client.search(request, RequestOptions.DEFAULT);
- //4.解析响应
- handlerResponse(search);
- }
这里可以对应着 DSL 语句来写.
对查询结果的处理如下:
- //4.处理自动补全结果
- Suggest suggest = response.getSuggest();
- if(suggest != null) {
- CompletionSuggestion suggestion = suggest.getSuggestion("testSuggestion");
- for (CompletionSuggestion.Entry.Option option : suggestion.getOptions()) {
- String text = option.getText().toString();
- System.out.println(text);
- }
- }
这里可以对应着 DSL 语句来写.
运行结果如下:
首先搜索框的自动补全功能.
最终实现效果就类似于 百度的搜索框,比如当我们输入 "byby",他就会立马自动补全出有关 byby 关键字的信息,如下图:
在搜索框中输入,会触发以下请求. 这里前端就传入一个参数 key.
这里约定,返回的是一个 List,内容就是自动补全的所有信息.
这里使用 @RequestParam 接收前端传入的参数,然后调用 IhotelService 接口处理即可.
- @RequestMapping("/suggestion")
- public List
suggestion(@RequestParam("key") String prefix) { - return hotelService.suggestion(prefix);
- }
在 IhotelService 接口中创建 suggestion 方法.
- public interface IHotelService extends IService
{ -
- PageResult search(RequestParams params);
-
- Map
> filters(RequestParams params); -
- List
suggestion(String prefix); - }
接着在 IhotelService 的实现类 HotelService 中实现该方法.
具体的实现,就和前面写的测试案例基本一致了~ 要注意的点就是补全的关键字不是写死的,而是前端传入的 prefix.
- @Override
- public List
suggestion(String prefix) { - try {
- //1.创建请求
- SearchRequest request = new SearchRequest("hotel");
- //2.准备参数
- request.source().suggest(new SuggestBuilder().addSuggestion(
- "mySuggestion",
- SuggestBuilders
- .completionSuggestion("suggestion")
- .prefix(prefix)
- .skipDuplicates(true)
- .size(10)
- ));
- //3.发送请求,接收响应
- SearchResponse response = client.search(request, RequestOptions.DEFAULT);
- //4.解析响应(处理自动补全结果)
- Suggest suggest = response.getSuggest();
- List
suggestionList = new ArrayList<>(); - if(suggest != null) {
- CompletionSuggestion suggestion = suggest.getSuggestion("mySuggestion");
- for (CompletionSuggestion.Entry.Option option : suggestion.getOptions()) {
- String text = option.getText().toString();
- suggestionList.add(text);
- }
- }
- return suggestionList;
- } catch (IOException e) {
- System.out.println("[HotelService] 自动补全失败!prefix=" + prefix);
- e.printStackTrace();
- return null;
- }
- }
- }
输入关键词,即可出现自动补全.