这几天写了一个关于es的工具类,主要封装了业务中常用es的常用方法。
本文中使用到的elasticsearch版本6.7,但实际上也支持es7.x以上版本,因为主要是对springboot提供的:ElasticsearchRestTemplate 提供的API做的二次封装。目的是:让不懂es的开发人员新手也能轻松上手。
整个工程分为es-api与es-server。
es-api为对外公共jar,包含了es映射实体;
es-server包含了具体的es工具类与Repository接口(下文会提到)。并通过necos配置中心统一管理es配置参数。
外部业务模块可引入es-api jar maven依赖,由Jar提供的入口,通过httpClient或feign调用(springcloud分布式项目)到es-server服务上的es工具类,得到需要的数据。
这里仅以springcloud分布式项目简单为例
业务方用户服务user 引入es-api maven
/**
* @author: shf
* description: es-server feign接口
*/
public interface EsServerClient {
@PostMapping(value = "/queryList", produces = {"application/json"})
public List queryList(@RequestBody T t);
}
在user服务中创建feignClient继承自es-api中的EsServerClient
@FeignClient(contextId = "esFeignClient", name = "es-server")
public interface EsFeignClient extends EsServerClient {
}
在user服务的代码中即可调用
@AutoWired
public EsFeignClient esFeignClient;
public void test() {
//.......如业务方Dto与es映射实体转换 等省略
//....
EmployeeEs employee = new EmployeeEs();
List queryList = Stream.of(employee.new QueryRelation("张三", EntityEs.SHOULD, 5F), employee.new QueryRelation("李四", EntityEs.SHOULD, 20F)).collect(Collectors.toList());
employee.setFieldQueryMap(new EsMapUtil().put(EmployeeEs::getUserName, queryList).put(EmployeeEs::getUserAge, employee.new RangeRelation(20, EntityEs.GTE, null, null, EntityEs.MUST)));
//排序查询
employee.setOrderMap(new EsMapUtil().put(EmployeeEs::getUserId, SortOrder.DESC));
List employeeEs = esFeignClient.queryList(employee);
//.....employeeEs与业务方Dto转换
}
es-api 引入es依赖
org.springframework.boot
spring-boot-starter-data-elasticsearch
org.elasticsearch.client
transport
org.elasticsearch.client
elasticsearch-rest-high-level-client
org.elasticsearch.client
transport
6.7.0
org.elasticsearch.client
elasticsearch-rest-high-level-client
6.7.0
排除后重新引入对应的Es版本6.7,避免因版本不一致导致的一些坑。
es-server 服务 application.yml配置es
spring:
elasticsearch:
rest:
#ES的连接地址,多个地址用逗号分隔
uris: localhost:9200
username: kibana
password: pass
#连接超时时间
connection-timeout: 1000
#读取超时时间
read-timeout: 1000
说明:
①设置的字段与该es对应的该索引完全对应,不存在多余字段。
②项目中引入了spring-boot-starter-data-elasticsearch,所以可直接使用注解形式设置索引信息与mapping结构信息,详见示例
③@JsonIgnoreProperties({"orderMap","pageNumber","pageSize","highlightFields","preTags","postTags","fieldQueryMap","scrollId","aggregationMap","multiLayerQueryList"})
作用:在保存文档到es时忽略父类EntityEs中的功能性字段,下文会提到
④@EsRepository(EmployeeEsRepository.class)
作用:通过注解过去该映射对应的Repository接口
/**
* 员工对象
*
* 注解:@Document用来声明Java对象与ElasticSearch索引的关系 indexName 索引名称 type 索引类型 shards 主分区数量,默认5
* replicas 副本分区数量,默认1 createIndex 索引不存在时,是否自动创建索引,默认true
*/
@Setter
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@EsRepository(EmployeeEsRepository.class)
@JsonIgnoreProperties({"orderMap","pageNumber","pageSize","highlightFields","preTags","postTags","fieldQueryMap","scrollId","aggregationMap","multiLayerQueryList"})
@Document(indexName = "employee_index", type = "employee_type", shards = 1, replicas = 0, createIndex = true)
public class EmployeeEs extends EntityEs {
@Id
@Field(type = FieldType.Keyword)
private Long userId;
//@Field(type = FieldType.Text, analyzer = "ik_max_word")
@MultiField(mainField = @Field(type = FieldType.Text, analyzer = "ik_max_word"), otherFields = @InnerField(suffix = "trueName", type = FieldType.Keyword))
private String userName;
@Field(type = FieldType.Keyword)
private String userCode;
@Field(type = FieldType.Integer)
private Integer userAge;
@Field(type = FieldType.Keyword)
private String userMobile;
@Field(type = FieldType.Date)
private Date birthDay;
@Field(type = FieldType.Keyword)
private String userSex;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String remarks;
}
/**
* @author: shf
* description: 可根据映射实体设置自动生成mapping结构;支持bean的增删改查操作
* date: 2022/2/23 10:47
*/
@Component
public interface EmployeeEsRepository extends CrudRepository {
}
说明:
①所有字段非es索引中的mapping属性字段,均为功能性字段,如排序、高亮、分页设置和一些常量设置等,详见贴码
②所有es映射实体类均需继承该实体
/**
* @author: shf description: 功能性字段(非mapping结构字段)
* date: 2022/3/1 15:07
*/
@Data
public class EntityEs {
/**
* 组合多查询常量
*/
/**
* 文档 必须 匹配这些条件才能被查询到。相当于sql中的and
*/
public static String MUST = "must";
/**
* 文档 必须不 匹配这些条件才能被查询到。相当于sql中的 not
*/
public static String MUST_NOT = "must_not";
/**
* 如果满足这些语句中的任意语句,将增加 _score ,否则,无任何影响。它们主要用于修正每个文档的相关性得分。相当于sql中的or
*/
public static String SHOULD = "should";
/**
* 必须 匹配,但它以不评分、过滤模式来进行。这些语句对评分没有贡献,只是根据过滤标准来排除或包含文档
*/
public static String FILTER = "filter";
/**
* 至少匹配一项should子句
*/
public static String MINIMUM_SHOULD_MATCH = "minimum_should_match";
/**
* 多字段排序查询
*/
public EsMapUtil orderMap;
/**
* 分页查询
*/
public Integer pageNumber;
public Integer pageSize;
/**
* 游标分页ID
*/
public String scrollId;
/**
* 游标分页ID有效期 单位:毫秒
*/
public static Long scrollIdExpireTime = 1000 * 60 * 2L;
/**
* 游标分页ID最小有效期 单位:毫秒
*/
public static Long scrollIdMinExpireTime = 1000L;
/**
* 高亮查询
*/
public List highlightFields;
public String preTags;
public String postTags;
/**
* 字段查询
*/
public EsMapUtil fieldQueryMap;
/**
* 聚合查询,当前只支持单个字段分组聚合count与sum,只针对keyword类型字段有效
*/
public EsMapUtil aggregationMap;
public static String COUNT = "count";
public static String SUM = "sum";
/**
* 多层(bool)查询
*/
public List multiLayerQueryList;
/**
* 范围查询常量
*/
public static String GT = "gt";
public static String GTE = "gte";
public static String LT = "lt";
public static String LTE = "lte";
@Data
public class RangeRelation {
//String fieldKey;
T fieldMinValue;
String fieldMinMode;
T fieldMaxValue;
String fieldMaxMode;
String queryMode;
public RangeRelation(T fieldMinValue, String fieldMinMode, T fieldMaxValue, String fieldMaxMode, String queryMode) {
this.fieldMinValue = fieldMinValue;
this.fieldMinMode = fieldMinMode;
this.fieldMaxValue = fieldMaxValue;
this.fieldMaxMode = fieldMaxMode;
this.queryMode = queryMode;
}
}
@Data
public class QueryRelation {
T fieldValue;
String queryMode;
Float boostValue;
public QueryRelation(T fieldValue, String queryMode) {
this.fieldValue = fieldValue;
this.queryMode = queryMode;
}
public QueryRelation(T fieldValue, String queryMode, Float boostValue) {
this.fieldValue = fieldValue;
this.queryMode = queryMode;
this.boostValue = boostValue;
}
}
@Data
public class MultiLayerRelation {
String queryMode;
EsMapUtil map;
List multiLayerList;
public MultiLayerRelation(String queryMode, EsMapUtil map) {
this.queryMode = queryMode;
this.map = map;
}
public MultiLayerRelation(String queryMode, EsMapUtil map, List multiLayerList) {
this.queryMode = queryMode;
this.map = map;
this.multiLayerList = multiLayerList;
}
}
}
说明:封装了一个map工具,编码简洁链式调用,应用了方法引用特性避免了字符串硬编码造成单词拼错的情况。
/**
* @author: shf description: 函数式接口 便于方法引用获取实体字段名称
* date: 2022/3/4 13:41
*/
@FunctionalInterface
public interface IGetterFunction extends Serializable{
Object get(T source);
}
/**
* @author: shf
* description
* date: 2019/11/13 18:30
*/
public class EsMapUtil extends LinkedHashMap {
public EsMapUtil put(IGetterFunction fn, Object value) {
String key = getFieldName(fn);
super.put(key, value);
return this;
}
public EsMapUtil putStr(String key, Object value) {
super.put(key, value);
return this;
}
private static Map CLASS_LAMDBA_CACHE = new ConcurrentHashMap<>();
/***
* 转换方法引用为属性名
* @param fn
* @return
*/
public String getFieldName(IGetterFunction fn) {
SerializedLambda lambda = getSerializedLambda(fn);
String methodName = lambda.getImplMethodName();
String prefix = null;
if (methodName.startsWith("get")) {
prefix = "get";
}
// 截取get之后的字符串并转换首字母为小写
return toLowerCaseFirstOne(methodName.replace(prefix, ""));
}
/**
* 首字母转小写
*
* @param s
*/
public String toLowerCaseFirstOne(String s) {
if (Character.isLowerCase(s.charAt(0))) {
return s;
} else {
return (new StringBuilder()).append(Character.toLowerCase(s.charAt(0))).append(s.substring(1)).toString();
}
}
public static SerializedLambda getSerializedLambda(Serializable fn) {
SerializedLambda lambda = CLASS_LAMDBA_CACHE.get(fn.getClass());
if (lambda == null) {
try {
Method method = fn.getClass().getDeclaredMethod("writeReplace");
method.setAccessible(Boolean.TRUE);
lambda = (SerializedLambda) method.invoke(fn);
CLASS_LAMDBA_CACHE.put(fn.getClass(), lambda);
} catch (Exception e) {
e.printStackTrace();
}
}
return lambda;
}
}
/**
* @author: shf description: es工具类,支持:分页(支持游标分页)、高亮(支持自定义标签)、范围查找、bool组合查询、多层bool套bool、多字段排序、加权重、聚合、二级字段查询
* date: 2022/2/23 10:54
*/
@Component
@Slf4j
public class EsService {
@Autowired
private ElasticsearchRestTemplate restTemplate;
@Autowired
private ApplicationContext context;
/**
* 判断索引是否存在
*
* @return boolean
*/
public boolean indexExists(Class clazz) {
return restTemplate.indexExists(clazz);
}
/**
* 判断索引是否存在
*
* @param indexName 索引名称
* @return boolean
*/
public boolean indexExists(String indexName) {
return restTemplate.indexExists(indexName);
}
/**
* 创建索引(推荐使用:因为Java对象已经通过注解描述了Setting和Mapping)
*
* @return boolean
*/
public boolean indexCreate(Class clazz) {
boolean createFlag = restTemplate.createIndex(clazz);
boolean mappingFlag = restTemplate.putMapping(clazz);
return createFlag && mappingFlag;
}
/**
* 索引删除
*
* @param indexName 索引名称
* @return boolean
*/
public boolean indexDelete(String indexName) {
return restTemplate.deleteIndex(indexName);
}
/**
* 新增数据
*
* @param bean 数据对象
*/
publ