Elasticsearch:从入门到生产落地
一、什么是 Elasticsearch?为什么我们需要它?
Elasticsearch(简称 ES)是一个开源的、分布式的、RESTful 风格的搜索引擎和数据分析引擎。它基于 Lucene 库构建,提供了简单易用的 API,隐藏了 Lucene 的复杂性。
ES 的核心特点
- 分布式:自动分片、自动负载均衡、自动故障转移
- 近实时:数据写入后 1 秒内即可被搜索到
- 全文检索:支持复杂的全文搜索、模糊搜索、高亮显示
- 数据分析:支持强大的聚合查询,能快速进行数据统计和分析
- 高可用:支持主从复制,单个节点故障不影响服务
- 可扩展:可以轻松扩展到上百个节点,处理 PB 级别的数据
为什么不用 MySQL 做搜索?
很多人会问:MySQL 也有 like 查询,为什么还要用 ES?
答案很简单:MySQL 的 like 查询在大数据量下性能极差,而且不支持全文检索和复杂的数据分析。
- MySQL 的 like '% 关键词 %' 查询会走全表扫描,百万级数据就会明显变慢
- MySQL 不支持分词,只能做简单的字符串匹配
- MySQL 不支持复杂的相关性排序
- MySQL 不支持聚合分析和统计
而 ES 就是为了解决这些问题而生的。它专门针对搜索和分析进行了优化,能在毫秒级返回千万级数据的查询结果。
二、ES 的核心应用场景
ES 的应用场景非常广泛,我总结了最常用的 8 个场景:
- 全文检索:商品搜索、文章搜索、用户搜索
- 日志分析:ELK(Elasticsearch+Logstash+Kibana)日志收集分析系统
- 指标监控:系统指标、业务指标的实时监控和告警
- 数据分析:用户行为分析、销售数据分析、运营数据分析
- 地理信息搜索:附近的人、附近的商家、地理位置查询
- 自动补全:搜索框的输入提示、自动补全功能
- 拼写纠错:搜索时的拼写检查和纠错
- 数据可视化:配合 Kibana 制作各种数据仪表盘
三、ES 的核心概念
要真正用好 ES,必须先搞懂它的核心概念。很多人学 ES 觉得难,就是因为这些概念和关系型数据库不一样。
我做了一个 ES 和 MySQL 的概念对比表,帮助你快速理解:
表格
| Elasticsearch | MySQL |
|---|---|
| Index(索引) | Database(数据库) |
| Document(文档) | Row(行) |
| Field(字段) | Column(列) |
| Mapping(映射) | Table Schema(表结构) |
| Query DSL | SQL |
| Shard(分片) | 分表 |
| Replica(副本) | 主从复制 |
核心概念详解
- Index(索引):ES 中存储数据的地方,相当于 MySQL 的数据库。一个索引就是一个逻辑上的数据集合。
- Document(文档):ES 中的最小数据单元,相当于 MySQL 的一行记录。文档以 JSON 格式存储。
- Field(字段):文档中的属性,相当于 MySQL 的列。每个字段都有自己的数据类型。
- Mapping(映射):定义索引中字段的数据类型、分词器、索引方式等,相当于 MySQL 的表结构。
- Shard(分片):将一个大的索引分成多个小的分片,分布在不同的节点上,实现水平扩展。
- Replica(副本):分片的备份,用于提高数据的可用性和查询性能。
四、Spring Boot 集成 ES 完整教程
下面我以 Spring Boot 2.7.x 为例,教你如何快速集成和使用 Elasticsearch。
第一步:引入依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
第二步:配置 ES 连接
yaml
spring:
elasticsearch:
rest:
uris: http://localhost:9200
username: elastic
password: 123456
connection-timeout: 5s
read-timeout: 30s
第三步:定义实体类和 Mapping
java
运行
@Data
@Document(indexName = "product", shards = 3, replicas = 1)
public class Product {
@Id
private Long id;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String name;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String description;
@Field(type = FieldType.Double)
private Double price;
@Field(type = FieldType.Integer)
private Integer stock;
@Field(type = FieldType.Keyword)
private String category;
@Field(type = FieldType.Date, format = DateFormat.date_time)
private Date createTime;
}
第四步:定义 Repository 接口
java
运行
public interface ProductRepository extends ElasticsearchRepository<Product, Long> {
// 根据名称搜索
List<Product> findByName(String name);
// 根据名称和描述搜索
List<Product> findByNameOrDescription(String name, String description);
// 根据价格区间查询
List<Product> findByPriceBetween(Double minPrice, Double maxPrice);
}
第五步:使用 Repository 操作 ES
java
运行
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
// 保存商品
public void saveProduct(Product product) {
productRepository.save(product);
}
// 根据ID查询
public Product findById(Long id) {
return productRepository.findById(id).orElse(null);
}
// 搜索商品
public List<Product> search(String keyword) {
return productRepository.findByNameOrDescription(keyword, keyword);
}
// 删除商品
public void deleteById(Long id) {
productRepository.deleteById(id);
}
}
第六步:复杂查询(Query DSL)
对于复杂的查询,我们需要使用 ElasticsearchRestTemplate 来编写 Query DSL:
java
运行
@Service
public class ProductSearchService {
@Autowired
private ElasticsearchRestTemplate elasticsearchTemplate;
public Page<Product> searchProducts(String keyword, String category,
Double minPrice, Double maxPrice,
int pageNum, int pageSize) {
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 构建查询条件
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 关键词搜索
if (StringUtils.hasText(keyword)) {
boolQuery.must(QueryBuilders.multiMatchQuery(keyword, "name", "description")
.type(MultiMatchQueryBuilder.Type.BEST_FIELDS)
.boost(3.0f));
}
// 分类过滤
if (StringUtils.hasText(category)) {
boolQuery.filter(QueryBuilders.termQuery("category", category));
}
// 价格区间过滤
if (minPrice != null && maxPrice != null) {
boolQuery.filter(QueryBuilders.rangeQuery("price").gte(minPrice).lte(maxPrice));
}
queryBuilder.withQuery(boolQuery);
// 排序:按价格升序
queryBuilder.withSort(SortBuilders.fieldSort("price").order(SortOrder.ASC));
// 分页
queryBuilder.withPageable(PageRequest.of(pageNum - 1, pageSize));
// 高亮显示
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("name").field("description");
highlightBuilder.preTags("<em style='color:red'>");
highlightBuilder.postTags("</em>");
queryBuilder.withHighlightBuilder(highlightBuilder);
// 执行查询
SearchHits<Product> searchHits = elasticsearchTemplate.search(
queryBuilder.build(), Product.class);
// 处理高亮结果
List<Product> products = searchHits.stream().map(hit -> {
Product product = hit.getContent();
// 替换高亮字段
if (hit.getHighlightFields().containsKey("name")) {
product.setName(hit.getHighlightFields().get("name").get(0));
}
if (hit.getHighlightFields().containsKey("description")) {
product.setDescription(hit.getHighlightFields().get("description").get(0));
}
return product;
}).collect(Collectors.toList());
return new PageImpl<>(products, PageRequest.of(pageNum - 1, pageSize),
searchHits.getTotalHits());
}
}
五、深入原理:ES 为什么这么快?
ES 的高性能是由多个关键技术共同决定的:
1. 倒排索引
这是 ES 最核心的数据结构。和关系型数据库的正排索引不同,倒排索引记录的是 "关键词 - 文档" 的映射关系。
举个例子:
- 文档 1:"我爱 Java"
- 文档 2:"我爱 Elasticsearch"
- 文档 3:"Java 是最好的编程语言"
倒排索引就是:
plaintext
我 → [1, 2]
爱 → [1, 2]
Java → [1, 3]
Elasticsearch → [2]
是 → [3]
最好的 → [3]
编程语言 → [3]
当你搜索 "Java" 时,ES 直接从倒排索引中找到包含 "Java" 的文档 1 和 3,速度非常快。
2. 分词器
ES 会将文本内容拆分成一个个关键词,然后建立倒排索引。分词器的质量直接决定了搜索结果的准确性。
ES 内置了多种分词器,最常用的中文分词器是 IK 分词器,它支持细粒度分词(ik_max_word)和粗粒度分词(ik_smart)两种模式。
3. 分布式架构
ES 天生就是分布式的,它将数据分成多个分片,分布在不同的节点上。查询时可以并行查询多个分片,然后合并结果,大大提高了查询速度。
4. 内存缓存
ES 将热点数据和索引缓存到内存中,查询时直接从内存获取,避免了磁盘 IO。
六、ES 常见问题与解决方案
在生产环境中使用 ES,最常见的问题有:
问题 1:深分页问题
问题描述:当使用 from+size 进行分页时,from 越大,查询越慢。当 from 超过 10000 时,ES 会直接报错。
原因:ES 需要从所有分片中查询前 from+size 条数据,然后在协调节点进行排序和合并,最后返回 size 条数据。from 越大,这个过程越耗时。
解决方案:
- 使用 search_after:适用于滚动加载场景,不支持跳页
- 使用 scroll:适用于批量导出数据,不适用于用户交互
- 限制分页深度:业务上限制用户只能翻到前 100 页
问题 2:索引设计不当导致查询慢
常见错误:
- 给不需要搜索的字段设置为 text 类型
- 使用默认的分词器处理中文
- 索引分片数设置不合理
- 没有设置合理的刷新间隔
解决方案:
- 只给需要搜索的字段设置为 text 类型,其他字段设置为 keyword 或数值类型
- 使用 IK 分词器处理中文
- 分片数设置为节点数的 1-3 倍,单个分片大小控制在 10-50GB
- 将刷新间隔(refresh_interval)设置为 30s 或更长,提高写入性能
问题 3:数据写入慢
问题描述:大量数据写入时,ES 性能下降严重。
解决方案:
- 使用批量写入(bulk API),每次批量写入 1000-5000 条数据
- 关闭自动刷新,写入完成后再手动刷新
- 增加副本数为 0,写入完成后再恢复副本
- 使用 SSD 磁盘
- 调整 JVM 堆内存大小,设置为物理内存的一半,不超过 32GB
问题 4:集群健康状态异常
常见状态:
- Green:所有主分片和副本都正常运行
- Yellow:所有主分片正常运行,部分副本不正常
- Red:部分主分片不正常
解决方案:
- Yellow 状态:检查节点是否正常,增加节点数
- Red 状态:检查故障节点,恢复数据,必要时重建索引
七、ES 生产环境最佳实践
最后,我分享几个我在生产环境中总结的 ES 最佳实践:
- 版本选择:建议使用 7.x 版本,不要使用太新或太旧的版本
- 集群规划:至少 3 个节点,避免脑裂问题
- 硬件配置:使用 SSD 磁盘,内存越大越好,CPU 核心数不少于 4 核
- 索引设计:
- 索引名使用小写字母,用下划线分隔
- 不要在一个索引中存储太多不同类型的数据
- 按时间分索引,比如每天一个索引,方便管理和删除
- 查询优化:
- 避免使用通配符开头的查询
- 避免查询大量数据
- 使用 filter 代替 query 进行过滤,filter 会缓存结果
- 只返回需要的字段,不要返回整个文档
- 安全配置:开启身份认证和权限控制,禁止外网直接访问 ES 集群
- 监控告警:监控集群健康状态、节点状态、索引大小、查询延迟等指标
- 备份恢复:定期备份数据,防止数据丢失
更多推荐




所有评论(0)