近期用到了spring boot3.0,当然jdk也一起跟着升级到了17,后面有需求要用到ES,前后捣鼓了一整子发现,大部分的人都还在用7.X及以下的ES,所以找了很久的资料,主要是ES的语法有点不太好用,一大堆的lambda,索性就自己封装了一个工具类,语法类似于mybatis-plus,这样的语法我想大家应该都比较熟悉,话不多说了,直接上代码
项目结构
TestController
package com.xxx.xxx.es.test.controller;
import co.elastic.clients.elasticsearch._types.query_dsl.RegexpQuery;
import com.alibaba.fastjson2.JSONObject;
import com.xxx.xxx.es.test.entity.User;
import com.xxx.xxx.es.test.utils.ElasticsearchUtils;
import com.xxx.xxx.es.test.utils.ElasticsearchUtils.*;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
@RestController
@RequestMapping("/es")
@RequiredArgsConstructor
public class TestController {
private final ElasticsearchUtils elasticsearchUtils;
@GetMapping("/test")
public Object test() throws IOException {
String index = "user-index";
//删除索引测试
deleteIndexTest(index);
//创建索引测试
createIndexTest(index);
//新增数据测试
addTest(index);
//删除数据测试
deleteTest(index);
//修改数据测试
updateTest(index);
//条件查询测试
List<?> listResult = getListTest(index);
if (false){
return getResultData(listResult);
}
//分页查询测试
List<?> pageResult = getPageTest(index);
if (false){
return getResultData(pageResult);
}
//count
long count = elasticsearchUtils.count(new EsSearchWrapper(index));
//ES未封装方法
List<?> esFunResult = getFunResultTest(index);
if (true){
return getResultData(esFunResult);
}
//最后我还做了分组的聚合,es虽然提供了分组的功能,但是没有帮我们把数据聚合,这里我按mysql的逻辑聚合了,用法也与之相似
//分组查询测试
List<Object> groupList = getGroupListTest(index);
return getResultData(groupList);
}
private List<Object> getGroupListTest(String index) throws IOException {
//加点数据
List<User> addBatchList = new ArrayList<>();
User user = new User();
user.setId(9);
user.setName("赵六小儿");
user.setSex(1);
user.setCreateTime(new Date());
addBatchList.add(user);
user = new User();
user.setId(10);
user.setName("孙七小儿");
user.setSex(2);
user.setCreateTime(new Date());
addBatchList.add(user);
elasticsearchUtils.addBatch(index, addBatchList);
//方法1
// EsSearchWrapper groupWrapper = new EsSearchWrapper<>(index);
// groupWrapper.groupBy("sex", "name.keyword");
//当然这里可以不select,我就没有让它报错(和mysql不同,mysql会报错), 但是如果只是要这两个字分组,就没必要查其他字段了
//groupWrapper.select("sex","name");
//lambda也支持,基本一直
//EsSearchWrapper<User> groupWrapper = new EsSearchWrapper<>();
//List<User> groupList = elasticsearchUtils.list(groupWrapper, User.class);
//相当于 select sex,name from user group by sex,name
// List<Object> groupList = elasticsearchUtils.list(groupWrapper);
//方法2
//分组如果要使用到函数的话 比如:max,min等 就要调用另一个重载方法
EsSearchWrapper groupWrapper = new EsSearchWrapper<>(index);
groupWrapper.groupBy(EsFunction.of("sex")
, EsFunction.of("name.keyword")
, EsFunction.of("id", EsFunctionEnum.SUM));
//相当于 select sex,name,sum(id) from user group by sex,name
List<Object> groupList = elasticsearchUtils.list(groupWrapper);
return groupList;
}
private List<?> getFunResultTest(String index) throws IOException {
//当然ES和mysql还是有点区别的,我所以保留了一些方法
List<Object> funRestulList = elasticsearchUtils.list(new EsSearchWrapper<>(index)
//分词,模糊查询 相当于 select * from user where (name like '%赵%' or name like '%小%')
//当然ES 用的是倒排索引,效率会比mysql高 这里只是说查询出来的效果相似
// .match(MatchQuery.of(f -> f.field("name").query("赵小")))
//精确查询
// .term(TermQuery.of(f -> f.field("name").value("赵六小儿")))
//keyword text whidcard 等String类型 不支持
// .range(RangeQuery.of(f -> f.field("id").gte(JsonData.of("7"))))
//就是match的缩减版 通过slop 这个去控制范围
//slop == 1 相当于 select * from user where name like '%赵_小%'
//slop == 2 相当于 select * from user where (name like '%赵_小%' or name like '%赵__小%')
//slop == 3 相当于 select * from user where (name like '%赵_小%' or name like '%赵__小%' or name like '%赵___小%')
//以此类推
// .matchPhrase(MatchPhraseQuery.of(f -> f.field("name").query("赵小").slop(1)))
//类似 select * from user where name like '赵%'
// .prefix(PrefixQuery.of(f -> f.field("name").value("赵")))
// //*:通配符 ?:占位符 类似like *相当于% ?相当于_
// .wildcard(WildcardQuery.of(f -> f.field("name.keyword").value("赵*??")))
//就是允许你错别字,非特殊情况还是别用,非常消耗性能不说,还很容易和产品扯皮
// .fuzzy(FuzzyQuery.of(f -> f.field("name.keyword").value("赵漏小儿")))
//正则匹配 value 正则就是表达式
.regexp(RegexpQuery.of(f -> f.field("name.keyword").value(".*")))
);
return funRestulList;
}
private List<?> getPageTest(String index) throws IOException {
//分页查询
EsPage<User> esPage = new EsPage<>(1, 10);
EsSearchWrapper<User> pageWrapper = new EsSearchWrapper<>(index);
pageWrapper.like(User::getName, "小儿");
//注意:是and里需要拼接or的条件则必须先调用.or(),调用一次后,后面的拼接条件都是or
pageWrapper.and(wrapper -> wrapper.or().eq(User::getId, "6").eq(User::getId, "7"));
elasticsearchUtils.page(esPage, pageWrapper, User.class);
return esPage.getRecords();
}
private List<?> getListTest(String index) throws IOException {
//条件查询(全量滚动查询)
EsSearchWrapper<User> listWrapper = new EsSearchWrapper<>(index);
//如果AUTO或Keyword类型,会自动拼接.keyword 默认精确匹配
listWrapper.like(User::getName, "小儿");
// listWrapper.like("name.keyword", "小儿");
List<User> userList = elasticsearchUtils.list(listWrapper, User.class);
return userList;
}
private void updateTest(String index) throws IOException {
//修改方法1
User user = new User();
user.setId(6);
user.setName("赵六小儿");
elasticsearchUtils.updateById(index, user);
//修改方法2
// EsUpdateWrapper<User> esUpdateWrapper = new EsUpdateWrapper<>(index);
// esUpdateWrapper.doc(user);
// //是否刷盘(true:更新成功后才返回,false:发送指令后直接返回成功,即异步更新)
// esUpdateWrapper.refresh(true);
List<User> updateBatchList = new ArrayList<>();
user = new User();
user.setId(7);
user.setName("孙七小儿");
updateBatchList.add(user);
user = new User();
user.setId(8);
user.setName("周八小儿");
updateBatchList.add(user);
//批量修改1
elasticsearchUtils.updateBatch(index, updateBatchList);
//批量修改2
// EsBulkWrapper<User> esBulkWrapper = new EsBulkWrapper<>(index);
// //是否刷盘(true:更新成功后才返回,false:发送指令后直接返回成功,即异步更新)
// esBulkWrapper.refresh(true);
// esBulkWrapper.update(updateBatchList);
// elasticsearchUtils.exec(esBulkWrapper);
}
private void deleteTest(String index) throws IOException {
//删除方法1
elasticsearchUtils.deleteById(index, "3");
//删除方法2
// EsDeleteWrapper esDeleteWrapper = new EsDeleteWrapper();
// esDeleteWrapper.id("1");
// //是否刷盘(true:更新成功后才返回,false:发送指令后直接返回成功,即异步更新)
// esDeleteWrapper.refresh(true);
// elasticsearchUtils.delete(esDeleteWrapper);
//批量删除方法1
elasticsearchUtils.deleteByIds(index, List.of("4", "5"));
//批量删除方法2
// EsBulkWrapper<User> esBulkWrapper = new EsBulkWrapper<>(index);
// //是否刷盘(true:更新成功后才返回,false:发送指令后直接返回成功,即异步更新)
// esBulkWrapper.refresh(true);
// esBulkWrapper.delete(List.of("2","3"));
// elasticsearchUtils.exec(esBulkWrapper);
}
private void addTest(String index) throws IOException {
User user = new User();
user.setId(3);
user.setName("张三");
user.setSex(1);
user.setCreateTime(new Date());
//方法1
elasticsearchUtils.add(index, user);
//方法2
// EsCreateWrapper<User> esCreateWrapper = new EsCreateWrapper<>(index);
// esCreateWrapper.document(user);
// elasticsearchUtils.add(esCreateWrapper);
List<User> addBatchList = new ArrayList<>();
user = new User();
user.setId(4);
user.setName("李四");
user.setSex(1);
user.setCreateTime(new Date());
addBatchList.add(user);
user = new User();
user.setId(5);
user.setName("王五");
user.setSex(2);
user.setCreateTime(new Date());
addBatchList.add(user);
user = new User();
user.setId(6);
user.setName("赵六");
user.setSex(2);
user.setCreateTime(new Date());
addBatchList.add(user);
user = new User();
user.setId(7);
user.setName("孙七");
user.setSex(2);
user.setCreateTime(new Date());
addBatchList.add(user);
user = new User();
user.setId(8);
user.setName("周八");
user.setSex(2);
user.setCreateTime(new Date());
addBatchList.add(user);
//批量新增方法1
elasticsearchUtils.addBatch(index, addBatchList);
//批量新增方法2
// EsBulkWrapper<User> esBulkWrapper = new EsBulkWrapper<>(index);
// //是否刷盘(true:更新成功后才返回,false:发送指令后直接返回成功,即异步更新)
// esBulkWrapper.refresh(true);
// esBulkWrapper.create(addBatchList);
// elasticsearchUtils.exec(esBulkWrapper);
}
private void createIndexTest(String index) throws IOException {
EsCreateIndexWrapper esCreateIndexWrapper = new EsCreateIndexWrapper(index);
//如果不设置,每次查询最多返回10000条,当然这个要根据情况而定设置太大会影响性能
esCreateIndexWrapper.setMaxResultWindow(100000);
/**
* 无论新增还是编辑,目前es是自动支持动态字段,所以加索引直接加实体字段即可,不需要主动去设置
* 当然如果自定义的话可以调用下面setDocClass方法
* 取@Field注解上的值 可以不设置,那么字段都是自动生成类型
*/
esCreateIndexWrapper.setDocClass(User.class);
elasticsearchUtils.createIndex(esCreateIndexWrapper);
}
private void deleteIndexTest(String index) throws IOException {
//删除索引方法1
elasticsearchUtils.deleteIndex(index);
//删除索引方法2
// EsDeleteIndexWrapper esDeleteIndexWrapper = new EsDeleteIndexWrapper(index);
// elasticsearchUtils.deleteIndex(esDeleteIndexWrapper);
}
private String getResultData(List<?> result) {
StringBuilder html = new StringBuilder();
html.append("<!DOCTYPE html>");
html.append("<html>");
html.append("<head>");
html.append("<meta charset=\"utf-8\" />");
html.append("<title></title>");
html.append("</head>");
html.append("<body>");
html.append("<table border=\"1\" width=\"50%\" align=\"left\">");
boolean title = true;
for (Object p : result) {
JSONObject jsonObject = JSONObject.parseObject(JSONObject.toJSONString(p));
if (title) {
html.append("<tr>");
for (String key : jsonObject.keySet()) {
html.append("<td>").append(key).append("</td>");
}
title = false;
}
html.append("</tr>");
html.append("<tr>");
for (String key : jsonObject.keySet()) {
html.append("<td>").append(Objects.nonNull(jsonObject.get(key)) ? String.valueOf(jsonObject.get(key)) : "").append("</td>");
}
html.append("</tr>");
}
html.append("</table>");
html.append("</body>");
html.append("</html>");
return html.toString();
}
}
User
package com.xxx.xxx.es.test.entity;
import lombok.Data;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import java.io.Serializable;
import java.util.Date;
@Data
public class User implements Serializable {
//必须有一个id主键字段
@Field(type= FieldType.Auto)
private Integer id;
//自动生成text+keyword,既支持分词又支持全匹配,当然如果数据量过大且需要模糊查询的话,会影响性能,可以考虑用Wildcard
@Field(type= FieldType.Auto)
private String name;
/**
* 1:男 0:女
*/
@Field(type= FieldType.Integer)
private Integer sex;
@Field(type= FieldType.Date)
private Date createTime;
}
ElasticsearchUtils
package com.xxx.xxx.es.test.utils;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.FieldValue;
import co.elastic.clients.elasticsearch._types.Refresh;
import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch._types.aggregations.*;
import co.elastic.clients.elasticsearch._types.mapping.Property;
import co.elastic.clients.elasticsearch._types.query_dsl.*;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.elasticsearch.core.bulk.BulkOperation;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.core.search.SourceFilter;
import co.elastic.clients.elasticsearch.indices.*;
import co.elastic.clients.json.JsonData;
import co.elastic.clients.util.ObjectBuilder;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import lombok.Data;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils;
import java.io.IOException;
import java.io.Serializable;
import java.lang.invoke.SerializedLambda;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
/**
* @author: chr
* @since: 1.0.0
* @date: 2024/2/28 11:21
* @des 基于8.7.1版本客户端代码封装
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ElasticsearchUtils {
private final ElasticsearchClient elasticsearchClient;
private static final String ID_FIELD = "id";
private static final String FLAG_KEYWORD = ".keyword";
//这个建议放到缓存中,如redis等,这个定义了静态是不会被回收的,是一种内存泄漏,这里只是案例,当然如果你的系统小型,接口不多,内存也足够,影响忽略不计
private static ConcurrentHashMap<Class<?>, String> lambdaCach = new ConcurrentHashMap<>(16);
/**
* 创建索引
*
* @param wrapper
* @return
* @throws IOException
*/
public boolean createIndex(EsCreateIndexWrapper wrapper) throws IOException {
wrapper.initIndexSetting();
log.info("索引:{} 创建-dsl json:{}", wrapper.getIndex(), wrapper.getCreateIndexRequestDsl().build().toString());
CreateIndexResponse createIndexResponse = elasticsearchClient.indices().create(c -> wrapper.getCreateIndexRequest());
log.info("索引:{} 创建成功{}", wrapper.getIndex(), createIndexResponse.toString());
return Boolean.TRUE;
}
/**
* 删除索引
*
* @param index 索引名称
* @return
* @throws IOException
*/
public boolean deleteIndex(String index) throws IOException {
return deleteIndex(new EsDeleteIndexWrapper(index));
}
/**
* 删除索引
*
* @param wrapper
* @return
* @throws IOException
*/
public boolean deleteIndex(EsDeleteIndexWrapper wrapper) throws IOException {
if (elasticsearchClient.indices().exists(request -> request.index(wrapper.getIndex())).value()) {
log.info("索引:{} 创建-dsl json:{}", wrapper.getIndex(), wrapper.getDeleteIndexRequestDsl().build().toString());
DeleteIndexResponse deleteIndexResponse = elasticsearchClient.indices().delete(s -> wrapper.getDeleteIndexRequest());
log.info("索引:{} 删除成功{}", wrapper.getIndex(), deleteIndexResponse.toString());
return Boolean.TRUE;
}
log.info("{}索引不存在", wrapper.getIndex());
return Boolean.FALSE;
}
/**
* 新增
*
* @param index 索引名称
* @param t doc
* @return
* @throws IOException
*/
public <T> boolean add(String index, T t) throws IOException {
return add(new EsCreateWrapper<T>(index).document(t).refresh(Boolean.TRUE));
}
/**
* 新增
*
* @param wrapper
* @return
* @throws IOException
*/
public <T> boolean add(EsCreateWrapper<T> wrapper) throws IOException {
log.info("索引:{} 新增-dsl json:{}", wrapper.getIndex(), wrapper.getCreateRequestDsl().build().toString());
CreateResponse createResponse = elasticsearchClient.create(c -> (ObjectBuilder) wrapper.getCreateRequest());
log.info("索引:{} 新增成功 {}", wrapper.getIndex(), createResponse.toString());
return Boolean.TRUE;
}
/**
* 删除
*
* @param index 索引名称
* @param id 实体id
* @return
* @throws IOException
*/
public boolean deleteById(String index, String id) throws IOException {
return delete(new EsDeleteWrapper(index).id(id).refresh(Boolean.TRUE));
}
/**
* 删除
*
* @param wrapper
* @return
* @throws IOException
*/
public boolean delete(EsDeleteWrapper wrapper) throws IOException {
log.info("索引:{} 删除-dsl json:{}", wrapper.getIndex(), wrapper.getDeleteRequestDsl().build().toString());
DeleteResponse deleteResponse = elasticsearchClient.delete(i -> wrapper.getDeleteRequest());
log.info("索引:{} 删除成功:{}", wrapper.getIndex(), deleteResponse.toString());
return Boolean.TRUE;
}
/**
* 更新
*
* @param index 索引名称
* @param t doc实体
* @return
* @throws IOException
*/
public <T> boolean updateById(String index, T t) throws IOException {
return updateById(new EsUpdateWrapper<T>(index).doc(t).refresh(Boolean.TRUE), t.getClass());
}
/**
* 更新
*
* @param wrapper
* @return
* @throws IOException
*/
public <T>