ElasticSearch 拼音分词和自动补全
在搜索过程中,大部分情况下会有智能提示功能,也就是开头匹配的自动补全功能,这就需要用到 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 工程,结构如下所示:
由于本篇博客的 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