springboot整合ElasticSearch详细

一、安装Elasticsearch相关插件

为了避免使用的Elasticsearch版本和SpringBoot采用的版本不一致导致的问题,尽量使用一致的版本。下表是对应关系:

image.png
安装Elasticsearch

Elasticsearch官网,点击跳转即可
1、下载上面链接的安装包
2、解压到任意目录
3、启动es /bin/elasticsearch.bat
4、查看安装结果,在网页输入localhost:9200,出现下图即为成功

image.png

接下来我们要通过修改Elasticsearch安装目录下的/config/elasticsearch.yml处理一些问题

解决IP无法进行访问

#增加IP访问配置
network.bind_host: 0.0.0.0
#注释此配置
#cluster.initial_master_nodes: ["node-1", "node-2"]
#修改为单节点配置
cluster.initial_master_nodes: ["node-1"]

解决跨域问题
由于前后端分离开发,所以会存在跨域问题,需要在服务端做CORS的配置。

#开启跨域支持
http.cors.enabled: true
#允许所有人跨域访问
http.cors.allow-origin: "*"
#浏览器允许在跨域请求中发送凭证(如 cookies、HTTP 认证信息等)
http.cors.allow-credentials: true
#允许特定的请求头
http.cors.allow-headers: Authorization,X-Requested-With,Content-Length,Content-Type

开启集群身份认证与用户鉴权和集群内部安全通信

# 集群身份认证与用户鉴权
xpack.security.enabled: true
# 集群内部安全通信
xpack.license.self_generated.type: basic
xpack.security.transport.ssl.enabled: true
xpack.security.transport.ssl.verification_mode: certificate
xpack.security.transport.ssl.keystore.path: D:\elasticsearch\elasticsearch-7.6.2\config\elastic-stack-ca.p12
xpack.security.transport.ssl.truststore.path: D:\elasticsearch\elasticsearch-7.6.2\config\elastic-stack-ca.p12

重启Elasticsearch服务以使更改生效

Elasticsearch 的默认账户为 elastic 默认密码为 changme
一、通过Kibana更改密码
1、登录到Kibana。
2、导航到“Management”(管理)> “Security”(安全)> “Users”(用户)。
3、找到你想要修改密码的用户,点击“Edit”(编辑)。
4、在“Change Password”(更改密码)部分,输入新密码,然后点击“Update”(更新)。

二、通过Elasticsearch REST API更改密码

curl -u elastic:your_current_password -X POST "localhost:9200/_security/user/username/_password" -H 'Content-Type: application/json' -d'
{
  "password" : "new_password"
}

替换your_current_password、username和new_password为实际的当前密码、用户名和新密码。

安装es-head插件,方便查看ES中的索引及数据

安装es-head插件需要把node和grunt配置好
node下载地址下载对应环境的node版本安装即可。
安装过程结束后,在dos窗口查看是否安装成功,使用命令:node -v,出现如下截图,则说明安装成功

image.png

在node安装路径下,使用命令安装:npm install -g grunt-cli 安装grunt。 安装结束后,使用命令grunt-version查看是否安装成功,出现如下截图,说明安装成功。
image.png

es-head下载地址
解压elasticsearch-head-master
在该目录下进入cmd命令,执行npm install
image.png

执行完成后运行命令 grunt server,
grunt server是启动命令
如果出现报错
image.png

则使用 cmd继续执行npm install grunt --save-dev。 这将向package.json添加最新版本。
image.png

如果不报错,则命令窗显示
image.png

浏览器输入127.0.0.1:9100
image.png

安装kibana,用途:便于通过rest api调试ES。

