本章知识点沿用知识点12的项目,介绍如何使用spring boot整合ES,没有ES的去我主页各类型大数据集群搭建文档-->大数据原生集群本地测试环境搭建三中可以看到ES如何搭建

不管你有没有ES,最好是没有,因为一定要知道一点,一定要去官网查一下你当前用的spring boot data es的版本是不是和你自己ES服务器所匹配的,这一点简直是天坑,spring boot提供的es封装API对es的版本要求相当苛刻,对不上就用不了,很多人折在版本问题,奉劝大家一句,除非正式的项目开发上,团队会给你提供需要版本的jar,正式开发本身版本都是经过架构师仔细考虑并且版本方面问题都解决了,自己学习自己开发,你最好不要再jar上做坚持,直接去改你的es服务版本,也不要轻信网上说的改pom版本,会连带着出很多不必要的问题,官网如下https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#preface.requirements

比如,我在写这篇博文的时候spring boot沿用的前面知识点项目,spring boot版本是2.7.0,自动导入的spring boot data es版本是4.4.0,我的ES就要用7.17.3的,但是我本地先前用的es是6.6.2的,含泪重装。

同时不同的spring boot会受版本的影响,导致不同的操作ES方式会出现不同的问题,最常见的是操作方式过时,所以如果大家使用的版本和我的不一样,很可能导致无法操作

第一步:添加依赖

<!--es的配置 根据springboot的版本写入的-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

第二步:修改spring boot配置文件,添加如下的配置

spring.elasticsearch.uris=http://192.168.88.187:9200
spring.data.elasticsearch.repositories.enabled=true

第三步:对操作的实体Bean加注解

对你要在es操作的数据添加文档注释,indexName 是索引名称
@Document(indexName = "users")

对id字段,必须加上Id注解
@Id

指定字段的索引方式,index是否索引、store是否存储、字段的分词方式、搜索时关键字分词的方式、type指定该字段的值以什么样的数据类型来存储
@Field(index=true,store=true,analyzer="ik_max_word",searchAnalyzer="ik_max_word",type=FieldType.Text)

解释一下,需要分词的字段就是查询做条件的字段,比如我要操作的User类

package com.wy.scjg.bean;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.experimental.Accessors;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.util.Date;

@ToString
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@Document(indexName = "users")
public class User implements Serializable {
    /**
     * 主键ID
     */
    @TableId(value = "id", type = IdType.AUTO)
    @Id
    private Long id;

    /**
     * 姓名
     */
    @Field(index=true,store=true,analyzer="ik_max_word",searchAnalyzer="ik_max_word",type= FieldType.Text)
    private String name;

    /**
     * 年龄
     */
    private Integer age;

    /**
     * 邮箱
     */
    private String email;

    /**
     * 生日
     */
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date brith;

    private Integer did;

    //表中不存在的字段
    @TableField(exist = false)
    private String dname;

    /**
     * 照片
     */
    private String zp;

    /**
     * 个人照,任意张
     */
    private String grz;
}

第四步:建立一个包,存放ES操作接口文件,并准备一个操作User数据的ES接口
在这里插入图片描述
包和类名大家自定义

package com.wy.scjg.repository;

import com.wy.scjg.bean.User;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;

/**
 *  ElasticsearchRepository<数据Bean,一般都是String>
 * 继承后即可拥有基本的操作方法
 */
@Repository
public interface UserRepository extends ElasticsearchRepository<User,String> {

}

第五步:用测试类,向ES操作初始化User表的数据

package com.wy.scjg;

import com.wy.scjg.bean.User;
import com.wy.scjg.repository.UserRepository;
import com.wy.scjg.service.UserService;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

@SpringBootTest
public class ESTest {

	//注入  UserRepository 对象操作ES
    @Autowired
    private UserRepository userRepository;

    @Autowired
    private UserService userService;

