ElasticSearch - 基于 拼音分词器 和 IK分词器 模拟实现“百度”搜索框自动补全功能

目录

一、自动补全

1.1、效果说明

1.2、安装拼音分词器

1.3、自定义分词器

1.3.1、为什么要自定义分词器

1.3.2、分词器的构成

1.3.3、自定义分词器

1.3.4、面临的问题和解决办法

问题

解决方案

[1.4、completion suggester 查询](#1.4、completion suggester 查询)

1.4.1、基本概念和语法

1.4.2、示例

1.4.3、示例(黑马旅游)

[a)修改 hotel 索引库结构,设置自定义拼音分词器.](#a)修改 hotel 索引库结构,设置自定义拼音分词器.)

[b)给 HotelDoc 类添加 suggestion 字段](#b)给 HotelDoc 类添加 suggestion 字段)

[c)将数据重新导入到 hotel 索引库中](#c)将数据重新导入到 hotel 索引库中)

[d)基于 JavaRestClient 编写 DSL](#d)基于 JavaRestClient 编写 DSL)

1.5、黑马旅游案例

1.5.1、需求

1.5.2、前端对接

[1.5.3、实现 controller](#1.5.3、实现 controller)

1.5.4、创建接口并实现.

1.5.5、效果展示


一、自动补全


1.1、效果说明

当用户在搜索框中输入字符时,我们应该提示出与该字符有关的搜索项.

例如百度中,输入关键词 "byby",他的效果如下:

1.2、安装拼音分词器

要实现根据字母补全,就需要对文档按照拼英分词. 在GitHub 上有一个 es 的拼英分词插件.

地址:GitHub - medcl/elasticsearch-analysis-pinyin: This Pinyin Analysis plugin is used to do conversion between Chinese characters and Pinyin.

这里的安装方式和 IK 分词器一样,分四步:

  1. 安装解压.
  1. 上传到云服务器中,es 的 plugin 目录.
  1. 重启 es.
  1. 测试.

这里可以看到,拼音分词器不光对每个字用拼音进行分词,还对每个字的首字母进行分词.

1.3、自定义分词器

1.3.1、为什么要自定义分词器

根据上述测试,可以看出.

  1. 拼音分词器是将一句话中的每一个字都分成了拼音,这没什么实际的用处.

  2. 这里并没有分出汉字,只有拼英. 实际的使用中,用户更多的是使用汉字去搜,有拼音只是锦上添花,但是也不能只用拼音分词器,把汉字丢了.

因此这里我们需要对拼音分词器进行一些自定义的配置.

1.3.2、分词器的构成

想要自定义分词器,首先要先了解 es 中分词器的构成.

分词器主要由以下三个部分组成:

  1. character filters:在 tokenizer 之前,对文本的特殊字符进行处理. 比如他会把文本中出现的一些特殊字符转化成汉字,例如 :) => 开心.
  2. tokenizer:将文本按照一定的规则切割成词条(term). 例如 "我很开心" 会切割成 "我"、"很"、"开心".
  3. tokenizer filter:对 tokenizer 进一步处理. 例如将汉字转化成拼音.

1.3.3、自定义分词器

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
        }
      }
    }
  }
}
  • "type": "pinyin":指定使用拼音过滤器进行拼音转换。
  • "keep_full_pinyin": false:表示不保留完整的拼音。如果设置为true,则会将完整的拼音保留下来。
  • "keep_joined_full_pinyin": true:表示保留连接的完整拼音。当设置为true时,如果某个词的拼音有多个音节,那么它们将被连接在一起作为一个完整的拼音。
  • "keep_original": true:表示保留原始词汇。当设置为true时,原始的中文词汇也会保留在分词结果中。
  • "limit_first_letter_length": 16:限制拼音首字母的长度。默认为16,即只保留拼音首字母的前16个字符。
  • "remove_duplicated_term": true:表示移除重复的拼音词汇。如果设置为true,则会移除拼音结果中的重复词汇。
  • "none_chinese_pinyin_tokenize": false:表示是否对非中文文本进行拼音分词处理。当设置为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.还有中文分词后的英文全拼,以及分词首字母.

1.3.4、面临的问题和解决办法

问题

上面实现的拼音分词器还不能应用到实际的生产环境中~

可以想象这样一个场景:

如果词库中有这两个词:"狮子" 和 "虱子",那么也就意味着,创建倒排索引时,通过上述自定义的 拼音分词器 ,就会把这两个词归为一个文档,因为他们在分词的时候,会分出共同的拼音 "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 分词器.
      }
    }
  }
}

1.4、completion suggester 查询

1.4.1、基本概念和语法

es 中提供了 completion suggester 查询来实现自动补全功能. 这个查询会匹配用户输入内容开头的词条并返回.

为了提高补全查询的效率,对于文档中的字段类型有一些约束,如下:

  1. 参与补全查询的字段必须是 completion 类型.
  2. 参与 自动补全字段 的内容一般是多个词条形成的数组.
