Elasticsearch 高级
建议阅读顺序:
- Elasticsearch 入门
- Elasticsearch 搜索
- Elasticsearch 搜索高级
- Elasticsearch高级(本文)
1. nested 类型
1.1 介绍
Elasticsearch 中的 nested
类型允许你在文档内存储复杂的数据结构,比如一个用户可能有多个地址,或者一个博客文章可能有多个标签等。nested
类型可以让你索引这些复杂数据,并且允许你对嵌套的数据进行查询。
1.2 添加 nested 文档
向商品映射中添加 nested 类型的字段 attr_list
- attr_list 表示商品属性
- attr_list 有两个字段:color、memory
PUT /items/_mapping
{
"properties":{
"attr_list":{
"type":"nested",
"properties":{
"name":{ "type":"keyword" },
"value":{ "type":"keyword" }
}
}
}
}
由于添加了字段,所以需要对 items 索引进行更新,但是建议先删后加:
DELETE /items
PUT /items
{
"mappings" : {
"properties" : {
"brand" : { "type" : "keyword" },
"category" : { "type" : "keyword" },
"commentCount" : { "type" : "integer", "index" : false },
"id" : { "type" : "keyword" },
"image" : { "type" : "keyword", "index" : false },
"isAD" : { "type" : "boolean" },
"name" : {
"type" : "text",
"analyzer" : "ik_max_word",
"search_analyzer" : "ik_smart"
},
"price" : { "type" : "integer" },
"sold" : { "type" : "integer" },
"stock" : { "type" : "integer" },
"updateTime" : { "type" : "date" },
"location" : { "type" : "geo_point" },
"attr_list":{
"type":"nested",
"properties":{
"name":{ "type":"keyword" },
"value":{ "type":"keyword" }
}
}
}
}
}
在实体类上添加属性:
// 商品属性
@ApiModelProperty("商品规格")
private List<Spec> attr_list;
@Data
public static class Spec {
private String name;
private String value;
}
向索引添加文档,可以添加单个文档也可以批量添加文档,添加文档时指定商品属性:
@Test
void testAddDocument2() throws Exception {
// 商品id
Long id = 317578L;
// 根据id查询商品
Item item = itemService.getById(id);
// 转为ItemDoc
ItemDoc itemDoc = BeanUtils.copyBean(item, ItemDoc.class);
ItemDoc.Spec spec_1 = new ItemDoc.Spec();
spec_1.setName("大小");
spec_1.setValue("60*40");
// 再设置一个新规格
ItemDoc.Spec spec_2 = new ItemDoc.Spec();
spec_2.setName("颜色");
spec_2.setValue("白色");
itemDoc.setAttr_list(List.of(spec_1, spec_2));
// 使用esClient添加文档
IndexResponse response = esClient.index(
i -> i.index("items").id(id.toString()).document(itemDoc)
);
// 打印结果
String s = response.result().jsonValue();
log.info("添加文档结果:{}", s);
}
查询文档:GET /items/_doc/{id}
1.3 搜索 nested
查询商品颜色是白色的商品:
GET /items/_search
{
"query": {
"nested": {
"path": "attr_list",
"query": {
"bool": {
"must": [
{ "term": { "attr_list.name": { "value": "颜色" } } },
{ "term": { "attr_list.value": { "value": "白色" } } }
]
}
}
}
}
}
"nested"
:这是一个嵌套查询,用于查询嵌套对象。它允许你在嵌套对象中执行更复杂的查询。
"path": "attr_list"
:指定了要查询哪个嵌套对象字段。在这个例子中,嵌套对象的字段名是attr_list
1.4 聚合 nested
先按商品属性名称聚合,再按属性值聚合:
GET /items/_search
{
"size": 0,
"aggs": {
"attr_aggs": {
"nested": { "path": "attr_list" },
"aggs": {
"attr_name_aggs": {
"terms": { "field": "attr_list.name", "size": 10 },
"aggs": {
"attr_value_aggs": {
"terms": { "field": "attr_list.value", "size": 10 }
}
}
}
}
}
}
}
1.5 Java Client
1.5.1 nested 查询
将 “查询商品颜色是白色的商品” 的 DSL 转为对应代码:
@Test
void testNested() throws Exception {
SearchRequest.Builder builder = new SearchRequest.Builder();
builder.index("items");
builder.query(
q -> q.nested(
n -> n.path("attr_list").query(
q1 -> q1.bool(
b -> b.must(
a -> a.term(
t -> t.field("attr_list.name").value("颜色")
)
).must(
a1 -> a1.term(
t -> t.field("attr_list.value").value("白色")
)
)
)
)
)
);
SearchRequest build = builder.build();
SearchResponse<ItemDoc> response = esClient.search(build, ItemDoc.class);
// 解析结果
List<Hit<ItemDoc>> hits = response.hits().hits();
hits.forEach(hit -> {
ItemDoc source = hit.source();
log.info("查询结果:{}", source);
});
}
1.5.2 nested 聚合
将 “先按商品属性名称聚合,再按属性值聚合” 的 DSL 转为对应代码:
@Test
void testNestedAggs() throws Exception {
SearchRequest.Builder builder = new SearchRequest.Builder();
builder.index("items2");
builder.size(0);
builder.aggregations(
"attr_aggs",
a -> a.nested(
n -> n.path("attr_list")
).aggregations(
"attr_name_aggs",
a1 -> a1.terms(
t -> t.field("attr_list.name").size(10)
).aggregations(
"attr_value_aggs",
a2 -> a2.terms(
t -> t.field("attr_list.value")
)
)
)
);
SearchRequest build = builder.build();
SearchResponse<ItemDoc> response = esClient.search(build, ItemDoc.class);
Map<String, Aggregate> aggregations = response.aggregations();
Aggregate attrAggs = aggregations.get("attr_aggs");
//解析结果
NestedAggregate nested = attrAggs.nested();
Map<String, Aggregate> attrNameAggs = nested.aggregations();
Aggregate aggregate = attrNameAggs.get("attr_name_aggs");
aggregate.sterms().buckets().array().forEach(bucket -> {
String key = bucket.key().stringValue();
Long docCount = bucket.docCount();
log.info("属性名:{},属性值数量:{}", key, docCount);
Map<String, Aggregate> aggregations1 = bucket.aggregations();
Aggregate attrValueAggs = aggregations1.get("attr_value_aggs");
attrValueAggs.sterms().buckets().array().forEach(bucket1 -> {
String key1 = bucket1.key().stringValue();
Long docCount1 = bucket1.docCount();
log.info("属性值:{},属性值数量:{}", key1, docCount1);
});
});
}
2. 同义词
2.1 设置同义词
搜索中同义词的需求:在搜索时输入一个关键字,包含关键字同义词的文档应该也可以搜索出来。
比如:输入“电脑”,会搜索出包含 “计算机” 的文档,输入 “黑马” 搜索出 “黑马程序员”、“传智播客” 的文章。
elasticsearch 的同义词有如下两种形式:
-
单向同义词:
heima,黑马=>黑马程序员,黑马、传智播客
箭头左侧的词都会映射成箭头右侧的词。
输入箭头左侧的词可以搜索出箭头右侧的词。
-
双向同义词:
马铃薯, 土豆, potato
双向同义词可以互相映射。
输入 “土豆” 可以搜索出 “potato”,输入 “potato” 可以搜索出 “土豆”
怎么设置同义词?
首先在同义词加到 synonyms.txt 文件中,synonyms.txt 文件在 es 的 config 目录下。
在 synonyms.txt 中加入:
中国,中华人民共和国,china
heima,黑马=>黑马程序员,黑马、传智播客
...
2.2 定义同义词分词器
在设置索引映射时自定义同义词分词器 my_synonyms_analyzer
,并且用于 “title” 字段的搜索。
PUT /test_index
{
"settings": {
"analysis": {
"filter": {
"my_synonym_filter": {
"type": "synonym",
"updateable": true,
"synonyms_path": "synonyms.txt"
}
},
"analyzer": {
"my_synonyms_analyzer": {
"tokenizer": "ik_smart",
"filter": [ "my_synonym_filter" ]
}
}
}
},
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "my_synonyms_analyzer"
}
}
}
}
参数说明:
my_synonym_filter
是自定义的词汇过滤器;my_synonyms_analyzer
是自定义的分析器,my_synonyms_analyzer
包含并引用了my_synonym_filter
;updateable
指示能否动态更新,必须为true
才能动态更新同义词;synonyms_path
指示同义词文件的位置;my_synonyms_analyzer
分析器里用ik_smart
的分词器,my_synonyms_analyzer 的分词流程是原始文本先经过ik_smart
分词的结果再用 my_synonym_filter 处理;mappings.properties.title.search_analyzer
指示title
字段在搜索时使用my_synonyms_analyzer
分析器。
2.3 测试
先向 test_index 索引中添加数据:
POST /_bulk
{"index": {"_index":"test_index", "_id": "5"}}
{"title": "china你好"}
{"index": {"_index":"test_index", "_id": "4"}}
{"title": "中国你好"}
{"index": {"_index":"test_index", "_id": "6"}}
{"title": "中华人民共和国你好"}
{"index": {"_index":"test_index", "_id": "7"}}
{"title": "China你好"}
{"index": {"_index":"test_index", "_id": "8"}}
{"title": "这是一匹黑马"}
{"index": {"_index":"test_index", "_id": "9"}}
{"title": "黑马是中国良心培训机构"}
{"index": {"_index":"test_index", "_id": "10"}}
{"title": "黑马程序员是中国良心培训机构"}
{"index": {"_index":"test_index", "_id": "11"}}
{"title": "传智播客一所IT培训机构"}
搜索关键字 “china”:
GET /test_index/_search
{
"query": {
"match": { "title": "china" }
}
}
分析查询到的结果,会发现,查询到的结果只有包含关键字的值,很显然同义词并没有生效,此时就要开启同义词生效,执行 POST /test_index/_reload_search_analyzers
。
3. 自动补全
3.1 介绍
当在搜索框输入字符时提示出与该字符有关的搜索项,这个效果就是自动补全。
Elasticsearch 如何实现自动补全?
要实现上述自动补全的需求需要完成两个功能:
- 拼音搜索
- 前缀搜索
3.2 拼音分词器
3.2.1 安装拼音分词器
与 IK 分词器一样,拼音分词器也有插件,在 GitHub 上有 elasticsearch 的拼音分词插件。
地址:https://github.com/medcl/elasticsearch-analysis-pinyin
找到与 Elasticsearch 版本一致的插件下载包。
安装方式与IK分词器一样,分三步:
- 解压 elasticsearch-analysis-pinyin-7.17.7.zip
- 上传到虚拟机中 elasticsearch 的 plugin 目录
- 重启 elasticsearch
- 测试
测试
POST /_analyze
{
"text": "黑马程序员",
"analyzer": "pinyin"
}
3.2.2 自定义分词器
默认的拼音分词器会将每个汉字单独分为拼音,而我们希望的是每个词条形成一组拼音,需要对拼音分词器做个性化定制,形成自定义分词器。
elasticsearch 中分词器(analyzer)的组成包含三部分:
character filters
:在 tokenizer 之前对文本进行处理。例如删除字符、替换字符tokenizer
:将文本按照一定的规则切割成词条(term)。例如 keyword,就是不分词;还有 ik_smarttokenizer filter
:将 tokenizer 输出的词条做进一步处理。例如大小写转换、同义词处理、拼音处理等

