Chapter 04

自测与场景辨析

前三章分别建立了名词地图(01)、写入路径(02)、检索与聚合(03)。这一章不教新东西——它逼你把这些回忆出来,并在跨章的真实场景里做判别。读不出答案的地方,就是还没真正打通的地方。

本章检验你能否

  • 脱离教程复述核心概念,并把机制讲到"为什么"那一层
  • 在一个跨章场景里,判断问题出在写入路径还是检索路径,并指出根因
  • 在 Elasticsearch 与 PostgreSQL / 专用向量库 / ClickHouse 之间做出有依据的选型
怎么用这一章

所有答案集中在页面最底部一个折叠块里。先合上前面的章节,把每道题的答案写在纸上或编辑器里,写完再展开对照。直接展开答案,等于把这一章当成第四遍阅读——主动回忆才有效果,看一眼答案"觉得自己会"是最典型的错觉。

题目分三层,认知要求从下到上递增。下层能复述、中层能讲清机制、上层能在没见过的场景里判别——三件不同的事。

应用判别层 跨 01+02+03 · 判别 原理层 讲清机制 · 02 / 03 概念层 能复述 · 01 要求↑
图 4.1三层梯度,认知要求向上递增。注意:在概念层做得又快又顺,不代表应用判别层过得了——后者要的是迁移,不是复述。

A概念层(对应 01 章)

每题一句话能答清即可。卡住的,回 01 章对应小节。

  1. document 和 _source 是什么关系?为什么搜索命中后还要单独存一份 _source?
  2. dynamic mapping 给一个字符串字段默认建出哪两样东西?各自能做什么?
  3. 用一句话说清 cluster / node / index / shard / segment 的包含关系。
  4. 为什么说"shard 是一摞只增不改的 segment",而不是一块可以原地改的整数据?
  5. 对一个默认 dynamic mapping 的 text 字段做 term 查询,为什么常常查不到?两种改法分别是什么?

B原理层(对应 02 + 03 章)

这一层答不出"为什么"就只是背了结论。每题都要落到机制。

  1. refresh 和 flush 各自负责什么?哪个决定"能不能搜到",哪个决定"崩溃会不会丢"?(02 §refresh / §translog)
  2. 删除一条文档后,它占的磁盘空间什么时候才真正回收?为什么不是立刻?(02 §merge)
  3. 倒排索引为什么让 quick AND fox 比行存全表扫描快?关键在"有序"两个字上——具体怎么用到?(02 §倒排索引)
  4. 主分片数为什么创建后不能改?把这个限制还原成一个公式。(03 §routing)
  5. 同一个 range 条件,放进 filter 和放进 must 有什么本质差别?filter 的缓存为什么能"免失效"?(03 §query/filter,根在 02 段不可变)
  6. BM25 的 k1 和 b 各控制什么?为什么一个把关键词塞进短标题的文档能排到前面?(03 §BM25)
  7. 两个内容完全相同的文档,分在不同分片,_score 会一样吗?dfs_query_then_fetch 补的是什么?(03 §两阶段)
  8. 为什么 text 字段不能直接聚合/排序,keyword 可以?从 doc_values 的角度回答。(03 §doc_values)

C应用判别层(跨章场景)

每个场景都不告诉你它属于哪一章——你要自己判断问题出在写入还是检索、根因在哪个机制。这才是"打通"的检验。

场景 1 · 写完搜不到

同事抱怨:"我刚 index 了一条文档,紧接着 search,搜不到,是不是 ES 有 bug?"。这问题出在写入路径还是检索路径?根因是什么?怎么验证、怎么让它在需要时立刻可见?

涉及:02 §refresh(near-real-time)

场景 2 · 相同文档不同分

线上两条几乎一模一样的文档,搜索时 _score 差很多。先从哪两个方向排查?哪个跟"文档落在哪个分片"有关,哪个跟"字段长度"有关?

涉及:03 §两阶段(per-shard IDF)+ 03 §BM25(字段长度归一)

场景 3 · 聚合就 OOM

对一个存商品全文描述的字段做 terms 聚合,节点频繁触发熔断 / OOM。根因是什么?为什么换一个字段就好了?正确做法是什么?

涉及:01 §text/keyword + 03 §doc_values(fielddata 退化)

场景 4 · 删了磁盘不降

批量删除了一半文档,磁盘占用没降、甚至先涨了一点。运维问"是不是删除没生效?"。怎么解释?数据到底删没删?空间什么时候回来?

涉及:02 §merge(soft delete + 段不可变)

