ElasticSearch 自动补全
1、前言
当用户在搜索框输入字符时,我们应该提示出与该字符有关的搜索项,根据用户输入的字母,提示完整词条的功能,就是自动补全。
2、安装拼音分词器
Github地址:https://github.com/infinilabs/analysis-pinyin
插件下载地址:https://release.infinilabs.com/analysis-pinyin/stable/
2.1 根据 ES 版本下载指定的插件包
2.2 安装步骤
- 下载插件并解压,重命名为:analysis-pinyin
- 上传 analysis-pinyin 到 es 的 plugin 目录
- 重启 es
- 测试
2.3 测试
DSL语法:
POST /_analyze
{
"text":"刘德华",
"analyzer":"pinyin"
}
测试结果:
3、自定义分词器
默认的拼音分词器会将每个汉字单独分为拼音。我们希望的是每个词条形成一组拼音,需要对拼音分词器做个性化定制,形成自定义分词器。
3.1 ES 分词器(analyzer)组成部分:
- 字符过滤器(Character Filters):它主要用于在分词之前对原始文本进行预处理。这一步骤可以对文本进行清洗、转换等操作,例如去除 HTML 标签、转换字符大小写、处理特殊字符等。
- 分词器(Tokenizer):分词器负责将经过字符过滤器处理后的文本按照一定的规则分解为单个的词元(Token)。这些规则可以基于空格、标点符号等进行分词,不同的分词器有不同的分词策略。例如,standard 分词器会按照空格和标点符号来划分词元,将文本拆分成一个个独立的单词或符号。
- 词元过滤器(Token Filters):词元过滤器用于对分词器产生的词元进行进一步的处理。这可以包括过滤掉停用词(如 “的”、“是” 等在某些情况下对搜索意义不大的词)、词形还原(将词元还原为其基本形式,如将 “running” 还原为 “run”)、同义词替换(将一个词元替换为其同义词,如将 “电脑” 替换为 “计算机”)等操作。
3.2 执行流程图
说明:特殊符号准换成指定的中文字符,然后借助 ik_smart 进行分词,再通过 pinyin 分词器 转换成 拼音词条。
3.3 测试
DSL 语法:
{
"settings": {
"analysis": {
"analyzer": {
"py_analyzer": {
"tokenizer": "ik_max_word",
"filter": "py"
}
},
"filter": {
"py": {
"type": "pinyin",
// "keep_separate_first_letter": true,
"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": "py_analyzer",
"search_analyzer": "ik_smart"
}
}
}
}
字段说明:
-
analysis:定义分析(analysis)部分,这里可以指定自定义的分析器、分词器、过滤器或字符过滤器。
- py_analyzer:自定义分析器名,
- ik_max_word:中文分词器,它会尽可能多地切分出词语。
- filter:“py” 是指在分词后将应用的过滤器名称
- filter:定义过滤器配置信息
- py: 自定义过滤器名称,与 py_analyzer 指定的 filter 一致
- type: 指定过滤器类型为pinyin,即拼音过滤器
- keep_separate_first_letter: 是否保留每个汉字的首字母,
- keep_full_pinyin: 是否保留全拼
- keep_joined_full_pinyin: 是否保留连在一起的完整拼音
- keep_original:是否保留原始文本
- limit_first_letter_length:限制首字母长度为N个字符
- remove_duplicated_term: 是否去除重复的词条
- none_chinese_pinyin_tokenize:是否对非中文内容不进行拼音分词
- py: 自定义过滤器名称,与 py_analyzer 指定的 filter 一致
- py_analyzer:自定义分析器名,
-
mappings:映射
- properties:定义文档字段
- name:字段名
- type:字段类型
- analyzer:在索引时使用 py_analyzer 分析器
- search_analyzer:在搜索时使用 ik_smart 分析器,它会根据上下文智能地选择分词方式
- name:字段名
- properties:定义文档字段
测试结果:
3.4 analyzer 与 search_analyzer 的区别
在 Elasticsearch 中,analyzer 和 search_analyzer 是用来控制文本如何被分析(即分词和转换)的两个不同阶段的设置。它们的主要区别在于应用的时机和目的。
Analyzer
- 应用时机:analyzer 设置应用于索引时间。当文档被索引时,字段的内容会通过指定的分析器进行处理,然后将结果存储在倒排索引中。
- 作用:确保数据以一种结构化的方式存储,使得后续的搜索更加高效和准确。例如,中文文本可能会被分词成单个词语,并可能转换为小写或拼音形式等,以便于检索。
Search Analyzer
- 应用时机:search_analyzer 设置应用于查询时间。当执行搜索请求时,查询字符串会通过指定的搜索分析器进行处理,以便生成与索引中的词条相匹配的形式。
- 作用:确保查询字符串按照与索引内容相同的方式进行处理,从而提高搜索的相关性和准确性。它允许你对索引和搜索过程使用不同的分析逻辑,比如可以在索引时采用更细粒度的分词,在搜索时采用更智能的分词方式。
区别总结
- 索引 vs 搜索:analyzer 用于索引阶段的数据预处理,而 search_analyzer 用于搜索阶段的查询字符串预处理。
- 灵活性:如果未显式定义 search_analyzer,则默认使用字段的 analyzer。但是,有时为了优化搜索体验,你可能希望在搜索时使用不同于索引时的分析策略,这时就可以单独配置 search_analyzer。
4、自动补全查询
ElasticSearch 提供了 Completion Suggester 查询来实现自动补全功能。这个查询会匹配以用户输入内容开头的词条并返回。
对于文档中字段的类型有一些约束:
- 参与补全查询的字段必须是 completion 类型
- 字段的内容一般是用来补全的多个词条形成的数组
demo 测试:
创建索引库:
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"]
}
查询所有文档:
自动补全查询:
定义 DSL 语法:
GET test/_search
{
"suggest": { // 使用 Suggest 功能
"title_suggest": { // 自定义 Suggest 名称,可以是任何标识符
"text": "s", // 输入文本 "s",表示自动补全的起始字符串
"completion":{ // 使用 Completion Suggestion 类型
"field":"title", // 提供自动补全建议的字段
"skip_duplicates":true, // 跳过重复的建议,避免显示重复内容
"size":10 // 限制返回建议的数量为 10 个
}
}
}
}
测试结果:
5、酒店搜索测试案例
问题:我们的 hotel 索引库还没有设置拼音分词器,需要修改索引库中的配置信息。但是我们知道索引库是无法修改的,只能删除然后重新创建。
分析:
- 修改 hotel 索引库结构,设置自定义拼音分词器,添加新字段,用来做自动补全,将 brand、suggestion、city等都放进去,作为自动补全的提示。
- 修改索引库的 name、all 字段,使用自定义分词器。
- 将 brand、business 添加到 suggestion 字段。
- 重新导入数据到索引库
5.1 定义 hotel 索引库
DSL 创建 hotel 索引:
PUT /hotel
{
"settings": {
"analysis": {
"analyzer": {
"text_analyzer": {
"tokenizer": "ik_max_word",
"filter": "py"
},
"completion_analyzer": {
"tokenizer": "keyword",
"filter": "py"
}
},
"filter": {
"py": {
"type": "pinyin",
// "keep_separate_first_letter": true,
"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_analyzer",
"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",
"copy_to": "all"
},
"starName": {
"type": "keyword"
},
"business": {
"type": "keyword",
"copy_to": "all"
},
"location": {
"type": "geo_point"
},
"pic": {
"type": "keyword",
"index": false
},
"all": {
"type": "text",
"analyzer": "text_analyzer",
"search_analyzer": "ik_smart"
},
"suggestion": {
"type": "completion",
"analyzer": "completion_analyzer"
}
}
}
}
5.2 插入 hotel 文档数据
将 brand、city、business 字段内容放入 suggestion 字段中。
public function batchAddDoc()
{
$hotels = Hotel::order(['id' => 'asc'])->select();
$params = [];
foreach ($hotels as $hotel) {
$params['body'][] = [
'index' => [
'_index' => 'hotel',
'_id' => $hotel->id,
]
];
# 添加搜索字段
$suggestions = [];
$suggestions[] = $hotel->brand;
$suggestions[] = $hotel->city;
if (strpos($hotel->business, '/') !== false) {
$suggestions = array_merge($suggestions, explode('/', $hotel->business));
} elseif (strpos($hotel->business, '、') !== false) {
$suggestions = array_merge($suggestions, explode('、', $hotel->business));
} else {
$suggestions[] = $hotel->business;
}
$params['body'][] = [
'id' => $hotel->id,
'name' => $hotel->name,
'address' => $hotel->address,
'price' => $hotel->price,
'score' => $hotel->score,
'brand' => $hotel->brand,
'city' => $hotel->city,
'starName' => $hotel->star_name,
'business' => $hotel->business,
'location' => $hotel->latitude . ',' . $hotel->longitude,
'pic' => $hotel->pic,
'suggestion' => $suggestions
];
}
$result = $this->client->bulk($params);
return json($result);
}
5.3 查询 hotel 文档数据
5.4 自动补全搜索
DSL 语句:
GET hotel/_search
{
"suggest": { // 使用 Suggest 功能
"search_suggest": { // 自定义 Suggest 名称,可以是任何标识符
"text": "rj", // 输入文本 "rj",表示自动补全的字符串
"completion":{ // 使用 Completion Suggestion 类型
"field":"suggestion", // 提供自动补全建议的字段
"skip_duplicates":true, // 跳过重复的建议,避免显示重复内容
"size":10 // 限制返回建议的数量为 10 个
}
}
}
}
测试结果:
PHP 测试示例:
public function search()
{
$prefix = 'bj';
$result = $this->client->search([
'index' => 'hotel',
'body' => [
'suggest' => [
'search_suggest' => [
'text' => $prefix,
'completion' => [
'field' => 'suggestion',
'skip_duplicates' => true,
'size' => 10
]
]
]
]
]);
$options = $result['suggest']['search_suggest'][0]['options'];
$data = [];
foreach ($options as $option) {
$data[] = $option['text'];
}
return json($data); // 输出:["北京","北京展览馆地区","北京站","北京西站","布吉"]
}