SpringBoot2 | 第三十二篇:SpringBoot整合Elasticsearch

Elasticsearch 是一个基于 Lucene 的搜索引擎,采用 Java 语言编写,使用 Lucene 构建索引,提供索引功能。Elasticsearch 的目的是让全文搜索变得更简单,开发者可以通过简单明了的 RESTFul API 轻松实现搜索功能,而 不必去面对 Lucene 的复杂性。

[TOC]

数据分类与搜索方法

数据分类

  • 结构化数据:指具有固定格式或有限长度的数据,如数据库、元数据等。对于结构化数据,我们一般都是可以通过关系型数据库(MySQL、Oracle 等)去存储和搜索,也可以建立索引,通过 B 树等数据结构快速搜索数据。
  • 非结构化数据全文数据,指不定长或无固定格式的数据,如:邮件、word 文档等。对于非结构化数据,主要有两种搜索方法:顺序扫描法全文搜索法

搜索方法

  • 顺序扫描法:从头到尾按顺序查找,文档内容几乎被通读了一遍。如果数据量大的话,那么这种方法效率会非常
  • 全文搜索法:将非结构化数据的一部分信息提取出来,重新组织,使其变得有一定结构,然后对这些有一定结构的数据建立索引,从而达到了搜索高效的目的。

全文搜索引擎

根据百度百科中的定义,全文搜索引擎是目前广泛应用的主流搜索引擎。它的工作原理是通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序根据索引进行查找,并将结果反馈给用户。常见的搜索引擎有 LuceneSolrElasticsearch

Elasticsearch 的优点

  • 可扩展Elasticsearch 的横向扩展非常灵活,当数据规模比较小的时候可以使用小规模的集群。随着数据的增长,需要更大的容量和更高的性能,此时只需添加更多的节点Elasticsearch自动发现机制识别新增的节点重新平衡地分配数据
  • 全文检索Elasticsearch 在后台使用 Lucene 来提供最强大的全文检索功能。
  • 近实时的搜索和分析:数据进入 Elasticsearch,可以达到近实时搜索。除了搜索,Elasticsearch也可以进行聚合分析操作。
  • 高可用:高可用主要体现在容错机制上,Elasticsearch 集群会自动发现新的或失败的节点,重组和重新平衡数据,确保数据是安全的和可访问的。
  • RESTFul API:几乎所有的操作都可以通过 RESTFul API 使用 JSON 基于 HTTP 请求来实现,客户端也可以使用多种语言。

Elasticsearch 与 Solr 的比较

  • 当单纯的对已有数据进行搜索时,Solr更快。

    1569311772042

  • 当实时建立索引时, Solr会产生io阻塞,查询性能较差, Elasticsearch具有明显的优势。

    1569311785252

  • 随着数据量的增加,Solr的搜索效率会变得更低,而Elasticsearch却没有明显的变化。

    1569311802957

Solr 在建立索引或数据量大的时候,索引效率不高。而 Elasticsearch 则比较稳定。

环境/版本一览:

  • 开发工具:Intellij IDEA 2019.1.3
  • springboot: 2.1.7.RELEASE
  • jdk:1.8.0_171
  • maven:3.3.9
  • elasticsearch:6.2.2

1、pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- 整合Lettuce Redis需要commons-pool2 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<!-- 版本用 5.x 的,与本地保持一致 -->
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

2、application.yml

1
2
3
4
5
6
7
8
spring:
output:
ansi:
enabled: always
data:
elasticsearch:
cluster-name: my-application
cluster-nodes: localhost:9300

3、entity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.fatal.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

/**
* @author Fatal
* @date 2019/9/3 0003 18:31
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = "city", type = "city_search", replicas = 0)
public class City {

@Id
private String id;

@Field(type = FieldType.Keyword)
private String name;

@Field(type = FieldType.Text, analyzer = "ik_smart", searchAnalyzer = "ik_smart")
private String theDetail;
}

4、respoitory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.fatal.repository;

import com.fatal.entity.City;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

/**
* @author Fatal
* @date 2019/9/25 0025 20:38
*/
public interface CityRepository extends ElasticsearchRepository<City, String> {

City findByTheDetail(String theDetail);

}