场景 5 · 先开 200 个分片?

一个新业务的索引,有人主张"先开 200 个主分片以防将来不够"。怎么评估这个决定?它和"分片数不可变""过度分片"分别有什么关系?数据量不大时这样做的代价是什么?

涉及:03 §routing(分片数不可变)+ 过度分片的开销

D选型辨析:到底要不要用 ES

"向量数据库主要有哪些"那类讨论里,ES 常被当成"全文 + 向量都能干"的万金油。但选型的第一道题,恰恰是要不要上 ES。下面这条判断流,按"先排除、再落地"的顺序走。

① 主导负载=海量日志/指标聚合? 是 ClickHouse 否 ② 主导负载=纯向量 ANN 检索? 是 专用向量库 Milvus / Qdrant 否 ③ 要相关性/混合检索且规模大? 是 Elasticsearch 否 ④ 否则 → PostgreSQL FTS / pgvector 中小规模优先,复用现有库少维护一个系统
图 4.2选型按"先排除特化场景、再落到通用方案"的顺序。注意:ES 不是默认选项——只有"要相关性排序的全文/混合检索 + 规模大"这条路径才真正轮到它;中小规模优先复用 PostgreSQL。
表 4.1 · 四种存储的适用边界
选项什么时候选它短板
Elasticsearch需要相关性排序的全文检索 + 词法/语义混合检索 + 聚合,且规模大运维重、存储放大;纯向量场景浪费内存(在跑一整套搜索引擎)
PostgreSQL FTS / pgvector中小规模;检索与 OLTP 数据同库;想少维护一个系统分词与相关性能力弱于 ES;向量超约 50–100M 后退化
专用向量库 Milvus / Qdrant向量是主要负载、超大规模 ANN 检索不擅长全文相关性与复杂聚合
ClickHouse海量日志/指标的聚合分析无相关性排序、无语言学检索;同样数据 ES 的存储约是其 12–19 倍

E亲手画一张图

亲手画图 · retrieval

合上教程,在纸上或 Excalidraw 里画两张图,只画核心节点:

(1)一条文档从写入到能被搜到的路径:buffer → ? → ? 直到可见;再单独画出 translog 那条持久化的轴。

(2)一次 query_then_fetch 在 3 个分片上的两个阶段,标出每个阶段在分片和协调节点之间传的是什么。

画完回到 02 §refresh 和 03 §两阶段对照:你画的写入图里,refresh 和 flush 是不是两条分开的线?你画的检索图里,第一阶段分片返回的是完整文档,还是只有 id + score?这两个点答错,就是该回去重读的地方。

✓答案

下面是全部答案,按三层 + 选型组织。对照时重点不是“对没对”,而是“能不能讲出为什么”。

展开全部答案(先做完再看)

概念层 A

  1. _source 是写入时原始 JSON 的完整副本,和"用于搜索的倒排/列式结构"是两份独立存储。搜索结构只存切词、统计这些便于检索的派生数据、不保留原文,所以命中后要靠 _source 把整条文档还原给调用方。
  2. 一个 text 字段(过 analyzer、可全文检索)+ 一个 .keyword 子字段(multi-field,原样、有 doc_values、可精确匹配/排序/聚合)。两者互补:全文搜用字段本身,聚合排序用 .keyword。
  3. cluster ⊃ node ⊃(index 被切成的)shard ⊃ Lucene segment。其中 index 是横跨多个 shard 的逻辑集合,shard 才是物理上的 Lucene index 边界,segment 在 shard 内部只增不改。
  4. 因为 Lucene segment 一旦写出就不可变;一个 shard 是若干 segment 累加而成。所谓"改"数据,实际是写新 segment + 在旧 segment 里把老版本标记为已删,而不是原地修改。
  5. 因为 text 字段存的是经 analyzer 切分、规范化(如小写化)后的 term,例如 "Hello World" 存成 [hello, world];而 term 查询不分词,拿整串原文去比对,对不上。改法:用 match(会分词),或查 field.keyword(原样存储的子字段)。

