springboot整合es进行搜索(二)

本文涉及的产品
检索分析服务 Elasticsearch 版,2核4GB开发者规格 1个月
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
简介: springboot整合es进行搜索

4.3 entity包

4.3.1 Result.java

package com.fox.es.entity;
import lombok.Data;
/**
 * 统一返回对象
 * 
 * @author 狐狸半面添
 * @create 2023-03-22 18:34
 */
@Data
public class Result {
    private Integer code;
    private String msg;
    private Object data;
    private Result(Integer code, String msg, Object data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }
    private Result(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    public static Result ok() {
        return new Result(200, "success");
    }
    public static Result ok(Object data) {
        return new Result(200, "success", data);
    }
    public static Result error(String msg) {
        return new Result(500, msg);
    }
    public Result error(Integer code, String msg) {
        return new Result(code, msg);
    }
}

4.3.2 JacksonObjectMapper.java

package com.fox.es.entity;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
/**
 * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
 * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
 * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
 *
 * @author 狐狸半面添
 * @create 2023-01-18 20:34
 */
public class JacksonObjectMapper extends ObjectMapper {
    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
    public JacksonObjectMapper() {
        super();
        //收到未知属性时不报异常
        this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
        //反序列化时,属性不存在的兼容处理
        this.getDeserializationConfig().withoutFeatures(FAIL_ON_UNKNOWN_PROPERTIES);
        SimpleModule simpleModule = new SimpleModule()
                .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
                .addSerializer(BigInteger.class, ToStringSerializer.instance)
                .addSerializer(Long.class, ToStringSerializer.instance)
                .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
        //注册功能模块 例如,可以添加自定义序列化器和反序列化器
        this.registerModule(simpleModule);
    }
}

4.4 config包

4.4.1 ElasticsearchConfig.java

package com.fox.es.config;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
 * @author 狐狸半面添
 * @create 2023-03-22 17:51
 */
@Configuration
public class ElasticsearchConfig {
    @Value("${elasticsearch.hostname}")
    private String hostname;
    @Value("${elasticsearch.port}")
    private Integer port;
    @Bean
    public RestHighLevelClient restHighLevelClient() {
        RestClientBuilder builder = RestClient.builder(
                new HttpHost(hostname, port, "http")
        );
        return new RestHighLevelClient(builder);
    }
}

4.4.2 WebMvcConfig.java

package com.fox.es.config;
import com.fox.es.entity.JacksonObjectMapper;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import java.util.List;
/**
 * @author 狐狸半面添
 * @create 2023-02-11 23:25
 */
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
        messageConverter.setObjectMapper(new JacksonObjectMapper());
        converters.add(0, messageConverter);
    }
}

4.5 BlogSimpleInfoDTO.java

package com.fox.es.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
 * @author 狐狸半面添
 * @create 2023-03-22 21:33
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BlogSimpleInfoDTO {
    /**
     * 主键id
     */
    private Integer id;
    /**
     * 用户id
     */
    private Long userId;
    /**
     * 标题
     */
    private String title;
    /**
     * 介绍
     */
    private String introduce;
    /**
     * 创建时间
     */
    private LocalDateTime createTime;
}

4.5 主启动类

package com.fox.es;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
 * @author 狐狸半面添
 * @create 2023-03-22 18:07
 */
@SpringBootApplication
public class ElasticsearchDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(ElasticsearchDemoApplication.class, args);
    }
}

4.6 ⭐测试是否连接 es 成功

package com.fox.es.controller;
import com.fox.es.entity.Result;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.core.MainResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.io.IOException;
/**
 * @author 狐狸半面添
 * @create 2023-03-22 18:33
 */
@RestController
public class TestController {
    @Resource
    private RestHighLevelClient restHighLevelClient;
    /**
     * 用于测试是否连接 es 成功
     *
     * @return 返回 es 的基本信息,等价于访问:http://127.0.0.1:9200
     * @throws IOException 异常信息
     */
    @GetMapping("/getEsInfo")
    public Result getEsInfo() throws IOException {
        MainResponse info = restHighLevelClient.info(RequestOptions.DEFAULT);
        return Result.ok(info);
    }
}

