一、安装Elasticsearch相关插件
为了避免使用的Elasticsearch版本和SpringBoot采用的版本不一致导致的问题,尽量使用一致的版本。下表是对应关系:
安装Elasticsearch
Elasticsearch官网,点击跳转即可
1、下载上面链接的安装包
2、解压到任意目录
3、启动es /bin/elasticsearch.bat
4、查看安装结果,在网页输入localhost:9200,出现下图即为成功
接下来我们要通过修改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,出现如下截图,则说明安装成功
在node安装路径下,使用命令安装:npm install -g grunt-cli 安装grunt。 安装结束后,使用命令grunt-version查看是否安装成功,出现如下截图,说明安装成功。
es-head下载地址
解压elasticsearch-head-master
在该目录下进入cmd命令,执行npm install
执行完成后运行命令 grunt server,
grunt server是启动命令
如果出现报错
则使用 cmd继续执行npm install grunt --save-dev。 这将向package.json添加最新版本。
如果不报错,则命令窗显示
浏览器输入127.0.0.1:9100
安装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
如果想使用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内。
- 测试ik分词器
- 重启elasticsearch
- 重启kibana
- 进入kibana的开发工具中执行命令测试 开发工具
ik_max_word
和 ik_smart
是 IK 分词器的两种分词模式,主要用于中文分词。以下是它们的区别:
-
ik_max_word
- 特点: 会将文本进行最细粒度的拆分,尽可能多地拆出词语。
- 适用场景: 当需要尽可能多的关键词匹配时使用,例如搜索引擎中的宽泛查询。
-
示例: 对于字符串“中华人民共和国国歌”,
ik_max_word
会拆分为:["中华人民共和国", "中华人民", "中华", "华人", "人民共和国", "人民", "共和国", "共和", "国", "国歌"]
。
image.png
-
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 数据库)。