cn.easy-es
easy-es-boot-starter
1.0.2
org.elasticsearch.client
elasticsearch-rest-high-level-client
7.14.0
org.elasticsearch
elasticsearch
7.14.0
org.springframework.boot
spring-boot-starter-web
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
easy-es:
keep-alive-millis: 18000 # 心跳策略时间 单位:ms
connectTimeout: 5000 # 连接超时时间 单位:ms
socketTimeout: 5000 # 通信超时时间 单位:ms
connectionRequestTimeout: 5000 # 连接请求超时时间 单位:ms
maxConnTotal: 100 # 最大连接数 单位:个
maxConnPerRoute: 100 # 最大连接路由数 单位:个
enable: true # 是否开启EE自动配置
address: 192.xxxx:9200 # es连接地址+端口 格式必须为ip:port,如果是集群则可用逗号隔开
schema: http # 默认为http
username: elastic #如果无账号密码则可不配置此行
password: 1111111111#如果无账号密码则可不配置此行
global-config:
process_index_mode: smoothly #索引处理模式,smoothly:平滑模式,默认开启此模式, not_smoothly:非平滑模式, manual:手动模式
print-dsl: true # 开启控制台打印通过本框架生成的DSL语句,默认为开启,测试稳定后的生产环境建议关闭,以提升少量性能
distributed: false # 当前项目是否分布式项目,默认为true,在非手动托管索引模式下,若为分布式项目则会获取分布式锁,非分布式项目只需synchronized锁.
asyncProcessIndexBlocking: true # 异步处理索引是否阻塞主线程 默认阻塞 数据量过大时调整为非阻塞异步进行 项目启动更快
activeReleaseIndexMaxRetry: 60 # 分布式环境下,平滑模式,当前客户端激活最新索引最大重试次数若数据量过大,重建索引数据迁移时间超过60*(180/60)=180分钟时,可调大此参数值,此参数值决定最大重试次数,超出此次数后仍未成功,则终止重试并记录异常日志
activeReleaseIndexFixedDelay: 180 # 分布式环境下,平滑模式,当前客户端激活最新索引最大重试次数 若数据量过大,重建索引数据迁移时间超过60*(180/60)=180分钟时,可调大此参数值 此参数值决定多久重试一次 单位:秒
db-config:
map-underscore-to-camel-case: false # 是否开启下划线转驼峰 默认为false
table-prefix: daily_ # 索引前缀,可用于区分环境 默认为空 用法和MP一样
# id-type: customize # id生成策略 customize为自定义,id值由用户生成,比如取MySQL中的数据id,如缺省此项配置,则id默认策略为es自动生成
field-strategy: not_empty # 字段更新策略 默认为not_null
enable-track-total-hits: true # 默认开启,查询若指定了size超过1w条时也会自动开启,开启后查询所有匹配数据,若不开启,会导致无法获取数据总条数,其它功能不受影响.
refresh-policy: immediate # 数据刷新策略,默认为不刷新
enable-must2-filter: false # 是否全局开启must查询类型转换为filter查询类型 默认为false不转换
Document
package com.easyes.entity;
import cn.easyes.annotation.HighLight;
import cn.easyes.annotation.IndexField;
import cn.easyes.annotation.IndexId;
import cn.easyes.annotation.IndexName;
import cn.easyes.common.constants.Analyzer;
import cn.easyes.common.enums.FieldStrategy;
import cn.easyes.common.enums.FieldType;
import cn.easyes.common.enums.IdType;
import cn.easyes.common.params.JoinField;
import lombok.Data;
import java.util.List;
/**
* @Author: Chenyw
* @Date: 2022/10/24/10:14
*/
@Data
@IndexName(value = "document",shardsNum = 3,replicasNum = 2,childClass = Comment.class)
public class Document {
/**
* es中的唯一id,如果你想自定义es中的id为你提供的id,比如MySQL中的id,请将注解中的type指定为customize,如此id便支持任意数据类型)
*/
@IndexId(type = IdType.NONE)
private String id;
/**
* 文档标题,不指定类型默认被创建为keyword类型,可进行精确查询
*/
private String title;
/**
* 文档内容,指定了类型及存储/查询分词器
*/
@HighLight(mappingField = "highlightContent")
@IndexField(fieldType = FieldType.TEXT, analyzer = Analyzer.IK_SMART, searchAnalyzer = Analyzer.IK_MAX_WORD)
private String content;
/**
* 作者 加@TableField注解,并指明strategy = FieldStrategy.NOT_EMPTY 表示更新的时候的策略为 创建者不为空字符串时才更新
*/
@IndexField(strategy = FieldStrategy.NOT_EMPTY)
private String creator;
/**
* 创建时间
*/
@IndexField(fieldType = FieldType.DATE, dateFormat = "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis")
private String gmtCreate;
/**
* es中实际不存在的字段,但模型中加了,为了不和es映射,可以在此类型字段上加上 注解@TableField,并指明exist=false
*/
@IndexField(exist = false)
private String notExistsField;
/**
* 地理位置经纬度坐标 例如: "40.13933715136454,116.63441990026217"
*/
@IndexField(fieldType = FieldType.GEO_POINT)
private String location;
/**
* 图形(例如圆心,矩形)
*/
@IndexField(fieldType = FieldType.GEO_SHAPE)
private String geoLocation;
/**
* 自定义字段名称
*/
@IndexField(value = "wu-la")
private String customField;
/**
* 高亮返回值被映射的字段
*/
private String highlightContent;
/**
* 嵌套类型
*/
@IndexField(fieldType = FieldType.NESTED, nestedClass = User.class)
private List<User> users;
/**
* 须通过注解在父文档及子文档的实体类中指明其类型为Join,及其父名称和子名称,这里的JoinField类框架已内置,无需重复造轮子
* JoinField类全路径为cn.easyes.common.params.JoinField,如果你非要自己造轮子,也支持,那么需要在@TableField注解中指明joinFieldClass=你造的轮子
*/
@IndexField(fieldType = FieldType.JOIN, parentName = "document", childName = "comment")
private JoinField joinField;
}
Comment
package com.easyes.entity;
import cn.easyes.annotation.IndexField;
import cn.easyes.annotation.IndexName;
import cn.easyes.common.enums.FieldType;
import cn.easyes.common.params.JoinField;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @Author: Chenyw
* @Date: 2022/10/24/15:43
*/
@IndexName(child = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Comment {
/**
* 父子关系字段 须通过注解在父文档及子文档的实体类中指明其类型为Join,子文档中的父子关系可省略
*/
@IndexField(fieldType = FieldType.JOIN)
private JoinField joinField;
private String id;
private String commentContent;
}
User
package com.easyes.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* @Author: Chenyw
* @Date: 2022/10/24/15:26
*/
@Data
@AllArgsConstructor
public class User {
private String username;
private String password;
}
package com.easyes;
import cn.easyes.starter.register.EsMapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EsMapperScan("com.easyes.mapper")
public class EasyesApplication {
public static void main(String[] args) {
SpringApplication.run(EasyesApplication.class, args);
}
}
package com.easyes;
import cn.easyes.common.params.JoinField;
import cn.easyes.core.biz.PageInfo;
import cn.easyes.core.conditions.LambdaEsQueryWrapper;
import cn.easyes.core.conditions.LambdaEsUpdateWrapper;
import cn.easyes.core.toolkit.FieldUtils;
import com.easyes.entity.Comment;
import com.easyes.entity.Document;
import com.easyes.entity.User;
import com.easyes.mapper.CommentMapper;
import com.easyes.mapper.DocumentMapper;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.geo.ShapeRelation;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.geometry.Circle;
import org.elasticsearch.search.sort.SortOrder;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.ArrayList;
import java.util.List;
@SpringBootTest
class EasyesApplicationTests {
@Autowired
private DocumentMapper documentMapper;
@Autowired
private CommentMapper commentMapper;
@Test
void createIndex() {
// 初始化-> 创建索引,相当于MySQL建表 | 此接口须首先调用,只调用一次即可
// documentMapper.createIndex();
}
@Test
void insert() {
// 初始化-> 新增数据
Document document = new Document();
document.setTitle("嵌套查询");
document.setContent("这是一条内容");
document.setCreator("二叔");
document.setGmtCreate("2022-10-24 00:00:00");
List<User> list = new ArrayList<>();
User user1 = new User("陈彦伟", "123456");
User user2 = new User("二叔", "654321");
list.add(user1);
list.add(user2);
document.setUsers(list);
documentMapper.insert(document);
}
@Test
void searchWrapper() {
LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
/**
* 匹配查询
*/
wrapper.eq(Document::getTitle, "嵌套查询");
/**
* 分词查询
*/
wrapper.match(Document::getContent, "内容");
List<Document> documents = documentMapper.selectList(wrapper);
documents.forEach(System.out::println);
}
@Test
void searchPage() {
/**
* 分页查询
*/
LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
wrapper.eq(Document::getTitle, "嵌套查询");
PageInfo<Document> documentPageInfo = documentMapper.pageQuery(wrapper, 1, 10);
System.out.println(documentPageInfo);
}
@Test
void testNestedMatch() {
// 嵌套查询 查询内容匹配人才且嵌套数据中用户名匹配"用户1"的数据
// 其中嵌套类的字段名称获取我们提供了工具类FieldUtils.val帮助用户通过lambda函数式获取字段名称,当然如果不想用也可以直接传字符串
LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
wrapper.match(Document::getContent,"内容");
wrapper.nestedMatch(Document::getUsers, FieldUtils.val(User::getUsername), "二叔");
System.out.println(documentMapper.selectList(wrapper));
}
/**
* join
*/
@Test
void testInsert() {
// 测试新增父子文档,此处开启自动挡模式,父子类型索引已被自动处理
// 新新增父文档,然后再插入子文档
Document document = new Document();
document.setId("1");
document.setTitle("父文档的标题");
document.setContent("父文档的内容");
JoinField joinField = new JoinField();
joinField.setName("document");
document.setJoinField(joinField);
documentMapper.insert(document);
// 这里特别注意,子文档必须指定其父文档的id,否则找不到父文档别怪我没提醒
joinField.setParent("1");
joinField.setName("comment");
// 插入子文档
Comment comment = new Comment();
comment.setId("2");
comment.setCommentContent("文档的评论1");
comment.setJoinField(joinField);
commentMapper.insert(comment);
// 插入子文档2
Comment comment1 = new Comment();
comment1.setId("3");
comment1.setCommentContent("文档的评论2");
comment1.setJoinField(joinField);
commentMapper.insert(comment1);
}
@Test
void testSelect() {
// 温馨提示,下面wrapper中的type实际上就是JoinField字段注解@TableField中指定的parentName和childName,与原生语法是一致的
// case1: hasChild查询,返回的是相关的父文档 所以查询用父文档实体及其mapper
LambdaEsQueryWrapper<Document> documentWrapper = new LambdaEsQueryWrapper<>();
documentWrapper.hasChild("comment", FieldUtils.val(Comment::getCommentContent), "评论");
List<Document> documents = documentMapper.selectList(documentWrapper);
System.out.println(documents);
// case2: hasParent查询,返回的是相关的子文档 所以查询用子文档实体及其mapper
LambdaEsQueryWrapper<Comment> commentWrapper = new LambdaEsQueryWrapper<>();
// 字段名称你也可以不用FieldUtils.val,直接传入字符串也行
commentWrapper.hasParent("document", "content", "内容");
List<Comment> comments = commentMapper.selectList(commentWrapper);
System.out.println(comments);
//
// case3: parentId查询,返回的是相关的子文档,与case2类似,所以查询用子文档实体及其mapper
commentWrapper = new LambdaEsQueryWrapper<>();
commentWrapper.parentId("1", "comment");
List<Comment> commentList = commentMapper.selectList(commentWrapper);
System.out.println(commentList);
}
@Test
void testUpdate() {
// case1: 父文档/子文档 根据各自的id更新
Document document = new Document();
document.setId("1");
document.setTitle("父标题");
documentMapper.updateById(document);
// case2: 父文档/子文档 根据各自条件更新
Comment comment = new Comment();
comment.setCommentContent("更新后的评论");
LambdaEsUpdateWrapper<Comment> wrapper = new LambdaEsUpdateWrapper<>();
wrapper.match(Comment::getCommentContent, "评论");
commentMapper.update(comment, wrapper);
}
@Test
void testDelete() {
// case1: 父文档/子文档 根据各自的id删除
documentMapper.deleteById("1");
//case2: 父文档/子文档 根据各自条件删除
LambdaEsQueryWrapper<Comment> wrapper = new LambdaEsQueryWrapper<>();
wrapper.match(Comment::getCommentContent, "评论");
commentMapper.delete(wrapper);
}
/**
* 排序
*/
@Test
void testSort(){
// 测试排序 为了测试排序,我们在Document对象中新增了创建时间字段,更新了索引,并新增了两条数据
LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
wrapper.likeRight(Document::getContent,"这");
wrapper.select(Document::getTitle,Document::getGmtCreate);
List<Document> before = documentMapper.selectList(wrapper);
System.out.println("before:"+before);
wrapper.orderByDesc(Document::getGmtCreate);
List<Document> desc = documentMapper.selectList(wrapper);
System.out.println("desc:"+desc);
}
/**
* 得分排序
*/
@Test
void testSortByScore(){
// 测试根据得分升序排列(分数低的排前面)
LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
wrapper.match(Document::getContent,"这是");
wrapper.sortByScore(SortOrder.ASC);
List<Document> documents = documentMapper.selectList(wrapper);
System.out.println(documents);
}
/**
* 地图由近及远
*/
@Test
void testOrderByDistanceAsc() {
// 测试给定中心点, 查询出中心点168.8km范围内的数据,并按照距中心点距离由近及远排序
LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
GeoPoint centerPoint = new GeoPoint(41.0, 116.0);
wrapper.match(Document::getCreator, "二叔")
.geoDistance(Document::getLocation, 168.8, centerPoint)
.orderByDistanceAsc(Document::getLocation, centerPoint);
List<Document> documents = documentMapper.selectList(wrapper);
System.out.println(documents);
}
/**
* 地图由远及近
*/
@Test
void testOrderByDistanceDesc() {
// 测试给定中心点, 查询出中心点168.8km范围内的数据,并按照距中心点距离由远及近排序
LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
GeoPoint centerPoint = new GeoPoint(41.0, 116.0);
wrapper.match(Document::getCreator, "二叔")
.geoDistance(Document::getLocation, 168.8, centerPoint)
.orderByDistanceDesc(Document::getLocation, centerPoint);
List<Document> documents = documentMapper.selectList(wrapper);
System.out.println(documents);
}
/**
* 分组聚合groupBy
*/
@Test
void testGroupBy() {
LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
wrapper.groupBy(Document::getCreator);
// 支持多字段聚合
// wrapper.groupBy(Document::getCreator,Document::getCreator);
SearchResponse response = documentMapper.search(wrapper);
System.out.println(response);
}
/**
* 分词查询
*/
@Test
void testMatch(){
// 会对输入做分词,只要所有分词中有一个词在内容中有匹配就会查询出该数据,无视分词顺序
LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
wrapper.match(Document::getContent,"技术");
List<Document> documents = documentMapper.selectList(wrapper);
System.out.println(documents.size());
}
@Test
void testMatchPhase() {
// 会对输入做分词,但是需要结果中也包含所有的分词,而且顺序要求一样,否则就无法查询出结果
// 例如es中数据是 技术过硬,如果搜索关键词为过硬技术就无法查询出结果
LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
wrapper.matchPhrase(Document::getContent, "技术");
List<Document> documents = documentMapper.selectList(wrapper);
System.out.println(documents);
}
@Test
void testMatchAllQuery() {
// 查询所有数据,类似mysql select all.
LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
wrapper.matchAllQuery();
List<Document> documents = documentMapper.selectList(wrapper);
System.out.println(documents);
}
@Test
void testMatchPhrasePrefixQuery() {
// 前缀匹配查询 查询字符串的最后一个词才能当作前缀使用
// 前缀 可能会匹配成千上万的词,这不仅会消耗很多系统资源,而且结果的用处也不大,所以可以提供入参maxExpansions,若不写则默认为50
LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
wrapper.matchPhrasePrefixQuery(Document::getCustomField, "乌拉巴拉", 10);
List<Document> documents = documentMapper.selectList(wrapper);
System.out.println(documents);
}
@Test
void testMultiMatchQuery() {
// 从多个指定字段中查询包含老王的数据
LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
wrapper.multiMatchQuery("老王", Document::getTitle, Document::getContent, Document::getCreator, Document::getCustomField);
// 其中,默认的Operator为OR,默认的minShouldMatch为60% 这两个参数都可以按需调整,我们api是支持的 例如:
// 其中AND意味着所有搜索的Token都必须被匹配,OR表示只要有一个Token匹配即可. minShouldMatch 80 表示只查询匹配度大于80%的数据
// wrapper.multiMatchQuery("老王",Operator.AND,80,Document::getCustomField,Document::getContent);
List<Document> documents = documentMapper.selectList(wrapper);
System.out.println(documents.size());
System.out.println(documents);
}
@Test
void queryStringQuery() {
// 从所有字段中查询包含关键词老汉的数据
LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
wrapper.queryStringQuery("内容");
List<Document> documents = documentMapper.selectList(wrapper);
System.out.println(documents);
}
@Test
void prefixQuery() {
// 查询创建者以"隔壁"打头的所有数据 比如隔壁老王 隔壁老汉 都能被查出来
LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
wrapper.prefixQuery(Document::getCreator, "隔壁");
List<Document> documents = documentMapper.selectList(wrapper);
System.out.println(documents);
}
/**
* 地图查询
* 矩形 原型 无规则
*/
@Test
void testGeoBoundingBox() {
// 查询位于左上点和右下点坐标构成的长方形内的所有点
LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
// 假设左上点坐标
GeoPoint leftTop = new GeoPoint(41.187328D, 115.498353D);
// 假设右下点坐标
GeoPoint bottomRight = new GeoPoint(39.084509D, 117.610461D);
wrapper.geoBoundingBox(Document::getLocation, leftTop, bottomRight);
// 查不在此长方形内的所有点
// wrapper.notInGeoBoundingBox(Document::getLocation, leftTop, bottomRight);
List<Document> documents = documentMapper.selectList(wrapper);
documents.forEach(System.out::println);
}
@Test
void testGeoDistance() {
// 查询以经度为41.0,纬度为115.0为圆心,半径168.8公里内的所有点
LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
// 其中单位可以省略,默认为km
wrapper.geoDistance(Document::getLocation, 168.8, DistanceUnit.KILOMETERS, new GeoPoint(41.0, 116.0));
//查询不在圆形内的所有点
// wrapper.notInGeoDistance(Document::getLocation, 168.8, DistanceUnit.KILOMETERS, new GeoPoint(41.0, 116.0));
// 上面语法也可以写成下面这几种形式,效果是一样的,兼容不同用户习惯而已:
// wrapper.geoDistance(Document::getLocation,"1.5km",new GeoPoint(41.0,115.0));
// wrapper.geoDistance(Document::getLocation, "1.5km", "41.0,115.0");
List<Document> documents = documentMapper.selectList(wrapper);
System.out.println(documents);
}
@Test
void testGeoPolygon() {
// 查询以给定点列表构成的不规则图形内的所有点,点数至少为3个
LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
List<GeoPoint> geoPoints = new ArrayList<>();
GeoPoint geoPoint = new GeoPoint(40.178012, 116.577188);
GeoPoint geoPoint1 = new GeoPoint(40.169329, 116.586315);
GeoPoint geoPoint2 = new GeoPoint(40.178288, 116.591813);
geoPoints.add(geoPoint);
geoPoints.add(geoPoint1);
geoPoints.add(geoPoint2);
wrapper.geoPolygon(Document::getLocation, geoPoints);
List<Document> documents = documentMapper.selectList(wrapper);
System.out.println(documents);
}
/**
* 图形由用户自定义(常用),本框架支持Es所有支持的图形:
* (Point,MultiPoint,Line,MultiLine,Circle,LineaRing,Polygon,MultiPolygon,Rectangle)
*/
@Test
public void testGeoShape() {
// 注意,这里查询的是图形,所以图形的字段索引类型必须为geoShape,不能为geoPoint,故这里用geoLocation字段而非location字段
LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
// 这里以圆形为例演示,其中x,y为圆心坐标,r为半径. 其它图形请读者自行演示,篇幅原因不一一演示了
Circle circle = new Circle(13,14,100);
// shapeRelation支持多种,如果不传则默认为within
wrapper.geoShape(Document::getGeoLocation, circle, ShapeRelation.INTERSECTS);
// 不符合的情况
// wrapper.notInGeoShape(Document::getGeoLocation, circle, ShapeRelation.INTERSECTS);
List<Document> documents = documentMapper.selectList(wrapper);
System.out.println(documents);
}
}