Spring Boot 集成 ElasticSearch 的简单示例

示例采用 Spring Boot 3.0.7、 ElasticSearch 8.16.0,其中 es 为本地配置的单机服务

1. 下载 ElastiSearch

官方 GitHub 上目前只有源码:github.com/elastic/ela...,并没有发布二进制文件,因此推荐使用 docker 来下载。

官方提供了一个 docker 下载的脚本:github.com/elastic/sta...

这里简单进行介绍一下:

1.1. 系统要求

  • 支持 Windows(需下载 WSL2)、Linux、macOS
  • 需要有 docker
  • 需要至少 5GB 的硬盘空间

其中 Windows 和 macOS 推荐下载 docker-desktop:www.docker.com/products/do...

1.2. 安装

终端里输入一下命令(需确保docker-desktop启动),可指定版本

sql 复制代码
curl -fsSL https://elastic.co/start-local | sh -s -- -v 8.16.0
  • 该命令会下载脚本并执行,创建elastic-start-local/ 目录,以及在目录里生成必要的配置文件,比如.env、docker-compose.yml,还会自动拉取官方镜像并启动容器。
  • 会包含 Elastic 企业版30天使用许可证,30天后自动降级为Basic(免费开源版)
  • 还会设置安全访问和控制
    • 自动生成强密码(存储在.env文件,注意,如果需要更改密码,在.env里更改是不会生效的,需要到docker里es的容器中去更改)
    • 自动生成 API Key
    • 仅绑定 localhost
    • 禁用 HTTPS(仅 HTTP + Basic Auth),适合本地调试。
  • 该脚本的其他详细特性可自行去官网查看。

安装成功的页面

elastic-search端口在9200,kibana(es可视化控制台)在5601

1.3. 安装 ik 中文分词器

ElasticSearch 默认的分词器对中文支持很差 ,因此我们需要安装专门的中文分词器:github.com/infinilabs/...

我们需要在 docker 中打开 elasticsearch 的容器(一般叫做 es-local-dev),然后将 ik 分词器安装至 bin/elasticsearch-plugin 目录。

安装命令如下:

bash 复制代码
bin/elasticsearch-plugin install https://get.infini.cloud/elasticsearch/analysis-ik/8.16.0

如果你下载了docker exec,可以更便捷的通过可视化进入容器中

这里我们直接点击 es-local-dev,然后再选择 exec

此时就可以在这里的终端输入上述的安装命令。

最后,我们在终端里输入

bash 复制代码
bin/elasticsearch-plugin list

如果出现 analysis-ik,就完成了下载!

2. 使用示例

2.1. 首先导入依赖

xml 复制代码
<!-- ElasticSearch -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

不需要指定版本,一般 Spring Boot 项目会继承 Spring Boot Starter Parent,或者使用 dependencyManagement。如果都没有,版本号指定为 Spring Boot 的版本号即可。

2.2. 配置 application.yaml

yaml 复制代码
spring:
  data:
    elasticsearch:
      repositories:
        enabled: true // 启用 Spring Data Elasticsearch 的 Repository 自动代理功能
  elasticsearch:
    uris: http://localhost:9200
    username: elastic
    password: 你的密码

2.3. 创建文档实体(ES 文档映射类)

根据你要搜索的内容,创建文档实体

kotlin 复制代码
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
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 java.util.Date;

/**
 * ElasticSearch 文档实体类 | 博客文章
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = "blog_articles")
public class ArticleDocument {

    /**
     * 文档唯一标识
     */
    @Id
    private String id;

    /**
     * 文章标题
     * 这里采用ik分词器
     * ik_smart用于建索引时粗粒度分词节省空间,ik_max_word用于搜索时细粒度分词提高召回率。
     */
    @Field(type = FieldType.Text, analyzer = "ik_smart", searchAnalyzer = "ik_max_word")
    private String title;

    /**
     * 文章内容
     */
    @Field(type = FieldType.Text, analyzer = "ik_smart", searchAnalyzer = "ik_max_word")
    private String content;

    /**
     * 作者id
     */
    @Field(type = FieldType.Keyword) // 不分词,用于精确匹配
    private String authorId;

    /**
     * 创建时间
     */
    @Field(type = FieldType.Date)
    private Date createTime;
}

2.4. 创建 Respository:ES 的操作接口

java 复制代码
package io.github.somehow.mysite.dao.mapper;

import io.github.somehow.mysite.elasticsearch.ArticleDocument;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

import java.util.List;

/**
 * Elasticsearch 持久层 | 博客文章
 */
public interface ArticleEsRepository extends ElasticsearchRepository<ArticleDocument, String> {

    /**
     * 根据标题搜索文章
     *
     * @param title 标题关键词
     * @param pageable 分页参数
     * @return 文章分页结果
     */
    Page<ArticleDocument> findByTitleContaining(String title, Pageable pageable);

    /**
     * 根据内容搜索文章
     *
     * @param content 内容关键词
     * @param pageable 分页参数
     * @return 文章分页结果
     */
    Page<ArticleDocument> findByContentContaining(String content, Pageable pageable);

