ElasticSearch 拼音分词和自动补全

发布时间 2023-10-22 15:20:52作者: 乔京飞

在搜索过程中,大部分情况下会有智能提示功能,也就是开头匹配的自动补全功能,这就需要用到 ElasticSearch 的 Suggest 查询功能。用户也可能输入拼音或者查询关键字的首字母简写,比如我想查询华为手机,我可以输入 hwsj 进行查询,这就需要用到拼音分词器。本篇博客将介绍如何安装拼音分词器,以及如何进行 Suggest 查询实现自动补全功能。博客最后提供源代码下载。


一、安装拼音分词器

拼音分词器的下载地址为:https://github.com/medcl/elasticsearch-analysis-pinyin/releases

由于我使用的 ElasticSearch 的版本是 8.8.2 ,所以我随便找了一个 8 版本中最高的版本(8.9.2)进行下载。

下载完成后进行解压,发现其实是拼音分词器的源代码,需要使用 IDEA 打开源代码,然后修改其 pom 文件的配置:

将 pom 文件中 properties 下的 elasticsearch.version 配置修改为 8.8.2 即可,如下所示:

<properties>
    <elasticsearch.version>8.8.2</elasticsearch.version>
</properties>

使用 IDEA 的 Maven 进行 Clean 和 Package 打包,然后在其 targart 下的 releases 目录下就能够找到 elasticsearch-analysis-pinyin-8.8.2.zip 压缩包,该压缩包里面只有 3 个文件,将其解压到一个文件夹中,比如将文件夹的名称取名为 pinyin 然后上传到 ElasticSearch 的 plugins 目录,最后重启 ElasticSearch 即可。

需要注意的是:上面修改的 elasticsearch.version 版本最高跟自己使用的 ElasticSearch 版本一致,否则可能会造成 ElasticSearch 无法启动。

最后就是验证一下拼音分词器的安装是否成功,在 Kibana 上输入以下 DSL 语句进行词条分析测试:

POST /_analyze
{
  "text": ["北京"],
  "analyzer": "pinyin"
}

如果能够得到以下查询结果,就表明拼音分词器已经安装成功:

{
  "tokens": [
    {
      "token": "bei",
      "start_offset": 0,
      "end_offset": 0,
      "type": "word",
      "position": 0
    },
    {
      "token": "bj",
      "start_offset": 0,
      "end_offset": 0,
      "type": "word",
      "position": 0
    },
    {
      "token": "jing",
      "start_offset": 0,
      "end_offset": 0,
      "type": "word",
      "position": 1
    }
  ]
}

二、自动补全查询

ElasticSearch 提供自动补全查询功能,也就是匹配以用户输入内容开头的词条并返回,但是有一个限制:参与查询的字段必须是 completion 类型。通常情况下我们会将 completion 类型的字段内容,存储为数组,这样可以更加灵活并提高查询效率。

搭建一个 SpringBoot 工程,结构如下所示:

image

由于本篇博客的 Demo 是在上篇博客的基础上进行了简单改造,大部分内容与上篇博客的 Demo 是相同的,因此这里只介绍差异内容。

最主要的内容是 SuggestTest 类中的测试方法,具体细节如下:

package com.jobs;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.search.suggest.Suggest;
import org.elasticsearch.search.suggest.SuggestBuilder;
import org.elasticsearch.search.suggest.SuggestBuilders;
import org.elasticsearch.search.suggest.completion.CompletionSuggestion;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.util.ResourceUtils;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.List;

@SpringBootTest
public class SuggestTest {

    @Autowired
    private RestHighLevelClient client;

    //删除之前的 myhotel 索引库
    @Test
    void deleteIndexTest() throws IOException {
        DeleteIndexRequest request = new DeleteIndexRequest("myhotel");
        client.indices().delete(request, RequestOptions.DEFAULT);
        System.out.println("删除索引库成功");
    }

    //重新创建 myhotel 创建索引库
    @Test
    void createIndexTest() throws IOException {
        CreateIndexRequest createIndexRequest = new CreateIndexRequest("myhotel");
        //读取 resourses/JSON/CreateMyHotelJson.txt 文件内容
        File file = ResourceUtils.getFile("classpath:JSON/CreateMyHotelJson.txt");
        String createJson;
        try (BufferedReader br = new BufferedReader(new FileReader(file))) {
            createJson = br.readLine();
        }
        createIndexRequest.source(createJson, XContentType.JSON);
        client.indices().create(createIndexRequest, RequestOptions.DEFAULT);
    }

