SpringBoot3 整合 Elasticsearch

发布于:2025-04-05 ⋅ 阅读:(15) ⋅ 点赞:(0)

(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;
}