ElasticSearch
开篇:为什么数据库查不动了
MySQL 的 LIKE '%关键词%' 在数据量大了之后会变得极慢,因为它是全表扫描。想象一下在一本 1000 页的书里找"云计算"这个词,如果没有目录和索引,你只能一页一页翻 -- 这就是 MySQL 模糊搜索的本质。
而 ElasticSearch(简称 ES)的做法是:先给每个词建一个"目录",记录它出现在哪些文档里。搜索时直接查目录,瞬间定位。这个"目录",就是倒排索引。
倒排索引原理
正排索引 vs 倒排索引
传统数据库用的是正排索引:从文档找关键词。
| 文档 ID | 内容 |
|---|---|
| 1 | "小米手机性价比高" |
| 2 | "华为手机拍照好" |
| 3 | "小米电视性价比高" |
如果搜"小米",需要遍历每条记录检查是否包含"小米"。
倒排索引反过来:从关键词找文档。
| 关键词 | 文档 ID 列表 |
|---|---|
| 小米 | [1, 3] |
| 手机 | [1, 2] |
| 性价比 | [1, 3] |
| 华为 | [2] |
| 拍照 | [2] |
| 电视 | [3] |
搜索"小米"时,直接从倒排索引查到文档 1 和 3,不需要扫描全部数据。
倒排索引的构建过程
- 分词:将文档内容拆分为词项(Term),如"小米手机性价比高" -> ["小米", "手机", "性价比", "高"]
- 建立映射:记录每个词项出现在哪些文档中,包括位置、频率等信息
- 排序存储:词项按字典序排列,便于快速查找
ES 核心概念
ES 的概念可以和关系型数据库做类比:
| ES | 关系型数据库 | 说明 |
|---|---|---|
| Index(索引) | Table(表) | 一类数据的集合 |
| Document(文档) | Row(行) | 一条具体的数据,JSON 格式 |
| Field(字段) | Column(列) | 文档中的一个属性 |
| Mapping(映射) | Schema(表结构) | 定义字段类型和分析器 |
| Shard(分片) | 分区 | 数据的水平拆分 |
集群与分片
几个关键规则:
- 每个节点是一个 ES 实例
- 主分片数创建后不能修改,副本数可以动态调整
- 一条文档只能存在于一个主分片中
- 主分片和它的副本不能在同一个节点上
- ES 会自动在节点间做分片均衡
Mapping 与字段类型
Mapping 定义了索引的字段类型、分词器等属性,类似数据库的表结构定义。
PUT /product
{
"mappings": {
"properties": {
"name": { "type": "text", "analyzer": "ik_max_word" },
"price": { "type": "integer" },
"tags": { "type": "keyword" },
"desc": { "type": "text" },
"date": { "type": "date", "format": "yyyy-MM-dd" }
}
}
}两个最重要的字段类型:
| 类型 | 是否分词 | 用途 | 配合查询 |
|---|---|---|---|
| text | 是 | 全文检索,如商品描述 | match |
| keyword | 否 | 精确匹配,如状态码、标签 | term |
如果数字字段不用于范围查询,用 keyword 性能比数值类型更高。
查询 DSL
ES 的查询通过 JSON 格式的 DSL(Domain Specific Language)表达,主要分为全文检索和精确查询两大类。
基础 CRUD
# 创建索引
PUT /product
# 插入文档
PUT /product/_doc/1
{
"name": "xiaomi phone",
"desc": "性价比之王",
"price": 3999,
"tags": ["性价比", "发烧", "不卡"]
}
# 查询文档
GET /product/_doc/1
# 更新文档(局部更新)
POST /product/_update/1
{ "doc": { "price": 4999 } }
# 删除文档
DELETE /product/_doc/1全文检索
# match: 分词后匹配(OR 逻辑)
GET /product/_search
{
"query": {
"match": { "name": "xiaomi phone" }
}
}
# match_phrase: 短语匹配(必须连续且顺序一致)
GET /product/_search
{
"query": {
"match_phrase": { "desc": "性价比之王" }
}
}
# multi_match: 多字段匹配
GET /product/_search
{
"query": {
"multi_match": {
"query": "xiaomi",
"fields": ["name", "desc"]
}
}
}精确查询
# term: 不分词的精确匹配
GET /product/_search
{
"query": {
"term": { "tags": "性价比" }
}
}
# range: 范围查询
GET /product/_search
{
"query": {
"range": {
"price": { "gte": 1000, "lte": 5000 }
}
}
}term 和 keyword 的区别:term 是查询方式,搜索词不分词;keyword 是字段类型,字段值不分词。两者经常配合使用。
组合查询 -- bool
bool 查询是实际开发中用得最多的,可以组合多个条件:
GET /product/_search
{
"query": {
"bool": {
"must": [
{ "match": { "name": "phone" } }
],
"filter": [
{ "range": { "price": { "lte": 5000 } } }
],
"should": [
{ "term": { "tags": "性价比" } }
],
"must_not": [
{ "term": { "tags": "二手" } }
]
}
}
}| 子句 | 含义 | 影响评分 |
|---|---|---|
| must | 必须满足 | 是 |
| filter | 必须满足(过滤器) | 否,有缓存 |
| should | 可能满足,满足则加分 | 是 |
| must_not | 必须不满足 | 否 |
filter 和 must 的区别:filter 不计算相关度评分且有缓存,性能更好。不需要排序的条件优先用 filter。
聚合查询
聚合(Aggregation)类似 SQL 的 GROUP BY + 聚合函数:
# 桶聚合:按标签分组统计数量
GET /product/_search
{
"size": 0,
"aggs": {
"tag_count": {
"terms": { "field": "tags", "size": 10 }
}
}
}
# 指标聚合:统计价格的最大/最小/平均值
GET /product/_search
{
"size": 0,
"aggs": {
"price_stats": {
"stats": { "field": "price" }
}
}
}
# 管道聚合:先按分类分桶,再算各分类平均价格,最后找最低的
GET /product/_search
{
"size": 0,
"aggs": {
"type_bucket": {
"terms": { "field": "type.keyword" },
"aggs": {
"avg_price": { "avg": { "field": "price" } }
}
},
"min_avg_price": {
"min_bucket": { "buckets_path": "type_bucket>avg_price" }
}
}
}性能优化
查询优化
- 合理使用 filter:不需要评分的条件用 filter 而非 must,有缓存加速
- 避免深分页:
from + size在深分页时性能急剧下降,用search_after或scroll替代 - 精确字段用 keyword:不需要分词的字段(ID、状态码)设为 keyword
- 禁用不需要的功能:不需要评分的字段关闭
norms,不需要排序聚合的字段关闭doc_values
写入优化
- 批量写入:使用
_bulkAPI 批量操作,减少网络往返 - 调整刷新间隔:
refresh_interval默认 1s,批量导入时可以设为 -1(导入完再手动刷新) - 合理设置副本:大量导入时可以先设为 0,导入完再恢复
集群规划
- 分片数量:单个分片建议 10~50GB,分片数不要超过节点数的 3 倍
- 硬件选择:SSD 对 ES 性能提升显著,内存建议不超过 32GB(JVM 指针压缩边界)
- 冷热分离:热数据放 SSD 节点,冷数据放 HDD 节点
Spring Boot 集成
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.6.2</version>
</dependency>@Configuration
public class EsConfig {
@Bean
public RestHighLevelClient restHighLevelClient() {
return new RestHighLevelClient(
RestClient.builder(new HttpHost("localhost", 9200, "http"))
);
}
}面试高频问答
Q1: 什么是倒排索引?为什么比 MySQL 快?
关键词:从词到文档的映射、避免全表扫描
正排索引是"文档 -> 词",查询时需要遍历每条记录。倒排索引是"词 -> 文档列表",先对文档分词,建立词项到文档 ID 的映射。搜索时直接通过词项定位文档,时间复杂度从 O(n) 降到接近 O(1)。
Q2: text 和 keyword 的区别?term 和 match 的区别?
关键词:分词 vs 不分词、字段类型 vs 查询方式
text 是字段类型,存储时会分词,适合全文检索。keyword 是字段类型,不分词,适合精确匹配。match 是查询方式,会对搜索词分词后匹配。term 是查询方式,不对搜索词分词,做精确匹配。通常 keyword + term 配合使用,text + match 配合使用。
Q3: query 和 filter 的区别?
关键词:评分 vs 过滤、缓存
query 会计算相关度评分,影响结果排序;filter 不计算评分,只做是否匹配的判断,并且有缓存机制。不需要排序的查询条件优先用 filter,性能更好。
Q4: ES 如何处理深分页问题?
关键词:from+size 的性能瓶颈、search_after、scroll
from + size 的实现是:每个分片都返回 from + size 条数据到协调节点,协调节点做全局排序后取 from 到 from + size 的部分。分页越深,每个分片返回的数据越多,性能越差。解决方案:实时翻页用 search_after(基于上一页最后一条的排序值),批量导出用 scroll(创建快照游标)。
Q5: ES 写入数据的过程?
关键词:内存 buffer -> refresh -> segment -> merge
数据先写入内存 buffer 和 translog(事务日志),每隔 1 秒(refresh_interval)将 buffer 中的数据生成一个新的 segment(这时数据才可搜索)。translog 保证了 refresh 之前数据不丢失。后台定期将小 segment 合并为大 segment(merge),减少文件数量。
小结
ES 的核心就是倒排索引 -- 从关键词直接定位文档,避免全表扫描。理解了 text vs keyword(分词 vs 不分词)、match vs term(查询词分词 vs 不分词)、query vs filter(评分 vs 过滤)这三组对比,ES 的查询体系就清楚了。性能优化的核心思路是:减少不必要的计算(用 filter 代替 query)、减少数据传输(避免深分页)、利用批量操作减少网络开销。