浏览器访问:http://localhost:9999/getEsInfo

4.7 ⭐搜索服务

4.7.1 controller层

package com.fox.es.controller;
import com.fox.es.entity.Result;
import com.fox.es.service.ShareResourceSearchService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
 * @author 狐狸半面添
 * @create 2023-03-22 20:16
 */
@RestController
@RequestMapping("/search")
public class ShareResourceSearchController {
    @Resource
    private ShareResourceSearchService shareResourceSearchService;
    /**
     * 通过关键词获取数据列表
     *
     * @param keyWords 关键词
     * @param pageNo   页码
     * @param pageSize 每页大小
     * @return 数据列表,按照相关性从高到低进行排序
     */
    @GetMapping("/list")
    public Result list(@RequestParam("keyWords") String keyWords,
                       @RequestParam("pageNo") Integer pageNo,
                       @RequestParam("pageSize") Integer pageSize) {
        return shareResourceSearchService.list(keyWords, pageNo, pageSize);
    }
}

4.7.2 service接口层

package com.fox.es.service;
import com.fox.es.entity.Result;
/**
 * @author 狐狸半面添
 * @create 2023-03-22 20:18
 */
public interface ShareResourceSearchService {
    /**
     * 通过关键词获取数据列表
     *
     * @param keyWords 关键词
     * @param pageNo 页码
     * @param pageSize 每页大小
     * @return 数据列表,按照相关性从高到低进行排序
     */
    Result list(String keyWords, int pageNo, int pageSize);
}

4.7.3 service实现层

package com.fox.es.service.impl;
import com.alibaba.fastjson.JSON;
import com.fox.es.dto.BlogSimpleInfoDTO;
import com.fox.es.entity.Result;
import com.fox.es.service.ShareResourceSearchService;
import lombok.extern.slf4j.Slf4j;
import org.apache.lucene.search.TotalHits;
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.BoolQueryBuilder;
import org.elasticsearch.index.query.MultiMatchQueryBuilder;
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.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * @author 狐狸半面添
 * @create 2023-03-22 20:18
 */