    @Test
    void imports(){
        List<User> list = userService.list();
        for(User user:list){
            userRepository.save(user);
        }
    }

}

运行代码后,查看es数据
在这里插入图片描述
这个时候我们就可以将详情功能优化一下。

第六步:修改查询详情的Controller,将数据从ES查询

@Autowired
private UserRepository userRepository;

/**
 * 详情
 * @param request
 * @param id
 * @return
 */
@RequestMapping("/detail")
public String detail(HttpServletRequest request,Long id){
    Optional<User> byId = userRepository.findById(id.toString());

    //存储
    request.setAttribute("user",byId.get());
    return "/user_detail";
}

运行看效果
在这里插入图片描述
注意:SpringBoot提供的超类ElasticsearchRepository,继承后就可以拥有基本的操作方法,如果要进行复杂点的查询,就需要我们自定义方法,自定义方法的方法名必须按照命名规则来进行命名

具体示例:更多的大家需要在网上自己找找了

//根据姓名查用户
List<User> findByName(String name);
	
//根据地址查询 findBy+字段名
List<User> findByAddress(String address);
	
//根据地址和姓名查询  findBy+多个字段名之间And分隔
List<User> findByAddressAndName(String address,String name);
	
//查询id小于某个值的数据  findBy+比大小的字段+LessThan
List<User> findByIdLessThan(int id);
	
//查询价格在多少-多少之间的   findBy+条件字段+Between
List<User> findByPriceBetween(double money,double money)

ES严格意义上并不是一个关系型数据库,它本身是一个空间换时间的索引库,以枚举分词查询条件的字段来弥补普通关系型数据库在like查询上的时间问题,通俗的将它提供了一种索引能力,所以它牺牲了其他普通关系型数据库哪像的性能,再加上国内使用ES通常都是IK分词器用来命中中文,因此ES一般只会被用来优化模糊匹配的查询需求,因为普通的数据库中只有like%能够触发索引。相较于其他的增删改来说ES虽然能做,但是它的效率没有普通关系数据库效率高,ES在存数据和修改数据都用的是save方法,不止要对某个字段分词,其他字段都会走一遍,效率很低。删除的话已有delete开头的方法,这些大家都可以自己试一试。

到此本篇知识点讲解结束,此外 本次整体讲解的spring boot项目已上传github


ES高亮

springboot封装程度很高,所以它把高亮功能封装在了ES客户端里面,因此想要使用springboot的ES高亮,除了导入data es之外还需要客户端

<!-- elasticsearch客户端-->
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>transport</artifactId>
</dependency>

配置文件无需多加,任然保持之前的就可以

spring.elasticsearch.uris=http://192.168.88.187:9200
spring.data.elasticsearch.repositories.enabled=true

随后在你的Service中就可以书写高亮代码,为了大家方便理解,首先给大家写一个单字段的高亮,高亮涉及很多不同包下的同名类,不要导错,防止大家分不清包,这里将整个UserService提供给大家