5、component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
package com.fatal.component;

import org.apache.commons.beanutils.PropertyUtils;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.ElasticsearchException;
import org.springframework.data.elasticsearch.core.DefaultResultMapper;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.stereotype.Component;

import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;

/**
* 自定义 ResultMapper
* @author Fatal
* @date 2019/9/3 0003 18:31
* @desc: 对返回结果进行高亮加工
*/
@Component
public class CustomResultMapper extends DefaultResultMapper {

private static final String ID = "id";

/**
* 填充高亮数据
* @param response
* @param clazz
* @param pageable
* @param <T>
* @return
*/
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
AggregatedPage<T> aggregatedPage = super.mapResults(response, clazz, pageable);
Map<String, Map<String, HighlightField>> highLightMap = Arrays.stream(response.getHits().getHits())
.collect(Collectors.toMap(SearchHit::getId, SearchHit::getHighlightFields));
aggregatedPage.forEach(t -> {
try {
Object id = PropertyUtils.getProperty(t, ID);
Map<String, HighlightField> highlightFields = highLightMap.get(id);
populateHighLightedData(t, highlightFields);
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
e.printStackTrace();
}
});
return aggregatedPage;
}

/**
* 填充高亮数据
* @param result
* @param highlightFields
* @param <T>
*/
private <T> void populateHighLightedData(T result, Map<String, HighlightField> highlightFields) {
for (HighlightField field : highlightFields.values()) {
try {
PropertyUtils.setProperty(result, field.getName(), concat(field.fragments()));
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
throw new ElasticsearchException("failed to set highlighted value for field: " + field.getName()
+ " with value: " + Arrays.toString(field.getFragments()), e);
}
}
}

/**
* 连接多个片段的值,片段数量对应 Stream#readVInt,片段值对应 StreamInput#readText
* 文本字数太多的话,不会一整块,而是将包含有关键字的部分一小段一小段放数组中。当然,保存到es中的文字数目要不要限制就看场景吧
* @param fragments
* @return
*/
private Object concat(Text[] fragments) {
StringBuilder sb = new StringBuilder();
for (Text text : fragments) {
sb.append(text.toString());
}
return sb.toString();
}
}

6、Test

TransportClientTests.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
package com.fatal.client;

import com.fatal.Chapter32ApplicationTests;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.action.update.UpdateResponse;
import org.elasticsearch.client.transport.TransportClient;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.util.Map;

