Elasticsearch搜索引擎深度解析:把搜索核心讲透,面试都是小菜
🎯 写在前面:在大数据时代,全文搜索是每个系统必备的功能。Elasticsearch作为分布式搜索的标杆,以其强大的全文搜索能力和水平扩展能力,成为搜索领域的不二之选。但你真的了解ES的底层原理吗?这篇文章,将带你深度剖析Elasticsearch!
一、核心原理:倒排索引
1.1 正排索引 vs 倒排索引
css
┌─────────────────────────────────────────────────────────────────────┐
│ 正排索引 vs 倒排索引 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 正排索引(MySQL默认): │
│ ┌─────────┬───────────────────────────────────┐ │
│ │ 文档ID │ 文档内容 │ │
│ ├─────────┼───────────────────────────────────┤ │
│ │ 1 │ 我爱北京天安门 │ │
│ │ 2 │ 北京是中国的首都 │ │
│ │ 3 │ 天安门在中国北京 │ │
│ └─────────┴───────────────────────────────────┘ │
│ 查询方式:文档ID → 文档内容 │
│ │
│ 倒排索引(ES采用): │
│ ┌───────────┬───────────────────────────────┐ │
│ │ 词 │ 文档ID │ │
│ ├───────────┼───────────────────────────────┤ │
│ │ 我 │ [1] │ │
│ │ 天安门 │ [1, 3] │ │
│ │ 北京 │ [1, 2, 3] │ │
│ │ 中国 │ [2, 3] │ │
│ │ 首都 │ [2] │ │
│ │ 是 │ [2] │ │
│ └───────────┴───────────────────────────────┘ │
│ 查询方式:词 → 包含该词的文档列表 │
│ │
│ 优势:查询"北京",直接返回[1,2,3],无需扫描全表! │
│ │
└─────────────────────────────────────────────────────────────────────┘
1.2 倒排索引详细结构
arduino
┌─────────────────────────────────────────────────────────────────────┐
│ 倒排索引结构 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ Dictionary(词典) │ │
│ │ 包含所有term,按字典序排列,支持二分查找 │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ Posting List(倒排列表) │ │
│ │ 每个term对应一个列表,记录包含该term的文档ID │ │
│ │ [doc1, doc2, doc3, ...] │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ FST(有限状态机) │ │
│ │ 用于前缀查询,如"开*"匹配"开放"、"开始" │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ Skip List(跳表) │ │
│ │ 用于AND/OR运算加速,避免逐个遍历Posting List │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ Bitmap(位图) │ │
│ │ 文档数量少时,使用bitset存储,节省空间 │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
1.3 分词器原理
java
// ES分词流程
"我是程序员,在北京工作"
↓ Analyzer
["我", "是", "程序", "程序员", "员", "在", "北京", "工作"]
/**
* 分词器组成
*/
public class AnalyzerComponents {
// 1. Character Filter(字符过滤器)
// - HTML Strip:去除HTML标签
// - Mapping:字符映射(如把":"替换为"_")
// - Pattern Replace:正则替换
// 2. Tokenizer(分词器)
// - Standard:默认分词,按单词分割
// - Keyword:不分词,整个字符串作为一个token
// - Whitespace:按空格分割
// - Punctuation:按标点分割
// 3. Token Filter(词元过滤器)
// - Lowercase:转小写
// - Stop:去停用词(the, a, is)
// - Synonym:同义词处理
// - Stemming:词干提取(running → run)
}
// 自定义分词器
PUT /my_index
{
"settings": {
"analysis": {
"char_filter": {
"my_char_filter": {
"type": "mapping",
"mappings": ["& => and"]
}
},
"tokenizer": {
"my_tokenizer": {
"type": "pattern",
"pattern": "[,\\s]+" // 按逗号和空格分割
}
},
"filter": {
"my_synonym": {
"type": "synonym",
"synonyms": [
"程序员,码农,程序猿",
"北京,帝都"
]
},
"my_stopwords": {
"type": "stop",
"stopwords": ["的", "了", "在"]
}
},
"analyzer": {
"my_analyzer": {
"type": "custom",
"char_filter": ["my_char_filter"],
"tokenizer": ["my_tokenizer"],
"filter": ["lowercase", "my_stopwords", "my_synonym"]
}
}
}
}
}
二、数据结构:Shard与Segment
2.1 ES数据存储架构
scss
┌─────────────────────────────────────────────────────────────────────┐
│ ES数据存储架构 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Index(索引) │
│ ↓ │
│ ┌────────┬────────┬────────┐ │
│ │ Shard 0│ Shard 1│ Shard 2│ ← 主分片(Primary Shard) │
│ │ (P0) │ (P1) │ (P2) │ │
│ └────────┴────────┴────────┘ │
│ ↓ ↓ ↓ │
│ ┌────────┬────────┬────────┐ │
│ │ Shard 0│ Shard 1│ Shard 2│ ← 副本分片(Replica Shard) │
│ │ (R0) │ (R1) │ (R2) │ │
│ └────────┴────────┴────────┘ │
│ │
│ Shard = Lucene Index │
│ ↓ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ Segment(分段) │ │
│ │ 每个Shard包含多个Segment(_segments API查看) │ │
│ │ 新数据写入新Segment,查询时并行搜索所有Segment │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ Document写入流程 │ │
│ │ 1. 写入内存Buffer │ │
│ │ 2. 写入translog(保证持久性) │ │
│ │ 3. refresh后生成Segment │ │
│ │ 4. flush时清空Buffer和translog │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
2.2 文档写入流程
java
/**
* 文档写入完整流程
*/
// 1. 客户端请求
PUT /my_index/_doc/1
{
"name": "张三",
"age": 28,
"skills": ["Java", "Python"]
}
// 2. 请求路由到主分片
// hash(document_id) % number_of_shards = primary_shard
// 3. 主分片写入流程
/**
* ┌─────────────────────────────────────────────────────────────────┐
* │ 写入流程 │
* │ │
* │ 1. 写入内存Buffer(不可搜索) │
* │ ↓ │
* │ 2. 写入Translog(保证持久性) │
* │ ↓ │
* │ 3. 定期refresh(默认1秒,可配置) │
* │ ↓ │
* │ 4. 生成新Segment(可搜索) │
* │ ↓ │
* │ 5. Segment定期flush(清空Buffer和Translog) │
* │ ↓ │
* │ 6. Segment定期merge(合并小Segment,优化查询) │
* │ │
* └─────────────────────────────────────────────────────────────────┘
*/
// 4. 并行复制到副本分片
// 只有所有副本都写入成功,才算写入成功(consistency参数可调)
2.3 Segment合并机制
css
┌─────────────────────────────────────────────────────────────────────┐
│ Segment合并原理 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 背景:ES不断写入新Segment,小Segment会越来越多 │
│ 问题:小Segment查询效率低(需要合并搜索多个Segment) │
│ │
│ 合并策略: │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ Elasticsearch后台线程定期执行Force Merge │ │
│ │ │ │
│ │ 小Segment合并为大Segment: │ │
│ │ [S1] + [S2] + [S3] → [S_new] │ │
│ │ 合并时自动删除已标记删除的文档 │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ 配置优化: │
│ PUT /my_index/_settings │
│ { │
│ "number_of_shards": 3, // 主分片数(创建时设置) │
│ "number_of_replicas": 1, // 副本数(可动态调整) │
│ "refresh_interval": "5s" // 刷新间隔(写多查少时调大) │
│ } │
│ │
└─────────────────────────────────────────────────────────────────────┘
三、查询DSL:深度解析
3.1 全文查询
json
// 1. match查询(标准全文搜索)
GET /my_index/_search
{
"query": {
"match": {
"title": {
"query": "Java编程入门",
"operator": "and" // and: 所有词都匹配, or: 任一词匹配(默认)
}
}
}
}
// 2. match_phrase(短语匹配)
GET /my_index/_search
{
"query": {
"match_phrase": {
"title": {
"query": "Java编程",
"slop": 1 // 允许词之间间隔1个词
}
}
}
}
// 3. multi_match(多字段匹配)
GET /my_index/_search
{
"query": {
"multi_match": {
"query": "Java分布式",
"fields": ["title^2", "content", "tags"],
"type": "best_fields", // 最佳字段(默认)
"tie_breaker": 0.3 // 其他字段的权重
}
}
}
// 4. query_string(高级查询语法)
GET /my_index/_search
{
"query": {
"query_string": {
"default_field": "content",
"query": "(Java AND 分布式) OR (Spring AND 微服务)",
"default_operator": "AND"
}
}
}
3.2 精确查询
json
// 1. term查询(不分词,精确匹配)
GET /my_index/_search
{
"query": {
"term": {
"status": "active" // 精确匹配,不会分词
}
}
}
// 2. terms查询(多值精确匹配)
GET /my_index/_search
{
"query": {
"terms": {
"status": ["active", "pending"]
}
}
}
// 3. range查询(范围查询)
GET /my_index/_search
{
"query": {
"range": {
"age": {
"gte": 18,
"lte": 30,
"boost": 2.0 // 权重提升
},
"create_time": {
"gte": "2024-01-01",
"lte": "now" // 当前时间
}
}
}
}
// 4. exists查询(非空查询)
GET /my_index/_search
{
"query": {
"exists": {
"field": "phone_number"
}
}
}
3.3 复合查询
json
// 1. bool查询(布尔组合)
GET /my_index/_search
{
"query": {
"bool": {
"must": [ // 必须匹配(AND)
{ "match": { "title": "Java编程" } }
],
"should": [ // 应该匹配(OR,增加相关性得分)
{ "match": { "content": "Spring" } },
{ "match": { "content": "微服务" } }
],
"must_not": [ // 必须不匹配(NOT)
{ "term": { "status": "deleted" } }
],
"filter": [ // 过滤(不计算得分,性能更好)
{ "range": { "age": { "gte": 18 } } },
{ "term": { "city": "北京" } }
],
"minimum_should_match": 1 // 最少should匹配数
}
}
}
// 2. constant_score(常量分数)
GET /my_index/_search
{
"query": {
"constant_score": {
"filter": { "term": { "status": "active" } },
"boost": 1.5
}
}
}
// 3. function_score(函数评分)
GET /my_index/_search
{
"query": {
"function_score": {
"query": { "match": { "title": "Java" } },
"functions": [
{
"filter": { "range": { "age": { "lte": 30 } } },
"gauss": {
"age": {
"origin": 25,
"scale": 10,
"decay": 0.5
}
}
},
{
"field_value_factor": {
"field": "popularity",
"factor": 1.2,
"modifier": "sqrt",
"missing": 1
}
}
],
"score_mode": "sum", // 得分模式
"boost_mode": "multiply" // 最终得分模式
}
}
}
3.4 聚合查询
json
// 1. 桶聚合(Bucket Aggregation)
GET /my_index/_search
{
"size": 0, // 不返回文档,只返回聚合结果
"aggs": {
"group_by_city": {
"terms": {
"field": "city",
"size": 10,
"min_doc_count": 5
},
"aggs": {
"avg_age": {
"avg": { "field": "age" }
},
"max_salary": {
"max": { "field": "salary" }
}
}
}
}
}
// 2. 指标聚合(Metric Aggregation)
GET /my_index/_search
{
"size": 0,
"aggs": {
"stats_age": {
"stats": { "field": "age" } // min, max, sum, avg, count
},
"percentile_salary": {
"percentiles": {
"field": "salary",
"percents": [25, 50, 75, 95, 99]
}
}
}
}
// 3. 嵌套聚合
GET /my_index/_search
{
"size": 0,
"aggs": {
"by_gender": {
"terms": { "field": "gender" },
"aggs": {
"avg_age": { "avg": { "field": "age" } },
"by_city": {
"terms": { "field": "city" },
"aggs": {
"avg_salary": { "avg": { "field": "salary" } }
}
}
}
}
}
}
四、性能优化:实战技巧
4.1 索引设计优化
json
// 1. 合理的分片数
/**
* 分片数 = 数据量 / 单分片容量
*
* 建议单分片容量:30-50GB
*
* 场景1:数据量1TB
* 建议分片数 = 1000GB / 40GB ≈ 25个主分片
*
* 场景2:数据量100GB,预计增长到500GB
* 建议分片数 = 500GB / 40GB ≈ 13个主分片
*/
// 2. 副本数设计
/**
* 副本数 = 数据重要程度 + 查询压力
*
* 高可用要求:至少1个副本
* 读压力大:副本数 = ceil(读QPS / 单节点QPS)
*/
// 3. mappings优化
PUT /my_index
{
"mappings": {
"properties": {
"user_id": { "type": "keyword" }, // 不需要全文搜索,用keyword
"title": {
"type": "text",
"analyzer": "ik_max_word", // 中文分词
"fields": {
"keyword": { "type": "keyword" } // 支持精确匹配
}
},
"age": { "type": "integer" },
"salary": { "type": "scaled_float", "scaling_factor": 100 },
"create_time": { "type": "date" },
"tags": { "type": "keyword" }
}
},
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"refresh_interval": "5s", // 写多查少时增大
"translog": {
"sync_interval": "10s",
"durability": "async" // 异步刷新,提升写入性能
}
}
}
4.2 查询优化
java
// 1. 使用filter替代must
// filter:不计算相关性得分,可缓存
// must:计算相关性得分,不可缓存
// ❌ 低效
GET /my_index/_search
{
"query": {
"bool": {
"must": [
{ "match": { "title": "Java" } },
{ "term": { "status": "active" } } // status不需要评分
]
}
}
}
// ✅ 高效
GET /my_index/_search
{
"query": {
"bool": {
"must": { "match": { "title": "Java" } },
"filter": { "term": { "status": "active" } }
}
}
}
// 2. 关闭source返回不必要的字段
GET /my_index/_search
{
"_source": ["title", "author", "publish_time"], // 只返回需要的字段
"query": { ... }
}
// 3. 分页深度查询优化
// ❌ 深分页问题:from+size深度过大会OOM
POST /my_index/_search
{ "from": 10000, "size": 10 } // 每次翻页都是前10010条
// ✅ 使用search_after(推荐)
POST /my_index/_search
{
"size": 10,
"query": { "match": { "title": "Java" } },
"sort": [
{ "publish_time": "desc" },
{ "_id": "asc" }
]
}
// 后续查询使用search_after
POST /my_index/_search
{
"size": 10,
"query": { "match": { "title": "Java" } },
"search_after": ["2024-03-01", "doc_id"],
"sort": [
{ "publish_time": "desc" },
{ "_id": "asc" }
]
}
// ✅ 使用scroll(大批量导出)
POST /my_index/_search?scroll=5m
{
"size": 1000,
"query": { "match_all": {} }
}
// 返回 scroll_id,后续使用
POST /_search/scroll
{
"scroll": "5m",
"scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4..."
}
4.3 写入性能优化
json
// 1. 批量写入
POST /my_index/_bulk
{ "index": { "_id": "1" } }
{ "title": "文档1", "content": "内容1" }
{ "index": { "_id": "2" } }
{ "title": "文档2", "content": "内容2" }
// 批量大小建议:5-15MB
// 2. 合理设置refresh_interval
PUT /my_index/_settings
{
"refresh_interval": "30s" // 写入期间临时关闭refresh
}
// 3. 副本设置为0,写入完成后恢复
PUT /my_index/_settings
{
"number_of_replicas": 0
}
// 写入完成后
PUT /my_index/_settings
{
"number_of_replicas": 1
}
// 4. 使用Routing减少搜索范围
PUT /my_index/_doc/1?routing=user_123
{ "title": "用户文章", "user_id": "user_123" }
// 查询时指定routing,减少搜索分片数
GET /my_index/_search?routing=user_123
{ "query": { "match": { "title": "Java" } } }
// 实际只搜索routing对应的分片,而不是全部分片
五、集群与高可用
5.1 集群架构
scss
┌─────────────────────────────────────────────────────────────────────┐
│ ES集群架构 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ │
│ │ Client Node │ ← 协调节点(可选) │
│ │ (协调请求) │ │
│ └──────┬──────┘ │
│ │ │
│ ┌─────────────────┼─────────────────┐ │
│ ↓ ↓ ↓ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Master Node │ │ Master Node │ │ Master Node │ │
│ │ (Master) │ │ (Candidate) │ │ (Candidate) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ ↓ ↓ ↓ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Data Node │ │ Data Node │ │ Data Node │ │
│ │ (P0, R1) │ │ (P1, R2) │ │ (P2, R0) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ 节点类型: │
│ - Master:管理集群元数据(索引创建/删除、分片分配) │
│ - Data:存储数据、参与搜索和聚合 │
│ - Coordinating:接收请求、转发给Data节点、聚合结果 │
│ - Ingest:数据预处理(pipeline) │
│ │
│ 分片分配策略: │
│ - 主分片故障 → 自动选举新主分片 │
│ - 副本分片故障 → 主分片复制数据到新副本 │
│ │
└─────────────────────────────────────────────────────────────────────┘
5.2 故障恢复机制
json
// 1. 分片恢复配置
PUT /_cluster/settings
{
"transient": {
"cluster.routing.allocation.enable": "all", // 允许分片分配
"cluster.routing.allocation.node_concurrent_recoveries": 2, // 并发恢复数
"indices.recovery.max_bytes_per_sec": "100mb" // 恢复速度限制
}
}
// 2. 手动触发分片重分配
POST /_cluster/reroute
{
"commands": [
{
"move": {
"index": "my_index",
"shard": 0,
"from_node": "node1",
"to_node": "node2"
}
}
]
}
// 3. 查看集群健康状态
GET /_cluster/health
{
"cluster_name": "my_cluster",
"status": "green", // green: 所有分片正常
"number_of_nodes": 3,
"active_primary_shards": 30,
"relocating_shards": 0,
"initializing_shards": 0,
"unassigned_shards": 0
}
六、常见面试题
Q1:ES和MySQL如何选型?
┌─────────────────────────────────────────────────────────────────────┐
│ ES vs MySQL 对比 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ MySQL适用场景: │
│ ✅ 事务要求高(强一致性) │
│ ✅ 数据量小(千万级以下) │
│ ✅ 关联查询复杂 │
│ ✅ 需要实时更新的数据 │
│ │
│ ES适用场景: │
│ ✅ 全文搜索需求 │
│ ✅ 数据量大(TB级) │
│ ✅ 查询延迟要求高(毫秒级) │
│ ✅ 聚合分析需求 │
│ ✅ 水平扩展能力 │
│ │
│ 最佳实践:MySQL + ES双写 │
│ - MySQL作为主存储,保证数据一致性 │
│ - ES作为搜索引擎,提供全文搜索能力 │
│ - 通过Canal/Binlog同步数据到ES │
│ │
└─────────────────────────────────────────────────────────────────────┘
Q2:ES如何保证高可用?
markdown
1. 分片副本机制
- 每个主分片可以有多个副本分片
- 副本分片分布在不同节点
- 主分片故障,自动从副本选举新主分片
2. 故障检测
- 节点间心跳检测(ping)
- Master主动检测Data节点状态
- 节点失联后,自动触发分片重分配
3. 集群健康状态
- green:所有分片正常
- yellow:所有主分片正常,但有副本分片未分配
- red:有主分片不可用
4. 写入一致性
- wait_for_active_shards:写入前等待的活跃分片数
- consistency: quorum(大多数节点确认)
Q3:ES如何解决相关性排序问题?
json
// 1. TF-IDF算法
/**
* TF-IDF = TF(词频) × IDF(逆文档频率)
*
* TF = 词在文档中出现次数
* IDF = log(总文档数 / 包含该词的文档数)
*
* 得分 = Σ(TF-IDF)
*/
// 2. BM25(ES默认算法)
/**
* 改进了TF-IDF的饱和函数
* 解决了词频过高导致得分饱和的问题
*
* BM25(k1=1.2, b=0.75)
* k1: 词频饱和参数
* b: 文档长度归一化参数
*/
// 3. 自定义评分
GET /my_index/_search
{
"query": {
"function_score": {
"query": { "match": { "title": "Java" } },
"functions": [
{ "field_value_factor": { "field": "popularity", "factor": 1.2 } },
{ "weight": 2, "filter": { "term": { "is_vip": true } } }
],
"score_mode": "sum",
"boost_mode": "multiply"
}
}
}
七、总结
python
┌─────────────────────────────────────────────────────────────────────┐
│ Elasticsearch知识地图 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 核心原理 ──────────────────────────────────────────────────────→ │
│ 倒排索引 → FST → 分词器 → Analyzer │
│ │
│ 数据结构 ──────────────────────────────────────────────────────→ │
│ Index → Shard → Segment → Document │
│ │
│ 查询DSL ──────────────────────────────────────────────────────→ │
│ match/term/range → bool复合查询 → 聚合查询 │
│ │
│ 性能优化 ──────────────────────────────────────────────────────→ │
│ mappings设计 → 分片策略 → 查询优化 → 写入优化 │
│ │
│ 集群高可用 ────────────────────────────────────────────────────→ │
│ 主从复制 → 故障检测 → 自动恢复 │
│ │
└─────────────────────────────────────────────────────────────────────┘
🎯 讨论话题
大家在使用ES时遇到过哪些性能问题?是怎么排查和解决的?
往期热门文章推荐:
如果这篇文章对你有帮助,欢迎点赞、收藏、转发!我们下期再见! 👋