原生Rest Level 许多重复操作,如Client不易使用、构建检索等。
部分增强了bbosselasticsearch:通过注释和实体类自动构建索引,自动刷入文档,复杂的业务检索需要在xml中写Dsl。用法和mybatisplus一样。
依赖
<dependency><groupId>org.elasticsearch</groupId><artifactId>elasticsearch</artifactId></dependency><dependency><groupId>com.bbossgroups.plugins</groupId><artifactId>bboss-elasticsearch-spring-boot-starter</artifactId><version>5.9.5</version><exclusions><exclusion><artifactId>slf4j-log4j12</artifactId><groupId>org.slf4j</groupId></exclusion></exclusions></dependency><!-- https://mvnrepository.com/artifact/org.projectlombok/lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.6</version><scope>provided</scope></dependency>
配置:
import com.rz.config.ElsConfig;import org.frameworkset.elasticsearch.boot.ElasticSearchBoot;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.ApplicationArguments;import org.springframework.boot.ApplicationRunner;import org.springframework.core.annotation.Order;import org.springframework.stereotype.Component;import java.util.HashMap;import java.util.Map;/** * 开始时,BBOSss初始化 * * @author sunziwen * @version 1.0 * @date 2019/12/12 16:54 **/@Component@Order(value = 1)public class StartElastic implements ApplicationRunner { @Autowired private ElsConfig config; @Override public void run(ApplicationArguments args) throws Exception { Map properties = new HashMap(); properties.put("elasticsearch.rest.hostNames", config.getElsClusterNodes()); ElasticSearchBoot.boot(properties); }}
注解和枚举:
package com.rz.szwes.annotations;import java.lang.annotation.*;/** * 标识实体对应的索引信息 * * @author sunziwen * 2019/12/13 10:14 * @version 1.0 **/@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface ESDsl { /** * xml的位置 */ String value(); String indexName(); /** * elasticsearch7.x版本删除了属性 */ String indexType() default "";}
package com.rz.szwes.annotations;import java.lang.annotation.*;/** * 指定字段的映射类型 * * @author sunziwen * 2019/12/14 10:06 * @version 1.0 **/@Target({ElementType.FIELD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface ESMapping { ///映射类型 ESMappingType value(); //加权 int boost() default 1; ///分词标识analyzed、not_analyzed String index() default "analyzed"; //分词器ik__max_word、standard String analyzer() default "ik_max_word"; //当String作为分组聚合字段时,需要设置为true boolean fildData() default false;}
package com.rz.szwes.annotations;/** * Es映射类型枚举(定义了大部分,如有缺失,请用户补充。当前版本基于elasticsearch 6.8 * * @author sunziwen * 2019/12/14 10:09 * @version 1.0 **/public enum ESMappingType { /** * 全文搜索。 */ text("text"), /** * keyword类型适用于索引结构化(排序、过滤、聚合),只能通过精确值搜索。 */ keyword("keyword"), / /** * -128~127 尽量选择范围较小的数据类型,以满足需求。 */ _byte("byte"), /** * -32768~32767 */ _short("short"), /** * -2^31~2^31-1 */ _integer("integer"), /** * -2^63~2^63-1 */ _long("long"), / /** * 64双精度IEEEEE 754浮点类型 */ _doule("doule"), /** * IEEEEEE32单位精度 754浮点类型 */ _float("float"), /** * IEEEEEE16半精度 754浮点类型 */ half_float("half_float"), /** * 缩放型浮点数 */ scaled_float("scaled_float"), / /** * 时间类型 */ date("date"), _boolean("boolean"), /** * 范围类型 */ range("range"), /** * 嵌套类型 */ nested("nested"), /** * 地理坐标 */ geo_point("geo_point"), /** * 地理地图 */ geo_shape("geo_shape"), /** * 二进制类型 */ binary("binary"), /** * ip 192.168.1.2 */ ip("ip"); private String value; ESMappingType(String value) { this.value = value; } public String getValue() { return value; }}
工具类:增强Hashmap
package com.rz.szwes.util;import java.util.HashMap;import java.util.function.Supplier;/** * 不支持Lambda表达式的原始Hashmap。特此包装一个 * * @author sunziwen * @version 1.0 * @date 2019/12/13 11:09 **/public class LambdaHashMap<K, V> extends HashMap<K, V> { public static <K, V> LambdaHashMap<K, V> builder() { return new LambdaHashMap<>(); } public LambdaHashMap<K, V> put(K key, Supplier<V> supplier) { super.put(key, supplier.get()); //流式 return this; }}
两个核心类:
package com.rz.szwes.core;import cn.hutool.core.util.ClassUtil;import com.alibaba.fastjson.JSON;import com.frameworkset.orm.annotation.ESId;import com.rz.szwes.annotations.ESDsl;import com.rz.szwes.annotations.ESMapping;import com.rz.szwes.util.LambdaHashMap;import org.springframework.util.StringUtils;import java.lang.reflect.Field;import java.lang.reflect.ParameterizedType;import java.time.LocalDate;import java.time.LocalDateTime;import java.time.LocalTime;import java.util.ArrayList;import java.util.Arrays;import java.util.Collections;import java.util.Date;/** * 抽象分析泛型 * * @author sunziwen * 2019/12/14 16:04 * @version 1.0 **/public abstract class AbstractElasticBase<T> { { ///初始化分析 ParameterizedType pt = (ParameterizedType) this.getClass().getGenericSuperclass(); // 获取第一类参数的真实类型 Class<T> clazz = (Class<T>) pt.getActualTypeArguments()[0]; parseMapping(clazz); } /** * 索引名称 */ protected String indexName; /** * 索引类型 */ protected String indexType; /** * es写dsl的文件路径 */ protected String xmlPath; /** * 索引映射 */ protected String mapping; ///将Class分析成映射JSONString private void parseMapping(Class<T> clazz) { if (clazz.isAnnotationPresent(ESDsl.class)) { ESDsl esDsl = clazz.getAnnotation(ESDsl.class); this.xmlPath = esDsl.value(); this.indexName = esDsl.indexName(); //如果类型是空的,以索引名为类型 this.indexType = StringUtils.isEmpty(esDsl.indexType()) ? esDsl.indexName() : esDsl.indexType(); } else { throw new RuntimeException(clazz.getName() + "缺失注解[@ESDsl]"); } //构建索引映射 LambdaHashMap<Object, Object> put = LambdaHashMap.builder() .put("mappings", () -> LambdaHashMap.builder() .put(indexType, () -> LambdaHashMap.builder() .put("properties", () -> { Field[] fields = clazz.getDeclaredFields(); LambdaHashMap<Object, Object> builder = LambdaHashMap.builder(); for (Field field : fields) { builder.put(field.getName(), () -> toEsjson(field)); } return builder; }))) ; this.mapping = JSON.toJSONString(put); } private LambdaHashMap<Object, Object> toEsjson(Field field) { //基本数据类型 if (ClassUtil.isSimpleTypeOrArray(field.getType())) { ///限制字符串的大小,设置分词 if (new ArrayList<Class>(Collections.singletonList(String.class)).contains(field.getType())) { LambdaHashMap<Object, Object> put = LambdaHashMap.builder() .put("type", () -> "text") .put("fields", () -> LambdaHashMap.builder() .put("keyword", () -> LambdaHashMap.builder() .put("type", () -> "keyword") .put("ignore_above", () -> 256))); if (field.isAnnotationPresent(ESMapping.class)) { ESMapping esMapping = field.getAnnotation(ESMapping.class); //设置聚合分组 if (esMapping.fildData()) { put.put("fildData", () -> true); } //设置加权 if (esMapping.boost() != 1) { put.put("boost", esMapping::boost); } //设置是否进入行分词 if (!"analyzed".equals(esMapping.index())) { put.put("analyzed", esMapping::analyzer); } //分词器 put.put("analyzer", esMapping::analyzer); } return put; } //设置默认类型 return LambdaHashMap.builder().put("type", () -> { if (field.isAnnotationPresent(ESMapping.class)) { ESMapping esMapping = field.getAnnotation(ESMapping.class); return esMapping.value().getValue(); } if (new ArrayList<Class>(Arrays.asList(byte.class, Byte.class, short.class, Short.class, int.class, Integer.class, long.class, Long.class)).contains(field.getType())) { return "long"; } else if (new ArrayList<Class>(Arrays.asList(double.class, Double.class, float.class, Float.class)).contains(field.getType())) { return "double"; } else if (new ArrayList<Class>(Arrays.asList(Date.class, java.sql.Date.class, LocalDate.class, LocalDateTime.class, LocalTime.class)).contains(field.getType())) { return "date"; } else if (new ArrayList<Class>(Arrays.asList(boolean.class, Boolean.class)).contains(field.getType())) { return "boolean"; } return "text"; }); } else { //设置对象类型 LambdaHashMap<Object, Object> properties = LambdaHashMap.builder() .put("properties", () -> { Field[] fields = field.getType().getDeclaredFields(); LambdaHashMap<Object, Object> builder = LambdaHashMap.builder(); for (Field field01 : fields) { builder.put(field01.getName(), toEsjson(field01); } return builder; }); if (field.isAnnotationPresent(ESMapping.class)) { ESMapping esMapping = field.getAnnotation(ESMapping.class); properties.put("type", esMapping.value().getValue()); } return properties; } }}
package com.rz.szwes.core;import lombok.extern.slf4j.Slf4j;import org.frameworkset.elasticsearch.boot.BBossESStarter;import org.frameworkset.elasticsearch.client.ClientInterface;import org.frameworkset.elasticsearch.client.ClientUtil;import org.springframework.beans.factory.annotation.Autowired;import java.util.*;/** * Elastic基本函数 * * @author sunziwen * @version 1.0 * @date 2019/12/13 9:56 **/@Slf4jpublic class ElasticBaseService<T> extends AbstractElasticBase<T> { @Autowired private BBossESStarter starter; /** * Xml创建索引 */ protected String createIndexByXml(String xmlName) { ClientInterface restClient = starter.getConfigRestClient(xmlPath); boolean existIndice = restClient.existIndice(this.indexName); if (existIndice) { restClient.dropIndice(indexName); } return restClient.createIndiceMapping(indexName, xmlName); } /** * 索引是自动创建的 */ protected String createIndex() { ClientInterface restClient = starter.getRestClient(); boolean existIndice = restClient.existIndice(this.indexName); if (existIndice) { restClient.dropIndice(indexName); } log.debug("创建索引:" + this.mapping); return restClient.executeHttp(indexName, this.mapping, ClientUtil.HTTP_PUT); } /** * 删除索引 */ protected String delIndex() { return starter.getRestClient().dropIndice(this.indexName); } /** * 添加文档 * * @param t 实体类 * @param refresh 是否强制刷新 */ protected String addDocument(T t, Boolean refresh) { return starter.getRestClient().addDocument(indexName, indexType, t, "refresh=" + refresh); } /** * 添加文档 * * @param ts 实体类集合 * @param refresh 是否强制刷新 */ protected String addDocuments(List<T> ts, Boolean refresh) { return starter.getRestClient().addDocuments(indexName, indexType, ts, "refresh=" + refresh); } /** * 页面-添加文档集合 * * @param ts 实体类集合 * @param refresh 是否强制刷新 */ protected void addDocumentsOfPage(List<T> ts, Boolean refresh) { this.delIndex(); this.createIndex(); int start = 0; int rows = 100; Integer size; do { List<T> list = pageDate(start, rows); if (list.size() > 0) { ///批量同步信息 starter.getRestClient().addDocuments(indexName, indexType, ts, "refresh=" + refresh); } size = list.size(); start += size; } while (size > 0); } /** * 使用分页添加文档必须重写此类文档 * * @param start 起始 * @param rows 项数 * @return */ protected List<T> pageDate(int start, int rows) { return null; } /** * 删除文档 * * @param id id * @param refresh 是否强制刷新 * @return */ protected String delDocument(String id, Boolean refresh) { return starter.getRestClient().deleteDocument(indexName, indexType, id, "refresh=" + refresh); } /** * 删除文档 * * @param ids id集合 * @param refresh 是否强制刷新 * @return */ protected String delDocuments(String[] ids, Boolean refresh) { return starter.getRestClient().deleteDocumentsWithrefreshOption(indexName, indexType, "refresh=" + refresh, ids); } /** * Id获取文档 * * @param id * @return */ protected T getDocument(String id, Class<T> clazz) { return starter.getRestClient().getDocument(indexName, indexType, id, clazz); } /** * ID更新文档 * * @param t 实体 * @param refresh 是否强制刷新 * @return */ protected String updateDocument(String id, T t, Boolean refresh) { return starter.getRestClient().updateDocument(indexName, indexType, id, t, "refresh=" + refresh); }}
xml写复杂Dsl:(如何写Dsl请参考bboss-elasticsearch文档,用法与mybatis标签相似)
<properties> </properties>
框架集成后,以下是使用示例:
定义数据模型:
package com.rz.dto;import com.frameworkset.orm.annotation.ESId;import com.rz.szwes.annotations.ESDsl;import com.rz.szwes.annotations.ESMapping;import com.rz.szwes.annotations.ESMappingType;import lombok.Data;import java.util.List;/** * 对应elasticsearch服务器的数据模型 * * @author sunziwen * @version 1.0 * @date 2019/12/16 11:08 **/@ESDsl(value = "elasticsearch/zsInfo.xml", indexName = "zsInfo")@Datapublic class ElasticZsInfoDto { @ESMapping(ESMappingType._byte) private int least_hit; private int is_must_zz; private int zs_level; private int cur_zs_ct; private int least_score_yy; private int least_score_yw; private int area_id; private String coll_name; private String coll_code; private long coll_pro_id; private int is_must_wl; private int cur_year; private int is_two; private long logo; @ESId private int id; private String area; private int college_id; private String is_must_yy; private int is_double; private int least_score_zz; private int least_score_wl; private String grade; private int is_nine; private String pro_name; private int least_score_sx; private int relevanceSort; private int pre_avg; private String is_must_dl; private String profession_code; private int least_score_sw; private String is_must_ls; private int grade_zk; private int least_score_wy; private int is_must_hx; private int profession_id; private String is_grad; private String is_must_yw; private int is_must_sw; private int least_score_ls; private int least_score_dl; private String zs_memo; private String is_must_sx; private String introduce; private int is_must_wy; private int grade_bk; private String pre_name; private int least_score_hx; private String coll_domain; private int pre_wch; private List<String> courses;}
定义服务
package com.rz.service;import com.rz.dto.ElasticZsInfoDto;import com.rz.szwes.core.ElasticBaseService;/** * 招生索引操作服务 * * @author sunziwen * @version 1.0 * @date 2019/12/16 11:02 **/public class ElasticZsInfoService extends ElasticBaseService<ElasticZsInfoDto> {}
完毕。可以进行索引和文档的crud操作,需要在xml中定义复杂的检索操作。这里只介绍我的增强功能,大部分功能定义在老板,读者可以看到老板文档(作者认为他唯一的缺陷是不能通过物理注释实现自动索引,每次手动指定xml位置,手动写mapping是一件非常痛苦的事情,这里增强)。