/**
* @author Fatal
* @date 2019/9/23 0023 9:55
*/
public class TransportClientTests extends Chapter32ApplicationTests {

@Autowired
private TransportClient client;

/**
* 根据 id 查询文档
*/
@Test
public void prepareGetTest() {
GetResponse getResponse = client.prepareGet("city", "city_search", "1d90Zm0BEkPQvsGlQXmi").get();
Map<String, Object> source = getResponse.getSourceAsMap();
if (!CollectionUtils.isEmpty(source)) {
System.out.println(source.get("id"));
System.out.println(source.get("name"));
System.out.println(source.get("theDetail"));
}
}

/**
* 新增文档
*/
@Test
public void indexTest() {
try {
XContentBuilder xContentBuilder = XContentFactory.jsonBuilder()
.startObject()
.field("name", "佛山")
.field("theDetail", "佛山文化")
.endObject();
IndexResponse indexResponse = client.prepareIndex("city", "city_search")
.setSource(xContentBuilder)
.get();
System.out.println(indexResponse.getId());
} catch (IOException e) {
e.printStackTrace();
}
}

/**
* 更新文档
*/
@Test
public void updateTest() {
UpdateRequest updateRequest = new UpdateRequest("city", "city_search", "uN_0ZW0BEkPQvsGlN3m_");
try {
XContentBuilder xContentBuilder = XContentFactory.jsonBuilder()
.startObject()
.field("name", "北京")
.field("theDetail", "北京文化")
.endObject();
updateRequest.doc(xContentBuilder);
UpdateResponse updateResponse = null;
try {
updateResponse = client.update(updateRequest).get();
System.out.println(updateResponse.getResult().toString());
} catch (Exception e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
}

/**
* 根据 id 删除文档
*/
@Test
public void prepareDeleteTest() {
DeleteResponse deleteResponse = client.prepareDelete("city", "city_search", "ud_-ZW0BEkPQvsGlBnl1").get();
System.out.println(deleteResponse.getResult());
}


// ---------------- 查询 ----------------

@Test
public void idsQueryTest() {
IdsQueryBuilder queryBuilder = QueryBuilders.idsQuery()
.addIds("uN_0ZW0BEkPQvsGlN3m_", "tN_uZW0BEkPQvsGlu3m_");
analysis(search(queryBuilder));
}

/**
* 单个字段匹配查询
*/
@Test
public void termQueryTest() {
TermQueryBuilder queryBuilder = QueryBuilders.termQuery("theDetail", "汕头");
analysis(search(queryBuilder));
}

/**
* 单个字段匹配查询
*/
@Test
public void matchQueryTest() {
MatchQueryBuilder queryBuilder = QueryBuilders.matchQuery("theDetail", "潮汕美食");
analysis(search(queryBuilder));
}

/**
* 匹配全部文档(这里匹配全部是指匹配指定索引和类型下的全部文档)
*/
@Test
public void matchAllQueryTest() {
MatchAllQueryBuilder queryBuilder = QueryBuilders.matchAllQuery();
analysis(search(queryBuilder));
}

/**
* 多个字段匹配某个值
*/
@Test
public void multiMatchQueryTest() {
MultiMatchQueryBuilder queryBuilder = QueryBuilders.multiMatchQuery("上海", "name", "theDetail");
analysis(search(queryBuilder));
}

/**
* 测试 term 和 match_phrase
* term: 不分词
* match_phrase: 分词之后逐一搜索,返回各个结果的并集
*/
@Test
public void termQueryWithMatchPhraseQueryTest() {
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("theDetail", "我是中国人");
System.out.println(" ============ termQuery ========== ");
analysis(search(termQueryBuilder));
MatchPhraseQueryBuilder matchPhraseQueryBuilder = QueryBuilders.matchPhraseQuery("theDetail", "我是中国人");
System.out.println(" ============ matchPhraseQuery ========== ");
analysis(search(matchPhraseQueryBuilder));
}

/**
* 测试 match_phrase 和 match_phrase_prefix
* match_phrase: 分词之后逐一搜索,返回各个结果的并集
* match_phrase_prefix: 分词之后逐一搜索,返回各个结果的并集。与 match_phrase 不同的是,
* 它支持最后一个词项(term)的前缀匹配(即以最后一个词项为前缀的关键字的文档)。
*/
@Test
public void MatchPhraseQueryWithMatchPhrasePrefixQueryTest() {
MatchPhraseQueryBuilder matchPhraseQueryBuilder = QueryBuilders.matchPhraseQuery("theDetail", "潮汕美");
System.out.println(" ============ matchPhraseQuery ========== ");
analysis(search(matchPhraseQueryBuilder));
MatchPhrasePrefixQueryBuilder matchPhrasePrefixQueryBuilder = QueryBuilders.matchPhrasePrefixQuery("theDetail", "潮汕美");
System.out.println(" ============ matchPhrasePrefixQuery ========== ");
analysis(search(matchPhrasePrefixQueryBuilder));
}

/**
* 测试高亮显示
*/
@Test
public void highLightTest() {
QueryStringQueryBuilder queryBuilder = QueryBuilders.queryStringQuery("汕头");
HighlightBuilder highlightBuilder = new HighlightBuilder()
.field("name")
.field("theDetail");
SearchResponse searchResponse = client.prepareSearch("city")
.setQuery(queryBuilder)
.highlighter(highlightBuilder)
.get();
SearchHits hits = searchResponse.getHits();
System.out.println("查询结果总记录数:" + hits.getTotalHits());
System.out.println(" --- 属性列表 --- ");
hits.forEach(hit -> {
System.out.println(hit.getSourceAsString());
Map<String, Object> source = hit.getSourceAsMap();
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
System.out.println(highlightFields.get("name").getFragments()[0].toString());
System.out.println(highlightFields.get("theDetail").getFragments()[0].toString());
System.out.println(source.get("id"));
System.out.println(source.get("name"));
System.out.println(source.get("theDetail"));
});
}

/**
* 根据条件查找索引为`city`类型为`city_search`的所有文档
* @param queryBuilder
* @return
*/
private SearchResponse search(QueryBuilder queryBuilder) {
return client.prepareSearch("city")
.setTypes("city_search")
.setQuery(queryBuilder)
.get();
}

/**
* 解析
* @param searchResponse
*/
private void analysis(SearchResponse searchResponse) {
SearchHits hits = searchResponse.getHits();
System.out.println("查询结果总记录数:" + hits.getTotalHits());
System.out.println(" --- 属性列表 --- ");
hits.iterator().forEachRemaining(hit -> {
System.out.println(hit.getSourceAsString());
Map<String, Object> source = hit.getSourceAsMap();
System.out.println(source.get("id"));
System.out.println(source.get("name"));
System.out.println(source.get("theDetail"));
});
}

}

CityRepositoryTests.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
package com.fatal.repository;

import com.fatal.Chapter32ApplicationTests;
import com.fatal.entity.City;
import org.elasticsearch.index.query.*;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Arrays;
import java.util.List;

/**
* @author Fatal
* @date 2019/9/3 0003 18:47
*/
public class CityRepositoryTests extends Chapter32ApplicationTests {

@Autowired
private CityRepository repository;

@Test
public void saveTest() {
City city = repository.save(City.builder()
.name("北京")
.theDetail("帝都文化")
.build());
System.out.println(city);
}

@Test
public void findByIdTest() {
City city = repository.findById("mA4zbG0Bm_JGdCcRf3TO")
.orElseThrow(RuntimeException::new);
System.out.println(city);
}

@Test
public void updateTest() {
City find = repository.findById("mA4zbG0Bm_JGdCcRf3TO")
.orElseThrow(RuntimeException::new);
find.setTheDetail("帝都文化~~");
City city = repository.save(find);
System.out.println(city);
}

@Test
public void deleteByIdTest() {
repository.deleteById("7zyQYW0BRs1ajIug43WK");
}

@Test
public void deleteAllTest() {
repository.deleteAll();
}

/**
* 底层也是 bulkIndex
*/
@Test
public void saveAllTest() {
List<City> cities = Arrays.asList(
City.builder().name("天津").theDetail("天津文化").build(),
City.builder().name("汕头").theDetail("汕头文化 潮汕美食,牛肉火锅").build(),
City.builder().name("上海").theDetail("上海文化").build(),
City.builder().name("深圳").theDetail("深圳文化").build(),
City.builder().name("广州").theDetail("广州文化,与上海的差异很大").build()
);
Iterable<City> result = repository.saveAll(cities);
result.forEach(System.out::println);
}

@Test
public void findAllTest() {
Iterable<City> all = repository.findAll();
all.forEach(System.out::println);
}

/**
* 底层也是dsl,也会根据@Document决定是否需要分词
*/
@Test
public void findByTheDetailTest() {
City city = repository.findByTheDetail("美食");
System.out.println(city);
}

@Test
public void matchQueryTest() {
MatchQueryBuilder queryBuilder = QueryBuilders.matchQuery("theDetail", "汕头文化");
Iterable<City> cities = repository.search(queryBuilder);
print(cities);
}

@Test
public void matchPhraseQueryTest() {
MatchPhraseQueryBuilder queryBuilder = QueryBuilders.matchPhraseQuery("theDetail", "汕头文化");
Iterable<City> cities = repository.search(queryBuilder);
print(cities);
}

@Test
public void matchAllQueryTest() {
MatchAllQueryBuilder queryBuilder = QueryBuilders.matchAllQuery();
Iterable<City> cities = repository.search(queryBuilder);
print(cities);
}

/**
* term查询的时候不被解析,只有查询词和文档中的词精确匹配才能被查到
*/
@Test
public void termQueryTest() {
TermQueryBuilder queryBuilder = QueryBuilders.termQuery("name", "深圳");
Iterable<City> cities = repository.search(queryBuilder);
print(cities);
}

/**
* 复合条件查询
*/
@Test
public void compoundConditionalQueryTest() {
BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery()
.should(QueryBuilders.matchQuery("name", "汕头"))
.should(QueryBuilders.matchQuery("theDetail", "差异"));
Iterable<City> cities = repository.search(queryBuilder);
print(cities);
}

private void print(Iterable<City> cities) {
cities.forEach(System.out::println);
}

}

ElasticsearchTemplateTests.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
package com.fatal.template;

import com.fatal.Chapter32ApplicationTests;
import com.fatal.component.CustomResultMapper;
import com.fatal.entity.City;
import org.elasticsearch.index.query.MatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.query.IndexQuery;
import org.springframework.data.elasticsearch.core.query.IndexQueryBuilder;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;

import java.util.Arrays;
import java.util.List;

/**
* @author Fatal
* @date 2019/9/24 0024 8:40
*/
public class ElasticsearchTemplateTests extends Chapter32ApplicationTests {

@Autowired
private ElasticsearchTemplate template;

@Autowired
private CustomResultMapper customResultMapper;

/**
* 新增文档
*/
@Test
public void indexTest() {
City city = City.builder()
.name("海贼王")
.theDetail("《航海王》是日本漫画家尾田荣一郎作画的少年漫画作品,在《周刊少年Jump》1997年第34号开始连载,电子版由漫番漫画连载。改编的电视动画《航海王》于1999年10月20日起在富士电...")
.build();
IndexQuery indexQuery = new IndexQueryBuilder()
.withObject(city)
.build();
String documentId = template.index(indexQuery);
System.out.println(documentId);
}

/**
* 批量新增文档
*/
@Test
public void bulkIndexTest() {
List<IndexQuery> queries = Arrays.asList(
new IndexQueryBuilder()
.withObject(City.builder().name("重庆").theDetail("重庆文化"))
.build(),
new IndexQueryBuilder()
.withObject(City.builder().name("长沙").theDetail("长沙文化"))
.build()
);
template.bulkIndex(queries);
}

@Test
public void queryStringTest() {
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.queryStringQuery("惠州"))
.withPageable(PageRequest.of(0, 5))
.build();
List<City> cities = template.queryForList(searchQuery, City.class);
cities.forEach(System.out::println);
}

@Test
public void matchQueryTest() {
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.matchQuery("theDetail", "美食"))
.withPageable(PageRequest.of(0, 5))
.build();
List<City> cities = template.queryForList(searchQuery, City.class);
cities.forEach(System.out::println);
}

/**
* 测试高亮查询
*/
@Test
public void highLightTest() {
MatchQueryBuilder queryBuilder = QueryBuilders.matchQuery("theDetail", "美食");
// 指定高亮的class属性值之后,具体样式交给前端设计。支持设置多种class属性值。
HighlightBuilder highlightBuilder = new HighlightBuilder()
.preTags(HighlightBuilder.DEFAULT_STYLED_PRE_TAG)
.postTags(HighlightBuilder.DEFAULT_STYLED_POST_TAGS);
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(queryBuilder)
.withHighlightFields(new HighlightBuilder.Field("theDetail"))
.withHighlightBuilder(highlightBuilder)
.build();
Page<City> page = template.queryForPage(searchQuery, City.class, customResultMapper);
page.forEach(System.out::println);
}


}

