Elasticsearch 是全球最流行的分布式搜索引擎之一,它基于开源的全文检索库 Apache Lucene 构建,天然支持水平扩展与近实时搜索能力。通过倒排索引、分词器以及丰富的聚合分析框架,Elasticsearch 能够以毫秒级延迟在海量数据中完成全文检索、结构化过滤与统计分析。无论是日志分析、站内搜索、商品推荐还是安全审计场景,Elasticsearch 都提供了成熟可靠的解决方案。
1. 核心概念
掌握 Elasticsearch 的前提是先理解其与关系型数据库之间的概念映射,这有助于我们在设计数据模型时避免范式误区。
| 概念 | 类比 RDBMS | 说明 |
|---|---|---|
| Index | Database | 索引,逻辑命名空间,包含一组具有相似特征的文档 |
| Type | Table | 7.x 已废弃,8.x 正式移除,一个索引仅含单一类型 |
| Document | Row | 一条 JSON 格式的数据记录,是 Lucene 文档的封装 |
| Field | Column | 字段,分为 text(全文索引)、keyword(精确匹配)等类型 |
| Mapping | Schema | 字段类型定义,可以动态推断也可以显式声明 |
| Shard | Partition | 分片,索引数据在物理上的横向切分单元 |
| Replica | Replica | 副本分片,提供读扩展能力与故障冗余保护 |
每个 Elasticsearch 索引实际上由多个 Lucene 索引组成。当写入一条文档时,数据首先进入内存缓冲区并写入 translog,随后经过 refresh 变成近实时可搜索的段(segment),最终经 flush 持久化到磁盘。查询请求则由协调节点(Coordinating Node )接收,广播到相关分片执行本地搜索后再汇聚结果。
2. 倒排索引
倒排索引(Inverted Index)是全文检索的基石。与正排索引(按文档编号存储内容)不同,倒排索引以词项(Term)为核心,记录每个词项在哪些文档中出现以及出现的具体位置。
文档集合:
Doc1: "Elasticsearch is a search engine"
Doc2: "Lucene is the core of Elasticsearch"
倒排索引结构:
"elasticsearch" → [Doc1: TF=1, Positions=[0]], [Doc2: TF=1, Positions=[5]]
"search" → [Doc1: TF=1, Positions=[3]]
"engine" → [Doc1: TF=1, Positions=[4]]
"lucene" → [Doc2: TF=1, Positions=[0]]
"core" → [Doc2: TF=1, Positions=[3]]
除了词项到文档的映射,ES 的每个 posting list 还携带了以下四种关键元数据:
- Term Frequency (TF):词项在当前文档中出现的次数,越高代表相关性越强。
- Document Frequency (DF):包含该词项的文档总数,用于计算 IDF(逆文档频率)。
- Norms:字段长度归一化因子,短字段中的词比长字段中的词权重更高。
- Positions:词项在文档中的位置序号,用于短语匹配(match_phrase)和邻近查询(span query)。
- Offsets:字节级别的字符偏移量,响应阶段用于高亮(highlight)渲染匹配片段。
理解倒排索引的结构有助于我们后续的查询优化与字段映射设计。
3. 分词与分析器
分析器(Analyzer)由三个组件构成:字符过滤器(Char Filter)、分词器(Tokenizer)和词项过滤器(Token Filter)。中文全文检索通常选用 IK 分词器,在索引阶段使用 ik_max_word 进行最细粒切分以提升召回率,在查询阶段使用 ik_smart 进行智能切分以避免无意义组合。
以下示例展示如何为一个商品索引配置 IK 分词器叠加拼音过滤器,从而实现「中文 + 拼音首字母」的双路检索:
PUT /products
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"analysis": {
"analyzer": {
"ik_pinyin": {
"tokenizer": "ik_max_word",
"filter": ["pinyin_filter"]
}
},
"filter": {
"pinyin_filter": {
"type": "pinyin",
"keep_separate_first_letter": false,
"keep_full_pinyin": true,
"keep_original": true,
"limit_first_letter_length": 16,
"lowercase": true
}
}
}
},
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart",
"fields": {
"pinyin": {
"type": "text",
"analyzer": "ik_pinyin"
}
}
},
"description": { "type": "text", "analyzer": "ik_max_word" },
"price": { "type": "float" },
"category": { "type": "keyword" },
"tags": { "type": "keyword" },
"create_time": { "type": "date" },
"sales": { "type": "integer" }
}
}
}
字段映射中使用了 fields(多字段)特性,title 同时支持 IK 全文索引与拼音索引,category 与 tags 使用 keyword 类型以保证聚合与排序时不需要额外的 text 转换开销。
4. 索引设计与映射
索引设计阶段的选择将直接影响后期的搜索质量与查询性能。首先需要在动态映射与显式映射之间做出选择:
- 动态映射(Dynamic Mapping):新字段首次出现时由 Elasticsearch 自动推断类型,适合快速原型与日志场景。
- 显式映射(Explicit Mapping):开发者在创建索引时完整定义所有字段类型,适合生产环境的稳定索引。
显式映射可以避免动态映射带来的两个经典问题:字符串被同时推断为 text 与 keyword 多字段、数值被错误推断为 long 导致存储膨胀。建议生产环境始终使用显式映射。
字段类型的选择同样关键:text 字段会被分析并入库到倒排索引,支持全文检索但不支持聚合与排序;keyword 字段保持原始字符串,适合精确匹配、范围过滤、聚合统计与排序操作。如果需要同时支持全文搜索与精确聚合,应使用多字段特性同时定义 text 与 keyword 子字段。
PUT /articles
{
"mappings": {
"dynamic": "strict",
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart",
"norms": false,
"fields": {
"keyword": { "type": "keyword", "ignore_above": 256 }
}
},
"content": {
"type": "text",
"analyzer": "ik_max_word",
"index_options": "offsets"
},
"author": {
"type": "keyword",
"ignore_above": 128
},
"tags": {
"type": "keyword",
"ignore_above": 64
},
"publish_time": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
},
"views": { "type": "integer" },
"rating": { "type": "float" }
}
}
}
上述映射中,“dynamic”: “strict” 禁止未知字段写入,防止脏数据污染索引结构。norms: false 禁用了 TF-IDF 评分中的长度归一化,适用于不需要相关性排序的纯文本字段,能有效降低磁盘占用。ignore_above 限制字符串长度,超过该值的字符串将被忽略且不索引,避免超长异常数据导致内存压力。index_options: offsets 仅存储偏移量用于高亮,在不需要位置信息时可比默认的 positions 节省空间。
5. 查询 DSL 进阶
Elasticsearch 的 Query DSL 是一套基于 JSON 的请求体查询语言,分为叶子查询(如 term、match、range)与复合查询(如 bool、function_score)。
5.1 Bool 复合查询
bool 查询通过 must、should、filter、must_not 四个子句组合多个查询条件。关键区别在于 must 子句参与相关性评分计算并影响最终得分,而 filter 子句仅做包含与否判定且不参与打分,同时 filter 结果会被缓存以加速重复过滤条件。
GET /products/_search
{
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "智能手机",
"fields": ["title^3", "description^1"],
"type": "best_fields",
"fuzziness": "AUTO"
}
}
],
"filter": [
{ "range": { "price": { "gte": 1000, "lte": 5000 } } },
{ "term": { "category": "电子产品" } },
{ "term": { "tags": "5G" } }
],
"must_not": [
{ "exists": { "field": "deleted_time" } }
],
"should": [
{ "term": { "tags": "新品" } }
],
"minimum_should_match": 0
}
},
"sort": [
{ "_score": "desc" },
{ "sales": "desc" }
],
"from": 0,
"size": 20,
"highlight": {
"pre_tags": ["<strong>"],
"post_tags": ["</strong>"],
"fields": {
"title": {
"number_of_fragments": 1,
"fragment_size": 200
},
"description": {
"number_of_fragments": 2,
"fragment_size": 150
}
}
},
"aggs": {
"by_category": {
"terms": { "field": "category" }
},
"price_stats": {
"stats": { "field": "price" }
}
}
}
5.2 Function Score 自定义排序
当默认的 TF-IDF 评分无法满足业务需求时,可以通过 function_score 引入自定义权重。常见场景包括:热销商品加权、新上架商品提权、高评分内容优先。
GET /products/_search
{
"query": {
"function_score": {
"query": {
"match": { "title": "无线耳机" }
},
"functions": [
{
"filter": { "term": { "tags": "新品" } },
"weight": 1.5
},
{
"field_value_factor": {
"field": "sales",
"factor": 0.001,
"modifier": "log1p",
"missing": 1.0
}
},
{
"gauss": {
"create_time": {
"origin": "now",
"scale": "7d",
"offset": "1d",
"decay": 0.5
}
}
}
],
"score_mode": "sum",
"boost_mode": "multiply"
}
}
}
5.3 Search After 深度分页
from + size 深分页在超过一万条时性能急剧下降,因为协调节点需要为每个分片获取大量文档再全局排序。search_after 基于排序值进行翻页,能够安全地处理百万甚至千万级分页请求。
GET /orders/_search
{
"size": 100,
"sort": [
{ "order_time": "desc" },
{ "order_id": "asc" }
],
"search_after": [1692009600000, "ORD-20230814-000927"]
}
5.4 Collapse 字段折叠
在搜索结果中按某个字段去重时,如果业务要求每组仅展示一条代表文档,可使用 collapse 功能替代聚合后手动去重的开销。
GET /news/_search
{
"query": {
"match": { "title": "区块链" }
},
"collapse": {
"field": "source_site",
"inner_hits": {
"name": "latest_articles",
"size": 3,
"sort": [{ "publish_time": "desc" }]
}
},
"sort": [{ "publish_time": "desc" }]
}
6. 聚合分析
聚合(Aggregations)是 Elasticsearch 最具特色的分析功能之一,分为桶聚合(Bucket)、指标聚合(Metric)与管道聚合(Pipeline)三大类。
6.1 桶聚合与指标聚合
桶聚合用于将文档分组到各个桶中,指标聚合则在每个桶内计算统计数据。
POST /sales/_search
{
"size": 0,
"aggs": {
"sales_over_time": {
"date_histogram": {
"field": "order_time",
"calendar_interval": "day",
"min_doc_count": 1,
"format": "yyyy-MM-dd"
},
"aggs": {
"revenue": {
"sum": { "field": "amount" }
},
"avg_order_value": {
"avg": { "field": "amount" }
},
"percentiles_latency": {
"percentiles": {
"field": "delivery_time_hours",
"percents": [50, 90, 99]
}
},
"unique_customers": {
"cardinality": { "field": "customer_id" }
}
}
},
"top_categories": {
"terms": {
"field": "category",
"size": 10,
"order": { "total_revenue": "desc" }
},
"aggs": {
"total_revenue": {
"sum": { "field": "amount" }
}
}
}
}
}
6.2 管道聚合
管道聚合不直接操作原始文档,而是基于其他聚合的结果进行二次计算。典型场景包括计算环比增长率与移动平均值。
POST /metrics/_search
{
"size": 0,
"aggs": {
"daily_cpu": {
"date_histogram": {
"field": "timestamp",
"calendar_interval": "hour"
},
"aggs": {
"avg_cpu": {
"avg": { "field": "cpu_percent" }
},
"cpu_derivative": {
"derivative": {
"buckets_path": "avg_cpu"
}
},
"cpu_moving_avg": {
"moving_avg": {
"buckets_path": "avg_cpu",
"window": 24,
"model": "linear"
}
}
}
}
}
}
6.3 聚合性能优化
聚合查询会消耗大量堆内存与 CPU 资源。生产环境应遵循以下原则:
- 时间范围聚合尽量使用索引模式与
time_zone参数对齐业务时区。 - 高基数字段(如 UUID、IP)使用
cardinality时注意 precision_threshold 的内存开销,必要时改用 HyperLogLog++ 预聚合。 - 嵌套聚合(sub-aggregation)深度不超过两层,否则查询延迟会呈指数级增长。
- 聚合请求中设置
"size": 0,避免在返回聚合结果时附带原始命中文档。
7. 集群运维
7.1 节点角色与架构
Elasticsearch 集群中的节点可按职责划分为多种角色:
- Master:负责集群状态管理、索引元数据变更与分片分配决策。生产环境应部署至少三个符合主节点条件的节点并设置
discovery.seed_hosts。 - Data:存储实际数据分片并承担查询与写入的执行任务。可按性能需求细分为 Hot、Warm、Cold、Frozen 四种数据层。
- Ingest:运行预处理管道(Pipeline),对写入文档执行富化、转换或结构化操作。
- Coordinating:仅负责接收、路由与聚合请求,不存储数据也不成为主节点。适合部署在大型集群前端分担协调压力。
- ML:专门用于运行机器学习异常检测与预测作业。
7.2 分片分配策略
分片是 Elasticsearch 实现分布式存储与并行查询的核心单元,但分片数量并非越多越好。单分片推荐保持在 10 到 50GB 之间,单节点承载分片数不宜超过 20 个,否则会导致堆内存与文件描述符压力剧增。JVM 堆内存应设置为不超过 30GB,因为在 32GB 以上会丧失压缩指针优化。
PUT /logs-2024-01
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"refresh_interval": "30s",
"index.routing.allocation.include.temperature": "hot"
}
}
7.3 ILM 索引生命周期管理
ILM 通过在 Hot、Warm、Cold、Frozen、Delete 五个阶段配置自动化策略,实现日志类时序数据的自动滚动、压缩、迁移与删除。
PUT _ilm/policy/logs_policy
{
"policy": {
"phases": {
"hot": {
"actions": {
"rollover": {
"max_size": "50GB",
"max_age": "30d",
"max_docs": 500000000
},
"set_priority": {
"priority": 100
}
}
},
"warm": {
"min_age": "7d",
"actions": {
"shrink": { "number_of_shards": 1 },
"forcemerge": { "max_num_segments": 1 },
"allocate": {
"require": { "data": "warm" }
},
"set_priority": { "priority": 50 }
}
},
"cold": {
"min_age": "30d",
"actions": {
"allocate": { "require": { "data": "cold" } },
"freeze": {},
"set_priority": { "priority": 0 }
}
},
"delete": {
"min_age": "90d",
"actions": { "delete": {} }
}
}
}
}
7.4 集群备份与快照
快照(Snapshot)是 Elasticsearch 进行物理级别备份的重要手段,它将索引数据写入外部仓库(如 S3、HDFS、共享文件系统)并支持增量备份与单索引恢复。
# 1. 注册快照仓库
PUT _snapshot/my_backup
{
"type": "fs",
"settings": {
"location": "/mount/backups",
"compress": true,
"chunk_size": "1gb",
"max_snapshot_bytes_per_sec": "100mb",
"max_restore_bytes_per_sec": "100mb"
}
}
# 2. 执行快照
PUT _snapshot/my_backup/snapshot_2024_01_15?wait_for_completion=true
{
"indices": "logs-2024-*",
"ignore_unavailable": true,
"include_global_state": false
}
# 3. 查看快照状态
GET _snapshot/my_backup/snapshot_2024_01_15/_status
# 4. 恢复索引
POST _snapshot/my_backup/snapshot_2024_01_15/_restore
{
"indices": "logs-2024-01-01",
"ignore_unavailable": true,
"include_aliases": false
}
快照仓库的配置中,chunk_size 控制每次上传的分片大小,max_snapshot_bytes_per_sec 限制备份与恢复带宽,避免网络或 IO 资源被长期占满。
8. 性能调优
Elasticsearch 的性能优化需要从写入吞吐、查询延迟与存储空间三个维度同时进行。
8.1 Refresh Interval 权衡
默认的 refresh_interval 为 1 秒,意味着每秒钟会将内存缓冲区中的数据刷新为新的段并使其可搜索。高吞吐写入场景下,增大刷新间隔可显著减少段文件数量与 refresh 开销。日志索引通常设置为 30 秒甚至更大。
PUT /logs/_settings
{
"index": {
"refresh_interval": "30s",
"number_of_replicas": 0
}
}
8.2 Bulk API 批量写入
单条写入每次都会触发网络往返、写 translog 与刷新检查。生产环境写入应始终使用 _bulk API,将多条文档合并到一个请求中。最优批量大小通常在 5MB 到 15MB 之间,可通过调整并发线程数进一步压榨写入吞吐。
curl -X POST "localhost:9200/_bulk?refresh=false" -H 'Content-Type: application/json' -d'
{ "index" : { "_index" : "products", "_id" : "1" } }
{ "title" : "无线降噪耳机", "price" : 1299.00, "category" : "电子产品", "tags" : ["蓝牙", "降噪"], "sales" : 5000 }
{ "index" : { "_index" : "products", "_id" : "2" } }
{ "title" : "机械键盘", "price" : 599.00, "category" : "电脑配件", "tags" : ["RGB", "热插拔"], "sales" : 3200 }
{ "index" : { "_index" : "products", "_id" : "3" } }
{ "title" : "4K 显示器", "price" : 2499.00, "category" : "电子产品", "tags" : ["4K", "HDR"], "sales" : 1800 }
'
8.3 Translog 持久化策略
Translog 是写入数据的第一份保障,记录了尚未 flush 到磁盘的操作日志。index.translog.durability 控制 translog 的 fsync 策略,async 模式可以降低写入延迟但需要容忍少量数据丢失风险,关键业务索引建议保持 request 同步策略。
PUT /critical_orders/_settings
{
"index": {
"translog": {
"durability": "request",
"sync_interval": "5s"
}
}
}
8.4 段合并与查询缓存
随着写入持续进行,小段(segment)文件会大量累积,既占用文件描述符又拖慢查询速度。forcemerge API 可以将段数量强制合并到指定值,但执行期间会产生显著磁盘 IO,应在业务低峰期操作。
POST /old_logs/_forcemerge?max_num_segments=1
查询缓存(Query Cache)默认将 filter 子句的位集结果缓存在节点级别的堆内存中。合理设计 filter 组合可以显著提升热点查询的响应速度。此外,ES 还维护请求缓存(Request Cache)用于缓存聚合结果与命中总数,在数据不频繁变更的索引上尤其有效。
PUT /reports/_settings
{
"index": {
"requests.cache.enable": true
}
}
9. 与 Spring Boot 及 Elastic Stack 集成
9.1 Spring Data Elasticsearch
Spring Data Elasticsearch 提供 Repository 模式与 ElasticsearchRestTemplate 两种编程方式,能够像操作 JPA 一样操作搜索索引。
// 1. Maven 依赖
// <dependency>
// <groupId>org.springframework.boot</groupId>
// <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
// </dependency>
// 2. 实体类
@Document(indexName = "products")
public class Product {
@Id
private String id;
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String title;
@Field(type = FieldType.Float)
private BigDecimal price;
@Field(type = FieldType.Keyword)
private String category;
@Field(type = FieldType.Keyword)
private List<String> tags;
@Field(type = FieldType.Date, format = DateFormat.date_time)
private LocalDateTime createTime;
@Field(type = FieldType.Integer)
private Integer sales;
// getters and setters
}
// 3. Repository 接口
public interface ProductRepository extends ElasticsearchRepository<Product, String> {
List<Product> findByCategory(String category);
@Query("{ \"bool\": { \"must\": [ { \"match\": { \"title\": \"?0\" } } ], \"filter\": [ { \"term\": { \"category\": \"?1\" } } ] } }")
List<Product> searchByTitleAndCategory(String title, String category);
}
ElasticsearchRestTemplate 则适合执行更复杂的查询与聚合操作,支持直接拼 DSL 并通过 NativeSearchQuery 执行。
9.2 Filebeat 与 Logstash Pipeline
Elastic Stack 中,Filebeat 负责轻量级日志采集,通过 prospector 监控日志文件的变化并将增量内容发送到 Elasticsearch 或 Logstash。
# filebeat.yml
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/nginx/access.log
fields:
service: nginx
env: production
fields_under_root: true
output.elasticsearch:
hosts: ["http://es-node1:9200", "http://es-node2:9200"]
index: "%{[fields.service]}-%{[agent.version]}-%{+yyyy.MM.dd}"
pipeline: "nginx_access_pipeline"
processors:
- add_host_metadata:
when.not.contains.tags: forwarded
- add_cloud_metadata: ~
Logstash 则作为更强大的数据处理中间件,支持 grok 解析、geoip enrich、mutate 转换等多阶段 Pipeline 处理。
input {
beats { port => 5044 }
}
filter {
grok {
match => { "message" => "%{IPORHOST:client_ip} - %{USER:auth} \[%{HTTPDATE:timestamp}\] \"%{WORD:method} %{URIPATHPARAM:request} HTTP/%{NUMBER:http_version}\" %{INT:status} %{INT:bytes} \"%{DATA:referrer}\" \"%{DATA:user_agent}\"" }
}
geoip {
source => "client_ip"
target => "geoip"
}
date {
match => [ "timestamp", "dd/MMM/yyyy:HH:mm:ss Z" ]
target => "@timestamp"
}
mutate {
add_field => { "service" => "nginx" }
remove_field => [ "message", "timestamp", "beat", "input", "host" ]
}
}
output {
elasticsearch {
hosts => ["http://es-node1:9200"]
index => "nginx-access-%{+yyyy.MM.dd}"
template_name => "nginx_template"
}
}
10. 集群架构与节点规划
Node 类型:
- Master: 管理集群状态与索引元数据(至少 3 个候选)
- Data Hot: 承载高频读写热数据(SSD 存储)
- Data Warm: 承载低频访问温数据(SATA 存储,段合并后只读)
- Data Cold: 承载归档冷数据(压缩存储,查询需解冻)
- Ingest: 执行文档预处理 Pipeline(过滤、富化、结构化)
- Coordinating: 仅路由请求与聚合结果(大集群前端专用)
- ML: 运行机器学习作业(异常检测、预测分析)
10.1 分片规划黄金法则
# 分片大小范围: 10 - 50 GB
# 单节点分片数上限: < 20 个
# JVM 堆内存上限: ≤ 30 GB(用于启用压缩指针)
# 总分片数估算: 数据总量(GB) / 30
#
# 创建索引时显式指定分片与副本
PUT /logs-2024-01
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"refresh_interval": "30s",
"index.lifecycle.name": "logs_policy",
"index.lifecycle.rollover_alias": "logs"
}
}
总结
- 分词策略:中文搜索优先选用 IK 分词器,索引阶段使用
ik_max_word提升召回率,查询阶段使用ik_smart保证结果精确度。 - 映射设计:能用
keyword的场景不要滥用text,聚合、排序与精确匹配均应依赖keyword。多字段特性(fields)可以在同一字段上同时支持两种能力。 - 查询优化:
must与filter分离使用,filter 子句不参与评分且结果会被缓存,是提升查询效率的关键手段。深分页使用search_after,去重展示使用collapse。 - 聚合分析:桶聚合负责分组,指标聚合负责统计,管道聚合负责二次计算。控制嵌套深度并在聚合请求中禁用文档回传(
size: 0)。 - 集群规划:Master 节点与数据节点必须分离,热温冷分层降低存储成本,ILM 自动管理索引生命周期,定期快照保障数据安全。
- 性能调优:写入端使用
_bulk批量提交并适当增大refresh_interval,查询端善用 filter 缓存与请求缓存,存储端按需执行段合并与冷热迁移。 - 生态集成:Spring Data Elasticsearch 让 Java 开发者能够以声明式方式操作搜索索引,Filebeat 与 Logstash 则构成了日志采集与处理的标准数据通路。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。