    //重新批量添加样例数据
    //BulkRequest 可以添加各种请求,如 IndexRequest,UpdateRequest,DeleteRequest
    @Test
    void bulkRequestTest() throws IOException {
        //读取 resourses/JSON/DemoJsonData.txt 文件内容
        File file = ResourceUtils.getFile("classpath:JSON/DemoJsonData.txt");
        //使用 BulkRequest 批量请求对象
        BulkRequest request = new BulkRequest();
        try (BufferedReader br = new BufferedReader(new FileReader(file))) {
            String json = br.readLine();
            JSONObject jsonObj;
            while (StringUtils.isNotBlank(json)) {
                jsonObj = JSON.parseObject(json);
                //为 BulkRequest 添加请求对象
                request.add(new IndexRequest("myhotel")
                        .id(jsonObj.getString("id"))
                        .source(json, XContentType.JSON));
                json = br.readLine();
            }
        }
        client.bulk(request, RequestOptions.DEFAULT);
    }

    //根据字母或汉字,查询出最多 10 条自动补全信息
    @Test
    void testSuggest() throws IOException {
        SearchRequest request = new SearchRequest("myhotel");
        request.source().suggest(new SuggestBuilder().addSuggestion("mySuggest",
                //指定要查询的 es 中的字段是 suggestion,注意:字段必须是 completion 类型
                SuggestBuilders.completionSuggestion("suggestion")
                        //指定要查询的关键字,去除重复内容,以及最多返回多少条记录
                        .prefix("hy").size(10).skipDuplicates(true)));
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        //解析返回的内容
        Suggest suggest = response.getSuggest();
        CompletionSuggestion suggestion = suggest.getSuggestion("mySuggest");
        //获取 options 中的 text 内容
        List<CompletionSuggestion.Entry.Option> options = suggestion.getOptions();
        for (CompletionSuggestion.Entry.Option option : options) {
            String str = option.getText().toString();
            System.out.println(str);
        }
    }
}

由于 ElasticSearch 无法更改索引库结构,因此之前的示例索引库 myhotel 需要删除后重新创建,DSL 语句如下:

PUT /myhotel
{
  "settings": {
    "analysis": {
      "analyzer": {
        "query_anlyzer": {
          "tokenizer": "ik_max_word",
          "filter": "mypinyin"
        },
        "suggest_analyzer": {
          "tokenizer": "keyword",
          "filter": "mypinyin"
        }
      },
      "filter": {
        "mypinyin": {
          "type": "pinyin",
          "keep_full_pinyin": false,
          "keep_joined_full_pinyin": true,
          "keep_original": true,
          "limit_first_letter_length": 10,
          "remove_duplicated_term": true,
          "none_chinese_pinyin_tokenize": false
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "id":{
        "type": "keyword"
      },
      "name":{
        "type": "text",
        "analyzer": "query_anlyzer",
        "search_analyzer": "ik_max_word",
        "copy_to": "all"
      },
      "address":{
        "type": "keyword",
        "index": false
      },
      "price":{
        "type": "integer"
      },
      "brand":{
        "type": "keyword",
        "copy_to": "all"
      },
      "city":{
        "type": "keyword"
      },
      "location":{
        "type": "geo_point"
      },
      "all":{
        "type": "text",
        "analyzer": "query_anlyzer",
        "search_analyzer": "ik_max_word"
      },
      "suggestion":{
          "type": "completion",
          "analyzer": "suggest_analyzer"
      }
    }
  }
}

这里自定义了 2 个分词器,分别是 query_anlyzer 和 suggest_analyzer,对于查询的字段使用 query_anlyzer,对于自动补全的字段使用 suggest_analyzer。可以发现新创建的 myhotel 索引库,新增加了一个字段 suggestion 其字段类型为 completion 。

在 SuggestTest 类中重新给 myhotel 索引库批量添加文档数据,其中给文档的 suggestion 字段添加的内容是由名称和品牌组成的数组,这样在自动补全提示时,可以直接显示出名称或者品牌。

在 SuggestTest 类中的 testSuggest 测试方法,就是自动补全的测试代码,其中 prefix 内容就是需要传入的查询内容。


本篇博客的 Demo 相对比较简单,具体代码已经测试无误,可以下载源代码进行测试验证。

源代码下载地址为:https://files.cnblogs.com/files/blogs/699532/springboot_suggest.zip