Elasticsearch 学习一、什么是 ElasticsearchES 是一个基于Lucene的分布式全文搜索引擎。你存进去的是 JSON查出来的是毫秒级的搜索结果。核心三句话 ① 倒排索引 — 不是逐行扫而是直接翻字典 ② 分布式 — 数据分片到多台机器并行搜索 ③ 近实时 — 写入 1 秒后就能搜到1.1 和 MySQL 的本质区别MySQLElasticsearch核心数据结构B Tree倒排索引查询方式精确匹配, LIKE全文搜索、模糊匹配、相关性排序典型 SQLWHERE name LIKE %手机%query: { match: { name: 手机 } }百万级全文搜索几十秒几毫秒事务ACID不支持事务用途核心业务数据搜索、日志分析、APM二、核心原理倒排索引2.1 正排索引 vs 倒排索引正排索引MySQL 文档1 → 词A, 词B, 词C 文档2 → 词A, 词D, 词E 找词D → 遍历所有文档 → 慢 O(N) 倒排索引ES 词A → [文档1, 文档2] 词B → [文档1] 词C → [文档1] 词D → [文档2] 找词D → 直接定位 → 快 O(1)2.2 倒排索引的构建过程原始文档 Doc1: The quick brown fox Doc2: The quick brown dog 步骤一分词Analyzer Doc1 → [the, quick, brown, fox] Doc2 → [the, quick, brown, dog] 步骤二去停用词Stop Words 去掉 the → [quick, brown, fox] → [quick, brown, dog] 步骤三构建倒排索引 Term → Posting List (文档ID, 词频, 位置) ───────────────────────────────────── quick → [(1, 1, pos:1), (2, 1, pos:1)] brown → [(1, 1, pos:2), (2, 1, pos:2)] fox → [(1, 1, pos:3)] dog → [(2, 1, pos:3)] 步骤四排序按词排序方便二分查找 brown → [(1,...),(2,...)] dog → [(2,...)] fox → [(1,...)] quick → [(1,...),(2,...)]三、核心概念3.1 类比 MySQLMySQL Elasticsearch ──────────────────────────────── Database → Cluster集群 Table → Index索引 Row → Document文档 Column → Field字段 Schema → Mapping映射 SQL → DSLQuery DSL SELECT → GET /index/_search INSERT → POST /index/_doc3.2 Index索引Index 一个逻辑命名空间指向一个或多个物理分片。 类比MySQL 的一张表但内部数据被切分到了多个分片上。3.3 Document文档{_index:products,// 属于哪个索引_id:1001,// 文档唯一 ID_source:{// 实际存储的数据name:iPhone 15 Pro,brand:Apple,price:6999,tags:[手机,5G,iOS],created:2025-09-15}}3.4 Shard分片— 分布式核心Index products 有 1 亿条数据 不分片 分成 5 个 Shard ┌─────────────────┐ ┌─────┬─────┬─────┬─────┬─────┐ │ products │ │ S0 │ S1 │ S2 │ S3 │ S4 │ │ 1 亿条数据 │ │2000W│2000W│2000W│2000W│2000W│ └─────────────────┘ └─────┴─────┴─────┴─────┴─────┘ ↑ 搜索时 5 个分片并行查询 结果合并返回 每个分片 一个完整的 Lucene 实例 独立的倒排索引Primary Shard vs Replica Shard┌──────────────────────────────────────┐ │ Node 1 Node 2 │ │ ┌──────┐ ┌──────┐ │ │ │ P0 │ ────────→ │ R0 │ │ P0 坏了 → R0 自动提升为 P0 │ │ R1 │ │ P1 │ │ │ └──────┘ └──────┘ │ └──────────────────────────────────────┘ Primary Shard — 数据写入的入口 Replica Shard — Primary 的副本只读 P 和 R 永远不会在同一个节点上同节点没意义四、写入原理近实时文档写入 ES 的完整路径 写入请求 │ ▼ ┌─── Memory Buffer ───┐ │ (内存缓冲区) │ ← 第一步先写到这里 └─────────┬───────────┘ │ 每 1 秒自动刷新refresh ▼ ┌─── Segment ─────────┐ │ (不可变的倒排索引) │ ← 第二步生成新 Segment │ 此时可以被搜索到 │ └─────────┬───────────┘ │ 积累到一定量或每 30 分钟flush ▼ ┌─── 磁盘 ────────────┐ │ 持久化 清空 Buffer │ ← 第三步真正持久化 └─────────────────────┘近实时的含义 写进去 → 1 秒后 refresh → 可以被搜索到 不是实时是近实时默认 1 秒延迟 不可变 Segment 每个 Segment 一旦生成就永不修改 删除 标记为 deleted.del 文件 更新 标记旧文档为 deleted 写一个新文档 等后台合并为什么用不可变 Segment1. 不需要锁 — 读写不互斥因为写是写新 Segment读读老 Segment 2. 缓存友好 — 加载到文件系统缓存后永远不会被修改从而失效 3. 便于压缩 — 一次性写入可以极致压缩五、搜索原理5.1 查询 vs 过滤QueryFilter会计算相关性得分✅ 会❌ 不会会缓存❌ 不缓存✅ 自动缓存性能慢快典型场景全文搜索、模糊匹配价格区间、状态筛选{query:{bool:{must:[// ← must 必须匹配 计算得分{match:{name:iPhone}}],filter:[// ← filter 过滤 不计算得分 缓存{term:{brand:Apple}},{range:{price:{gte:5000,lte:10000}}}]}}}5.2 相关性评分TF-IDF / BM25ES 7.0 默认使用 BM25Best Match 25 BM25 的直觉 - 搜索词在文档中出现多次 → 得分高TF - 搜索词在有很多文档里都出现 → 得分低IDF - 短文档里的词比长文档更重要文档长度归一化 - 搜索词是 iPhone 和 充 → iPhone 权重大得多词频上限 BM25 改进 TF-IDF 的地方 IDF 影响的上限被控 → 超级冷门词和一般冷门词的差距缩小 词频不会再是单纯越高越好 → 防止刷关键词六、Analyzer分词器6.1 分词器三组件Analyzer Character Filter Tokenizer Token Filter Character Filter — 预处理去除 HTML 标签、转换特殊符号 │ Tokenizer — 切词把文本切成一个个 token │ Token Filter — 后处理转小写、去停用词、词干提取6.2 中文分词ES 默认分词器Standard对中文无效 我爱北京天安门 Standard → [我,爱,北,京,天,安,门] ← 逐字拆分没意义 中文需要专门的分词器 IK 分词器最常用 我爱北京天安门 ik_smart → [我,爱,北京,天安门] ← 粗粒度 ik_max_word → [我,爱,北京,天安门,天安,门] ← 细粒度七、聚合查询Aggregations搜索 找到符合条件的文档 聚合 对搜索结果做统计分析 类比 SQL SELECT brand, AVG(price), COUNT(*) FROM products WHERE name LIKE %手机% GROUP BY brand// ES 里的聚合查询按品牌统计手机均价{query:{match:{name:手机}},aggs:{by_brand:{terms:{field:brand,size:10},aggs:{avg_price:{avg:{field:price}}}}}}聚合类型用途类比 SQLTerms按字段分组GROUP BYAvg / Sum / Min / Max统计值AVG(), SUM()Date Histogram按时间区间分组GROUP BY DATERange按数值区间分组CASE WHENTop Hits每组取前 N 条LIMIT GROUP BY八、集群架构8.1 节点角色Master Node 管理集群创建/删除索引、分配分片、协调节点加入/离开 不存数据不处理搜索请求 脑裂风险 → 最少 3 个 master-eligible 节点 Data Node 存数据、执行搜索和聚合 需要大内存、快磁盘SSD Ingest Node 写入时预处理数据改字段名、加时间戳等 协调节点Coordinating Node 接收客户端请求 → 转发到相关 Data Node → 合并结果 → 返回客户端 所有节点默认都是协调节点8.2 搜索的执行过程客户端发送搜索 → 协调节点收到 │ ┌───────────────┼───────────────┐ ▼ ▼ ▼ Data Node 1 Data Node 2 Data Node 3 (Shard 0) (Shard 1) (Shard 2) │ │ │ 各自搜索自己的分片并行都有倒排索引 │ │ │ └───────────────┼───────────────┘ ▼ 协调节点合并 排序 │ ▼ 返回 Top N 结果九、倒排索引的高阶优化9.1 SkipList跳表— 加速多词查询查 quick AND brown 普通做法 quick 的 posting list: [1, 5, 8, 13, 21, 34] brown 的 posting list: [2, 5, 9, 13, 22] 逐一遍历对比 → O(MN) SkipList 优化 quick [1──────→21──→34] 加了跳跃指针 brown [2──────→22] 加了跳跃指针 跳过大量不需要的文档 → 快很多9.2 Roaring Bitmap — 压缩 posting list传统的 posting list 存所有文档 ID → [1, 2, 3, ... 100000] → 太大了 Roaring Bitmap 把文档 ID 分成块 0~65535 → 用 Bitmap16KB 表示 65536 个可能的 ID 65536~131071 → 另一个 Bitmap ... 块内如果 bit 密度高 → 用 Bitmap 块内如果 bit 密度低 → 用数组 压缩比可达 1/10009.3 Doc Values — 排序和聚合的秘密倒排索引擅长搜索不擅长排序。 倒排索引Term → [Doc1, Doc2...] 排序需要Doc → 字段值 倒排索引干不了这个。 Doc Values 正排索引专门为排序/聚合而生 Doc1 → price: 6999, brand: Apple Doc2 → price: 4999, brand: Samsung ... 存在磁盘上按列存储查找 O(1)。 这就是为什么 ES 能对千万级数据秒级聚合。十、实战CRUD 和搜索# 创建索引 PUT /products{settings:{number_of_shards:3, //3个主分片number_of_replicas:1// 每个主分片1个副本},mappings:{properties:{name:{type:text,analyzer:ik_max_word},brand:{type:keyword},price:{type:double},tags:{type:keyword},created:{type:date}}}}# 插入文档 POST /products/_doc{name:iPhone 15 Pro,brand:Apple,price:6999,tags:[手机,5G],created:2025-09-15}# 搜索 GET /products/_search{query:{bool:{must:[{match:{name:iPhone}}],filter:[{range:{price:{gte:5000}}}]}}}十一、分页问题ES 不适合深分页 from10000, size10 协调节点从每个分片取 10010 条 → 50010 条 → 排序 → 丢弃前 10000 条 → 返回 10 条 10000 条背后是 50010 条的读取和排序 → 内存和 CPU 都炸了 解决方案 search_after推荐用上一页最后一条的排序值作为游标 scroll生成快照一次拉取全部适合导出场景十二、常见问题排查问题 1搜索不到刚插入的数据 → ES 是近实时的默认 1 秒后可见 → 手动刷新POST /_refresh → 但不要频繁调会影响写入性能 问题 2数据量大了之后写入变慢 → Segment 太多 → 后台 merge 跟不上了 → 调大 refresh_interval如 30s → 批量写入用 bulk API 问题 3聚合结果不准确 → terms 聚合拆到多个分片 → 每个分片返回 Top N → 合并可能漏掉 → 增加 shard_size 参数比 size 大 2-3 倍 → 或者把字段设为 routing 问题 4内存溢出 → ES 重度依赖文件系统缓存至少把总数据量的 50% 设为 heap → heap 不超过 32GB超过后指针压缩失效十三、总结ES 三部曲 ① 存储 → JSON 文档 → 分词 → 倒排索引 ② 搜索 → 查倒排索引 → 多分片并行 → 合并排序 ③ 扩展 → 加节点 → 分片自动迁移 → 负载均衡 核心原理就一张图 ┌──────────────────────────────────────┐ │ 原始文档 → 分词 → 倒排索引 │ │ 我爱北京天安门 │ │ ↓ │ │ 词 → Posting List │ │ 我 → [1, 2, 5] │ │ 爱 → [1, 3] │ │ 北京 → [1, 2, 4, 5] │ │ 天安门→ [1, 4] │ │ │ │ 搜索北京 → O(1) 直接定位到 [1,2,4,5] │ └──────────────────────────────────────┘