0
点赞
收藏
分享

微信扫一扫

自动补全

一、自动补全需求说明

当用户在搜索框输入字符时,就应该提示出与该字符有关的搜索项,如图:

自动补全_analyzer

二、安装拼音分词器

要实现根据字母做补全,就必须对文档按照拼音分词。在GitHub上恰好有elasticsearch的拼音分词插件。下载地址:https://github.com/medcl/elasticsearch-analysis-pinyin,安装方式与IK分词器一样,分四步:

2.1.下载

这里插件的版本需要和elasticsearch的版本需要保持一致,我的elasticsearch版本是7.17.5,那么拼音分词器的版本应该也是7.17.5,如下:

自动补全_分词器_02

2.2.解压

下载下来的分词器进行解压,

自动补全_analyzer_03

2.3.上传到虚拟机中,elasticsearch的plugin目录

将上面解压后的整个文件夹上传到/var/lib/docker/volumes/es-plugins/_data/目录,如下:

自动补全_分词器_04

2.4.重启elasticsearch

这里重启容器即可:

自动补全_analyzer_05

2.5.测试

测试分词查询 :使用ik_max_word:进行分词 :它分的词语比ik_smart:分的词语更多

自动补全_自动补全_06

使用拼音分词器

自动补全_自动补全_07

上面这就是拼音分词器的用法,以后我们在创建拼音分词器的时候可以mapping映射去定义拼音分词器,作为我们的分词器使用了

三、自定义分词器

上面的拼音分词器,还有一些问题,这里把拼音的首字母放到这里,也说明了这句话没有被分词,而是作为一个整体出现的,还把每一个字都形成了一个拼音,这也没什么用,这里只剩下了拼音,我们用拼音搜索的情况占少数的,大多数情况下我们想用中文搜索

自动补全_analyzer_08

3.1.自定义分词器说明

elasticsearch中分词器(analyzer)的组成包含三部分:

  • characterfilters:在tokenizer之前对文本进行处理。例如删除字符、替换字符
  • tokenizer:将文本按照一定的规则切割成词条(term)。例如keyword,就是不分词;还有ik_smart
  • tokenizerfilter:将tokenizer输出的词条做进一步处理。例如大小写转换、同义词处理、拼音处理等

自动补全_自动补全_09

上面的自定义分词器例子就解决了拼音分词器不能分词的问题

3.2.自定义分词器语法

3.2.1.语法

我们可以在创建索引库时,通过settings来配置自定义的analyzer(分词器),语法如下:

自动补全_自动补全_10

上面的参数:

  • 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": "在动物园能看到狮子"
    }
  }
}

执行后结果如下:

自动补全_自动补全_11

根据shizi的拼音搜索,发现把同音字也搜到了,这是有问题的,如下

自动补全_分词器_12

3.4.自定义分词器使用场景

上面根据shizi的拼音搜索,发现把同音字也搜到了,这是有问题的,拼音分词器适合在创建倒排索引的时候使用,但不能在搜索的时候使用。创建倒排索时

自动补全_分词器_13

这就是问题的所在,在搜索时也用了拼音选择器,拿拼音去搜,就搜出来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的文档信息查询出来

自动补全_分词器_14

四、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开头的信息,如下:

自动补全_自动补全_15

五、修改酒店索引库结构

拼音搜索功能实现hotel索引库的自动补全,实现思路如下:

  1. 修改hotel索引库结构,设置自定义拼音分词器
  2. 修改索引库的name、all字段,使用自定义分词器
  3. 索引库添加一个新字段suggestion,类型为completion类型,使用自定义的分词器
  4. 给Hotel Doc类添加suggestion字段,内容包含brand、business
  5. 重新导入数据到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字段,其值是由酒店品牌和商圈构建出来的

自动补全_分词器_16

但是像上面的 "江湾、五角商业广场",但是有的使用、有的使用/分割,是由两个值构成,所以需要进行切割,所以改造实体类代码: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);
        }
    }
}

再次删除索引库、重新创建,导入数据,然后查询数据,发现已经根据指定符合进行切割如下:

自动补全_分词器_17

5.4.测试自动补全

测试语法如下:

# 测试查询数据
GET /hotel/_search
{
  "suggest": {
    "suggestions": {//起个名字
      "text": "h",//自动补全查询的字母
      "completion": {
        "field": "suggestion",//字段名
        "skip_duplicates":true,//跳过重复
        "size":10//10条数据
      }
    }
  }
}

执行后结果如下:可以实现自动补全查询了

自动补全_分词器_18

六、Rest API实现自动补全查询

6.1.语法说明如下:

和DSL对照语法如下:

自动补全_自动补全_19

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);
    }

执行后如下:

自动补全_analyzer_20

6.3.结果解析

语法如下:

自动补全_analyzer_21

编写单元测试方法

@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);
        }
    }

执行后结果如下:

自动补全_analyzer_22

七、实现搜索框自动补全

实现酒店搜索页面输入框的自动补全

查看前端页面,可以发现当我们在输入框键入时,前端会发起ajax请求:

自动补全_自动补全_23

在服务端编写接口,接收该请求,返回补全结果的集合,类型为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,然后会将自动补全的结果展示在下面,这样就实现了拼音搜索的自动补全功能

自动补全_analyzer_24



举报

相关推荐

0 条评论