@Slf4j
@Service
public class ShareResourceSearchServiceImpl implements ShareResourceSearchService {
    @Resource
    private RestHighLevelClient restHighLevelClient;
    @Value("${elasticsearch.blog.index}")
    private String blogIndexStore;
    @Value("${elasticsearch.blog.source_fields}")
    private String blogFields;
    public Result list(String keyWords, int pageNo, int pageSize) {
        // 1.设置索引 - blog
        SearchRequest searchRequest = new SearchRequest(blogIndexStore);
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        // 2.source源字段过虑
        String[] sourceFieldsArray = blogFields.split(",");
        searchSourceBuilder.fetchSource(sourceFieldsArray, new String[]{});
        // 3.关键字
        if (StringUtils.hasText(keyWords)) {
            // 哪些字段匹配关键字
            MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery(keyWords, "title", "tags", "introduce", "content");
            // 设置匹配占比(表示最少匹配的子句个数,例如有五个可选子句,最少的匹配个数为5*70%=3.5.向下取整为3,这就表示五个子句最少要匹配其中三个才能查到)
            multiMatchQueryBuilder.minimumShouldMatch("70%");
            // 提升字段的Boost值
            multiMatchQueryBuilder.field("title", 10);
            multiMatchQueryBuilder.field("tags", 7);
            multiMatchQueryBuilder.field("introduce", 5);
            boolQueryBuilder.must(multiMatchQueryBuilder);
        }
        // 4.分页
        int start = (pageNo - 1) * pageSize;
        searchSourceBuilder.from(start);
        searchSourceBuilder.size(pageSize);
        // 布尔查询
        searchSourceBuilder.query(boolQueryBuilder);
        // 6.高亮设置
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        highlightBuilder.preTags("<font color='red'>");
        highlightBuilder.postTags("</font>");
        // 设置高亮字段
        ArrayList<HighlightBuilder.Field> fields = new ArrayList<>();
        fields.add(new HighlightBuilder.Field("title"));
        fields.add(new HighlightBuilder.Field("introduce"));
        highlightBuilder.fields().addAll(fields);
        searchSourceBuilder.highlighter(highlightBuilder);
        // 请求搜索
        searchRequest.source(searchSourceBuilder);
        SearchResponse searchResponse;
        try {
            searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
        } catch (IOException e) {
            log.error("博客搜索异常:{}", e.getMessage());
            return Result.error(e.getMessage());
        }
        // 结果集处理
        SearchHits hits = searchResponse.getHits();
        SearchHit[] searchHits = hits.getHits();
        // 记录总数
        long totalHitsCount = hits.getTotalHits().value;
        // 数据列表
        List<BlogSimpleInfoDTO> list = new ArrayList<>();
        for (SearchHit hit : searchHits) {
            String sourceAsString = hit.getSourceAsString();
            // json 转 对象
            BlogSimpleInfoDTO blog = JSON.parseObject(sourceAsString, BlogSimpleInfoDTO.class);
            // 取出高亮字段内容
            Map<String, HighlightField> highlightFields = hit.getHighlightFields();
            if (highlightFields != null) {
                blog.setTitle(parseHighlightStr(blog.getTitle(), highlightFields.get("title")));
                blog.setIntroduce(parseHighlightStr(blog.getIntroduce(), highlightFields.get("introduce")));
            }
            list.add(blog);
        }
        // 封装信息返回前端
        HashMap<String, Object> resultMap = new HashMap<>(4);
        // 页码
        resultMap.put("pageNo", pageNo);
        // 每页记录数量
        resultMap.put("pageSize", pageSize);
        // 总记录数
        resultMap.put("total", totalHitsCount);
        // 该页信息
        resultMap.put("items", list);
        return Result.ok(resultMap);
    }
    public String parseHighlightStr(String text, HighlightField field) {
        if (field != null) {
            Text[] fragments = field.getFragments();
            StringBuilder stringBuilder = new StringBuilder();
            for (Text str : fragments) {
                stringBuilder.append(str.string());
            }
            return stringBuilder.toString();
        } else {
            return text;
        }
    }
}

这里我们使用 apipost7 进行测试:

5.源码获取

这个demo如果你按照我的前三步进行了执行,那么直接运行源码应该是没有问题的。

源码地址:Mr-Write/SpringbootDemo: 各种demo案例 (github.com)

对应的是 elasticsearch-demo 包模块。

6.存在的问题与进阶说明

我们还存在另外一个需要解决的重要问题:与MySQL的数据一致性问题

❓ 为什么不直接使用ES存储所有的项目数据呢?

  • 非关系型表达,大量的反范式存储,导致维护基础数据异常(新增异常,修改异常,删除异常),需要MySQL做托底。
  • 不支持事务,一致性需要自己维护。
  • 从存储的角度来说,Elasticsearch是基于“字段”的,大行超多字段性能并不好。

数据同步的方式有很多种,比如通过ES的API进行增删改查,或者通过中间件像canalMQ进行数据全量、增量的同步。

具体解决方案,可参考:docker环境安装mysql、canal、elasticsearch,基于binlog利用canal实现mysql的数据同步到elasticsearch中

