在上次的文章Spring Data Elasticsearch的使用中解释了我在项目中使用ES
的原因,但是元旦前公司检查出来使用的ES
没有设置用户名和密码,因为当时安装的时候ES
是在内网访问的,所以就没有设置用户名和密码,本来设置密码简简单单就完事了,结果项目整合ES
就出了问题。这里说下相关的版本问题,项目使用的Spring Boot
是2.1.13
,ES
版本则是6.8.6
。网上看了下相关的资料如果使用用户名和密码访问ES
需要使用x-pack-client
,但是引入之后项目启动各种报错。后来看了下文档,发现版本有差别,而且自己也尝试了几种方案最后都是失败了,无奈之下只能放弃使用Spring Data Elasticsearch
,然后我看到文档说建议使用 High Level REST Client,所以我就换上了RestHighLevelClient
,所以今天就改用它来对ES
进行操作。
一、 修改项目依赖
如果你的Spring Data Elasticsearch
版本是3.2.*
或以上是包含了High Level REST Client
依赖的。而公司项目使用的Spring Data Elasticsearch
是3.1.*
,所以在公司的项目中我是移除了Spring Data Elasticsearch
依赖,单独重新添加了elasticsearch
和elasticsearch-rest-high-level-client
两个依赖。当然本次项目是Spring Data Elasticsearch
版本是4.1.1
,不过我也是移除了Spring Data Elasticsearch
依赖,添加elasticsearch
和elasticsearch-rest-high-level-client
两个依赖,本次项目是在上次项目的基础上进行修改,项目源码见我的github,本次项目pom.xml
如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.ypc.spring.data</groupId>
<artifactId>elastic</artifactId>
<version>1.0-SNAPSHOT</version>
<name>elastic</name>
<description>ES project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<es.version>7.6.0</es.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>${es.version}</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>${es.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
二、配置RestHighLevelClient
项目的配置文件修改如下
spring.elasticsearch.rest.uris=127.0.0.1:9200
spring.elasticsearch.rest.connection-timeout=6s
spring.elasticsearch.rest.read-timeout=10s
spring.elasticsearch.rest.password=123456
spring.elasticsearch.rest.username=elastic
因为没有了自动配置,因此需要我们自己创建相应的Bean
,另外因为 ES
设置了用户名和密码,在配置类当中也要进行权限配置,代码如下:
@Configuration
public class ESConfig {
@Value("${spring.elasticsearch.rest.uris}")
private List<String> uris;
@Value("${spring.elasticsearch.rest.password}")
private String userName;
@Value("${spring.elasticsearch.rest.username}")
private String password;
@Bean
public RestHighLevelClient restHighLevelClient() {
HttpHost[] httpHosts = createHosts();
RestClientBuilder restClientBuilder = RestClient.builder(httpHosts)
.setHttpClientConfigCallback(new RestClientBuilder.HttpClientConfigCallback() {
@Override
public HttpAsyncClientBuilder customizeHttpClient(HttpAsyncClientBuilder httpClientBuilder) {
CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY,new UsernamePasswordCredentials(userName,password));
return httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
}
});
RestHighLevelClient restHighLevelClient = new RestHighLevelClient(restClientBuilder);
return restHighLevelClient;
}
private HttpHost[] createHosts() {
HttpHost[] httpHosts = new HttpHost[uris.size()];
for (int i = 0; i < uris.size(); i++) {
String hostStr = uris.get(i);
String[] host = hostStr.split(":");
httpHosts[i] = new HttpHost(StrUtil.trim(host[0]),Integer.valueOf(StrUtil.trim(host[1])));
}
return httpHosts;
}
}
三、RestHighLevelClient使用
RestHighLevelClient
的使用无非就是对ES
进行一些操作,增删改查等等。另外因为我们没有使用了Spring Data Elasticsearch
所以没法通过注解的方式来创建index
、setting
和mapping
。这些都需要我们自己去创建。所以需要创建一个Runner
在项目启动之后校验index
是否存在,不存在则创建index
和它的setting
、mapping
,代码如下:
@Slf4j
@Component
public class ElasticsearchRunner implements ApplicationRunner {
@Autowired
private RestHighLevelClient restHighLevelClient;
private static final String USER_INDEX_NAME = "user_entity";
private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
@Override
public void run(ApplicationArguments args) throws Exception {
GetIndexRequest getIndexRequest = new GetIndexRequest(USER_INDEX_NAME);
Boolean exist = restHighLevelClient.indices().exists(getIndexRequest, RequestOptions.DEFAULT);
// 不存在则创建index和setting mapping
if (!exist) {
CreateIndexRequest createIndexRequest = new CreateIndexRequest(USER_INDEX_NAME);
Settings settings = Settings.builder()
.put("index.number_of_shards",1)
.put("index.number_of_replicas",1)
.build();
Map<String,Object> propertyMap = createIndexMapping();
createIndexRequest.settings(settings).mapping(propertyMap);
CreateIndexResponse createIndexResponse = restHighLevelClient.indices().create(createIndexRequest,RequestOptions.DEFAULT);
if (!createIndexResponse.isAcknowledged()) {
log.error(">>>> 创建索引和映射关系失败! <<<<");
throw new RuntimeException("创建索引和映射关系失败");
}
}
}
private Map<String,Object> createIndexMapping() {
Map<String,Object> resultMap = new HashMap<>();
Map<String,Object> fieldsMap = new HashMap<>();
UserEntity userEntity = new UserEntity();
Map<String,Object> beanMap = BeanUtil.beanToMap(userEntity,false,false);
for (Map.Entry<String,Object> entry : beanMap.entrySet()) {
String key = entry.getKey();
Map<String,Object> map = new HashMap<>();
if ("id".equals(key)) {
Map<String,Object> map2 = new HashMap<>();
map2.put("type","keyword");
map2.put("ignore_above",256);
Map<String,Object> map1 = new HashMap<>();
map1.put("keyword",map2);
map.put("type","text");
map.put("fields",map1);
} else if ("orderEntityList".equals(key)) {
map = createNested();
} else {
map.put("type","keyword");
map.put("store",true);
}
fieldsMap.put(key,map);
}
resultMap.put("properties",fieldsMap);
return resultMap;
}
private Map<String, Object> createNested() {
Map<String,Object> resultMap = new HashMap<>();
resultMap.put("type","nested");
Map<String,Object> nestedMap = generateMap();
resultMap.put("properties",nestedMap);
return resultMap;
}
private Map<String, Object> generateMap() {
OrderEntity orderEntity = new OrderEntity();
Map<String,Object> map = BeanUtil.beanToMap(orderEntity,false,false);
Map<String,Object> resultMap = new HashMap<>();
for (Map.Entry<String,Object> entry: map.entrySet()) {
String key = entry.getKey();
Map<String,Object> field = new HashMap<>();
if ("updateTime".equals(key) || "createTime".equals(key)) {
field.put("type","date");
field.put("store",true);
field.put("format",DATE_FORMAT);
} else {
field.put("type","keyword");
field.put("store",true);
}
resultMap.put(key,field);
}
return resultMap;
}
}
上面的代码其实比较麻烦的就是创建索引的mapping
,当然创建的方式有多种,我这里使用了Map
,其实最终都是json
的形式。如果字段多确实比较繁琐,这时候还是觉得Spring Data Elasticsearch
比较方便。
接下来我们就开始使用RestHighLevelClient
进行增删改查,为了方便我将基本沿用原来代码,只是会修改了具体实现。
1 、添加
因为嵌套类型的关于时间的属性的类型是Date
,而在上面创建mapping
的代码中可以看出,我指定了时间格式,因此必须将Date
类型转换为指定格式的字符串,这点一定需要注意,不然会报错。
新增代码如下:
@Override
public UserEntity save(UserEntity userEntity) {
String id = IdUtil.simpleUUID();
userEntity.setId(id);
List<OrderEntity> orderEntityList = new ArrayList<>();
for (int i = 0; i < 4; i++) {
OrderEntity orderEntity = new OrderEntity();
setProperties(orderEntity,i);
orderEntity.setUserId(id);
orderEntityList.add(orderEntity);
}
userEntity.setOrderEntityList(orderEntityList);
Map<String,Object> sourceMap = createSourceMap(userEntity);
IndexRequest indexRequest = new IndexRequest(USER_INDEX_NAME)
.opType(DocWriteRequest.OpType.CREATE).id(id).source(sourceMap);
try {
IndexResponse indexResponse = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
if (indexResponse.status().getStatus() != RestStatus.CREATED.getStatus()) {
log.error(">>>> 新增数据失败,返回结果状态码={},错误信息={} <<<<",indexResponse.status().getStatus());
}
} catch (IOException e) {
log.error(">>>> 新增数据出现异常,异常信息={} <<<<",e.getMessage());
}
return userEntity;
}
private Map<String, Object> createSourceMap(UserEntity userEntity) {
Map<String,Object> resultMap = BeanUtil.beanToMap(userEntity,false,true);
List<Map<String,Object>> nestedMap = createNestedMap(userEntity.getOrderEntityList());
resultMap.put("orderEntityList",nestedMap);
return resultMap;
}
private List<Map<String, Object>> createNestedMap(List<OrderEntity> orderEntityList) {
List<Map<String,Object>> list = new ArrayList<>(orderEntityList.size());
String format = "yyyy-MM-dd HH:mm:ss";
for (OrderEntity orderEntity : orderEntityList) {
Map<String,Object> beanMap = BeanUtil.beanToMap(orderEntity,false,true);
Date createTime = (Date) beanMap.get("createTime");
if (Objects.nonNull(createTime)) {
beanMap.put("createTime",DateUtil.format(createTime,format));
}
Date updateTime = (Date) beanMap.get("updateTime");
if (Objects.nonNull(updateTime)) {
beanMap.put("updateTime",DateUtil.format(updateTime,format));
}
list.add(beanMap);
}
return list;
}
这里还需要注意点,IndexRequest
的opType
不管有没有赋值,最终indexResponse
返回的状态码都是201
,开始我是以200
判断,但是后来查了一下发现有ES
数据。
2 、删除
这里根据id
删除也是比较简单的,代码如下:
@Override
public void deleteById(String id) {
DeleteRequest deleteRequest = new DeleteRequest(USER_INDEX_NAME).id(id);
try {
DeleteResponse deleteResponse = restHighLevelClient.delete(deleteRequest,RequestOptions.DEFAULT);
if (deleteResponse.status().getStatus() != RestStatus.OK.getStatus()) {
log.error(">>>> 删除id={}数据失败,返回状态码={} <<<<",id,deleteResponse.status().getStatus());
}
} catch (IOException e) {
log.error(">>>> 删除数据发生异常,id={},异常信息={} <<<<",id,e.getMessage());
}
}
这里比较简单就不再细述了。
3 、修改
修改其实也比较简单,通过下面的代码看下:
@Override
public UserEntity update(UserEntity userEntity) {
String id = userEntity.getId();
Map<String,Object> sourceMap = BeanUtil.beanToMap(userEntity,false,true);
if (CollUtil.isNotEmpty(userEntity.getOrderEntityList())) {
sourceMap.put("orderEntityList",createNestedMap(userEntity.getOrderEntityList()));
}
try {
UpdateRequest updateRequest = new UpdateRequest(USER_INDEX_NAME,id).doc(sourceMap);
UpdateResponse updateResponse = restHighLevelClient.update(updateRequest,RequestOptions.DEFAULT);
if (updateResponse.status().getStatus() != RestStatus.OK.getStatus()) {
log.error(">>>> 修改id={}数据失败,返回状态码={} <<<<",id,updateResponse.status().getStatus());
}
} catch (IOException e) {
e.printStackTrace();
}
return queryById(userEntity.getId());
}
4 、查询
查询的话根据id
查询和条件查询(分页),条件查询的用法和使用TransportClient
基本没什么区别,通过下面的代码看一下:
### 根据id
@Override
public UserEntity queryById(String id) {
GetRequest getRequest = new GetRequest(USER_INDEX_NAME).id(id);
UserEntity userEntity = null;
try {
GetResponse getResponse = restHighLevelClient.get(getRequest,RequestOptions.DEFAULT);
Map<String,Object> map = getResponse.getSource();
userEntity = BeanUtil.mapToBean(map,UserEntity.class,false,CopyOptions.create());
} catch (IOException e) {
e.printStackTrace();
}
return userEntity;
}
### 分页条件查询
@Override
public Page<UserEntity> pageQuery(QueryDTO queryDTO) {
String[] includes = {"userName","id","userCode","userMobile","userGrade","status"};
// 分页默认从0开始,按照userGrade逆向排序
PageRequest pageRequest = PageRequest.of(queryDTO.getPageNum() - 1,queryDTO.getPageSize(), Sort.by(Sort.Direction.DESC,"userAge"));
Page<UserEntity> page = null;
// 条件查询
BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery().mustNot(QueryBuilders.matchQuery("status","00"));
if (StrUtil.isNotBlank(queryDTO.getUserCode())) {
queryBuilder.must(QueryBuilders.termQuery("userCode",queryDTO.getUserCode()));
}
int pageNum = queryDTO.getPageNum() - 1;
int pageSize = queryDTO.getPageSize();
SearchSourceBuilder searchSourceBuilder = SearchSourceBuilder.searchSource().fetchSource(includes,null)
.query(queryBuilder).sort("userGrade", SortOrder.DESC)
.from(pageNum * pageSize).size(pageSize);
SearchRequest searchRequest = new SearchRequest(USER_INDEX_NAME).source(searchSourceBuilder);
try {
SearchResponse searchResponse = restHighLevelClient.search(searchRequest,RequestOptions.DEFAULT);
long total = searchResponse.getHits().getTotalHits().value;
SearchHit[] searchHits = searchResponse.getHits().getHits();
List<UserEntity> records = convertSource2List(searchHits);
page = new PageImpl(records,pageRequest,total);
} catch (IOException e) {
log.error(">>>> 查询失败,异常信息={} <<<<",e.getMessage());
}
return page;
}
private List<UserEntity> convertSource2List(SearchHit[] searchHits) {
if (ArrayUtil.isEmpty(searchHits)) {
return Collections.EMPTY_LIST;
}
List<UserEntity> resultList = new ArrayList<>(searchHits.length);
for (SearchHit hit : searchHits) {
String jsonString = hit.getSourceAsString();
UserEntity userEntity = JSONUtil.toBean(jsonString,UserEntity.class);
resultList.add(userEntity);
}
return resultList;
}
当然根据id
查询也可以使用SearchRequest
,但是个人感觉SearchRequest
相对更偏底层一些,当然也可能是自己对GetRequest
不是很熟悉,因为确实没怎么看这个类有那些功能,后面有时间的话自己再看吧。另外在分页查询中使用的查询某些属性在GetRequest
也可以使用的,其实本质都差不多。通过上面的几个方法我们可以看出每种操作基本都对应着一个请求。其实综合来看各种请求还是比较多的,比如我想查询一个index
的mapping
那就需要创建一个GetMappingsRequest
,一个index
的setting
同样有GetSettingsRequest
。关于这几个方法的测试这里就都省略了。
四、总结
通过上面的学习来看RestHighLevelClient
使用起来并不太复杂,如果对ES
熟悉的话可以说马上就能上手使用。相对之前使用Spring Data Elasticsearch
来讲确实稍微麻烦了一点,尤其是创建索引的mapping
,当然我觉得其实不妨自己实现一下相关的功能。另外我的pom.xml
引入了Spring Data Commons
,自己不妨尝试下能否自动创建索引以及setting、mapping
。整体来看使用起来还是比较简单的,当然还是那句话,关键是自己对ES
本身的了解。好了,今天的学习就到这里,我的代码已经推送到我的github。另外,希望大家能多多关注我的VX个人号超超学堂
,非常感谢大家,如果各位小伙伴有什么疑问也可以和我联系。