kibana官方下载地址
1、解压
2、修改 kibana-7.12.0-windows-x86_64/config/kibana.yml 32行
3、改为elasticsearch.hosts: [“http://127.0.0.1:9200”]
4、保存之后,运行bin/kibana.bat
5、浏览器中访问kibana开发工具:http://localhost:5601/app/kibana#/dev_tools/console

image.png

如果想使用ip访问kibana,需要修改 kibana-7.12.0-windows-x86_64/config/kibana.yml 7行
改为 server.host: "0.0.0.0"
如果想使用kibana汉化 需要修改 kibana-7.12.0-windows-x86_64/config/kibana.yml 最后一行
i18n.locale: "zh-CN"

安装ik分词器,注意:下载的ik分词器版本号要和安装的elasticsearch版本一致

把下载的ik分词器解压至Elasticsearch的安装目录/plugins/ik内。


image.png
image.png
  1. 测试ik分词器
  2. 重启elasticsearch
  3. 重启kibana
  4. 进入kibana的开发工具中执行命令测试 开发工具

ik_max_wordik_smart 是 IK 分词器的两种分词模式,主要用于中文分词。以下是它们的区别:

  1. ik_max_word

    • 特点: 会将文本进行最细粒度的拆分,尽可能多地拆出词语。
    • 适用场景: 当需要尽可能多的关键词匹配时使用,例如搜索引擎中的宽泛查询。
    • 示例: 对于字符串“中华人民共和国国歌”,ik_max_word 会拆分为:["中华人民共和国", "中华人民", "中华", "华人", "人民共和国", "人民", "共和国", "共和", "国", "国歌"]
      image.png
  2. ik_smart

    • 特点: 会以更智能的方式对文本进行拆分,尽量减少冗余词语,保留语义完整。
    • 适用场景: 当需要更精确的分词结果时使用,适合对查询性能要求较高的场景。
    • 示例: 对于字符串“中华人民共和国国歌”,ik_smart 会拆分为:["中华人民共和国", "国歌"]
      image.png

我个人建议是:

  • analyzer = "ik_max_word" 表示在索引阶段使用 ik_max_word 模式进行分词。
  • searchAnalyzer = "ik_smart" 表示在搜索阶段使用 ik_smart 模式进行分词。
    这种配置可以让索引阶段尽可能多地提取关键词,而在搜索阶段提供更精准的匹配结果。

二、整合SpringBoot和Elasticearch

引入依赖

 <!-- Spring Boot 对 Elasticsearch 的启动器,提供在 Spring Boot 应用中方便地使用 Elasticsearch 的方式。 -->
        <dependency>
            <groupId>org.elasticsearch</groupId>
            <artifactId>elasticsearch</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>

yml文件增加配置

spring:
  # elasticsearch配置
  elasticsearch:
    rest:
      # 用户名
      username: elastic
      # 密码
      password: 123456
      # 服务器地址,支持多个uri,用','隔开
      uris: localhost:9200
      # 连接超时时间
      connection-timeout: 10s
      # 读取超时时间
      read-timeout: 30s
      # 最大连接数
      max-connect-requests: 10
      # 空闲连接的最大存活时间
      idle-timeout: 60s
      # 是否启用自动重试
      retry-on-failure: true
      # 重试次数
      max-retry-timeout: 3

BaseEntity实体类

package com.point.pack.entity.base;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.point.pack.constant.DateFormatConstant;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.DateFormat;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.format.annotation.DateTimeFormat;

import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * Entity基类,提供了实体对象的基本属性,如主键、创建时间和更新时间。
 * 这是一个抽象类,用于定义所有实体对象共有的属性和行为。
 *
 * @param <ID> 实体的主键类型,继承自Serializable,允许不同的实体使用不同的主键类型。
 * @return 无返回值
 * @date 2020.03.27
 */
@Data
public abstract class BaseEntity<ID extends Serializable> implements Serializable {

    @Id
    @ApiModelProperty(value = "主键")
    // MyBatis-Plus 注解,适用于 MySQL
    @TableId(type = IdType.ASSIGN_ID)
    // Spring Data Elasticsearch 注解,适用于 ES
    @Field(name = BaseIndexField.ID, type = FieldType.Keyword)
    @JsonSerialize(using = ToStringSerializer.class)
    private ID id;

    @ApiModelProperty(value = "创建时间")
    // MyBatis-Plus 注解,适用于 MySQL
    @TableField(fill = FieldFill.INSERT)
    // Spring Data Elasticsearch 注解,适用于 ES
    @Field(name = BaseIndexField.CREATE_TIME, type = FieldType.Date, format = DateFormat.date_time)
    @DateTimeFormat(pattern = DateFormatConstant.DEFAULT_DATETIME_FORMAT)
    @JsonFormat(pattern = DateFormatConstant.DEFAULT_DATETIME_FORMAT, timezone = "GMT+8")
    private LocalDateTime createTime;

    @ApiModelProperty(value = "更新时间")
    // MyBatis-Plus 注解,适用于 MySQL
    @TableField(fill = FieldFill.INSERT_UPDATE)
    // Spring Data Elasticsearch 注解,适用于 ES
    @Field(name = BaseIndexField.UPDATE_TIME, type = FieldType.Date, format = DateFormat.date_time)
    @DateTimeFormat(pattern = DateFormatConstant.DEFAULT_DATETIME_FORMAT)
    @JsonFormat(pattern = DateFormatConstant.DEFAULT_DATETIME_FORMAT, timezone = "GMT+8")
    private LocalDateTime updateTime;
}

FileInfo实体类

package com.point.pack.entity.common;

import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.point.pack.entity.base.BaseEntity;
import com.point.pack.entity.es.FileInfoIndexField;
import com.point.pack.enums.StorageType;
import com.point.pack.enums.YesNo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = FileInfoIndexField.INDEX_NAME)
@ApiModel(value = "FileInfo", description = "文件表")
public class FileInfo extends BaseEntity<Long> {
    private static final long serialVersionUID = 1L; // 序列化ID,用于版本控制

