示例采用 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 在启动时会根据方法名自动生成查询实现,因此无需手动编写代码。
- 注意,此处的 Page 是 org.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));
}
}
如果有错误或纰漏,欢迎大家在评论区留言指正!