    /**
     * 根据作者ID列表搜索文章
     *
     * @param authorIds 作者ID列表
     * @param pageable 分页参数
     * @return 文章分页结果
     */
    Page<ArticleDocument> findByAuthorIdIn(List<String> authorIds, Pageable pageable);
}
  • 这里定义的方法不需要去实现,这是 Spring Data 的 Repository 动态代理机制Spring 在启动时会根据方法名自动生成查询实现,因此无需手动编写代码。
  • 注意,此处的 Pageorg.springframework.data.domain.Page,如果你使用了 Mybatis-Plus,需要注意和Mybatis-Plus 的 Page 区别开。
  • ps:底层 ES 查询并不强制分页;不过,Repository 接口方法如果返回 Page 类型,会自动进行分页 处理,这是框架层面的封装。

2.5. 在 Service 业务逻辑层编写 ES 的相关逻辑

ini 复制代码
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
import io.github.somehow.mysite.commons.framework.exception.ClientException;
import io.github.somehow.mysite.dao.entity.ArticleDO;
import io.github.somehow.mysite.dao.entity.UserDO;
import io.github.somehow.mysite.dao.entity.UserFavoriteArticleDO;
import io.github.somehow.mysite.dao.mapper.ArticleEsRepository;
import io.github.somehow.mysite.dao.mapper.ArticleMapper;
import io.github.somehow.mysite.dao.mapper.UserFavoriteArticleMapper;
import io.github.somehow.mysite.dao.mapper.UserMapper;
import io.github.somehow.mysite.dto.req.article.*;
import io.github.somehow.mysite.dto.resp.ArticlePageQueryRespDTO;
import io.github.somehow.mysite.dto.resp.ArticleSelectRespDTO;
import io.github.somehow.mysite.elasticsearch.ArticleDocument;
import io.github.somehow.mysite.service.ArticleService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * 文章业务逻辑实现层
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, ArticleDO> implements ArticleService {

    private final ArticleEsRepository esRepository;
    private final UserMapper userMapper;

    @Override
    @Transactional
    public void createArticle(ArticleCreateReqDTO requestParam) {

        // 略,此处为文章创建逻辑,并填入数据库
        articleMapper.insert(articleDO);

        // 需要将数据同步到 ElasticSearch 中
        ArticleDocument doc = ArticleDocument.builder()
                .id(articleDO.getId().toString())
                .title(requestParam.getTitle())
                .content(requestParam.getContent())
                .authorId(articleDO.getAuthorId().toString())
                .createTime(articleDO.getCreateTime())
                .build();
        esRepository.save(doc);
    }

    @Override
    public void updateArticle(ArticleUpdateReqDTO requestParam) {

        // 略,此处为更新操作
        baseMapper.update(updateWrapper);

        // 更新ES中的文档
        ArticleDO updatedArticle = baseMapper.selectOne(Wrappers.lambdaQuery(ArticleDO.class)
                .eq(ArticleDO::getId, requestParam.getId())
                .eq(ArticleDO::getDelFlag, 0));
        
        if (updatedArticle != null) {
            ArticleDocument articleDocument = ArticleDocument.builder()
                    .id(updatedArticle.getId().toString())
                    .title(updatedArticle.getTitle())
                    .content(updatedArticle.getContent())
                    .authorId(updatedArticle.getAuthorId().toString())
                    .createTime(updatedArticle.getCreateTime())
                    .build();

            esRepository.save(articleDocument);
        }
    }

    @Override
    public void deleteArticle(Long id) {

        // 略,此处为删除逻辑
        articleDO.setDelFlag(1);
        baseMapper.update(articleDO, updateWrapper);

        // 删除同步到 ES
        esRepository.deleteById(id.toString());
    }

    @Override
    public IPage<ArticlePageQueryRespDTO> pageQueryArticle(ArticlePageQueryReqDTO requestParam) {
        // 构建ES分页参数
        PageRequest pageRequest = PageRequest.of(
                (int) (requestParam.getCurrent() - 1), 
                (int) requestParam.getSize());
        
        // 获取搜索关键词
        String keyword = StrUtil.blankToDefault(requestParam.getKeyword(), "");
        
        // 获取搜索类型,默认按标题搜索
        String searchType = StrUtil.blankToDefault(requestParam.getSearchType(), "title");
        
        org.springframework.data.domain.Page<ArticleDocument> esPage;
        switch (searchType) {
            case "title":
                // 按标题搜索
                esPage = esRepository.findByTitleContaining(keyword, pageRequest);
                break;
            case "content":
                // 按内容搜索
                esPage = esRepository.findByContentContaining(keyword, pageRequest);
                break;
            case "author":
                // 按作者搜索
                List<UserDO> matchedUsers = userMapper.selectByUsernameLike(keyword);
                if (!CollectionUtils.isEmpty(matchedUsers)) {
                    List<String> authorIds = matchedUsers.stream()
                            .map(user -> user.getId().toString())
                            .collect(Collectors.toList());
                    esPage = esRepository.findByAuthorIdIn(authorIds, pageRequest);
                } else {
                    // 没有匹配的用户,返回空结果
                    return new Page<>();
                }
                break;
            default:
                // 默认按标题搜索
                esPage = esRepository.findByTitleContaining(keyword, pageRequest);
                break;
        }
        
        // 将ES查询结果转换为需要的DTO格式
        if (esPage.hasContent()) {
            List<Long> articleIds = esPage.getContent().stream()
                    .map(doc -> Long.valueOf(doc.getId()))
                    .collect(Collectors.toList());
                    
            // 从数据库获取完整文章信息
            List<ArticleDO> articles = baseMapper.selectList(Wrappers.<ArticleDO>lambdaQuery()
                    .in(ArticleDO::getId, articleIds)
                    .eq(ArticleDO::getDelFlag, 0));
            
            // 获取作者信息
            List<Long> authorIds = articles.stream()
                    .map(ArticleDO::getAuthorId)
                    .distinct()
                    .collect(Collectors.toList());
            
            List<UserDO> authors = userMapper.selectList(Wrappers.<UserDO>lambdaQuery()
                    .in(UserDO::getId, authorIds));
            
            // 构建返回结果
            List<ArticlePageQueryRespDTO> records = articles.stream()
                    .map(article -> {
                        ArticlePageQueryRespDTO dto = new ArticlePageQueryRespDTO();
                        dto.setId(article.getId());
                        dto.setTitle(article.getTitle());
                        dto.setSummary(article.getSummary());
                        dto.setViewCount(article.getViewCount());
                        dto.setFavoriteCount(article.getFavoriteCount());
                        
                        // 设置作者名称
                        String authorName = authors.stream()
                                .filter(user -> user.getId().equals(article.getAuthorId()))
                                .map(UserDO::getUsername)
                                .findFirst()
                                .orElse("");
                        dto.setAuthorName(authorName);
                        
                        return dto;
                    })
                    .collect(Collectors.toList());
            
            // 构造MyBatis Plus分页对象
            IPage<ArticlePageQueryRespDTO> result = new Page<>(requestParam.getCurrent(), requestParam.getSize(), esPage.getTotalElements());
            result.setRecords(records);
            return result;
        } else {
            return new Page<>();
        }
    }

}