原理层 B

  1. refresh 把内存 buffer 写成一个新 segment 并打开新 reader,决定文档能不能被搜到(默认 1s,near-real-time);flush 把 segment fsync 落盘并清空 translog,配合 translog 决定崩溃会不会丢。一个管可见性,一个管持久化,是两条正交的轴。
  2. 等到后台 merge 把包含该文档的若干 segment 重写成新 segment 时才回收。因为 segment 不可变,删除当下只能在 .liv 位图里标记 soft delete,旧文档仍占空间;merge 前磁盘甚至可能因新旧 segment 并存而先涨。
  3. 倒排索引把每个 term 映射到一个有序的 postings list(doc id 升序)。查询词用同一 analyzer 处理后,在有序 term 词典里二分定位,再对多个 term 的有序 postings 做归并求交/并——全程不碰不相关文档;行存则要逐行扫描、对每个字段做匹配。
  4. shard = hash(_routing) % number_of_primary_shards,_routing 默认是 _id。主分片数是公式里的除数(模数),一旦改变,hash % N 会把已存在的每条文档解析到不同分片,旧数据再也定位不到——所以创建后不可变,要改只能 reindex / split / shrink。
  5. filter 只问"是/否"、跳过打分,结果按每个不可变 segment缓存成一个 bitset;因为 segment 永不改,这个缓存永不需要失效、可被任意复用。must(query context)要算 _score、无法这样缓存,把本该是 filter 的精确/范围条件放进去,白算分还不进缓存。
  6. k1 控制词频饱和:tf 以 tf/(tf+k1) 形式进入,是渐近线,第 10 次出现某词几乎不再加分;b 控制字段长度归一,长于平均的字段被惩罚。所以一个把查询词塞进很短标题的文档,因为字段短、长度归一几乎不罚、词频占比又高,会被 BM25 顶到前面——即使它整体并不相关。
  7. 默认不一定一样。IDF 是每个分片用本地文档频率算的,相同 term 在不同分片 IDF 不同;文档少或路由不均时,两条相同文档因落在不同分片而得到不同分。dfs_query_then_fetch 先加一轮收集全局词频,让所有分片用同一套统计打分(多一次往返、更准)。分片填满后偏差自然收敛。
  8. 聚合/排序需要"doc → value"的访问方向,倒排索引是反方向的"term → docs"。keyword 默认有 doc_values(写入时就建好的列式磁盘结构,走 OS 文件缓存、堆外读取),所以能聚合排序;text 默认没有 doc_values,要聚合/排序就退化到查询时在 JVM 堆上反转索引的 fielddata,几十 GB、触发 GC/OOM。

应用判别层 C

  1. 场景 1:写入路径问题,不是 bug。文档写入后默认要等下一次 refresh(最长约 1s)才进入可搜索的 segment,这就是 near-real-time。验证:等 1s 再搜,或对写入加 ?refresh=wait_for。不要用 ?refresh=true 批量刷,会制造大量小 segment、加重 merge。
  2. 场景 2:两个方向。①分片维度:两条文档可能落在不同分片,per-shard IDF 不同导致打分不同——可用 dfs_query_then_fetch 或在数据量足够时观察是否收敛。②字段维度:BM25 的字段长度归一(b)和词频,使较短字段、词频占比高的那条打分更高。先确认是不是同一分片,再看字段长度差异。
  3. 场景 3:根因是对 text 字段聚合。text 没有 doc_values,聚合退化到堆上的 fielddata,把整列加载进 JVM 堆 → 熔断/OOM。换成 keyword(或字段的 .keyword 子字段)就好,因为它有列式、堆外的 doc_values。正确做法:聚合/排序一律用 keyword,不要打开 text 的 fielddata。
  4. 场景 4:删除已生效,但只是 soft delete——在 .liv 里标记为删,文档仍占着不可变 segment 的空间,所以磁盘不立刻降;新写入/标记还会产生新 segment,短期甚至先涨。等后台 merge 把旧 segment 重写、丢弃被删文档后,空间才回收。可观察 segment 数与 merge 进度,必要时(谨慎)触发 force merge。
  5. 场景 5:不合理。分片数创建后不可变,"以防万一先开多"意味着把一个错误锁死;而过度分片本身有代价——每个 shard 是一个独立 Lucene 实例,带固定的堆、文件句柄、集群状态开销,并占用搜索线程。数据量不大时,几百个小分片只会拖慢查询、加重集群状态管理。正确做法:按目标单分片 10–50GB 估算分片数,需要时用 rollover / ILM 扩展。

选型 D

按图 4.2 的顺序排除:海量日志聚合 → ClickHouse;纯向量大规模 → Milvus/Qdrant;要相关性排序的全文/混合检索且规模大 → Elasticsearch;其余中小规模优先 PostgreSQL(FTS / pgvector),少维护一个系统。核心判断:ES 的不可替代价值在"相关性排序的全文 + 混合检索 + 聚合"三合一;只要其中没有"相关性全文检索"这一项,几乎都有更省的专用或通用方案。