    @ApiModelProperty(value = "文件名称")
    @Field(name = FileInfoIndexField.FILE_NAME, analyzer = "ik_max_word", searchAnalyzer = "ik_smart", type = FieldType.Text)
    private String fileName;

    @ApiModelProperty(value = "文件字节大小")
    @Field(name = FileInfoIndexField.FILE_SIZE, type = FieldType.Long)
    @JsonSerialize(using = ToStringSerializer.class)
    private Long fileSize;

    @ApiModelProperty(value = "文件类型,如image/jpeg")
    @Field(name = FileInfoIndexField.FILE_TYPE, type = FieldType.Keyword)
    private String fileType;

    @ApiModelProperty(value = "文件存储路径")
    @Field(name = FileInfoIndexField.FILE_PATH, type = FieldType.Keyword)
    private String fileSavePath;

    @ApiModelProperty(value = "文件访问路径")
    @Field(name = FileInfoIndexField.FILE_URL, type = FieldType.Keyword)
    private String fileAccessUrl;

    @ApiModelProperty(value = "存储类型")
    @Field(name = FileInfoIndexField.STORAGE_TYPE, type = FieldType.Keyword)
    private StorageType storageType;

    @ApiModelProperty(value = "备注")
    @Field(name = FileInfoIndexField.REMARK, type = FieldType.Keyword)
    private String remark;

    @ApiModelProperty(value = "是否已删除")
    @Field(name = FileInfoIndexField.IS_DELETE, type = FieldType.Keyword)
    private Integer isDelete = YesNo.NO.getValue();

}

FileInfoRepository类,这个非常重要
Spring Data Elasticsearch只有在应用程序启动时,带有索引实体的ElasticsearchRepository发现该索引不存在时,才会自动写入该索引的映射。
否则,不会对索引自动执行任何操作。映射不会被重写。因此,如果向实体添加某些属性,即使实体上有注释,也不会创建新映射。
您可以做的是,在添加属性之后,在使用这个新属性插入新数据之前,使用IndexOperations.putMapping()方法编写更新的映射。

  • 重要提示:只能向索引添加新属性/字段,不能删除或更新现有属性/字段;这是Elasticsearch的限制。
package com.point.user.service.repository;

import com.point.pack.entity.common.FileInfo;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface FileInfoRepository extends ElasticsearchRepository<FileInfo, Long> {

}

FileInfoController类

package com.point.user.controller.web;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.point.common.annotation.ResponseResult;
import com.point.pack.entity.common.FileInfo;
import com.point.pack.exception.CustomException;
import com.point.user.dto.IdDTO;
import com.point.user.dto.IdsDTO;
import com.point.user.dto.fileInfo.FileInfoAddDTO;
import com.point.user.dto.fileInfo.FileInfoQueryPageDTO;
import com.point.user.dto.fileInfo.FileInfoUpdateDTO;
import com.point.user.service.FileInfoService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@ResponseResult
@RequestMapping("/web/fileInfo")
@Api(value = "文件模块相关接口", tags = "文件模块相关接口")
public class FileInfoController {

