ElasticSearch(一):核心概念、倒排索引与分词映射
ElasticSearch(一):核心概念、倒排索引与分词映射
导语:ES 面试的地基是「概念 + 数据结构」。本篇先理清集群/节点/索引/分片/副本这套术语与关系型数据库的对应关系,再深入倒排索引与正排索引的分工、BM25 打分、Analyzer 三件套,以及 Mapping、nested 这些用错就直接影响功能的建模细节,共 13 题。
一、核心概念与集群结构
1. ES 的核心概念有哪些?和关系型数据库怎么类比?
答:
| ES 概念 | 关系型数据库类比 | 说明 |
|---|---|---|
| Cluster(集群) | 一个数据库实例(或整个库) | 由唯一名称标识的一组节点,提供联合索引与搜索能力 |
| Node(节点) | 单个服务进程 | 集群中的一个实例,有角色之分(见第 4 题) |
| Index(索引) | 表(Table) | 文档的集合,映射到若干主分片,配有 Mapping |
| Document(文档) | 行(Row) | JSON 格式的最小存储与检索单元,有唯一 _id |
| Field(字段) | 列(Column) | 文档中的键值,类型由 Mapping 决定 |
| Mapping | 表结构(Schema) | 定义字段类型、分词器、是否索引等 |
| Shard(分片) | 分库分表的「分片」 | 索引水平切分后的单元,每个分片是一个独立的 Lucene 索引 |
| Replica(副本) | 主从副本 | 分片的拷贝,提供高可用与读扩展 |
重要修正(版本差异):很多资料说「索引 ≈ 数据库,type ≈ 表」,那是 6.x 及之前的类比(当时一个索引下可以放多个 type)。7.0 起 type 被彻底移除,一个索引只能有一种文档结构,所以索引对应的是「表」。面试按 7.x/8.x 说「索引 ≈ 表」更准确。
两个必须记住的约束:
- 主分片数(
number_of_shards)创建后不可修改,副本数(number_of_replicas)可随时调整; - 分片是数据分布与并行的最小单位:写入按分片路由,查询按分片并行(见《ElasticSearch(二)》)。
2. 为什么需要分片和副本?主分片数为什么不能改?
答:
分片解决「单机装不下 + 单机算得慢」:数据水平切分到多节点,突破单机磁盘与算力上限,并让查询可以并行。
副本解决「高可用 + 读吞吐」:主分片所在节点宕机时副本顶上;查询请求可以分摊到副本上,提升并发能力。
一个索引 3 主分片 + 1 副本,共多少分片?
共 6 个分片(3 主 + 3 副本)。数据被水平切成 3 份,每份存两遍(1 主 1 副),所以磁盘占用约为原始数据的 2 倍——副本只增加冗余,不增加数据种类。
为什么主分片数不能改(高频追问):
文档定位分片的公式是:
shard = hash(_routing) % number_of_shards # _routing 默认是 _id这个取模是文档定位的唯一依据。 如果主分片数发生变化,所有文档的路由结果都会改变——原来存在分片 1 的文档会算到分片 2,等于数据全部"找不到了"。所以:
- 想改主分片数,只能 reindex 重建索引(用别名做零停切换,见《ElasticSearch(四)》);
- 或者用
_shrink/_splitAPI 做本地重建:_shrink(收缩):减少主分片数,目标数必须是原分片数的因数,同时需要先把分片集中到同一个节点、索引设为只读;_split(拆分):增加主分片数,目标数必须是原分片数的整数倍;
- 副本数改动只是元数据变更,改完会触发一次分片分配(写操作),不会影响数据路由。
3. 分片数量该如何规划?
答: 分片数是一锤定音的决策(见第 2 题),规划不合理只能重建索引,所以要按经验值算:
两条官方经验法则:
| 维度 | 经验值 | 说明 |
|---|---|---|
| 单个分片大小 | 30~50GB(日志场景可放宽到 50GB) | 太小则分片数过多、固定开销大;太大则恢复慢、并行度不足 |
| 分片数与堆内存 | 每 1GB 堆内存不超过 20~25 个分片 | 分片是有状态的,每个分片都要占用堆内存与文件句柄 |
分片过多的代价(比想象中大):
- 每个分片都是一个独立 Lucene 索引,有自己的段、文件句柄、内存开销;
- 集群状态(cluster state)膨胀:所有分片的元数据要在 master 与各节点间同步,分片越多 master 压力越大;
- 一次查询要打到更多分片,协调节点合并结果的成本上升,尾延迟变差(
search线程池排队)。
分片过少的代价:
- 单分片过大 → 故障恢复慢(分片是最小恢复单位,一个 200GB 的分片恢复要很久);
- 无法充分利用多节点并行。
实践建议:
- 不要提前过度分片:数据量不确定时,先用少量主分片(1~3 个)+ 按时间滚动索引(Rollover),随着数据增长由 ILM 自动切新索引;
- 不要盲目「每节点一个分片」;
- 8.x 默认
number_of_shards: 1(7.0 起从 5 改为 1),说明官方也倾向「宁少勿多,靠索引滚动来控制规模」; - 冷数据用
_shrink合并分片、force_merge合并段来降成本(见《ElasticSearch(四)》)。
一句话:分片数要按「目标分片大小 + 堆内存上限」倒推,并优先用「索引滚动」而不是「多分片」来扩展。
4. 节点有哪些角色?生产如何规划?
答: 角色决定节点干什么活,也决定它该配多少资源。
| 角色 | 职责 | 资源特点 |
|---|---|---|
| master-eligible | 参与选主、管理集群元数据(索引/映射/分片分配) | 轻量,不做重查询、不存数据 |
| data | 存储分片、执行 CRUD 与搜索 | 吃 CPU / 内存 / 磁盘 IO |
| ingest | 执行 ingest pipeline 做写入前预处理 | 吃 CPU |
| coordinating | 只做请求路由与结果聚合(不承担任何角色的节点即为纯协调节点) | 吃 CPU + 内存(聚合要汇数据) |
| ml / transform | 机器学习与数据转换 | 吃 CPU/内存,通常独立部署 |
数据分层角色(7.9+ 引入,是冷热架构的基础):
data_content(内容类数据)、data_hot、data_warm、data_cold、data_frozen;日志与指标场景另有 data_logs、data_metrics、data_synthetics。配合 ILM 可以把数据从 hot 自动滚到 warm/cold/frozen,实现成本分层。
版本红线(8.x 必知):
- 7.x:用
node.master: true/node.data: true这类布尔开关配置角色; - 8.0 起这些开关已被移除,必须用
node.roles: [...]显式声明角色; node.roles: []表示该节点不承担任何角色,即纯协调节点;- 单机开发用
discovery.type: single-node;8.x 新装默认开启安全(xpack.security.enabled: true),需要处理证书与账号,否则连不上。
生产规划建议:
- 小集群(< 10 节点):混合角色即可,简单运维;
- 大集群:按职责分离——专用 master(3 台奇数台,不存数据) + data(按 hot/warm/cold 分层)+ coordinating(承接重聚合查询)+ ingest;
- master 一定要"轻":master 卡住会导致整个集群元数据变更停摆(无法建索引、无法分配分片);重查询或 GC 停顿可能让它被误判失联而反复触发选主;
- 候选 master 用奇数台(3/5):既满足多数派,又避免平票(见《ElasticSearch(四)》)。
二、倒排索引与相关性
5. 什么是倒排索引?为什么检索快?
答: 倒排索引(Inverted Index)把「文档 → 词」反过来,变成「词 → 文档列表」,是 ES 检索快的根本原因。
两个核心结构:
- 词典(Term Dictionary):所有不重复的词,按字典序排列;
- 倒排表(Posting List):每个词对应的文档列表,常附带词频 TF、位置 position(用于短语查询)、文档频率 DF。
检索 "elasticsearch" 时,直接查词典定位到该词,取出它的倒排表就得到了所有相关文档 ID,完全不需要遍历文档内容——这就是它相对「全表扫描」的本质优势。
词典为什么省内存:FST
Lucene 用 FST(Finite State Transducer) 压缩存储词典:
- 它是一张有向无环图,公共前缀与后缀路径被复用,因此比普通 Trie 更省空间;
- 查找复杂度与词长相关(逐字符沿状态转移),与词典规模无关;
- 配合「按块存储 + 块索引」,还能避免把整个词典常驻堆内存。
倒排表为什么也省空间(三项压缩技术):
| 技术 | 作用 |
|---|---|
| 增量编码(delta)+ FOR | docId 在倒排表中是递增有序的,所以只存「与上一个 docId 的差值」,再用 Frame of Reference 按 128 个一组定长压缩 → 大数变小,压缩率很高 |
| Roaring Bitmap | 对稀疏的大 docId 集合(如 filter 缓存)改用分桶位图,交并运算极快 |
| 跳表(Skip List) | 在 posting list 上建多级"跳表",多条件求交(AND)时可以跳跃前进,而不必逐条对齐 |
一句话分工:倒排索引负责「找到有哪些文档」(检索),正排索引 doc_values 负责「文档的这个字段值是多少」(排序/聚合)——见第 7 题。
6. 倒排索引是怎么构建出来的?
答: 构建流程:
原始文档
→ Analyzer(字符过滤 → 分词 → 词项过滤) 得到一串词项
→ 为每个词项记录 (term, docId, 词频, 位置)
→ 按 term 汇总排序 形成词典
→ 每个 term 挂上它的倒排表(docId 列表 + 位置等)关键点:
- 分词决定了「能搜到什么」:分词器把什么切成词,词典里就有什么词。分错了,用户就搜不到;
- 英文按空格/标点切分即可;中文必须用中文分词器(IK、jieba、THULAC 等)。ES 内置的
standard分词器对中文是按单字切分的(如「搜索引擎」→ 搜/索/引/擎),这也是「为什么中文搜索效果差、必须装 IK」的原因; - 同义词、停用词过滤、词干提取(stemming)都在 Analyzer 阶段完成,属于写时处理;
- 位置信息(position) 是实现
match_phrase短语查询和highlight高亮的基础; - 词频与文档频率是 BM25 打分的输入(见第 8 题)。
重要修正:
ik_max_word/ik_smart不是 ES 内置分词器,而是第三方 IK 插件(elasticsearch-analysis-ik)提供的。ES 内置的是standard、simple、whitespace、keyword、pattern、stop、english、ngram等。答错这点会被认为没实操过。
7. 什么是正排索引(doc_values)?为什么排序聚合比检索快?
答: 倒排索引的方向是「词 → 文档」,正排索引的方向是「文档 → 值」。 两者分工明确,缺一不可。
为什么倒排不够用:倒排表里只有 docId,要回答「把结果按价格排序」「统计各品牌的商品数」这类问题,倒排索引无能为力——它不知道「文档 42 的价格是多少」,只能反查每个文档(开销极大)。
doc_values(正排索引) 就是为此而生:
- 列式(columnar)存储:同一字段的所有值按文档顺序连续存放,天然适合「按字段扫描」的排序与聚合;
- 默认开启(
text类型除外):磁盘友好,可 mmap 访问,不占 JVM 堆; - 适用:
keyword、数值、日期、boolean等不分词字段。
fielddata 是什么,为什么不能用:
text 字段默认没有 doc_values(因为分词后一个文档会对应该字段的多个词项,无法按"文档→单值"列式存储)。如果强行对 text 做排序/聚合,需要开启 fielddata: true,它会把词项及其文档关系加载到 JVM 堆内存:
- 极高内存开销,很容易 OOM;
- 加载与失效代价大;
- 生产不推荐,正确做法是给字段加一个
keyword子字段(见第 10 题)。
记忆点:
| 需求 | 依赖 | 存储位置 |
|---|---|---|
全文检索(match、term) | 倒排索引 | 磁盘段文件 |
| 排序、聚合、脚本取值 | 正排 doc_values | 磁盘(mmap),不占堆 |
对 text 强制排序聚合 | fielddata | JVM 堆(危险) |
8. TF-IDF 与 BM25 有什么区别?
答: 两者都是相关性打分模型,用来决定「哪个文档更相关」,但 BM25 是前者的改良版。
TF-IDF:
score = TF × IDF
TF = 词在本文档中出现的频率(越高越相关)
IDF = log(总文档数 / 包含该词的文档数) (越罕见越重要)两个明显缺陷:
- TF 无上限:出现 100 次就得 100 分,权重线性增长——但实际上出现 10 次和 100 次的相关性差异没那么大;
- 没有文档长度归一化:长文档天然包含更多词、TF 更高,会无条件占优。
BM25(Lucene 6 / ES 5.x 起默认): 在 TF-IDF 上引入两个关键改进——
| 改进 | 作用 | 参数 |
|---|---|---|
| 词频饱和 | TF 增长到一定程度后边际收益递减(趋于一个上限),避免"堆词"刷分 | k1(默认 1.2) |
| 字段长度归一化 | 相对于该字段的平均长度做归一化,短文档中命中的词更有价值 | b(默认 0.75) |
面试要点:
- 说「ES 用 TF-IDF 打分」是错的——TF-IDF 是理论模型,ES 实际用的是 BM25;
- ES 的
_score除了 BM25,还受boost、query normalization、多字段(multi_match、best_fields/most_fields)等影响; - 纯文本相关性往往不够,业务排序要叠加业务权重:用
function_score(按销量/时间/距离加权)或rank_feature做 LTR 风格的排序; - 分片数会影响打分:每个分片本地计算 IDF,小数据量时不同分片之间打分不可比,需要更准的分值就用 DFS Query Then Fetch(见《ElasticSearch(三)》)。
三、分词与 Mapping
9. Analyzer(分词器)由哪几部分组成?常见分词器怎么选?
答: Analyzer 是「文本 → 词项」的三段式流水线:
| 组件 | 数量 | 职责 | 例子 |
|---|---|---|---|
| Character Filters | 0..n(可选) | 对原始文本做预处理 | 去 HTML 标签(html_strip)、字符映射替换 |
| Tokenizer | 1(必选) | 切词 | standard(按 Unicode 规则)、whitespace、keyword(整串不切)、ik_smart |
| Token Filters | 0..n(可选) | 对词项加工 | 转小写(lowercase)、去停用词(stop)、同义词(synonym)、词干提取(stemmer) |
常见分词器:
| 分词器 | 来源 | 说明 |
|---|---|---|
standard | ES 内置(默认) | 英文友好;中文按单字切分,效果差 |
whitespace / keyword / pattern / english | ES 内置 | 按空格切 / 整串不切 / 正则切 / 英文词干 |
ik_max_word / ik_smart | 第三方 IK 插件 | 中文分词:max_word 细粒度(召回高、索引词多),smart 粗粒度(更准、索引词少) |
| jieba / THULAC / hanlp | 第三方 | 其他中文分词方案 |
analyzer 与 search_analyzer 的区别(高频考点):
- 索引期用
analyzer,查询期默认用同一个;但对中文常用两种粒度分开配置:
"content": {
"type": "text",
"analyzer": "ik_max_word", // 索引时:细粒度切,词多,召回率高
"search_analyzer": "ik_smart" // 查询时:粗粒度切,语义准,避免过度拆分导致噪声
}调试手段:用 _analyze API 直接看分词结果,是排查「为什么搜不到」的第一手段:
GET /_analyze
{ "analyzer": "ik_max_word", "text": "星辰逐梦的搜索引擎" }10. keyword 与 text 类型有什么区别?怎么选?
答:
| 维度 | text | keyword |
|---|---|---|
| 是否分词 | 会分词(按 Analyzer 切) | 不分词,整串作为一个词项 |
| 适用 | 全文检索(标题、正文、描述) | 精确匹配、聚合、排序(订单号、状态、标签、用户 ID) |
| 聚合/排序 | 不支持(除非开 fielddata,不推荐) | 支持(走 doc_values) |
| 查询方式 | match、match_phrase | term、terms |
最佳实践:一个字段同时映射两种(多字段 fields)——这就是 ES 默认 dynamic mapping 对字符串的处理方式:
"title": {
"type": "text", // 主字段:全文检索
"fields": {
"keyword": { "type": "keyword", "ignore_above": 256 } // 子字段:精确/聚合/排序
}
}这也是「为什么我的字符串字段自动多了一个 .keyword 子字段」的标准答案:ES 希望你既能 match 全文检索,又能对同一份数据做 term 精确匹配与聚合排序。
ignore_above 的作用:超过该长度的值不写入该子字段的索引(但仍保留在 _source 中),防止超长文本把 keyword 索引撑爆。常见坑:字段值超过 256 字符导致聚合时"少了一部分数据"。
选型一句话:要搜(模糊、分词、相关度)→
text;要精确/聚合/排序 →keyword;两者都要 → 多字段。
11. 什么是 Mapping?dynamic mapping 与显式 mapping 有什么区别?
答: Mapping 是索引的"表结构",定义每个字段的类型、分词器、是否索引、是否支持聚合等。分为两种生产方式:
1)dynamic mapping(自动推断):写入文档时 ES 自动为未知字段推断类型,规则大致为:
| JSON 值 | 推断结果 |
|---|---|
| 字符串 | text + keyword 子字段(并且可能尝试识别为 date) |
| 整数 / 浮点 | long / float |
| 布尔 | boolean |
| 对象 | object(会被扁平化) |
自动推断的三个风险(高频):
- 类型推断错误难以修正:已存在的字段类型不能修改,只能
reindex重建。典型的坑是「数字型 ID 被推成long,之后想当字符串精确匹配、想保留前导零就晚了」; - 日期字符串被识别成
date:"2026-01-01"这类值会被推成date,之后无法再按 keyword 做排序分词(虽然 date 能排序,但语义不同); - 对象数组被扁平化导致关联丢失(见第 12 题)。
2)显式 mapping:生产必须显式定义(或用索引模板统一约定),字段级别的常用开关:
| 参数 | 作用 |
|---|---|
index: false | 不建索引(不能搜),但仍存在 _source 中,节省空间 |
doc_values: false | 不支持排序/聚合(不需要时关闭可省磁盘) |
enabled: false | 整个对象不索引、不解析(常用于存原始 JSON 备查) |
dynamic: true / false / strict | 允许自动加字段 / 新字段只存不索引 / 遇到未知字段直接报错(生产推荐 strict) |
analyzer / search_analyzer | 分词器(见第 9 题) |
null_value | 为 null 时使用的替代值(默认 null 不可索引) |
两个必知结论:
- Mapping 只能"新增字段",不能修改已有字段类型——想改只能 reindex(或
_shrink/新建索引+别名切换); _source保存原始文档:用于返回结果、更新(部分更新要先读_source+ 重建)、以及 reindex 的数据来源。设置_source: false虽然省空间,但会丢掉 reindex 与高亮能力,慎用。
12. object 与 nested 类型有什么区别?
答: 核心差异是「数组内的字段关联关系是否被保留」。
默认的 object 类型会被扁平化(flatten):
// 原始文档:两个用户,分别是 (小明,18) 和 (小红,20)
{ "users": [ { "name": "小明", "age": 18 }, { "name": "小红", "age": 20 } ] }
// ES 内部实际索引成(数组内部的对应关系丢失了):
users.name = ["小明", "小红"]
users.age = [18, 20]后果:查询 users.name = "小明" AND users.age = 20 会错误命中这条文档——因为扁平化后「小明」和「20」看起来属于同一组,但实际上它们来自不同的对象。
nested 类型怎么解决:把数组中的每个对象单独索引成一个隐藏的独立文档,并保留与父文档的关联,查询时必须显式用 nested query 指定路径:
"users": { "type": "nested" }{ "query": { "nested": { "path": "users",
"query": { "bool": { "must": [
{ "match": { "users.name": "小明" } },
{ "match": { "users.age": 20 } }
] } } } } }nested 的代价(面试要主动说):
- 查询更慢:每个 nested 查询都要做额外的 join 式匹配;
- 文档数变多:每个嵌套对象都是独立 Lucene 文档,占用更多空间;
- 写入/更新更重:修改一个 nested 对象要重建整个 nested 文档(不能只改其中一个子对象);
- 聚合需要
nested+reverse_nested才能回到父文档维度。
其他可选方案:
- 改成"平行数组":把
users拆成user_name[]和user_age[]两个独立字段——只有在业务上确实不需要跨字段关联时才可行(但这就和扁平化一样丢了关联); join(父子文档)类型:parent-child关系,适合「一对多且子文档数量大、需要频繁增删子文档」的场景,比 nested 写入更轻,但查询(has_child/has_parent)更慢、实现更复杂;- 拆成独立索引:彻底解耦,但失去单文档内的原子关联。
选型口诀:数组元素需要跨字段组合查询 →
nested;只是多值筛选(互不关联)→ 普通数组;量大且频繁变更 →join或独立索引。
四、生态与选型
13. ES 与 Solr、OpenSearch 有什么区别?怎么选型?
答: 三者都基于 Lucene(Java 全文检索库),差异在分布式能力、生态与治理方式。
ES vs Solr:
| 维度 | Elasticsearch | Solr |
|---|---|---|
| 定位 | 分布式搜索与分析引擎,REST + JSON 友好 | 成熟的企业级搜索平台,查询语言强大 |
| 分布式 | 天生分布式,分片/副本/水平扩展开箱即用 | 4.x 后支持 SolrCloud,配置与运维更重 |
| 实时性 | NRT 友好(refresh 1s) | 相对偏批处理,实时性稍弱 |
| 接口 | REST + JSON(DSL) | HTTP/XML、多种响应格式 |
| 生态 | ELK/Elastic Stack、日志与可观测性事实标准、社区活跃 | 传统企业搜索、功能全面但偏重 |
| 现状 | 新项目主流选择 | 存量 Solr 资产、复杂查询特性场景仍在用 |
OpenSearch(重要的生态考点):
- 2021 年 AWS 基于 ES 7.10.2 分叉而成,起因是 Elastic 把许可从 Apache 2.0 改为 SSPL / Elastic License
- (后续 Elastic 又在 8.0 时代新增 AGPLv3 作为可选许可,但社区分叉已经形成独立生态)
- OpenSearch 由 AWS 主导、Apache 2.0 许可,API 大体兼容 ES 7.x,但不兼容 ES 8.x 的新 API 与部分插件;
- 选型影响:云上托管、AWS 生态倾向 OpenSearch;新项目、需要最新特性选 ES。
其他替代方案(日志场景常被拿来对比):
- Loki:只索引标签不索引日志内容,成本极低,适合「按标签查日志」;
- ClickHouse:列存,适合结构化日志的大规模聚合分析,但不做全文检索;
- 选型逻辑:需要全文检索 + 相关性排序 + 聚合分析 → ES/OpenSearch;只需要日志检索与低成本存储 → Loki;纯结构化聚合 → ClickHouse。
下一篇:《ElasticSearch(二)》进入写入链路——NRT 的本质、translog 与 refresh/flush/merge 的分工、文档增删改的"标记"实现、段合并策略,以及写一致性、乐观锁与写入性能调优。