声明自定义分词器的语法如下:
PUT /test_index2
{
"settings": {
"analysis": {
// 自定义分词器
"analyzer": {
// 分词器名称
"my_analyzer": { "tokenizer": "ik_max_word", "filter": "py" }
},
// 自定义tokenizer filter
"filter": {
// 过滤器名称
"py": {
// 过滤器类型,这里是pinyin
"type": "pinyin",
"keep_full_pinyin": true,
"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"
}
}
}
}
属性说明:
-
type: "pinyin"
:这指定了过滤器的类型为
pinyin
,即使用拼音分词器。 -
keep_full_pinyin: false
:控制是否保留完整的拼音形式。设置为
false
表示不保留完整的拼音形式。 -
keep_joined_full_pinyin: true
:控制是否保留连接的完整拼音形式。设置为
true
表示保留连接的完整拼音形式,例如 “你好” 可能会被转换为 “nihao”。 -
keep_original: true
:控制是否保留原文本。设置为
true
表示保留原文本,这在某些情况下很有用,例如需要同时支持拼音和原文本的搜索。 -
limit_first_letter_length: 16
:控制首字母的最大长度。例如,“你好” 的首字母形式为 “n h”,这个值控制了首字母的最大长度。
-
remove_duplicated_term: true
:控制是否移除重复的词条。设置为
true
表示移除重复的词条,这有助于减少索引大小。 -
none_chinese_pinyin_tokenize: false
:控制是否对非中文文本也进行拼音分词。设置为
false
表示不对非中文文本进行拼音分词。
测试自定义分词器:
POST /test_index2/_analyze
{
"text": "黑马程序员",
"analyzer": "my_analyzer"
}
3.3 自动补全查询
3.3.1 completion
Elasticsearch 专门设计 completion
查询用于自动补全,completion
查询可以实现前缀搜索的效果,性能比前缀搜索更快。
completion
查询会匹配以用户输入内容开头的词条并返回,使用 completion
查询对文档中字段的类型有一些约束:
- 参与补全查询的字段必须是 completion 类型。
- 字段的内容一般是用来补全的多个词条形成的数组。
在 test_index2 索引中添加 suggestion 字段并且设置为 completion 类型:
PUT /test_index2/_mapping
{
"properties": {
"suggestion": { "type":"completion" }
}
}
3.3.2 测试
更新原有文档,文档中指定了自动补全的内容:
POST /test_index2/_update/100
{
"doc": { "suggestion": ["拉杆箱","托运箱"] }
}
POST /test_index2/_update/101
{
"doc": { "suggestion": ["拉杆箱","旅行箱","莎米特"] }
}
测试:
GET /test_index2/_search
{
"suggest" : {
"suggestion_suggest" : {
"completion" : {
"field": "suggestion",
"size": 10,
"skip_duplicates": true
},
"text" : "旅"
}
}
}
参数说明:
-
suggest
:这是建议器的顶级对象,用于配置建议器。 -
suggestion_suggest
:这是建议器的名称,可以自定义。它用于标识建议器。 -
completion
:这是指定建议器类型的部分。在这里,我们使用的是
completion
类型,它是专门为自动补全设计的建议器。 -
field
:这是用于建议的字段名称。在这个例子中,我们使用名为
suggestion
的字段,该字段应该已经被配置为completion
类型的字段。 -
size
:这个参数控制返回的建议数量。在这个例子中,我们设置了
size
为10,意味着最多返回10个建议。 -
skip_duplicates
:这个参数用于控制是否在返回的建议中跳过重复的条目。在这个例子中,我们设置为
true
,意味着如果某个建议在多个文档中出现,只会返回一次。 -
text
:这是用户输入的文本,用于生成建议。
3.3.3 Java Client
@Test
void testSuggest() throws IOException {
SearchRequest.Builder builder = new SearchRequest.Builder();
builder.index("test_index2");
builder.suggest(
s -> s.suggesters(
"suggestion_suggest",
ss -> ss.completion(
c -> c.field("suggestion").size(10).skipDuplicates(true)
).text("拉")
)
);
SearchRequest request = builder.build();
SearchResponse<Index2> response = esClient.search(request, Index2.class);
Map<String, List<Suggestion<Index2>>> suggest = response.suggest();
List<Suggestion<Index2>> suggestion_suggest = suggest.get("suggestion_suggest");
suggestion_suggest.stream().forEach(suggestion -> {
suggestion.completion().options().forEach(option -> {
String text = option.text();
System.out.println(text);
});
});
}
/**
* 测试自动补全模型类
*/
@Data
public static class Index2 {
//id
private Long id;
//name
private String name;
private List<String> suggestion;
}
3.4 拼音自动补全
3.4.1 创建自动补全字段
自定义分词器:
PUT /test_index3
{
"settings": {
"analysis": {
"analyzer": {
"completion_analyzer": { "tokenizer": "keyword", "filter": "py" }
},
"filter": {
"py": {
"type": "pinyin",
"keep_full_pinyin": true,
"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": "ik_max_word",
"search_analyzer": "ik_smart"
},
"suggestion":{ "type": "completion", "analyzer": "completion_analyzer" }
}
}
}
3.4.2 更新/新增文档
POST test_index3/_doc/100
{
"id":100,
"name":"RIMOWA 30寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4",
"suggestion": ["拉杆箱","托运箱"]
}
POST test_index3/_doc/101
{
"id":101,
"name":"莎米特SUMMIT 旅行拉杆箱28英寸PC材质大容量旅行行李箱PC154 黑色",
"suggestion": ["拉杆箱","旅行箱","莎米特"]
}
POST test_index3/_doc/102
{
"id":102,
"name":"拉菲斯汀(La Festin)612026 新款女士钱包 头层牛皮短款钱包 凯利黑",
"suggestion": ["拉菲斯汀","女包"]
}
3.4.3 测试
GET /test_index3/_search
{
"suggest": {
"suggestion_suggest": {
"completion": {
"field": "suggestion",
"size": 2,
"skip_duplicates": true
},
"text": "la"
}
}
}