2.6. Contriller 中设置 API 接口

kotlin 复制代码
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.github.somehow.mysite.commons.framework.result.Result;
import io.github.somehow.mysite.commons.framework.web.Results;
import io.github.somehow.mysite.dto.req.article.*;
import io.github.somehow.mysite.dto.resp.ArticlePageQueryRespDTO;
import io.github.somehow.mysite.dto.resp.ArticleSelectRespDTO;
import io.github.somehow.mysite.service.ArticleService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

/**
 * 文章数据控制层
 */
@RestController
@RequiredArgsConstructor
@Tag(name = "文章数据管理")
public class ArticleController {

    private final ArticleService articleService;

    @Operation(summary = "创建文章")
    @PostMapping("/api/article/create")
    public Result<Void> createArticle(@RequestBody ArticleCreateReqDTO requestParam) {
        articleService.createArticle(requestParam);
        return Results.success();
    }

    @Operation(summary = "更新文章")
    @PutMapping("/api/article/update")
    public Result<Void> updateArticle(@RequestBody ArticleUpdateReqDTO requestParam) {
        articleService.updateArticle(requestParam);
        return Results.success();
    }

    @Operation(summary = "删除文章")
    @DeleteMapping("/api/article/delete/{id}")
    public Result<Void> deleteArticle(@PathVariable("id") String id) {
        articleService.deleteArticle(Long.parseLong(id));
        return Results.success();
    }

    @Operation(summary = "分页搜索文章信息")
    @GetMapping("/api/article/search")
    public Result<IPage<ArticlePageQueryRespDTO>> pageQueryArticle(ArticlePageQueryReqDTO requestParam) {
        return Results.success(articleService.pageQueryArticle(requestParam));
    }

}

如果有错误或纰漏,欢迎大家在评论区留言指正!

相关推荐
vx_bisheyuange3 小时前
基于SpringBoot的老年一站式服务平台
java·spring boot·后端·毕业设计
计算机毕设VX:Fegn08953 小时前
计算机毕业设计|基于Java + vue水果商城系统(源码+数据库+文档)
java·开发语言·数据库·vue.js·spring boot·课程设计
老华带你飞5 小时前
校务管理|基于springboot 校务管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·spring
JosieBook5 小时前
【部署】Spring Boot + Vue框架项目生产环境部署完整方案
vue.js·spring boot·后端
油丶酸萝卜别吃5 小时前
springboot项目中与接口文档有关的注解
java·spring boot·后端
小码哥0685 小时前
家政服务管理-家政服务管理平台-家政服务管理平台源码-家政服务管理平台java代码-基于springboot的家政服务管理平台
java·开发语言·spring boot·家政服务·家政服务平台·家政服务系统·家政服务管理平台源码
Java爱好狂.5 小时前
复杂知识简单学!Springboot加载配置文件源码分析
java·spring boot·后端·spring·java面试·后端开发·java程序员
invicinble5 小时前
easyexcel的基本使用
spring boot
小贝IT~6 小时前
基于SpringBoot的图书个性化推荐系统-048
java·spring boot·后端