作者:洛水石

阅读时间:约 14 分钟

关键词:Elasticsearch、全文检索、倒排索引、分词、集群、性能调优

---

目录

  1. [引言:为什么需要 Elasticsearch?](#引言)
  2. [核心概念:倒排索引与分片机制](#核心概念)
  3. [快速上手:索引设计与 CRUD](#快速上手)
  4. [搜索实战:Query DSL 深度解析](#搜索实战)
  5. [聚合分析:从数据中发现价值](#聚合分析)
  6. [集群部署与高可用](#集群部署与高可用)
  7. [生产环境性能调优](#生产环境性能调优)
  8. [总结](#总结)

---

引言:为什么需要 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实战 —

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