    @Resource
    private FileInfoService fileInfoService;

    @PostMapping("/queryPage")
    @ApiOperation(value = "分页查询文件", notes = "分页查询文件")
    public Page<FileInfo> queryPage(@Validated @RequestBody FileInfoQueryPageDTO condition) throws CustomException {
        return fileInfoService.queryPage(condition);
    }

    @PostMapping("/add")
    @ApiOperation(value = "添加文件", notes = "添加文件")
    public void add(@Validated @RequestBody FileInfoAddDTO condition) throws CustomException {
        fileInfoService.add(condition);
    }

    @PostMapping("/detail")
    @ApiOperation(value = "查询文件详情", notes = "查询文件详情")
    public FileInfo detail(@Validated @RequestBody IdDTO condition) throws CustomException {
        return fileInfoService.detail(condition);
    }

    @PostMapping("/update")
    @ApiOperation(value = "编辑文件", notes = "编辑文件")
    public void update(@Validated @RequestBody FileInfoUpdateDTO condition) throws CustomException {
        fileInfoService.update(condition);
    }

    @PostMapping("/deleteByIds")
    @ApiOperation(value = "批量删除文件", notes = "批量删除文件")
    public void deleteByIdList(@Validated @RequestBody IdsDTO condition) throws CustomException {
        fileInfoService.deleteByIdList(condition);
    }
}

FileInfoService类

package com.point.user.service;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.point.pack.entity.common.FileInfo;
import com.point.pack.exception.CustomException;
import com.point.user.dto.IdDTO;
import com.point.user.dto.IdsDTO;
import com.point.user.dto.fileInfo.FileInfoAddDTO;
import com.point.user.dto.fileInfo.FileInfoQueryPageDTO;
import com.point.user.dto.fileInfo.FileInfoUpdateDTO;

public interface FileInfoService {

    /**
     * 分页查询文件
     *
     * @param condition 分页查询条件,包含需要查询的文件和分页参数
     * @return 返回查询到的文件分页对象
     * @throws CustomException 当查询过程中发生错误时抛出自定义异常
     */
    Page<FileInfo> queryPage(FileInfoQueryPageDTO condition) throws CustomException;

    /**
     * 添加文件
     *
     * @param condition 添加文件的DTO,包含所需信息
     * @throws CustomException 添加过程中可能出现的自定义异常
     */
    Long add(FileInfoAddDTO condition) throws CustomException;

    /**
     * 查询文件详情
     *
     * @param condition 文件详情查询条件,包含用于筛选查询结果的具体参数
     * @return 返回一个FileInfo对象,该对象包含查询到的文件详情
     */
    FileInfo detail(IdDTO condition) throws CustomException;

    /**
     * 编辑文件
     *
     * @param condition 更新文件的DTO,包含所需信息
     * @throws CustomException 更新过程中可能出现的自定义异常
     */
    void update(FileInfoUpdateDTO condition) throws CustomException;

    /**
     * 批量删除文件
     *
     * @param condition 包含待删除文件项ID列表的DTO对象,用于指定删除条件
     * @throws CustomException 当删除操作失败时抛出自定义异常,以便调用者处理错误情况
     */
    void deleteByIdList(IdsDTO condition) throws CustomException;
}

FileInfoServiceImpl

package com.point.user.service.impl;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.point.pack.entity.common.FileInfo;
import com.point.pack.entity.es.FileInfoIndexField;
import com.point.pack.enums.YesNo;
import com.point.pack.exception.CustomException;
import com.point.user.dto.IdDTO;
import com.point.user.dto.IdsDTO;
import com.point.user.dto.fileInfo.FileInfoAddDTO;
import com.point.user.dto.fileInfo.FileInfoQueryPageDTO;
import com.point.user.dto.fileInfo.FileInfoUpdateDTO;
import com.point.user.service.FileInfoService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.index.query.IdsQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;

