(1)Windows 安装和使用 ElasticSearch
(2)【已解决】SpringBoot 整合 Spring Data Elasticsearch 启动报错 Bean 冲突
1.实现的后端接口
2.Maven依赖
<!-- Spring Data Elasticsearch 客户端 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
<!-- <version>${elasticsearch-client.version}</version> --> <!-- 使用默认的版本 -->
</dependency>
3.application.yml
spring:
elasticsearch:
uris: http://localhost:9200 # ES服务地址
# username: elastic
# password: xxx
4.操作 ES Index(索引)
4.1 Controller
package com.dragon.springboot3vue3.controller;
import cn.dev33.satoken.util.SaResult;
import com.dragon.springboot3vue3.service.ESIndexService;
import com.dragon.springboot3vue3.utils.StringDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
@Tag(name = "ES-Index 接口")
@RestController
@RequestMapping("/es/index")
public class ESIndexController {
@Autowired
private ESIndexService esIndexService;
@Operation(summary = "创建ES索引")
@PostMapping("/create")
public SaResult create(@RequestBody StringDTO stringDTO) {
String str = String.valueOf(esIndexService.create(stringDTO.getStr()));
return SaResult.ok(str);
}
@Operation(summary = "删除ES索引")
@DeleteMapping("/delete")
public SaResult delete(@RequestBody StringDTO stringDTO) {
String str = String.valueOf(esIndexService.delete(stringDTO.getStr()));
return SaResult.ok(str);
}
@Operation(summary = "ES索引是否存在")
@PostMapping("/exist")
public SaResult exist(@RequestBody StringDTO stringDTO) {
String str = String.valueOf(esIndexService.exist(stringDTO.getStr()));
return SaResult.ok(str);
}
@Operation(summary = "根据索引名,获取ES索引详细信息")
@PostMapping("/get")
public SaResult get(@RequestBody StringDTO stringDTO) {
return esIndexService.get(stringDTO.getStr());
}
@Operation(summary = "ES索引列表")
@GetMapping("/getAll")
public SaResult getAll() throws IOException {
return esIndexService.getAll();
}
}
4.2 Service
package com.dragon.springboot3vue3.service;
import cn.dev33.satoken.util.SaResult;
import java.io.IOException;
public interface ESIndexService {
boolean create(String indexNme);
boolean delete(String indexNme);
boolean exist(String indexName);
SaResult getAll() throws IOException;
SaResult get(String indexName);
}
4.3 ServiceImpl
package com.dragon.springboot3vue3.service.impl;
import cn.dev33.satoken.util.SaResult;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.indices.GetIndexResponse;
import com.dragon.springboot3vue3.service.ESIndexService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.IndexOperations;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class ESIndexServiceImpl implements ESIndexService {
@Autowired
private ElasticsearchOperations elasticsearchOperations;
@Autowired
private ElasticsearchClient elasticsearchClient;
@Override
public boolean create(String indexName) {
IndexOperations indexOps = elasticsearchOperations.indexOps(IndexCoordinates.of(indexName));
if (!indexOps.exists()) {
indexOps.create();
return true;
}
return false; // 索引已存在,无需创建
}
@Override
public boolean delete(String indexName) {
IndexOperations indexOps = elasticsearchOperations.indexOps(IndexCoordinates.of(indexName));
if (indexOps.exists()) {
indexOps.delete();
return true;
}
return false; // 索引不存在,无法删除
}
@Override
public boolean exist(String indexName) {
IndexOperations indexOps = elasticsearchOperations.indexOps(IndexCoordinates.of(indexName));
return indexOps.exists();
}
@Override
public SaResult getAll() throws IOException {
// * 匹配所有索引,-.* 排除以"."开头的
GetIndexResponse response = elasticsearchClient.indices().get(b -> b.index("*,-.*"));
// keySet() 提取 Map 中所有Key,并以Set返回
List<String> list = response.result().keySet().stream().toList();
// 遍历每个索引,将详细信息存入map
Map<String, Object> map = new HashMap<>();
for (String indexName : list) {
IndexOperations indexOps = elasticsearchOperations.indexOps(IndexCoordinates.of(indexName));
map.put(indexName, indexOps.getInformation().getFirst());
}
return SaResult.ok().setData(map);
}
@Override
public SaResult get(String indexName) {
IndexOperations indexOps = elasticsearchOperations.indexOps(IndexCoordinates.of(indexName));
return SaResult.ok().setData(indexOps.getInformation().getFirst());
}
}
5.操作 ES Document(文档)
5.1 Model
package com.dragon.springboot3vue3.es.model;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.*;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@Document(indexName = "articles") // 指定索引名称
//@Setting(shards = 3, replicas = 1) // 分片数和副本数
public class Articles implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Id // Elasticsearch文档ID
@Field(type = FieldType.Keyword) // ID通常设为keyword类型
private String id;
@Field(type = FieldType.Keyword) // 分类ID适合用keyword类型
private String categoryId;
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart") // 标题使用IK分词
private String title;
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart") // 内容使用IK分词
private String content;
@Field(type = FieldType.Keyword) // URL通常不需要分词
private String coverImg;
@Field(type = FieldType.Keyword) // 状态适合用keyword类型
private String status;
@Field(type = FieldType.Keyword) // 创建人ID适合用keyword类型
private String creatorId;
@Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second) // ES日期格式
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second) // ES日期格式
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime ts;
@Field(type = FieldType.Integer) // 逻辑删除(0:未删除,1:删除)
private Integer deleteFlag = 0;
}
5.2 Controller
package com.dragon.springboot3vue3.controller;
import cn.dev33.satoken.util.SaResult;
import com.dragon.springboot3vue3.controller.dto.pageDto.ArticlesPageDto;
import com.dragon.springboot3vue3.es.model.Articles;
import com.dragon.springboot3vue3.service.ArticlesService;
import com.dragon.springboot3vue3.utils.StringDTO;
import com.dragon.springboot3vue3.utils.StringsDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@Tag(name = "ES 文章接口")
@RestController
@RequestMapping("/es/articles")
public class ArticlesController {
@Autowired
private ArticlesService articlesService;
@Operation(summary = "新增或更新")
@PostMapping("/saveOrUpdate")
public SaResult saveOrUpdate(@RequestBody Articles articles) {
return articlesService.saveOrUpdate(articles);
}
@Operation(summary = "根据ID查询")
@PostMapping("/getById")
public SaResult getById(@RequestBody StringDTO stringDTO) {
return articlesService.getById(stringDTO.getStr());
}
@Operation(summary = "所有列表")
@GetMapping("/getAll")
public SaResult getAll() {
return articlesService.getAll();
}
@Operation(summary = "分页列表")
@PostMapping("/list")
public SaResult list(@RequestBody ArticlesPageDto pageDto) {
return articlesService.list(pageDto);
}
@Operation(summary = "全文搜索")
@PostMapping("/search")
public SaResult search(@RequestBody StringDTO stringDTO) {
return articlesService.search(stringDTO.getStr());
}
@Operation(summary = "删除")
@DeleteMapping("/delete")
public SaResult deleteArticle(@RequestBody StringsDTO stringsDTO) {
return articlesService.delete(stringsDTO.getStrings());
}
@Operation(summary = "逻辑删除")
@PutMapping("/logicalDelete")
public SaResult logicalDelete(@RequestBody StringDTO stringDTO) {
return articlesService.logicalDelete(stringDTO.getStr());
}
@Operation(summary = "逻辑删除列表")
@GetMapping("/logicalDeleteList")
public SaResult logicalDeleteList() {
return articlesService.logicalDeleteList();
}
}
5.3 Service
package com.dragon.springboot3vue3.service;
import cn.dev33.satoken.util.SaResult;
import com.dragon.springboot3vue3.controller.dto.pageDto.ArticlesPageDto;
import com.dragon.springboot3vue3.es.model.Articles;
import java.util.Collection;
public interface ArticlesService {
SaResult saveOrUpdate(Articles articles);
SaResult getById(String id);
SaResult search(String keyword);
SaResult getAll();
SaResult list(ArticlesPageDto pageDto);
SaResult logicalDelete(String id);
SaResult delete(Collection<String> ids);
SaResult logicalDeleteList();
}
5.4 ServiceImpl
package com.dragon.springboot3vue3.service.impl;
import cn.dev33.satoken.util.SaResult;
import com.dragon.springboot3vue3.controller.dto.pageDto.ArticlesPageDto;
import com.dragon.springboot3vue3.es.model.Articles;
import com.dragon.springboot3vue3.es.repository.ArticlesRepository;
import com.dragon.springboot3vue3.service.ArticlesService;
import com.dragon.springboot3vue3.utils.ESPageResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
public class ArticlesServiceImpl implements ArticlesService {
@Autowired
private ArticlesRepository articlesRepository;
@Autowired
private ElasticsearchOperations elasticsearchOperations;
@Override
public SaResult saveOrUpdate(Articles articles) {
if (articles.getId() == null) {
articles.setCreateTime(LocalDateTime.now());
}
articles.setTs(LocalDateTime.now());
articlesRepository.save(articles);
return SaResult.ok();
}
@Override
public SaResult getById(String id) {
return SaResult.ok().setData(articlesRepository.findById(id));
}
@Override
public SaResult search(String keyword) {
return SaResult.ok().setData(articlesRepository.findByContentContaining(keyword));
}
@Override
public SaResult getAll() {
List<Articles> list = new ArrayList<>();
articlesRepository.findAll().forEach(item->{
if(item.getDeleteFlag()==0){
list.add(item);
}
});
return SaResult.ok().setData(list);
}
@Override
public SaResult list(ArticlesPageDto pageDto) {
// 1. 构建查询条件
Criteria criteria = new Criteria("deleteFlag").is(0);
// 标题查询
// ES 版本不够,不能使用 Articles::getTitle
if (StringUtils.isNotBlank(pageDto.getTitle())) {
criteria.and(new Criteria("title").contains(pageDto.getTitle()));
}
// 内容查询
if (StringUtils.isNotBlank(pageDto.getContent())) {
criteria.and(new Criteria("content").contains(pageDto.getContent()));
}
// 日期范围查询
if (StringUtils.isNotBlank(pageDto.getStartDate()) && StringUtils.isNotBlank(pageDto.getEndDate())) {
LocalDateTime start = LocalDateTime.parse(pageDto.getStartDate(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
LocalDateTime end = LocalDateTime.parse(pageDto.getEndDate(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
criteria.and(new Criteria("createTime").between(start, end));
}
// 2. 排序字段
Sort sort = Sort.by(pageDto.getSortField());
sort = "asc".equalsIgnoreCase(pageDto.getSortOrder()) ? sort.ascending() : sort.descending();
// 3. 构建查询
Query query = new CriteriaQuery(criteria)
.setPageable(PageRequest.of(pageDto.getPage(), pageDto.getSize()))
.addSort(sort);
// 4. 执行查询
SearchHits<Articles> searchHits = elasticsearchOperations.search(query, Articles.class);
// 5. 处理结果
List<Articles> articles = searchHits.stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
ESPageResponse response = new ESPageResponse(
articles,
searchHits.getTotalHits(),
pageDto.getPage(),
pageDto.getSize(),
(int) Math.ceil((double) searchHits.getTotalHits() / pageDto.getSize()));
return SaResult.ok().setData(response);
}
@Override
public SaResult logicalDelete(String id) {
// 1. 根据ID查找文章
Optional<Articles> articlesOptional = articlesRepository.findById(id);
// 2. 检查文章是否存在
if (!articlesOptional.isPresent()) {
return SaResult.error("文章不存在");
}
// 3. 获取文章对象
Articles article = articlesOptional.get();
// 4. 检查是否已被删除(避免重复操作)
if (article.getDeleteFlag() == 1) {
return SaResult.error("文章已被删除");
}
article.setDeleteFlag(1);
articlesRepository.save(article);
return SaResult.ok();
}
@Override
public SaResult delete(Collection<String> ids) {
articlesRepository.deleteAllById(ids);
return SaResult.ok();
}
@Override
public SaResult logicalDeleteList() {
List<Articles> list = new ArrayList<>();
articlesRepository.findAll().forEach(item->{
if(item.getDeleteFlag()==1){
list.add(item);
}
});
return SaResult.ok().setData(list);
}
}
5.5 Repository
package com.dragon.springboot3vue3.es.repository;
import com.dragon.springboot3vue3.es.model.Articles;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ArticlesRepository extends ElasticsearchRepository<Articles, String> {
List<Articles> findByContentContaining(String keyword);
}
5.6 其他
package com.dragon.springboot3vue3.utils;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.List;
@Schema(description = "ES分页响应数据")
@Data
@AllArgsConstructor
public class ESPageResponse<T> {
private List<T> data; // 当前页数据
private long total; // 总记录数
private int currentPage; // 当前页码
private int pageSize; // 每页大小
private int pages; // 总页数
}
package com.dragon.springboot3vue3.utils;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
@Data
public class StringDTO {
@NotEmpty
@Schema(description = "字符串")
private String str;
}
package com.dragon.springboot3vue3.utils;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.util.Collection;
@Data
public class StringsDTO {
@NotEmpty
@Schema(description = "字符串数组")
private Collection<String> strings;
}