7、测试

启动 ES,并且启动 head 插件。这里的 ID 都是 ES 自动生成的

批量添加

执行 CityRepositoryTests.saveAll()

查看控制台

1569477525129

你会发现,id 都是 null。郁闷呢。其实这里不影响使用。查询的时候它就给你显示出来了。

看看 head 插件

索引信息

1569477992763

数据浏览

1569478039399

更新

执行 CityRepositoryTests.updateTest()

查看控制台

1569483440139

看看 head 插件

每更新一次,版本号 + 1

1569483541751

查询

执行 CityRepositoryTests.compoundConditionalQueryTest()

查看控制台

1569478168315

高亮查询

执行 ElasticsearchTemplateTests.highLightTest()

查看控制台

1569478253881

其他…

自己测

笔记

常用注解和枚举

@Document

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public @interface Document {
// 索引库名
String indexName();
// 类型名
String type() default "";
// 是否使用服务器配置
boolean useServerConfiguration() default false;
// 分片数,默认5
short shards() default 5;
// 副本数,默认1
short replicas() default 1;
// 刷新间隔,默认1秒
String refreshInterval() default "1s";
// 索引存储类型
String indexStoreType() default "fs";
// 是否创建索引
boolean createIndex() default true;
// 版本类型
VersionType versionType() default VersionType.EXTERNAL;
}