@Slf4j
@Service
public class FileInfoServiceImpl implements FileInfoService {

    @Resource
    private ElasticsearchRestTemplate elasticsearchRestTemplate;

    /**
     * 分页查询文件
     *
     * @param condition 分页查询条件,包含需要查询的文件和分页参数
     * @return 返回查询到的文件分页对象
     * @throws CustomException 当查询过程中发生错误时抛出自定义异常
     */
    @Override
    public Page<FileInfo> queryPage(FileInfoQueryPageDTO condition) throws CustomException {
        // 构造查询条件
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        queryBuilder.withFilter(QueryBuilders.termQuery(FileInfoIndexField.IS_DELETE, YesNo.NO.getValue()));
        if (StringUtils.isNotBlank(condition.getFileName())) {
            queryBuilder.withFilter(QueryBuilders.matchQuery(FileInfoIndexField.FILE_NAME, condition.getFileName()));
        }
        if (StringUtils.isNotBlank(condition.getFileType())) {
            queryBuilder.withFilter(QueryBuilders.matchQuery(FileInfoIndexField.FILE_TYPE, condition.getFileType()));
        }
        if (condition.getStorageType() != null) {
            queryBuilder.withFilter(QueryBuilders.matchQuery(FileInfoIndexField.STORAGE_TYPE, condition.getStorageType()));
        }

        // 设置分页
        Page<FileInfo> page = condition.getPage();
        int current = (int) (page.getCurrent() - 1); // 显式转换为int
        int size = (int) page.getSize(); // 显式转换为int
        Pageable pageable = PageRequest.of(current, size);
        queryBuilder.withPageable(pageable);
        // 设置排序
        queryBuilder.withSort(SortBuilders.fieldSort(FileInfoIndexField.ID).order(SortOrder.DESC));
        NativeSearchQuery query = queryBuilder.build();
        // 执行查询
        SearchHits<FileInfo> searchHits = elasticsearchRestTemplate.search(query, FileInfo.class);
        //设置总数
        page.setTotal(searchHits.getTotalHits());
        // 返回的行数
        List<SearchHit<FileInfo>> searchHitList = searchHits.getSearchHits();
        if (CollectionUtils.isNotEmpty(searchHitList)) {
            List<FileInfo> records = new ArrayList<>();
            for (SearchHit<FileInfo> searchHit : searchHits) {
                FileInfo fileInfo = searchHit.getContent();
                records.add(fileInfo);
            }
            page.setRecords(records);
        }

        return page;
    }

    /**
     * 添加文件
     *
     * @param condition 包含新文件的数据传输对象
     * @throws CustomException 如果添加过程中出现异常
     */
    @Override
    public Long add(FileInfoAddDTO condition) throws CustomException {
        FileInfo fileInfo = new FileInfo();
        fileInfo.setId(Math.abs(UUID.randomUUID().getMostSignificantBits()));
        BeanUtils.copyProperties(condition, fileInfo);
        elasticsearchRestTemplate.save(fileInfo);
        return fileInfo.getId();
    }

    /**
     * 查询文件详情
     *
     * @param condition 文件详情查询条件,包含用于筛选查询结果的具体参数
     * @return 返回一个FileInfo对象,该对象包含查询到的文件详情
     */
    @Override
    public FileInfo detail(IdDTO condition) throws CustomException {
        // 根据ID从Elasticsearch中查询文件
        String id = String.valueOf(condition.getId());
        FileInfo fileInfo = elasticsearchRestTemplate.get(id, FileInfo.class);
        if (fileInfo == null) {
            throw new CustomException("文件不存在");
        }

        if (Objects.equals(fileInfo.getIsDelete(), YesNo.YES.getValue())) {
            throw new CustomException("文件已删除");
        }

        return fileInfo;
    }