package com.wy.scjg.service.impl;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wy.scjg.bean.User;
import com.wy.scjg.mapper.UserMapper;
import com.wy.scjg.service.UserService;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 实现类固定继承ServiceImpl类,目的是直接注入Dao层以及操作的实例类,并实现自定义的service接口
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

	//高亮注入
    @Resource
    private RestHighLevelClient highLevelClient;

	//这个和高亮无关,是前面的查询功能,不用看
    @Override
    public IPage<User> getUserList(Page page, User user) {
        return baseMapper.getUserList(page,user);
    }

	//高亮Service
    @Override
    public Map searchWC(Integer page, Integer limit, String kw) {
        //1、请求ES索引连接,构造参数是ES的索引库名
        SearchRequest request = new SearchRequest("users");
        //2、索源构造器,承载搜索条件
        SearchSourceBuilder builder = new SearchSourceBuilder();
        //3、高亮构造器
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        //4、搜索结果
        SearchResponse searchResponse = null;
        //5、存放最终响应数据的Map 以及 具体数据的List
        Map searchResultMap = new HashMap<>();
        List list = new ArrayList<>();//保存结果

        //6、给索源构造器配置搜索条件:关键字模糊查询、分页、高亮
        if(kw != null && !"".equals(kw)){
            builder.query(QueryBuilders.matchQuery("name", kw));
        }
        //前端传递通常1开始,但是ES是0页开始的
        builder.from(page - 1);
        builder.size(limit);
        highlightBuilder.preTags("<font style='color: red'>");
        highlightBuilder.postTags("</font>");
        highlightBuilder.field("name");
        //同一字段中存在多个高亮值 设置都高亮
        highlightBuilder.requireFieldMatch(true);
        builder.highlighter(highlightBuilder);

        //7、给请求添加查询条件
        request.source(builder);
        try {
            //8、查询并处理结果
            searchResponse = highLevelClient.search(request, RequestOptions.DEFAULT);
            //保存分页信息:总条数total、总页数pages、结果当前页currPage
            long hitsTotalValue = searchResponse.getHits().getTotalHits().value;
            searchResultMap.put("total", hitsTotalValue);
            //页数(ceil后结果类型是double)
            searchResultMap.put("pages", (long) Math.ceil(hitsTotalValue / limit));
            searchResultMap.put("currPage", page);

            //解析高亮数据
            SearchHits hits = searchResponse.getHits();
            for(SearchHit hit: hits){
                //原始数据,不包含高亮的数据
                Map<String, Object> sourceMap = hit.getSourceAsMap();
                //高亮数据
                Map<String, HighlightField> highlightFields = hit.getHighlightFields();
                HighlightField highlightTitle = highlightFields.get("name");
                //替换
                if(highlightTitle != null){
                    Text[] fragments = highlightTitle.getFragments();
                    if(fragments != null && fragments.length > 0){
                        sourceMap.replace("name", fragments[0].toString());
                    }
                }
                list.add(sourceMap);//循环将数据添加入列表
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        searchResultMap.put("dataList",list);

        return searchResultMap;
    }

}

之后在Controller准备

//根据关键字搜索 参数:page当前页、limit每页多少数据、kw高亮值,produces可以没有它的目的只是设置返回值的类型和编码集
@ResponseBody
@RequestMapping(value = "/searchWC", produces = "application/json; charset=utf-8")
public Map searchWC(Integer page, Integer limit, String kw){
    return userService.searchWC(page, limit, kw);
}

随后启动项目,前端查看结果
在这里插入图片描述

可是上面这种高亮只是用在简单的高亮场景,但是我们实际使用时往往有着不同的需求,而且不大可能只有一个字段高亮,同时大家编程完上面的简单高亮后会发现 RestHighLevelClient 是一个过时不推荐使用的高亮工具类,因此实际开发中更加实用的高亮如下

package com.wy.scjg.service.impl;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wy.scjg.bean.User;
import com.wy.scjg.mapper.UserMapper;
import com.wy.scjg.service.UserService;
import org.elasticsearch.common.lucene.search.function.FunctionScoreQuery;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 实现类固定继承ServiceImpl类,目的是直接注入Dao层以及操作的实例类,并实现自定义的service接口
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

	//高亮功能被整合在了此对象中
    @Resource
    private ElasticsearchRestTemplate elasticsearchRestTemplate;

    @Override
    public IPage<User> getUserList(Page page, User user) {
        return baseMapper.getUserList(page,user);
    }

	//高亮
    @Override
    public Map searchWC(Integer page, Integer limit, String kw) {
        //1 准备一个ES提供的索引查询构造器  这个类可以和SortBuilders配合指定更加丰富的排序
        NativeSearchQueryBuilder searchQueryBuilder = new NativeSearchQueryBuilder();
        //2 封装分页对象
        Pageable pageable = PageRequest.of(page - 1, limit);
        searchQueryBuilder.withPageable(pageable);
        //3 封装查询条件 这个类和QueryBuilders配合可以搭配出丰富的查询条件
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        if (kw!=null && !"".equals(kw)){
            boolQueryBuilder.should(QueryBuilders.multiMatchQuery(kw, "name", "age"));
        }
        //配置好后应当提交给索引查询构造器,但本案例中是为了让大家知道有这个东西就行
        //至于用这两个字段过滤的功能在高亮运行时就已经实现了,同时也体现出一个事情:
        //   高亮和ES的查询构造器不是必须共生,而是高亮是索引查询的一个功能
        //searchQueryBuilder.withFilter(boolQueryBuilder);

        //4 自定义一个ES的排序 指定SUM是匹配度总和 以及最小匹配度值
        FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery(boolQueryBuilder).scoreMode(FunctionScoreQuery.ScoreMode.SUM).setMinScore(1);
        searchQueryBuilder.withQuery(functionScoreQueryBuilder);

        //5 封装配置的高亮字段
        HighlightBuilder.Field[] fields = new HighlightBuilder.Field[2];
        fields[0] = new HighlightBuilder.Field("name").preTags("<font color='red'>").postTags("</font>").requireFieldMatch(false);
        fields[1] = new HighlightBuilder.Field("age").preTags("<font color='red'>").postTags("</font>").requireFieldMatch(false);
        searchQueryBuilder.withHighlightFields(fields);

        //6 用索引条件构造器生成一个查询条件对象
        NativeSearchQuery searchQuery = searchQueryBuilder.build();

        //7 查询
        SearchHits<User> searchHits = elasticsearchRestTemplate.search(searchQuery, User.class);

        if(searchHits.getTotalHits()<=0){
            return null;
        }

        //8 处理结果
        List list = new ArrayList();
        Map searchResultMap = new HashMap<>();

        List<SearchHit<User>> result = searchHits.getSearchHits();
        result.forEach(r -> {
            //原始数据
            User user = r.getContent();
            //高亮数据
            Map<String, List<String>> highlightFields = r.getHighlightFields();
            //处理name字段
            List<String> names = highlightFields.get("name");
            if (names != null && names.size() > 0) {
                StringBuffer buffer = new StringBuffer();
                for (String s : names) {
                    buffer.append(s);
                }
                user.setName(buffer.toString());
            }
            //处理age字段
            List<String> ages = highlightFields.get("age");
            if (ages != null && ages.size() > 0) {
                StringBuffer buffer = new StringBuffer();
                //这里这个循环,大家不要有什么疑惑,就是springboot框架返回了一个集合,其实里面就一个高亮后的数据
                for (String s : ages) {
                    buffer.append(s);
                }
                //实体bean需要新增一个字段,因为原来的是数字,高亮后是个字符串
                user.setAge_HT(buffer.toString());
            }

            //装入数据集
            list.add(user);
        });
        searchResultMap.put("dataList",list);
        //保存分页信息:总条数total、总页数pages、结果当前页currPage
        long hitsTotalValue = searchHits.getTotalHits();
        searchResultMap.put("total", hitsTotalValue);
        searchResultMap.put("pages", (long) Math.ceil(hitsTotalValue / limit));
        searchResultMap.put("currPage", page);

        return searchResultMap;
    }

}

此时其他内容不变,访问接口拿数据,即可发现name、age字段已经高亮成功
在这里插入图片描述
在这里插入图片描述


最后我要说一个雷点,我们国内用的IK分词器是国人写的,它和ES在正常使用的时候,如果中文分词的字段中,某一条恰好存的没有中文,IK分词器是无法分词的,因此该值高亮时就不会有高亮效果,如我上图的高亮数据中,name字段值是高亮测试2,但如果存的是其他的,比方说2222zzz2这种没有一个中文的值,那么这条数据查询后,即使我的高亮关键字是2,name字段也是没有高亮效果的

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