springboot操作ES之ElasticSearch_EasyEs

springboot操作ES之ElasticSearch_EasyEs

前置环境

  • es:7.x
  • springboot:2.6.0
  • easyes:1.0.2

1.导入依赖

   
            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
        

2.配置文件

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不转换

3.新建实体类

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;
}

4.启动类上面加EsMapperScan

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);
    }

}

5.最后测试各种操作

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);
    }
}

你可能感兴趣的:(搜索引擎,elasticsearch,spring,boot,1024程序员节)