因为最近项目中需要用到Elasticsearch7的新特性,并且环境中只有7.7.1版本的组件可以用,所以弃用了spring-boot-starter-data-elasticsearch(请注意:SpringBoot是2.2.0.RELEASE才兼容elasticsearch 7.x)。
本Demo使用elasticsearch原生客户端,模仿spring-boot-starter-data-elasticsearch实现一套相简单的增删改查功能,目的是满足业务上的面对es版本灵活切换,大家如果有相似需求可以参考此式例。
先来添加引用
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>${elasticsearch.version}</version>
<exclusions>
<exclusion>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
</exclusion>
<exclusion>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>${elasticsearch.version}</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>${elasticsearch.version}</version>
</dependency>
指定版本
<elasticsearch.version>7.7.1</elasticsearch.version>
创建es配置文件
EsProp.java
@Data
@Component
@ConfigurationProperties(prefix = "elasticsearch")
public class EsProp {
/**
* 是否开启es
*/
private Boolean enabled;
/**
* 使用冒号隔开ip和端口
*/
private String[] address;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 7.X 设置为http
*/
private String scheme;
}
EsConfiguration.java
@Slf4j
@Component
@Configuration
public class EsConfiguration {
//权限验证
final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
@Autowired
private EsProp esProp;
@Bean
public RestClientBuilder restClientBuilder() {
HttpHost[] hosts = Arrays.stream(esProp.getAddress())
.map(this::makeHttpHost)
.filter(Objects::nonNull)
.toArray(HttpHost[]::new);
log.debug("hosts:{}", Arrays.toString(hosts));
//配置权限验证
credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(esProp.getUsername(), esProp.getPassword()));
RestClientBuilder restClientBuilder = RestClient.builder(hosts).setHttpClientConfigCallback(new RestClientBuilder.HttpClientConfigCallback() {
@Override
public HttpAsyncClientBuilder customizeHttpClient(HttpAsyncClientBuilder httpClientBuilder) {
return httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
}
});
return restClientBuilder;
}
@Bean(name = "restHighLevelClient")
public RestHighLevelClient highLevelClient(@Autowired RestClientBuilder restClientBuilder) {
// restClientBuilder.setMaxRetryTimeoutMillis(60000);
return new RestHighLevelClient(restClientBuilder);
}
/**
* 处理请求地址
*
* @param s
* @return HttpHost
*/
private HttpHost makeHttpHost(String s) {
String[] address = s.split(":");
if (address.length == address.length) {
String ip = address[0];
int port = Integer.parseInt(address[1]);
return new HttpHost(ip, port, esProp.getScheme());
} else {
return null;
}
}
}
创建注解类@EsDocument,用以标注索引名称和属性
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface EsDocument {
String indexName();
int shards() default 1;
int replicas() default 0;
String mappingId() default "id";
}
创建注解类@EsField,用以标注字段名称和属性
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EsField {
String name() default "";
String type() default "";
int dims() default 0;
boolean fielddata() default false;
}
创建字段类型常量类EsFieldType.java,这里我只实现了几个用到的类型,大家可以自行扩展
public class EsFieldType {
public static final String TEXT = "text";
public static final String INTEGER = "integer";
public static final String LONG = "long";
public static final String DOUBLE = "double";
public static final String DATE = "date";
public static final String DENSE_VECTOR = "dense_vector";
}
下面实现一套基础的增删改查基础实现
创建BaseEntity.java
@Data
public class BaseEsEntity implements Serializable {
@EsField(name = "id")
private String id;
@EsField(name = "creat_date", type = EsFieldType.DATE)
private Date createDate;
}
创建BaseEsService.java
public interface BaseEsService<T, ID> {
/**
* 创建索引
*/
void createIndex();
/**
* 删除索引
*/
void deleteIndex();
/**
* 判断索引是否存在
* @return
* @throws Exception
*/
boolean isExistsIndex() throws Exception;
/**
* 插入
* @param entity
*/
void index(T entity);
/**
* 插入(异步)
* @param entity
*/
void indexAsync(T entity);
/**
* 批量插入
* @param list
*/
void insertBatch(List<T> list);
/**
* 批量删除
* @param idList
*/
void deleteBatch(Collection<T> idList);
/**
* 条件删除
* @param builder
*/
void deleteByQuery(QueryBuilder builder);
/**
* 条件查询
* @param builder
* @return
*/
List<T> search(SearchSourceBuilder builder);
创建BaseEsServiceImpl.java
@Slf4j
public class BaseEsServiceImpl<T extends BaseEsEntity, ID> {
protected Class<T> entityClass;
protected String indexName;
protected int shards;
protected int replicas;
protected String mappingId;
public BaseEsServiceImpl() {
EsDocument annotation = resolveReturnedClassFromGenericType().getAnnotation(EsDocument.class);
this.indexName = annotation.indexName();
this.mappingId = annotation.mappingId();
this.shards = annotation.shards();
this.replicas = annotation.replicas();
}
/**
* 创建索引
*/
public void createIndex() {
log.info("es 创建索引:{}", getIndexName());
try {
if (this.isExistsIndex()) {
log.error(" idxName={} 已经存在", getIndexName());
return;
}
CreateIndexRequest request = new CreateIndexRequest(getIndexName());
buildSetting(request);
buildMapping(request);
CreateIndexResponse res = restHighLevelClient().indices().create(request, RequestOptions.DEFAULT);
if (!res.isAcknowledged()) {
throw new RuntimeException("初始化失败");
} else {
log.info("es 索引创建成功: {}", getIndexName());
}
} catch (Exception e) {
e.printStackTrace();
System.exit(0);
}
}
/**
* 删除索引
*/
public void deleteIndex() {
deleteIndex(getIndexName());
}
/**
* 删除索引
*
* @param idxName
*/
protected void deleteIndex(String idxName) {
try {
if (!this.isExistsIndex()) {
log.error("DELETE Index error, idxName={} 不存在.", idxName);
return;
}
restHighLevelClient().indices().delete(new DeleteIndexRequest(idxName), RequestOptions.DEFAULT);
log.info("DELETE Index [{}].", idxName);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 索引是否存在
*
* @return
* @throws Exception
*/
public boolean isExistsIndex() throws Exception {
return isExistsIndex(getIndexName());
}
/**
* 索引是否存在
*
* @param idxName
* @return
* @throws Exception
*/
protected boolean isExistsIndex(String idxName) throws Exception {
return restHighLevelClient().indices().exists(new GetIndexRequest(idxName), RequestOptions.DEFAULT);
}
/**
* 保存对象
*
* @param entity
*/
public void index(T entity) {
IndexRequest request = new IndexRequest(indexName);
request.id(entity.getId());
request.type("_doc");
request.source(JSONObject.toJSONString(entity), XContentType.JSON);
// request.source(entity, XContentType.JSON);
try {
restHighLevelClient().index(request, RequestOptions.DEFAULT);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
/**
* 保存对象(异步)
*
* @param entity
*/
public void indexAsync(T entity) {
IndexRequest request = new IndexRequest();
request.id(entity.getId());
ActionListener listener = new ActionListener() {
@Override
public void onResponse(Object o) {
}
@Override
public void onFailure(Exception e) {
}
};
try {
restHighLevelClient().indexAsync(request, RequestOptions.DEFAULT, listener);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
/**
* 批量插入
*
* @param list
*/
public void insertBatch(List<T> list) {
BulkRequest request = new BulkRequest();
list.forEach(item -> request.add(new IndexRequest(getIndexName()).id(item.getId())
.source(JSONObject.toJSONString(item), XContentType.JSON)));
try {
restHighLevelClient().bulk(request, RequestOptions.DEFAULT);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 批量删除
*
* @param idList
*/
public void deleteBatch(Collection<T> idList) {
BulkRequest request = new BulkRequest();
idList.forEach(item -> request.add(new DeleteRequest(getIndexName(), item.toString())));
try {
restHighLevelClient().bulk(request, RequestOptions.DEFAULT);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 根据条件删除
*
* @param builder
*/
public void deleteByQuery(QueryBuilder builder) {
DeleteByQueryRequest request = new DeleteByQueryRequest(getIndexName());
request.setQuery(builder);
//设置批量操作数量,最大为10000
request.setBatchSize(10000);
request.setConflicts("proceed");
try {
restHighLevelClient().deleteByQuery(request, RequestOptions.DEFAULT);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 查询
*
* @param builder
* @return
*/
public List<T> search(SearchSourceBuilder builder) {
SearchRequest request = new SearchRequest(getIndexName());
request.source(builder);
try {
SearchResponse response = restHighLevelClient().search(request, RequestOptions.DEFAULT);
SearchHit[] hits = response.getHits().getHits();
List<T> res = new ArrayList<>(hits.length);
for (SearchHit hit : hits) {
res.add(JSON.parseObject(hit.getSourceAsString(), getEntityClass()));
}
return res;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 设置分片&副本
*
* @param request
*/
private void buildSetting(CreateIndexRequest request) {
request.settings(Settings.builder().put("index.number_of_shards", shards)
.put("index.number_of_replicas", replicas));
}
/**
* 设置Mapping
*
* @param request
*/
private void buildMapping(CreateIndexRequest request) {
JSONObject properties = new JSONObject();
Field[] fields = getAllFields(getEntityClass());
Arrays.stream(fields).forEach(field -> {
EsField annotation = field.getAnnotation(EsField.class);
if (null != annotation) { //判断字段是否包含EsField注解
String name = StringUtils.isEmpty(annotation.name()) == true ? field.getName() : annotation.name();
String type = StringUtils.isEmpty(annotation.type()) == true ? "text" : annotation.type();
boolean fielddata = annotation.fielddata();
int dims = annotation.dims();
JSONObject attribute = new JSONObject();
attribute.put("type", type);
if (fielddata) {
attribute.put("fielddata", fielddata);
}
if (dims != 0) {
attribute.put("dims", dims);
}
properties.put(name, attribute);
}
});
JSONObject json = new JSONObject();
json.put("properties", properties);
log.info(json.toJSONString());
request.mapping(json.toJSONString(), XContentType.JSON);
}
private Field[] getAllFields(Class<?> clazz) {
List<Field> fieldList = new ArrayList<>();
while (clazz != null) {
fieldList.addAll(new ArrayList<>(Arrays.asList(clazz.getDeclaredFields())));
clazz = clazz.getSuperclass();
}
Field[] fields = new Field[fieldList.size()];
fieldList.toArray(fields);
return fields;
}
public RestHighLevelClient restHighLevelClient() {
return (RestHighLevelClient) SpringUtils.getObject("restHighLevelClient");
}
@SuppressWarnings("unchecked")
private Class<T> resolveReturnedClassFromGenericType() {
ParameterizedType parameterizedType = resolveReturnedClassFromGenericType(getClass());
return (Class<T>) parameterizedType.getActualTypeArguments()[0];
}
private ParameterizedType resolveReturnedClassFromGenericType(Class<?> clazz) {
Object genericSuperclass = clazz.getGenericSuperclass();
if (genericSuperclass instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
return parameterizedType;
}
return resolveReturnedClassFromGenericType(clazz.getSuperclass());
}
protected Class<T> getEntityClass() {
if (null == entityClass) {
try {
this.entityClass = resolveReturnedClassFromGenericType();
} catch (Exception e) {
throw new RuntimeException("Unable to resolve EntityClass. Please use according setter!", e);
}
}
return entityClass;
}
protected String getIndexName() {
return indexName;
}
protected String getMappingId() {
return mappingId;
}
}
好了,接下来我们就可以使用它实现表索引创建和基础的增删改查操作了。
这里复杂一点的查询操作我选择用DSL实现,后期会针对业务使用RestHighLevelClient做出一些拓展,将会维护到我的github。
如果大家有一些建议和想法请与我留言。