搜索引擎架构 把倒排之上的集群设计、分片陷阱与运维故事讲清了;存储工程·索引结构 把倒排当作众多索引结构之一介绍了;日志栈选型 在可观测场景里比较了 Elasticsearch 与 Loki / ClickHouse;向量检索引擎 专讲 ANN 与 Segment 生命周期;PG 内核·GIN 则把倒排嵌在 RDBMS 页模型里。中间仍缺一层:一条文档如何经 Analyzer 变成 Lucene Segment 里的 term 与 postings,BM25 如何在 postings 上打分,Elasticsearch 的 refresh / flush / translog 各自保证什么。
本文是「全文检索引擎」系列第 1 篇(共 18 篇)。目标不是复述 architecture/42 的集群叙事,也不是再写一遍 HNSW 或 RAG 流水线,而是回答三个定位问题:
- 专用全文引擎相对行存 / 列存 / 向量 / 日志后端,差在哪一层?
- Lucene 库与 Elasticsearch 服务各自钉住哪些抽象?
- 本系列 18 篇与相邻系列的分工边界在哪里?
本文是「全文检索引擎」系列第 1 篇(共 18 篇)。→ 系列目录
篇目 核心内容 第 1 篇 · 全文引擎全景 分工地图、Lucene/ES 分层、系列路线 第 2 篇 · Lucene 文档模型 Field、docID、正排/倒排 第 3 篇 · 分析链 Analyzer CharFilter→Tokenizer→TokenFilter 第 5 篇 · Postings codec freqs、positions、skip、impacts
版本锚定:Apache Lucene 9.x / 10.x(官方 Introduction to Apache Lucene、IndexWriter、PostingsFormat 文档);Elasticsearch 8.x Reference(Architecture、Near real-time search)。机制结论以官方文档与经典倒排综述为准;本篇不粘贴未在本机执行的吞吐数字。
一、最小故事:同一条文档,五种「搜索」会怎么走
假设业务要搜一篇技术文章里的短语「倒排索引」,还要按发布时间排序、高亮标题里的匹配词、在租户
42 下做权限过滤。
| 存储 / 引擎 | 你会得到什么 | 缺什么 |
|---|---|---|
| 行存 B+Tree(PostgreSQL 主表) | LIKE '%倒排%' 全表扫或 trigram
勉强加速 |
无词项级 postings、无 BM25 饱和、短语位置代价高 |
列存(ClickHouse tokenbf_v1 等) |
日志 / 宽表上的近似 token 过滤 | 非 Lucene 段模型;排序聚合语义与 ES 不同 |
PG GIN(tsvector) |
事务内倒排、SQL 与 join 一体 | Entry Tree + pending list,非 Segment codec 路径 |
| 向量引擎(Milvus 等) | 语义近邻 | 稀疏词项相交、短语约束不是主路径 |
| Lucene / ES | Analyzer→term→postings→BM25;短语靠 positions;排序聚合走 DocValues | 分布式运维、mapping、refresh 语义需另学 |
全文检索引擎(本系列以 Lucene 库 + Elasticsearch 服务为主线)要同时回答:文本如何被切成可搜词项、词项如何落到不可变段文件、查询如何在 postings 上相交与打分、近实时可见性与崩溃恢复各在哪一层兑现。后面 Document / Analyzer / FST / postings / IndexWriter / refresh 等篇目,都是为这条故事服务的。
二、为什么需要「引擎内核」这一层
倒排索引的核心操作可以写成:给定查询词项集合 \(Q\),在文档集合 \(D\) 上返回与 \(Q\) 相关的文档子集,并按相关性或业务字段排序。Zobel & Moffat 在倒排文件综述里把这一范式钉为现代文本检索的默认结构:词典(lexicon)+ 倒排列表(inverted lists),查询代价主要由列表长度与相交算法决定(ACM Computing Surveys, 2006)。
算法与数据结构教科书讲清了「词项→文档 ID 列表」。生产系统还要回答教材通常不包办的问题:
| 问题 | 概念层通常不做 | 引擎内核要做 |
|---|---|---|
| 文本规范化 | 举例分词 | Analyzer 链、写放大、多语言 mapping |
| 段文件布局 | 抽象 posting list | FST 词典、block codec、skip / impacts |
| 相关性 | TF-IDF 公式 | BM25 Similarity、norms、avgdl 工程落点 |
| 增量写入 | 批量建索引 | IndexWriter 缓冲、flush、merge、liveDocs |
| 分布式 | 单机倒排 | Shard / Replica、refresh / translog、query+fetch |
| 混合检索 | 稀疏或稠密二选一 | 同 Segment 上 BM25 与 dense_vector 的接口边界(第 15 篇) |
因此「全文检索引擎」不是「给 PostgreSQL 加一个 GIN 索引」那么简单,也不是「会写 Elasticsearch DSL」就够。它处在 倒排索引理论 与 搜索架构 / RAG 应用 之间:下层消费 Lucene codec 与查询执行,上层被 observability/08、llm-infra/17–18 里的召回服务调用。
flowchart LR
arch42["architecture 42<br/>cluster design"]
storage29["storage 29<br/>index structures"]
engine["This series<br/>Lucene + ES kernel"]
vector["vector-engine<br/>ANN segment"]
rag["llm-infra 17/18<br/>RAG recall"]
arch42 --> engine
storage29 --> engine
engine --> rag
vector --> engine
三、五类东西:不要混成一个词
站内与业界常把下列对象统称「搜索引擎」。本系列严格区分:
| 类型 | 代表 | 优化目标 | 本系列角色 |
|---|---|---|---|
| 倒排概念 / 多索引入门 | storage/29 | 理解词典与 posting list | 第 1–2 篇深化到 Lucene 文件 |
| 搜索系统架构 | architecture/42 | 集群、分片、故障叙事 | 第 1、11、17 篇对读,不复述故事 |
| Lucene 库 | 嵌入式 Java 库 | Segment、codec、IndexWriter | 第 2–10 篇主线 |
| Elasticsearch 服务 | 分布式搜索与分析 | NRT、分片、聚合、REST | 第 11–14 篇 |
| RDBMS 倒排 | PG GIN | SQL 事务内倒排 | 第 4、18 篇对照一句 |
| 向量引擎 | Milvus 等 | ANN、Growing/Sealed | 第 15 篇混合边界 |
| 日志后端 | Loki / CH / ES | 可观测流水线 | 第 18 篇回链 observability/08 |
Lucene 与 Elasticsearch 的分层(A 级:Elasticsearch 8.x Reference Architecture 明确写搜索能力由 Lucene 提供):
flowchart TB
subgraph service ["Elasticsearch 8.x service layer"]
rest["REST / Query DSL"]
shard["Index / Shard / Replica"]
nrt["refresh / flush / translog"]
agg["Aggregations / Mapping"]
end
subgraph library ["Apache Lucene 9.x/10.x library"]
iw["IndexWriter"]
seg["Immutable Segment"]
codec["PostingsFormat / FST / DocValues"]
search["Searcher / Similarity / Collector"]
end
rest --> shard
shard --> iw
nrt --> iw
agg --> codec
iw --> seg
seg --> codec
search --> codec
Elasticsearch 的一个 分片(shard) 底层对应一个 Lucene Index(多个 Segment 的集合)。协调节点上的查询,最终落到各分片 Lucene Searcher 的 postings 相交与 Collector 截断上——这是本系列与 architecture/42 的分工:42 讲「有多少分片、集群怎么炸」;本系列讲「分片里面是什么文件、查询怎么在 postings 上跑」。
OpenSearch 与 Solr:与 Lucene 同源或深度集成,许可与发行路径不同;第 16 篇作对照,不单开 Lucene 内核章。
3.1 常见误解
- 「会运维 ES 就等于懂
Lucene」。
refresh_interval、circuit breaker、分片数来自服务层;postings block、FST、TieredMergePolicy 在库层——排障时需要两边对齐(第 7、12、17 篇)。 - 「倒排索引 = Elasticsearch」。PG GIN、Solr、嵌入式 Lucene、旧式 IR 系统都用倒排;差异在段不可变性、codec、分布式语义与事务边界。
- 「全文检索已被向量检索取代」。RAG 实践里稀疏 BM25 与稠密向量常并存;向量解决语义近邻,词项相交与短语约束仍走 postings(第 6、15、18 篇)。争论(有文献支撑):学习排序(LTR)与神经重排可在 BM25 召回之上提升 nDCG,但训练数据偏移、延迟与可解释性使 BM25 仍是生产基线——Robertson & Zaragoza, BM25 and Beyond, FnTIR 2009 与后续 LTR 工业部署文献各自优化目标不同,本系列第 6 篇展开公式,第 18 篇收束选型。
四、学术谱系:从倒排文件到 Lucene Segment
4.1 奠基:倒排文件与压缩
| 工作 | 出处 | 本文引用点 |
|---|---|---|
| Zobel & Moffat, Inverted Files for Text Search Engines | ACM Computing Surveys 38(2), 2006 | 词典 + 倒排列表范式;压缩、跳跃、相交代价模型 |
| Witten, Moffat & Bell, Managing Gigabytes | Morgan Kaufmann, 1999 | 工程化压缩与索引构建;与 Lucene block codec 精神同源 |
| Robertson & Zaragoza, The Probabilistic Relevance Framework: BM25 and Beyond | FnTIR 4(2), 2009 | 相关性打分从 TF-IDF 到 BM25;第 6 篇公式来源 |
Zobel & Moffat 综述的假设包括:词项分布偏斜、文档长度差异大、查询多为少量词项的布尔或加权组合。Lucene 的 不可变 Segment、差分编码 docID、skip list(第 5 篇)都是这一脉络在工程上的落点,而非 ES 独有发明。
4.2 工程分叉:IR 库 → Lucene → Elasticsearch
| 阶段 | 系统 / 文献 | 相对前一代 |
|---|---|---|
| 1990s–2000s | 各类 IR 系统与 MG/SGML 工具链 | 单机倒排为主 |
| Lucene 1.x → 9.x/10.x | Apache Lucene 官方发布 | 可插拔 codec、NRT IndexWriter、FST 词典 |
| Elasticsearch 0.x → 8.x | Elastic Reference | 分片、REST、聚合、translog;Lucene 为嵌入库 |
| OpenSearch | AWS 分叉 | API 大体兼容;Lucene 版本随发行版绑定 |
工程间隙:论文与教科书常以「静态索引 +
批处理查询」为假设;生产则是持续写入、软删除、段合并滞后、refresh
窗口内的近实时可读。Lucene
NRT(DirectoryReader.open(IndexWriter))与 ES
refresh 不是同一语义——第 7、12
篇分别钉库层与服务层。
4.3 开放问题(系列贯穿)
- 统一多模 Segment vs
专用稀疏/稠密路径:Lucene/ES
在统一段上同时承载倒排与
dense_vector时,合并、缓存与 breaker 如何分摊(第 15 篇)。 - 学习排序何时替代 BM25 默认 Similarity:离线训练与在线 concept drift、explain 可审计性之间的张力(第 6、18 篇)。
- 强一致可读 vs 近实时:事务型业务能否接受 refresh 窗口;与 PG GIN 同事务可见性的对比(第 12、18 篇)。
五、写入与查询的最小路径(本系列坐标系)
下列时序是 Lucene 库层的极简视图;ES 在之外包装分片路由与 translog(第 11–13 篇)。
sequenceDiagram
participant App as Application
participant IW as IndexWriter
participant An as Analyzer
participant Seg as NewSegment
participant SR as Searcher
App->>IW: addDocument(fields)
IW->>An: tokenize per field
An-->>IW: terms + positions
IW->>IW: buffer in RAM
IW->>Seg: flush immutable segment
App->>SR: open NRT reader
SR->>Seg: seek term in FST
SR->>Seg: intersect postings + score
步骤与 Lucene 官方 IndexWriter / Searching 文档一致:
- 应用构造
Document与多个Field(第 2 篇:indexed / stored / docValues 正交)。 IndexWriter对需分词的字段调用Analyzer,得到TokenStream(第 3 篇)。- 内存缓冲累积 posting;
flush写出不可变 Segment,含 FST 词典与 postings 文件(第 4–5 篇)。 IndexSearcher在 FST 上定位词项,解码 postings,经Similarity(BM25)与Collector返回TopDocs(第 6、9 篇)。
检索最小故事:查询解析 → 各子句在词典上
seek → postings 相交/并集 → 打分 → Top-K 截断 →(可选)fetch
stored fields
做高亮。这就是本系列要钉住的「引擎层」——不是「match
与 term 用哪个 DSL」,而是
词项如何落在段文件、相交算法如何跳过无关
docID。
六、与相邻系列的分工
| 话题 | 本系列 | 相邻系列 |
|---|---|---|
| 集群分片 / 8000 shard 类陷阱 | 第 11、17 篇机制回链 | architecture/42 |
| B+Tree / 倒排入门 | 第 1–2 篇深化 | storage/29 |
| 日志:ES vs Loki vs CH | 第 18 篇回链 | observability/08 |
| PG GIN Entry Tree | 第 4 篇一句对照 | postgresql-kernel/16 |
| HNSW / DiskANN | 第 15 篇只谈接口 | vector-engine、db-frontier/08 |
| RAG 稀疏召回 | 第 6、15、18 篇回链 | llm-infra/17–18 |
七、十八篇地图
第一部分(01–05):生态定位、Document 模型、Analyzer、FST 词典、postings codec——共同词汇与段文件剖面。
第二部分(06–10):BM25、IndexWriter NRT、MergePolicy、Boolean 查询执行、DocValues / stored——打分与读写路径。
第三部分(11–14):Shard / Replica、refresh·flush·translog、协调节点 query+fetch、聚合与 mapping——Elasticsearch 服务层。
第四部分(15–18):混合检索、OpenSearch/Solr、排障、选型决策树——对照与收束。
系列贯穿的五个问题见 系列 index;本篇只建立坐标系。
flowchart TD
A["01 Overview"] --> B["02 Document"]
B --> C["03 Analyzer"]
B --> D["04 FST"]
D --> E["05 Postings"]
E --> F["06 BM25"]
C --> G["07 IndexWriter"]
G --> H["08 Merge"]
F --> I["09 Query exec"]
E --> J["10 DocValues"]
A --> K["11 Shard"]
G --> L["12 refresh translog"]
K --> L
L --> M["13 Search path"]
J --> N["14 Agg mapping"]
M --> N
F --> O["15 Hybrid"]
K --> P["16 OpenSearch"]
N --> Q["17 Troubleshoot"]
O --> R["18 Selection"]
P --> R
Q --> R
八、实验与证据约定
本系列性能相关实验放在 reproduce/(随第
7、12 篇启动)。环境以本机 Lucene 示例工程
或 Elasticsearch 8.x 单节点 Docker
为主,逐篇交代版本。
硬约束(与 PLAN.md 一致):
- 未跑的 benchmark 不写进结论。
- 千节点吞吐只作带出处的外部引用,不作自测伪装。
- 无本地环境时,正文给出可复现步骤(依赖版本、最小 API),不伪造命令输出。
示例:观察 flush 后段文件(需在本地安装 Lucene 或启动 ES 后执行,以下为步骤而非本机输出):
# Elasticsearch 8.x 单节点:写入后列出分片 Lucene 文件(路径因安装而异)
curl -s -X POST "localhost:9200/demo/_doc" -H 'Content-Type: application/json' \
-d '{"title":"inverted index","body":"lucene segment fst postings"}'
# 在数据节点 path.data 下进入索引分片目录,可见 _N.si、_N.cfs 或拆分的 _N_*.doc 等九、小结
三句话小结
- 全文检索引擎补的是 Analyzer→Segment→postings→BM25 与 ES NRT/分片 两层,不是 architecture/42 的重复,也不是向量引擎的 ANN 重讲。
- Lucene 管段文件与查询执行,Elasticsearch 管分布式可见性与运维语义;排障时要能对齐两层。
- 读本系列时先记住「一条文档如何变成段里的 term」,再去看 refresh 与分片——否则集群故事没有内核抓手。
下一篇进入数据模型:Lucene 文档模型。
参考资料
核心论文
- Zobel, J. & Moffat, A., Inverted Files for Text Search Engines, ACM Computing Surveys 38(2), 2006(倒排文件范式与压缩/跳跃)。
- Robertson, S. & Zaragoza, H., The Probabilistic Relevance Framework: BM25 and Beyond, FnTIR 4(2), 2009(相关性基线;第 6 篇展开)。
- Witten, I. H., Moffat, A. & Bell, T. C., Managing Gigabytes: Compressing and Indexing Documents and Images, 2nd ed., 1999(工程压缩脉络)。
规范 / 文档
- Apache Lucene 9.x/10.x Documentation, Introduction to Apache Lucene、IndexWriter、Searching。
- Elasticsearch 8.x Reference, Architecture、Near real-time search。
站内
返回 系列目录 | 下一篇:Lucene 文档模型
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【全文检索引擎】Lucene · BM25 · Segment · Elasticsearch NRT
补齐搜索架构叙事与 RAG/向量引擎之间的全文检索内核层:以 Lucene 9.x/10.x 拆解 Analyzer、FST、postings、BM25 与 IndexWriter,并以 Elasticsearch 8.x 拆解分片、refresh/translog 与查询路径;OpenSearch 对照与选型收束。
【全文检索引擎】BM25 与 Similarity:公式如何落到 Lucene
从 Robertson & Zaragoza (2009) 的 BM25 公式出发,拆解 Lucene BM25Similarity 如何把 TF 饱和、文档长度归一与 IDF 落到 postings 与 norms;对照 TF-IDF 工程差异,并划定与学习排序的生产边界。
【全文检索引擎】IndexWriter 与 NRT:从缓冲到可打开的 Reader
拆解 Lucene IndexWriter 的 RAM 缓冲、flush 出段与 DirectoryReader.open(IndexWriter) 近实时语义;说明 Searcher 刷新与「刚写入即可搜」在库层与 ES refresh 层各自保证什么。
【全文检索引擎】混合检索边界:BM25 与 dense_vector
只谈 Elasticsearch 8.x / Lucene 上 BM25 与 kNN 的组合接口与段内共存代价,ANN 算法与专用向量引擎细节外链 vector-engine 与 db-frontier/08;指出倒排与向量同 Segment 生命周期尚未统一的开放问题。