相关实践学习
使用阿里云Elasticsearch体验信息检索加速
通过创建登录阿里云Elasticsearch集群,使用DataWorks将MySQL数据同步至Elasticsearch,体验多条件检索效果,简单展示数据同步和信息检索加速的过程和操作。
ElasticSearch 入门精讲
ElasticSearch是一个开源的、基于Lucene的、分布式、高扩展、高实时的搜索与数据分析引擎。根据DB-Engines的排名显示,Elasticsearch是最受欢迎的企业搜索引擎,其次是Apache Solr(也是基于Lucene)。 ElasticSearch的实现原理主要分为以下几个步骤: 用户将数据提交到Elastic Search 数据库中 通过分词控制器去将对应的语句分词,将其权重和分词结果一并存入数据 当用户搜索数据时候,再根据权重将结果排名、打分 将返回结果呈现给用户 Elasticsearch可以用于搜索各种文档。它提供可扩展的搜索,具有接近实时的搜索,并支持多租户。
相关文章
|
7月前
|
Java 数据安全/隐私保护 Spring
百度搜索:蓝易云【SpringBoot—jasypt加解密库的使用方法。】
希望以上内容对你理解和使用Spring Boot中的Jasypt加解密库有所帮助。如果需要更详细的信息和示例,请参考Jasypt库的官方文档和示例代码。
84 0
|
2月前
|
Web App开发 JavaScript Java
elasticsearch学习五:springboot整合 rest 操作elasticsearch的 实际案例操作,编写搜索的前后端,爬取京东数据到elasticsearch中。
这篇文章是关于如何使用Spring Boot整合Elasticsearch,并通过REST客户端操作Elasticsearch,实现一个简单的搜索前后端,以及如何爬取京东数据到Elasticsearch的案例教程。
223 0
elasticsearch学习五:springboot整合 rest 操作elasticsearch的 实际案例操作,编写搜索的前后端,爬取京东数据到elasticsearch中。
|
5月前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的文献搜索系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的文献搜索系统附带文章源码部署视频讲解等
47 0
|
5月前
|
人工智能 Java
AI大模型----SpringBoot添加放行最简单的方式@AuthAccess,问题库构思,概念title,答案text,搜索search
AI大模型----SpringBoot添加放行最简单的方式@AuthAccess,问题库构思,概念title,答案text,搜索search
|
6月前
|
JavaScript Java 测试技术
基于SpringBoot+Vue的文献搜索系统的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue的文献搜索系统的详细设计和实现(源码+lw+部署文档+讲解等)
65 0
|
7月前
|
XML 存储 Java
百度搜索:蓝易云【springboot增加logback日志记录ip详解】
通过以上步骤,您可以在Spring Boot应用程序中使用Logback记录客户端的IP地址。请根据实际需求和日志记录规则进行适当调整和配置。
112 0
|
canal 存储 关系型数据库
Spring Boot业务系统如何实现海量数据高效实时搜索
我们都知道随着业务系统的发展和使用,数据库存储的业务数据量会越来越大,逐渐成为了业务系统的瓶颈。在阿里巴巴开发手册中也建议:单表行数超过500万行或者单表容量超过2GB才推荐进行分库分表,如果预计三年后数据量根本达不到这个级别,请不要在创建表时就分库分表。数据库最终都是存储在磁盘上,随着数据量变大,会导致数据操作变得缓慢,无论是计算还是IO,但是话又说回来,单表数据量大就一定要进行分库分表操作吗?答案是否定的,因为分库分表本身是一个“很重”的操作,这里就不卖关子了,直接来看看分库分表带来的以下问题和挑战
497 1
Spring Boot业务系统如何实现海量数据高效实时搜索
|
前端开发 Java Spring
百度搜索:蓝易云【SpringBoot解决跨域的方法详细教程。】
通过以上步骤,你可以在Spring Boot中配置跨域支持。根据实际需求,可以灵活调整跨域规则来满足项目的具体需求。
55 0
|
前端开发 Java 数据库连接
基于SpringBoot+Mybatis plus+React.js实现条件选择切换搜索功能
基于SpringBoot+Mybatis plus+React.js实现条件选择切换搜索功能
65 0
|
自然语言处理 数据可视化 数据库
Elasticsearch 搜索测试与集成Springboot3
它能够一定程度上解决,在一个普通数据库处理上亿条数据时的查询效率低下的同时无法优秀地排列好用户所需要的数据,一次性上亿条数据没有经过正确地排列,用户很难找到想要的数据。并且,用户输入的数据可能不太准确,它也能够进行模糊查询,这种模糊查询是依靠计算得来的,而不是简单地匹配数据。本系列博文将从零开始一步步实现将 ES 集成到 springboot3 中,并在一个社区项目中进行实际应用测试,本文为系列第一篇,后续,博文仍在整理,请持续关注博主,了解更多相关知识。
166 0
Elasticsearch 搜索测试与集成Springboot3

热门文章

最新文章