Elasticsearch 搜索引擎实战:从入门到生产环境调优
作者:洛水石
阅读时间:约 14 分钟
关键词:Elasticsearch、全文检索、倒排索引、分词、集群、性能调优
---
目录
- [引言:为什么需要 Elasticsearch?](#引言)
- [核心概念:倒排索引与分片机制](#核心概念)
- [快速上手:索引设计与 CRUD](#快速上手)
- [搜索实战:Query DSL 深度解析](#搜索实战)
- [聚合分析:从数据中发现价值](#聚合分析)
- [集群部署与高可用](#集群部署与高可用)
- [生产环境性能调优](#生产环境性能调优)
- [总结](#总结)
---
引言:为什么需要 Elasticsearch?
MySQL 的 LIKE 查询在百万级数据时就开始变慢,到了千万级基本 unusable。而 Elasticsearch(以下简称 ES)基于倒排索引,在亿级文档上也能做到毫秒级响应。
ES 的典型应用场景:
- **站内搜索**:电商商品搜索、文档库检索、日志搜索
- **日志分析**:ELK 栈(Elasticsearch + Logstash + Kibana)
- **指标监控**:结合 Metricbeat 存储时序数据
- **安全分析**:SIEM 场景下的海量日志关联分析
单机 ES 轻松支撑 **千万级文档**,集群模式下可达 **百亿级**。
---
核心概念
倒排索引(Inverted Index)
MySQL 用 B+ 树存数据,查"Java"需要全表扫描。ES 把文档拆成词项,建立词项 → 文档列表的映射:
|
词项 |
文档列表 |
|
Java |
Doc1, Doc3, Doc5 |
|
Spring |
Doc2, Doc3 |
|
Boot |
Doc3, Doc7 |
|
Java |
Doc1, Doc3, Doc5 |
|
Spring |
Doc2, Doc3 |
|
Boot |
Doc3, Doc7 |
|
Spring |
Doc2, Doc3 |
|
Boot |
Doc3, Doc7 |
搜"Java"直接命中 Doc1/3/5,时间复杂度 O(1)。
核心术语对照
|
ES 概念 |
关系型数据库类比 |
说明 |
|
Index |
Database |
索引,逻辑数据容器 |
|
Type |
Table |
类型(ES 7+ 已废弃) |
|
Document |
Row |
文档,JSON 格式 |
|
Field |
Column |
字段 |
|
Mapping |
Schema |
字段类型定义 |
|
Shard |
Partition |
分片,数据水平拆分 |
|
Replica |
副本 |
分片的复制,保证高可用 |
|
Index |
Database |
索引,逻辑数据容器 |
|
Type |
Table |
类型(ES 7+ 已废弃) |
|
Document |
Row |
文档,JSON 格式 |
|
Field |
Column |
字段 |
|
Mapping |
Schema |
字段类型定义 |
|
Shard |
Partition |
分片,数据水平拆分 |
|
Replica |
副本 |
分片的复制,保证高可用 |
|
Type |
Table |
类型(ES 7+ 已废弃) |
|
Document |
Row |
文档,JSON 格式 |
|
Field |
Column |
字段 |
|
Mapping |
Schema |
字段类型定义 |
|
Shard |
Partition |
分片,数据水平拆分 |
|
Replica |
副本 |
分片的复制,保证高可用 |
|
Document |
Row |
文档,JSON 格式 |
|
Field |
Column |
字段 |
|
Mapping |
Schema |
字段类型定义 |
|
Shard |
Partition |
分片,数据水平拆分 |
|
Replica |
副本 |
分片的复制,保证高可用 |
|
Field |
Column |
字段 |
|
Mapping |
Schema |
字段类型定义 |
|
Shard |
Partition |
分片,数据水平拆分 |
|
Replica |
副本 |
分片的复制,保证高可用 |
|
Mapping |
Schema |
字段类型定义 |
|
Shard |
Partition |
分片,数据水平拆分 |
|
Replica |
副本 |
分片的复制,保证高可用 |
|
Shard |
Partition |
分片,数据水平拆分 |
|
Replica |
副本 |
分片的复制,保证高可用 |
分片与副本
Index: blog
├── Shard 0 (Primary) → Replica 0
├── Shard 1 (Primary) → Replica 1
├── Shard 2 (Primary) → Replica 2
└── Shard 3 (Primary) → Replica 3
设计原则:
- 分片数 = 节点数 × 1~3 倍,避免分片过大或过多
- 副本数 ≥ 1,保证高可用
- 单分片大小控制在 **20-50GB**
---
快速上手
1. 创建索引与 Mapping
PUT /product
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"analysis": {
"analyzer": {
"ik_smart": {
"type": "custom",
"tokenizer": "ik_smart"
}
}
}
},
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "ik_smart",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"price": { "type": "float" },
"category": { "type": "keyword" },
"tags": { "type": "keyword" },
"create_time": { "type": "date" },
"description": {
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
2. 文档 CRUD
新增文档
curl -X POST "localhost:9200/product/_doc/1" -H 'Content-Type: application/json' -d'
{
"name": "iPhone 15 Pro",
"price": 7999.00,
"category": "手机",
"tags": ["苹果", "5G", "旗舰"],
"create_time": "2024-09-15",
"description": "搭载 A17 Pro 芯片,钛金属设计"
}'
查询文档
curl -X GET "localhost:9200/product/_doc/1"
更新文档
curl -X POST "localhost:9200/product/_update/1" -H 'Content-Type: application/json' -d'
{
"doc": { "price": 7499.00 }
}'
删除文档
curl -X DELETE "localhost:9200/product/_doc/1"
3. Java 客户端操作
@Configuration
public class ElasticsearchConfig {
@Bean
public RestHighLevelClient elasticsearchClient() {
RestClientBuilder builder = RestClient.builder(
new HttpHost("localhost", 9200)
);
return new RestHighLevelClient(builder);
}
}
@Service
public class ProductService {
@Autowired
private RestHighLevelClient client;
public void indexProduct(Product product) throws IOException {
IndexRequest request = new IndexRequest("product")
.id(product.getId())
.source(JSON.toJSONString(product), XContentType.JSON);
client.index(request, RequestOptions.DEFAULT);
}
public Product getById(String id) throws IOException {
GetRequest request = new GetRequest("product", id);
GetResponse response = client.get(request, RequestOptions.DEFAULT);
if (response.isExists()) {
return JSON.parseObject(response.getSourceAsString(), Product.class);
}
return null;
}
}
---
搜索实战
1. Match 查询(全文检索)
GET /product/_search
{
"query": {
"match": {
"name": "苹果手机"
}
}
}
ES 会把"苹果手机"拆成"苹果"、"手机"分别搜索,然后算相关性得分。
2. Term 查询(精确匹配)
GET /product/_search
{
"query": {
"term": {
"category": "手机"
}
}
}
term 不会分词,直接匹配词项。注意:如果 category 是 text 类型,会被分词,要用 category.keyword。
3. Bool 复合查询
GET /product/_search
{
"query": {
"bool": {
"must": [
{ "match": { "name": "iPhone" } },
{ "range": { "price": { "lte": 8000 } } }
],
"must_not": [
{ "term": { "tags": "二手" } }
],
"should": [
{ "match": { "description": "旗舰" } }
],
"filter": [
{ "term": { "category": "手机" } }
]
}
}
}
|
子句 |
作用 |
参与算分 |
|
must |
必须满足 |
是 |
|
must_not |
必须不满足 |
否 |
|
should |
应该满足(影响相关性) |
是 |
|
filter |
必须满足(仅过滤) |
否,有缓存 |
|
must |
必须满足 |
是 |
|
must_not |
必须不满足 |
否 |
|
should |
应该满足(影响相关性) |
是 |
|
filter |
必须满足(仅过滤) |
否,有缓存 |
|
must_not |
必须不满足 |
否 |
|
should |
应该满足(影响相关性) |
是 |
|
filter |
必须满足(仅过滤) |
否,有缓存 |
|
should |
应该满足(影响相关性) |
是 |
|
filter |
必须满足(仅过滤) |
否,有缓存 |
4. 高亮显示
GET /product/_search
{
"query": {
"match": { "name": "iPhone" }
},
"highlight": {
"fields": {
"name": {
"pre_tags": ["<em>"],
"post_tags": ["</em>"]
}
}
}
}
5. Java 实现复杂搜索
public SearchResponse searchProducts(String keyword, String category,
Double minPrice, Double maxPrice) throws IOException {
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 全文检索
if (StringUtils.hasText(keyword)) {
boolQuery.must(QueryBuilders.multiMatchQuery(keyword, "name", "description"));
}
// 分类过滤(不参与算分,走缓存)
if (StringUtils.hasText(category)) {
boolQuery.filter(QueryBuilders.termQuery("category", category));
}
// 价格区间
if (minPrice != null || maxPrice != null) {
RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("price");
if (minPrice != null) rangeQuery.gte(minPrice);
if (maxPrice != null) rangeQuery.lte(maxPrice);
boolQuery.filter(rangeQuery);
}
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder()
.query(boolQuery)
.from(0)
.size(20)
.sort("_score", SortOrder.DESC)
.sort("price", SortOrder.ASC)
.highlighter(new HighlightBuilder().field("name"));
SearchRequest searchRequest = new SearchRequest("product").source(sourceBuilder);
return client.search(searchRequest, RequestOptions.DEFAULT);
}
---
聚合分析
1. 指标聚合:统计平均价格
GET /product/_search
{
"size": 0,
"aggs": {
"avg_price": {
"avg": { "field": "price" }
}
}
}
2. 桶聚合:按分类统计数量
GET /product/_search
{
"size": 0,
"aggs": {
"by_category": {
"terms": { "field": "category" },
"aggs": {
"avg_price": { "avg": { "field": "price" } },
"max_price": { "max": { "field": "price" } }
}
}
}
}
3. 日期直方图:按天统计订单量
GET /order/_search
{
"size": 0,
"aggs": {
"sales_over_time": {
"date_histogram": {
"field": "create_time",
"calendar_interval": "day"
}
}
}
}
---
集群部署与高可用
3 节点集群配置
elasticsearch.yml (Node 1)
cluster.name: es-production
node.name: node-1
node.roles: [master, data, ingest]
network.host: 0.0.0.0
discovery.seed_hosts: ["node-1", "node-2", "node-3"]
cluster.initial_master_nodes: ["node-1", "node-2", "node-3"]
分片与副本
index.number_of_shards: 3
index.number_of_replicas: 1
内存锁定(防止 swap)
bootstrap.memory_lock: true
节点角色规划
|
节点 |
角色 |
职责 |
|
Node 1-3 |
Master + Data |
存储数据,参与选主 |
|
Node 4-5 |
Data Only |
仅存储数据,不参竞选 |
|
Node 6 |
Coordinating |
仅接收请求,不存数据 |
|
Node 1-3 |
Master + Data |
存储数据,参与选主 |
|
Node 4-5 |
Data Only |
仅存储数据,不参竞选 |
|
Node 6 |
Coordinating |
仅接收请求,不存数据 |
|
Node 4-5 |
Data Only |
仅存储数据,不参竞选 |
|
Node 6 |
Coordinating |
仅接收请求,不存数据 |
脑裂防护
discovery.zen.minimum_master_nodes: 2 # ES 6.x
ES 7+ 自动处理,无需配置
---
生产环境性能调优
JVM 调优
jvm.options
-Xms16g
-Xmx16g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:+HeapDumpOnOutOfMemoryError
**关键:Xms = Xmx,且不超过 32GB**(超过会禁用 Compressed Oops)
索引层面优化
PUT /log-2024-01
{
"settings": {
"number_of_shards": 5,
"number_of_replicas": 1,
"refresh_interval": "30s", // 降低刷新频率,提升写入
"translog.durability": "async", // 异步刷盘
"translog.sync_interval": "30s"
}
}
写入优化
|
优化项 |
配置 |
效果 |
|
批量写入 |
`bulk` API,每批 5-15MB |
减少网络往返 |
|
禁用 refresh |
`refresh_interval: -1` |
导入数据时临时禁用 |
|
减少副本 |
`number_of_replicas: 0` |
导入完成后再恢复 |
|
使用自动生成 ID |
避免版本控制开销 |
提升 20% 写入 |
|
批量写入 |
`bulk` API,每批 5-15MB |
减少网络往返 |
|
禁用 refresh |
`refresh_interval: -1` |
导入数据时临时禁用 |
|
减少副本 |
`number_of_replicas: 0` |
导入完成后再恢复 |
|
使用自动生成 ID |
避免版本控制开销 |
提升 20% 写入 |
|
禁用 refresh |
`refresh_interval: -1` |
导入数据时临时禁用 |
|
减少副本 |
`number_of_replicas: 0` |
导入完成后再恢复 |
|
使用自动生成 ID |
避免版本控制开销 |
提升 20% 写入 |
|
减少副本 |
`number_of_replicas: 0` |
导入完成后再恢复 |
|
使用自动生成 ID |
避免版本控制开销 |
提升 20% 写入 |
查询优化
|
优化项 |
说明 |
|
避免深分页 |
用 `search_after` 替代 `from/size` |
|
使用 filter |
filter 不计算相关性,有缓存 |
|
限制字段返回 |
`_source: ["name", "price"]` |
|
预热热点索引 |
`index.warmer.enabled: true` |
|
避免深分页 |
用 `search_after` 替代 `from/size` |
|
使用 filter |
filter 不计算相关性,有缓存 |
|
限制字段返回 |
`_source: ["name", "price"]` |
|
预热热点索引 |
`index.warmer.enabled: true` |
|
使用 filter |
filter 不计算相关性,有缓存 |
|
限制字段返回 |
`_source: ["name", "price"]` |
|
预热热点索引 |
`index.warmer.enabled: true` |
|
限制字段返回 |
`_source: ["name", "price"]` |
|
预热热点索引 |
`index.warmer.enabled: true` |
监控指标
集群健康
curl -X GET "localhost:9200/_cluster/health"
节点统计
curl -X GET "localhost:9200/_nodes/stats"
慢查询日志
PUT /_cluster/settings
{
"transient": {
"index.search.slowlog.threshold.query.warn": "10s",
"index.indexing.slowlog.threshold.index.warn": "5s"
}
}
---
总结
ES 使用 checklist
索引设计:
- [ ] 合理设置分片数(3-5 个/节点)
- [ ] text + keyword 多字段映射
- [ ] 日期字段用 date 类型
- [ ] 禁用 `_all` 字段(ES 6+ 已默认禁用)
搜索优化:
- [ ] 过滤条件用 `filter` 而非 `must`
- [ ] 深分页用 `search_after`
- [ ] 聚合查询设置 `size: 0`
- [ ] 限制返回字段减少网络传输
运维监控:
- [ ] JVM 堆内存 ≤ 32GB
- [ ] 监控集群状态(green/yellow/red)
- [ ] 开启慢查询日志
- [ ] 定期清理过期索引(ILM)
一句话总结
**ES 的核心是倒排索引,搜索要快用 filter 缓存,写入要快用 bulk 批量,集群要稳关注分片和副本。**
---
更多硬核技术文章每周更新。
配图1:倒排索引原理

配图2:Query DSL 查询类型对比

配图3:ES集群架构与分片副本机制

— 作者:洛水石 | 搜索引擎 | Elasticsearch实战 —
更多推荐

所有评论(0)