java 复制代码
POST /test2/_search
{
  "suggest": {
    "title_suggest": { //自定义补全名
      "text": "s",  //用户在搜索框中输入的关键字
      "completion": { // completion 是自动补全中的一种类型(最常用的)
        "field": "补全时需要查询的字段名", //这里的字段名指向的是一个数组(字段必须是 completion 类型),就是要根据数组中的字段进行查询,然后自动补全
        "skip_duplicates": true,  //如果查询时有重复的词条,是否自动跳过(true 为跳过)
        "size": 10 // 获取前 10 条结果.
      }
    }
  }
}

1.4.2、示例

这里我用一个示例来演示 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
      }
    }
  }
}

1.4.3、示例(黑马旅游)

这里我们基于之前实现的黑马旅游案例来做栗子,实现步骤如下:

a)修改 hotel 索引库结构,设置自定义拼音分词器.

1.设置自定义分词器.

  1. 修改索引库的 name、all 字段(建立倒排索引使用 拼音分词器,搜索时使用 ik 分词器).

  2. 给索引库添加一个新字段 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"
    }
    }
    }
    }

b)给 HotelDoc 类添加 suggestion 字段

suggestion 字段(包含多个字段的数组,这里可以使用 List 表示),内容包含 brand、business.

Ps:name、all 是可以分词的,自动补全的 brand、business 是不可分词的,要使用不同的分词器组合.

java 复制代码
@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<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 = new ArrayList<>();
        suggestion.add(brand);
        suggestion.add(business);
    }
}
c)将数据重新导入到 hotel 索引库中

将 hotel 索引库删了,然后重建(a 中的 DSL). 通过单元测试将所有信息从数据库同步到 es 上.

java 复制代码
    @Test
    public void testBulkDocument() throws IOException {
        //1.获取酒店所有数据
        List<Hotel> 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);
    }
d)基于 JavaRestClient 编写 DSL

例如自动补全关键为 "h" 的内容.

java 复制代码
    @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 语句来写.

对查询结果的处理如下:

java 复制代码
        //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 语句来写.

运行结果如下:

1.5、黑马旅游案例

1.5.1、需求

首先搜索框的自动补全功能.

最终实现效果就类似于 百度的搜索框,比如当我们输入 "byby",他就会立马自动补全出有关 byby 关键字的信息,如下图:

1.5.2、前端对接

在搜索框中输入,会触发以下请求. 这里前端就传入一个参数 key.

这里约定,返回的是一个 List,内容就是自动补全的所有信息.

1.5.3、实现 controller

这里使用 @RequestParam 接收前端传入的参数,然后调用 IhotelService 接口处理即可.

java 复制代码
    @RequestMapping("/suggestion")
    public List<String> suggestion(@RequestParam("key") String prefix) {
        return hotelService.suggestion(prefix);
    }

1.5.4、创建接口并实现.

在 IhotelService 接口中创建 suggestion 方法.

java 复制代码
public interface IHotelService extends IService<Hotel> {

    PageResult search(RequestParams params);

    Map<String, List<String>> filters(RequestParams params);

    List<String> suggestion(String prefix);
}

接着在 IhotelService 的实现类 HotelService 中实现该方法.

具体的实现,就和前面写的测试案例基本一致了~ 要注意的点就是补全的关键字不是写死的,而是前端传入的 prefix.

java 复制代码
    @Override
    public List<String> 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<String> 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;
        }
    }
}

1.5.5、效果展示

输入关键词,即可出现自动补全.

相关推荐
晨欣6 分钟前
Elasticsearch和Lucene之间是什么关系?(ChatGPT回答)
elasticsearch·chatgpt·lucene
zmd-zk1 小时前
kafka+zookeeper的搭建
大数据·分布式·zookeeper·中间件·kafka
激流丶1 小时前
【Kafka 实战】如何解决Kafka Topic数量过多带来的性能问题?
java·大数据·kafka·topic
测试界的酸菜鱼1 小时前
Python 大数据展示屏实例
大数据·开发语言·python
时差9531 小时前
【面试题】Hive 查询:如何查找用户连续三天登录的记录
大数据·数据库·hive·sql·面试·database
Mephisto.java1 小时前
【大数据学习 | kafka高级部分】kafka中的选举机制
大数据·学习·kafka
Mephisto.java1 小时前
【大数据学习 | kafka高级部分】kafka的优化参数整理
大数据·sql·oracle·kafka·json·database
道可云1 小时前
道可云人工智能&元宇宙每日资讯|2024国际虚拟现实创新大会将在青岛举办
大数据·人工智能·3d·机器人·ar·vr
成都古河云1 小时前
智慧场馆:安全、节能与智能化管理的未来
大数据·运维·人工智能·安全·智慧城市
软工菜鸡2 小时前
预训练语言模型BERT——PaddleNLP中的预训练模型
大数据·人工智能·深度学习·算法·语言模型·自然语言处理·bert