各位好,我们把之前的坑填一下,我在上上篇文章中写了springboot集成es7 的方法,并且集成了es原生客户端 High Level Rest Client, 也说明了原因, 我用的版本较高, spring-data封装的es版本较低,所以使用了原生的。
当我们把这一切都准备好的时候,剩下的就是要体验Es的功能了,Es中突出能力就是他的搜索能力。 要想搜索,必须先有数据,而在es中的数据结构, 是由索引,类型和文档组成的,分别对应关系数据库中的,库,表,行。所以使用es的第一步,就是设计我们的数据结构。 那就需要先得有索引,而在创建索引的时候,就必须给出他的结构,比如有哪些字段,是什么类型,需不需要分词。
关于ES中的数据类型,可参照官网: https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html
关于ES中索引的api, 可参照官网: https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.x/java-rest-high-create-index.html
关于使用java HighLevelRestClient如何操作索引,可参照官网: https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.x/java-rest-high-create-index.html
接下来书接上回,上次我们封装了一个工具类用于创建索引,举例创建了一个只有四个字段的索引是如何创建的,比如我创建一个Person的索引,索引结构包括: 姓名,年龄, 描述, 和id . ES中的索引其实就是一个大的json结构,所以我们可以直接通过restful请求,发送json参数来实现。我们这里中电说下如何使用 java api 完成。 再贴一遍代码:
/*** 创建索引(默认分片数为5和副本数为1)* @param indexName* @throws IOException*/public boolean createIndex(String indexName) throws IOException {CreateIndexRequest request = new CreateIndexRequest(indexName);request.settings(Settings.builder()// 设置分片数为3, 副本为2.put("index.number_of_shards", 3).put("index.number_of_replicas", 2));// 这里创建索引结构request.mapping(generateBuilder());CreateIndexResponse response = restHighLevelClient.indices().create(request, RequestOptions.DEFAULT);// 指示是否所有节点都已确认请求boolean acknowledged = response.isAcknowledged();// 指示是否在超时之前为索引中的每个分片启动了必需的分片副本数boolean shardsAcknowledged = response.isShardsAcknowledged();if (acknowledged || shardsAcknowledged) {log.info("创建索引成功!索引名称为{}", indexName);return true;}return false;}private XContentBuilder generateBuilder() throws IOException {XContentBuilder builder = XContentFactory.jsonBuilder();builder.startObject();{builder.startObject("properties");{// es7及以后去掉了映射类型--personbuilder.startObject("name");{builder.field("type", "text");builder.field("analyzer", "ik_smart");}builder.endObject();}{builder.startObject("age");{builder.field("type", "integer");}builder.endObject();}{builder.startObject("desc");{builder.field("type", "text");builder.field("analyzer", "ik_smart");}builder.endObject();}{builder.startObject("id");{builder.field("type", "integer");}builder.endObject();}builder.endObject();}builder.endObject();/*.startObject().field("properties").startObject().field("person").startObject("name").field("type" , "text").field("analyzer", "ik_smart").endObject().startObject("age").field("type" , "int").endObject().startObject("desc").field("type", "text").field("analyzer", "ik_smart").endObject().endObject().endObject();*/return builder;}
上面可以看到,这里调用了一个generateBuilder() 方法来创建接构,这个方法里就是组装我们的数据结构。 也就是我们的四个字段。
这里我们将name 和 desc 设置成了text字段, text字段属于String, 并指定了ik_smart 就是ik的中文分词器,也就是这个字段就在存储的时候就会自动分词,那么我们查询的时候就可以根据分词进行查询,除了text类型,String中还有一种叫做keyword,就是关键字,keyword是不会分词的,会把整个字段作为关键字搜索, 比如身份证号,订单号,这些不是由一些单词组成的,一般都是整个匹配的,那么就是用keyword。
好了回归正题, 这个时候我们发现如果我们要设计一个相对比较复杂的索引结构,按照上面的写法就太麻烦了,各种大括号,可能早就绕晕了,其实我们无非是想把我们需要索引的字段按照一定的格式设置到json结构中去,所以我们完全可以将一个实体映射成一个我们想要的索引结构,比如有一个类Person ,里面有上面的四个属性,我们可以根据Person的类结构,获取里面的所有字段去构建索引结构,唯一的问题就是,我们可能没法区分哪些字段映射成哪些类型。那么我们完全可以通过一些标识来标注生成索引时他们映射成什么类型。所以可以使用自定义注解。
这里我们定义一个Field注解,用在类的属性上,标注这个字典升射成es中的什么类型,以及使用什么分词器。
/*** 作用在字段上,用于定义类型,映射关系* @author ls*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
@Inherited
public @interface Field {FieldType type() default FieldType.TEXT;/*** 指定分词器* @return*/AnalyzerType analyzer() default AnalyzerType.STANDARD;}
这里边的FieldType和 AnalyzerType是定义好的枚举,保存一些常用类型
/*** es 类型参看* https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html*/
@Getter
public enum FieldType {/*** text*/TEXT("text"),KEYWORD("keyword"),INTEGER("integer"),DOUBLE("double"),DATE("date"),/*** 单条数据*/OBJECT("object"),/*** 嵌套数组*/NESTED("nested"),;FieldType(String type){this.type = type;}private String type;}
@Getter
public enum AnalyzerType {NO("不使用分词"),/*** 标准分词,默认分词器*/STANDARD("standard"),/*** ik_smart:会做最粗粒度的拆分;已被分出的词语将不会再次被其它词语占有*/IK_SMART("ik_smart"),/*** ik_max_word :会将文本做最细粒度的拆分;尽可能多的拆分出词语*/IK_MAX_WORD("ik_max_word");private String type;AnalyzerType(String type){this.type = type;}}
然后在我们的类上标识: 我这里用一个事件类,并且做了对象嵌套, Event类中还有一个action
/*** @className: Event* @description: 定义事件的文档类型* @author: sh.Liu* @create: 2020-05-27 10:02*/
@Data
public class Event {private static final long serialVersionUID=1L;@Field(type = FieldType.KEYWORD)private Integer eventId;/*** 唯一标识码*/@Field(type = FieldType.KEYWORD)private String uniqueCode;/*** 任务号*/@Field(type = FieldType.KEYWORD)private String eventCode;/*** 事件来源编号*/@Field(type = FieldType.INTEGER)private Integer eventSrcId;/*** 事件来源名称*/@Field(type = FieldType.TEXT, analyzer = AnalyzerType.IK_SMART)private String eventSrcName;/*** 来源分组*/@Field(type = FieldType.KEYWORD)private String srcGroupCode;/*** 事件大类编码*/@Field(type = FieldType.KEYWORD)private String eventTypeCode;/*** 事件大类名称*/@Field(type = FieldType.KEYWORD)private String eventTypeName;/*** 事件小类编码*/@Field(type = FieldType.KEYWORD)private String eventSubtypeCode;/*** 事件小类名称*/@Field(type = FieldType.KEYWORD)private String eventSubtypeName;/*** 重要程度*/@Field(type = FieldType.INTEGER)private Integer eventGradeId;/*** 重要程度名称*/@Field(type = FieldType.KEYWORD)private String eventGradeName;/***紧急程度标识*/@Field(type = FieldType.INTEGER)private Integer eventUrgencyId;/***紧急程度名称*/@Field(type = FieldType.KEYWORD)private String eventUrgencyName;/***事件级别标识*/@Field(type = FieldType.INTEGER)private Integer eventLevelId;/***事件级别名称*/@Field(type = FieldType.KEYWORD)private String eventLevelName;/***事件升级标志*/@Field(type = FieldType.INTEGER)private Integer eventUpgradeFlag;/***处置级别标识*/@Field(type = FieldType.INTEGER)private Integer dealLevelId;/***处置级别标识*/@Field(type = FieldType.KEYWORD)private String dealLevelName;/***公众上报人名称*/@Field(type = FieldType.TEXT , analyzer = AnalyzerType.IK_SMART)private String publicReporterName;/***公众上报人身份证号*/@Field(type = FieldType.KEYWORD)private String publicReporterIdcard;/***公众上报人联系方式*/@Field(type = FieldType.KEYWORD)private String publicReporterTel;/*** 事件描述*/@Field(type = FieldType.TEXT, analyzer = AnalyzerType.IK_SMART)private String eventDesc;/*** 地址*/@Fieldprivate String address;/*** 地区名称*/@Field(type = FieldType.KEYWORD)private String areaRegionName;/*** 地区编码*/@Field(type = FieldType.KEYWORD)private String areaRegionCode;/*** 社区名称*/@Field(type = FieldType.KEYWORD)private String commRegionName;/*** 区编码*/@Field(type = FieldType.KEYWORD)private String commRegionCode;/*** 街道名称*/@Field(type = FieldType.KEYWORD)private String streetRegionName;/*** 街道编码*/@Field(type = FieldType.KEYWORD)private String streetRegionCode;/*** 社区名称*/@Field(type = FieldType.KEYWORD)private String regionName;/*** 社区编码*/@Field(type = FieldType.KEYWORD)private String regionCode;/*** 经度*/private BigDecimal coordX;/*** 纬度*/private BigDecimal coordY;/***坐标系*/private String mapcoordinate;/***网格员标识*/private Integer griderId;/***网格员标识*/private String griderName;/***网格员电话*/private String griderPhone;/***核实状态标识*/@Field(type = FieldType.INTEGER)private Integer verifyStateId;/***核查状态标识*/@Field(type = FieldType.INTEGER)private Integer checkStateId;/***事件建立时间*/@Field(type = FieldType.DATE)@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8")private Date createTime;/***流程结束时间*/@Field(type = FieldType.DATE)@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8")private Date endTime;/***延期天数*/private Float postponedDays;/***延期标志*/private Integer postponedFlag;/***受理时间*/@Field(type = FieldType.DATE)@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8")private Date acceptTime;/***立案时间*/@Field(type = FieldType.DATE)@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8")private Date establishTime;/***调度时间*/@Field(type = FieldType.DATE)@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8")private Date dispatchTime;/***流程开始时间*/@Field(type = FieldType.DATE)@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8")private Date procStartTime;/***流程结束时间*/@Field(type = FieldType.DATE)@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8")private Date procEndTime;/***流程截止时间*/@Field(type = FieldType.DATE)@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8")private Date procDeadLine;/***流程警告时间*/@Field(type = FieldType.DATE)@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8")private Date procWarningTime;/***处置开始时间*/@Field(type = FieldType.DATE)@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8")private Date funcBeginTime;/***处置完成时间*/@Field(type = FieldType.DATE)@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8")private Date funcFinishTime;/***自处置标识*/private Integer gridDealFlag;/***跨网格标志*/private Integer overGridFlag;/***是否督办*/@Field(type = FieldType.INTEGER)private Integer pressFlag;/***是否催办*/@Field(type = FieldType.INTEGER)private Integer hurryFlag;/***超期标志*/@Field(type = FieldType.INTEGER)private Integer overtimeFlag;/***活动属性*/@Field(type = FieldType.INTEGER)private Integer actPropertyId;/***活动属性名称*/@Field(type = FieldType.KEYWORD)private String actPropertyName;/***流程实例标识*/@Field(type = FieldType.KEYWORD)private String procInstId;/***流程定义标识*/@Field(type = FieldType.KEYWORD)private String procDefId;/***事件状态*/@Field(type = FieldType.INTEGER)private Integer eventStateId;/*** 上一操作项*/@Field(type = FieldType.KEYWORD)private String preActionName;/*** 登记人Id*/@Field(type = FieldType.INTEGER)private Integer registerId;/*** 登记人姓名*/@Field(analyzer = AnalyzerType.IK_SMART)private String registerName;/*** 回访标识:0-未回访 1-已回访 2-无法回访*/@Field(type = FieldType.INTEGER)private Integer visitorStateId;/*** 删除标识*/@Field(type = FieldType.INTEGER)private Integer deleteFlag;/*** 删除用户标识*/@Field(type = FieldType.INTEGER)private Integer deleteUserId;/*** 删除时间*/@Field(type = FieldType.DATE)@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8")private Date deleteTime;/*** 是否下发督查* 0:否,1:是*/@Field(type = FieldType.INTEGER)private Integer overseerFlag;@Field(type = FieldType.DATE)@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8")private Date updateTime;@Field(type = FieldType.OBJECT)private Act action;}
注意,最后的act就是一个对象, es中还有嵌套数组的支持。这里以嵌套对象为例
/*** @className: Action* @description: 事件表* @author: sh.Liu* @create: 2020-05-27 10:15*/
@Data
public class Act implements Serializable {private static final long serialVersionUID = 1L;@Field(type = FieldType.INTEGER)private Integer actId;/*** 任务标识*/@Field(type = FieldType.KEYWORD)private String taskId;/*** 流程定义标识*/@Field(type = FieldType.KEYWORD)private String procDefId;/*** 流程实例标识*/@Field(type = FieldType.KEYWORD)private String procInstId;/*** 子流程实例标识*/@Field(type = FieldType.KEYWORD)private String subInstId;/*** 节点定义标识*/@Field(type = FieldType.KEYWORD)private String nodeId;/*** 节点定义名称*/@Field(type = FieldType.KEYWORD)private String nodeName;/*** 业务主键标识*/@Field(type = FieldType.KEYWORD)private String bizId;/*** 参与者标识*/@Field(type = FieldType.KEYWORD)private String partId;/*** 参与者姓名*/@Field(type = FieldType.TEXT)private String partName;/*** 部门id*/@Field(type = FieldType.KEYWORD)private String unitId;/*** 部门名称*/@Field(type = FieldType.TEXT)private String unitName;/*** 角色标识*/@Field(type = FieldType.KEYWORD)private String roleId;/*** 角色名称*/@Field(type = FieldType.TEXT, analyzer = AnalyzerType.IK_SMART)private String roleName;/*** 上一活动标识*/@Field(type = FieldType.TEXT, analyzer = AnalyzerType.IK_SMART)private String preActId;/*** 上一活动参与者标识*/private String prePartId;/*** 上一活动参与者名称*/@Field(type = FieldType.TEXT, analyzer = AnalyzerType.IK_SMART)private String prePartName;/*** 上一活动定义标识*/private String preNodeId;/*** 上一活动定义名称*/@Field(type = FieldType.TEXT, analyzer = AnalyzerType.IK_SMART)private String preNodeName;/*** 上一活动意见*/@Field(type = FieldType.TEXT, analyzer = AnalyzerType.IK_SMART)private String preOpinion;/*** 上一活动操作项名称*/@Field(type = FieldType.TEXT, analyzer = AnalyzerType.IK_SMART)private String preActionName;/*** 上一活动操作项显示名称*/@Field(type = FieldType.TEXT, analyzer = AnalyzerType.IK_SMART)private String preActionLabel;/*** 创建时间*/@Field(type = FieldType.DATE)private Date createTime;/*** 截止时间*/@Field(type = FieldType.DATE)private Date deadLine;/*** 警告时间*/@Field(type = FieldType.DATE)private Date warningTime;/*** 结束时间*/@Field(type = FieldType.DATE)private Date endTime;/*** 活动红绿灯*/@Field(type = FieldType.INTEGER)private Integer actTimeStateId;/*** 活动时限*/@Field(type = FieldType.DOUBLE)private BigDecimal timeLimit;/*** 计时单位*/@Field(type = FieldType.INTEGER)private Integer timeUnit;/*** 活动时限分钟*/@Field(type = FieldType.INTEGER)private Integer timeLimitM;/*** 已用时*/@Field(type = FieldType.INTEGER)private Integer actUsedTime;/*** 剩余时*/@Field(type = FieldType.INTEGER)private Integer actRemainTime;/*** 活动时限信息*/@Field(type = FieldType.TEXT, analyzer = AnalyzerType.IK_SMART)private String actLimitInfo;/*** 活动已用时间字符串*/@Field(type = FieldType.TEXT, analyzer = AnalyzerType.IK_SMART)private String actUsedTimeChar;/*** 活动剩余时间字符串*/@Field(type = FieldType.TEXT, analyzer = AnalyzerType.IK_SMART)private String actRemainTimeChar;/*** 累计计时标志*/@Field(type = FieldType.INTEGER)private Integer timeAddUpFlag;/*** 暂停前节点用时*/@Field(type = FieldType.INTEGER)private Integer actUsedTimeBeforeStop;/*** 恢复计时时间*/@Field(type = FieldType.DATE)private Date actRestart;/*** 已读标志*/@Field(type = FieldType.INTEGER)private Integer readFlag;/*** 已读时间*/@Field(type = FieldType.DATE)private Date readTime;/*** 批转意见*/@Field(type = FieldType.TEXT, analyzer = AnalyzerType.IK_SMART)private String transOpinion;/*** 操作项名称*/@Field(type = FieldType.TEXT, analyzer = AnalyzerType.IK_SMART)private String actionName;/*** 操作项显示名称*/@Field(type = FieldType.TEXT, analyzer = AnalyzerType.IK_SMART)private String actionLabel;/*** 活动属性id*/@Field(type = FieldType.INTEGER)private Integer actPropertyId;/*** 活动属性名称*/@Field(type = FieldType.TEXT, analyzer = AnalyzerType.IK_SMART)private String actPropertyName;/*** 抄送参与者*/@Field(type = FieldType.TEXT, analyzer = AnalyzerType.IK_SMART)private String ccPart;/*** 抄送参与者名称*/@Field(type = FieldType.TEXT, analyzer = AnalyzerType.IK_SMART)private String ccPartName;/*** 抄送标志*/@Field(type = FieldType.INTEGER)private Integer ccFlag;
然后我们在工具类中定义个方法,创建一个索引,传入Class类型的参数,这个根据Class对象我们就可以获取这个类上的所以属性,以及属性上的注解,根据注解我们可以得到它所映射的es类型,以及分词器,然后创建出我们想要的索引。
public XContentBuilder generateBuilder(Class clazz) throws IOException {// 获取索引名称及类型// Document doc = (Document) clazz.getAnnotation(Document.class);// System.out.println(doc.index());// System.out.println(doc.type());XContentBuilder builder = XContentFactory.jsonBuilder();builder.startObject();builder.startObject("properties");Field[] declaredFields = clazz.getDeclaredFields();for (Field f : declaredFields) {if (f.isAnnotationPresent(com.lsqingfeng.action.es.annotation.Field.class)) {// 获取注解com.lsqingfeng.action.es.annotation.Field declaredAnnotation = f.getDeclaredAnnotation(com.lsqingfeng.action.es.annotation.Field.class);// 如果嵌套对象:/*** {* "mappings": {* "properties": {* "region": {* "type": "keyword"* },* "manager": {* "properties": {* "age": { "type": "integer" },* "name": {* "properties": {* "first": { "type": "text" },* "last": { "type": "text" }* }* }* }* }* }* }* }*/if (declaredAnnotation.type() == FieldType.OBJECT) {// 获取当前类的对象-- ActionClass<?> type = f.getType();Field[] df2 = type.getDeclaredFields();builder.startObject(f.getName());builder.startObject("properties");// 遍历该对象中的所有属性for (Field f2 : df2) {if (f2.isAnnotationPresent(com.lsqingfeng.action.es.annotation.Field.class)) {// 获取注解com.lsqingfeng.action.es.annotation.Field declaredAnnotation2 = f2.getDeclaredAnnotation(com.lsqingfeng.action.es.annotation.Field.class);builder.startObject(f2.getName());builder.field("type", declaredAnnotation2.type().getType());// keyword不需要分词if (declaredAnnotation2.type() == FieldType.TEXT) {builder.field("analyzer", declaredAnnotation2.analyzer().getType());}builder.endObject();}}builder.endObject();builder.endObject();}else{builder.startObject(f.getName());builder.field("type", declaredAnnotation.type().getType());// keyword不需要分词if (declaredAnnotation.type() == FieldType.TEXT) {builder.field("analyzer", declaredAnnotation.analyzer().getType());}builder.endObject();}}}// 对应propertybuilder.endObject();builder.endObject();return builder;}
这里直接重载了generateBuilder() 方法,传一个Class即可。 这样你调用最上面的createIndex方法,把里边的generateBuilder() 改为 generateBuilder(Event.class); 这样就会根据Event的实体结构创建出Event对应的索引结构。 这里其实我们也可以定义一个注解来标识你想定义的索引名称。
这样在执行createIndex方法的时候,就按照Event类生成了相应的索引结构。虽然写的注解比较多,但是应该比多而乱的大括号好一些,当然大家也可以直接读取类中所有字段,这样就不用每个字段上都加注解了,然后有注解的特殊处理,其余的走默认值,这样也能减少不少代码。主要是原理搞清楚了,剩下的就靠自己发挥了。
好了这篇文章就介绍到这里,下次我们介绍下,es中的一些常用查询方法,如何实现全文检索,分页和高亮显示。
如果文章对你有帮助,打赏就不用了,给波关注吧,如果有问题,欢迎评论区下方留言,一起交流。