@Field

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public @interface Field {
// 字段类型
FieldType type() default FieldType.Auto;
// 指定字段是否被索引,默认是
boolean index() default true;
// 时间格式
DateFormat format() default DateFormat.none;
// 用于指定时间格式
String pattern() default "";
// 是否存储,默认否
boolean store() default false;
//
boolean fielddata() default false;
// 指定搜索时使用的分词器,默认使用standard分词器
String searchAnalyzer() default "";
// 指定建立索引时使用的分词器,默认使用standard分词器
String analyzer() default "";
// 用于解析前的标准化配置
String normalizer() default "";
//
String[] ignoreFields() default {};
//
boolean includeInParent() default false;
// 用于自定义 _all 字段,可以把多个字段的值复制到一个超级字段
String[] copyTo() default {};
}

FieldType 的 text 和 keyword 的区别

  • text :如果一个字段是要被全文搜索的,比如邮件内容,产品描述,新闻内容,应该使用 text 类型。设置 text 类型之后,字段内容会被分析,在生成倒排索引以前,字符串会被分词器分成一个一个词项。text 类型的字段不用与排序,很少用于聚合(termsAggregation 除外)。
  • keyword :适用于索引结构化的数据,比如 email 地址、主机名、状态码和标签,通常用于过滤(比如查找已发布在微博中 status 属性为 published 的文章)、排序聚合。类型为 keyword 的字段只能用于精确值搜索,区别于 text 类型。注意,keyword 是不参与分词的。