    /**
     * 编辑文件
     *
     * @param condition 更新文件的DTO,包含所需信息
     * @throws CustomException 更新过程中可能出现的自定义异常
     */
    @Override
    public void update(FileInfoUpdateDTO condition) throws CustomException {
        // 获取要编辑的文件
        FileInfo updateFileInfo = elasticsearchRestTemplate.get(String.valueOf(condition.getId()), FileInfo.class);
        if (updateFileInfo == null) {
            throw new CustomException("文件不存在");
        }

        if (!Objects.equals(condition.getFileName(), updateFileInfo.getFileName()) || !Objects.equals(condition.getRemark(), updateFileInfo.getRemark())) {
            // 将更新后的信息复制到原字典类型对象并保存
            BeanUtils.copyProperties(condition, updateFileInfo);
            elasticsearchRestTemplate.save(updateFileInfo);
        }
    }

    /**
     * 批量删除文件
     *
     * @param condition 包含待删除文件项ID列表的DTO对象,用于指定删除条件
     * @throws CustomException 当删除操作失败时抛出自定义异常,以便调用者处理错误情况
     */
    @Override
    public void deleteByIdList(IdsDTO condition) throws CustomException {
        // 构造查询条件
        IdsQueryBuilder idsQueryBuilder = QueryBuilders.idsQuery();
        for (Long id : condition.getIds()) {
            idsQueryBuilder.addIds(String.valueOf(id));
        }

        NativeSearchQuery query = new NativeSearchQueryBuilder()
                .withQuery(idsQueryBuilder)
                .build();

        // 执行查询
        SearchHits<FileInfo> searchHits = elasticsearchRestTemplate.search(query, FileInfo.class);
        List<FileInfo> fileInfos = new ArrayList<>();
        for (SearchHit<FileInfo> searchHit : searchHits) {
            fileInfos.add(searchHit.getContent());
        }

        if (CollectionUtils.isEmpty(fileInfos)) {
            return;
        }

        for (FileInfo fileInfo : fileInfos) {
            fileInfo.setIsDelete(YesNo.YES.getValue());
        }
        elasticsearchRestTemplate.save(fileInfos);
    }
}
Elasticsearch(ES)虽然是一款强大的分布式搜索和分析引擎,但它并不是为所有类型的数据存储和管理场景设计的。以下是Elasticsearch不适合做数据存储的一些原因:
1、事务支持不足

Elasticsearch 不支持传统关系型数据库中的 ACID 事务(Atomicity, Consistency, Isolation, Durability)。
它更适合最终一致性模型,而不是强一致性需求的场景。

2、高存储成本

Elasticsearch 的倒排索引和分片机制会导致数据占用更多的存储空间。
对于大规模写入和存储密集型的应用,存储成本会显著增加。

3、复杂查询性能问题

虽然 ES 在全文搜索和聚合查询上表现优异,但对于复杂的多表联结(JOIN)或深度嵌套查询,性能较差。
这些操作可能会导致资源消耗过高,影响整体系统稳定性。

4、数据更新开销大

Elasticsearch 是基于 LSM 树(Log-Structured Merge Tree)的实现,频繁的更新操作会导致段合并(Segment Merging)压力增大。
段合并会影响写入性能,并可能引发磁盘 I/O 瓶颈。

5、缺乏严格的权限控制

默认情况下,Elasticsearch 的安全性和权限控制功能较弱(需要额外购买 X-Pack 插件)。
对于敏感数据存储,可能存在安全隐患。

6、不适用于结构化数据

如果数据是高度结构化的(如金融交易记录、用户信息等),关系型数据库(如 MySQL、PostgreSQL)更为合适。
ES 更适合非结构化或半结构化数据(如日志、文档、媒体元数据)。

7、实时性限制

Elasticsearch 的近实时(Near Real-Time, NRT)特性意味着数据从写入到可搜索之间存在延迟(通常为秒级)。
对于要求毫秒级实时性的场景,ES 可能无法满足需求。

8、维护复杂度高

集群管理和调优需要较高的技术门槛,包括分片分配、副本管理、内存优化等。
对于小型团队或资源有限的项目,维护成本较高。

总结

Elasticsearch 最适合用于全文搜索、日志分析、监控数据处理等场景,而对于需要强一致性、复杂事务、严格权限控制或低成本存储的场景,建议选择其他更合适的数据库解决方案(如关系型数据库或专门的 NoSQL 数据库)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容