问题

  1. Lombok@Accessors(chain = true)PropertyUtils 有冲突。具体体现在同时使用的话,PropertyUtils.setProperty(final Object bean, final String name, final Object value) 方法解析不了 setter 方法,进而报错 NoSuchMethodException…。

    原因如下:PropertyUtils 解析时,要求 settter 方法不能有返回值。

  2. @Builder 注解加到类上时程序找不到类的无参构造方法

    原因:加 @Builder 之后默认生成一个 default 级别的全参构造器,所以无参构造器就不会自动生成了。

    如果只加 @NoArgsConstructor ,那么 @Builder 就不会生成全参构造器,这时候编译就不会通过,所以解决方法时,使用 @Builder 的时候,带上 @NoArgsConstructor@AllArgsConstructor 即可。

参考资料

ElasticSearch入门

解释为什么ES可以做到近实时搜索

搜索引擎选择: Elasticsearch与Solr

elasticsearch技术解析与实战

从Lucene到Elasticsearch:全文检索实战

和我一起打造个简单搜索之SpringDataElasticSearch关键词高亮

@Builder注解加到类上时程序找不到类的无参构造方法

总结

SpringBoot的知识已经有前辈在我们之前探索了。比较喜欢的博主有:唐亚峰 | Battcn方志朋的专栏程序猿DD纯洁的微笑。对这门技术感兴趣的可以去他们的博客逛逛。谢谢他们的分享~~

以上文章是我用来学习的Demo,都是基于 SpringBoot2.x 版本。

源码地址: https://github.com/ynfatal/springboot2-learning/tree/master/chapter32

学习 瓦力老师 前辈的经验