当产品需要真正的搜索——容错、按相关性排序、在百万级文档上飞快——关系型的 WHERE text LIKE '%query%' 就崩了,因为它要扫每一行、还没法排序。Elasticsearch(构建在 Apache Lucene 之上)用一种根本不同的数据结构——倒排索引(inverted index)——加上一层分布式引擎来扩展,解决了这个问题。它支撑了全文搜索、日志分析(ELK 技术栈里的 "E")和可观测性后端。理解它,基本就是理解倒排索引和相关性打分。
- 倒排索引把每个词项(term)映射到包含它的文档列表,所以搜索是一次快速查表,而不是全表扫描。
- 分析器(analyzer)在索引和查询两端把文本切成词项(小写化、词干提取、去停用词)——这就是为什么搜 "Running" 能命中 "run"。
- 不只是匹配,而是相关性——结果用 BM25 打分并排序,最匹配的排在前面。
- 文档 → 索引 → 分片 → 副本——JSON 文档放在 index 里,切成分片(shard)散布到节点,每个分片再做副本(replica)。
- 近实时,不是即时——新建索引的文档要等一次 refresh(默认约 1 秒)后才可搜。
- filter 与 query——filter 是"是/否"且可缓存(快);query 计算相关性分数。
- 它不是你的主存储——它是放在真实存储旁边的搜索索引,不是替代品。
Elasticsearch 把数据模型反了过来:不是"对每篇文档扫它的文本",而是建一个倒排索引("对每个词项,哪些文档有它?"),让全文搜索变成一次查表。分析器对文本分词并归一化,使匹配更灵活;BM25 按相关性排序。数据分片并跨节点做副本,一次查询扇出到所有分片再汇总。它是近实时的,区分可缓存的 filter 与计分的 query,而且是补充而非替代你的主数据库。
倒排索引
普通数据库索引(或暴力扫描)是按文档组织的:给一行,找它的文本。全文搜索需要反过来:给一个词,找所有包含它的文档。倒排索引正是干这个——它是从每个不同词项到一个倒排列表(posting list)(该词出现的文档及位置)的映射。
文档: 1="the quick fox" 2="quick brown dog" 3="lazy fox"
倒排索引:
"quick" → [1, 2]
"fox" → [1, 3]
"brown" → [2]
"lazy" → [3]
搜 "quick fox" → 求交集 [1,2] ∩ [1,3] = 文档 1 排最高
(两词都中);文档 2、3 各中一词
搜 "quick fox" 会查每个词项的倒排列表再合并——瞬间完成,与语料规模无关,因为不需要扫文档正文。这与我们 自动补全(typeahead) 里的 trie 是同一个思路,只是推广到了文档中任意位置的任意词。
分析:分词与分析器
要让搜索"聪明",原始文本在被索引前必须归一化。分析器跑一条流水线:分词器(tokenizer)把文本切成词项,然后词项过滤器对它们变换——小写化("Fox" → "fox")、词干提取(stemming)("running"/"ran" → "run")、去停用词("the"、"a")等等。关键是,索引时和查询时跑同一个分析器,所以查询被归一化到与被索引词项一致。这就是为什么搜 "Running" 能找到含 "ran" 的文档——两者都归到词干 "run"。挑选分析器(按语言、加同义词等)是做好搜索质量的大部分功夫。
相关性打分
搜索不只是"哪些文档匹配",而是"哪个匹配得最好"。Elasticsearch 给每个匹配文档打分并按排名返回。默认算法 BM25(TF-IDF 的改进)奖励查询词出现频繁的文档(词频),但折扣那些在整个语料里都很常见的词(逆文档频率),并对文档长度做归一化。结果:用户搜的一个稀有、具体的词比常见词更值钱,一篇短小却密集含该词的文档会排在只提一次该词的长文之前。相关性排序正是搜索引擎区别于数据库过滤的特性。
文档、索引、分片与副本
数据模型是分层的。文档(document)是一个 JSON 对象。索引(index)是一组同类文档(像一张表)。因为一个索引可能很大,它被切成分片(shard)——每个分片是一个自包含的 Lucene 索引,持有一部分文档——分片分布到各节点。每个分片有一个主分片(primary)加一个或多个副本(replica),用于容错和读扩展。
索引 "logs" → 分片 0 | 分片 1 | 分片 2 (各在一节点,+ 副本)
查询 → 协调节点(coordinating node)
├─ 扇出到分片 0、1、2 (并行搜索)
├─ 每个返回各自的本地 top-k
└─ 汇总 + 合并 + 重排 → 全局 top-k → 客户端
一次搜索是 scatter-gather:协调节点把查询发给每个分片,各分片找出本地最佳结果,协调节点把它们合并成全局排名。注意一个关键取舍:分片数基本在建索引时就定死(改分片数意味着重建索引),所以你要根据预期数据量提前规划分片大小。
近实时搜索
Elasticsearch 是近实时(near-real-time),不是即时。被索引的文档先写入内存缓冲;一次周期性的 refresh(默认约 1 秒)把缓冲变成一个可搜的 Lucene 段(segment)。所以你刚索引的文档大约 1 秒后才可搜,而不是立刻。段是不可变的,后台会周期性合并;持久性由 translog(预写日志)提供。这个 refresh 延迟是索引效率的代价,而且可调(refresh 越快开销越大)。
query 与 filter
一个关键的性能区别:query 问"匹配得多好?"并计算相关性分数;filter 问"匹配吗,是或否?"且不打分。filter 更便宜、结果可缓存,所以结构化约束(status = "active"、日期范围、分类)应当用 filter,而自由文本那部分用计分的 query。
| 方面 | query(must) | filter |
|---|---|---|
| 问题 | "多相关?"(计分) | "匹配吗?是/否"(不计分) |
| 可缓存 | 否 | 是 |
| 用于 | 自由文本相关性 | 精确约束(状态、范围、标签) |
除搜索外,Elasticsearch 还做聚合(aggregation)——对匹配文档做快速的分组/指标统计(按分类计数、按时间直方图)——这让它成为面向仪表盘和日志的实时分析引擎。
用来干什么
- 全文搜索——商品目录、文档、内容站,带排序和容错。
- 日志与事件分析——ELK / Elastic 技术栈(Elasticsearch + Logstash/Beats + Kibana)做集中式日志。
- 可观测性——搜索与聚合 metrics、logs、traces(见可观测性)。
- 自动补全与建议——edge-ngram 分析器或 completion suggester。
Elasticsearch 对比其他方案
对比关系型数据库:SQL 库是你的事实来源,有事务和连接;Elasticsearch 是一个反规范化、最终一致的搜索索引,你从该库灌进去(常通过 CDC 变更数据捕获)。对比向量数据库:经典 Elasticsearch 做的是词法搜索(匹配词项),而向量库做语义搜索(用 embedding 匹配含义)——现代 Elasticsearch 也支持向量/kNN 搜索,把两者结合的混合搜索(hybrid search)越来越常见。
常见坑
- 不是主存储——它是搜索索引;把权威数据放在真正的数据库里,再重建索引进 ES。
- mapping 爆炸——动态索引任意 JSON 的每个字段会把索引撑爆;主动定义 mapping。
- 深分页——"第 10000 页"很贵(scatter-gather 得先排完所有再取);用
search_after/ scroll 代替。 - 分片大小——太多小分片浪费开销,太少限制并行。提前规划,因为重分片意味着重建索引。
Elasticsearch 就是把倒排索引做成了分布式且带排序。倒排索引把搜索变成查表;分析器让匹配灵活;BM25 按相关性排序;分片/副本加 scatter-gather 让它能扩展且高可用。把它当作从主数据库灌入的搜索/分析层——精确约束用 filter,相关性用 query,并记住它是近实时的。
为什么不直接用 SQL LIKE?LIKE 要扫每一行且无法排序;倒排索引(词项→文档)把搜索变成查表,BM25 按相关性排序。
分析器干什么?在索引和查询两端对文本分词并归一化(小写、词干、停用词),所以 "Running" 能命中 "ran"。
query 对比 filter?query 计算相关性分数(不可缓存);filter 是是/否匹配(可缓存)。精确约束用 filter,自由文本用 query。
分布式搜索怎么跑?scatter-gather:协调节点并行查询每个分片,各返回本地 top-k,协调节点合并成全局排名。
它是实时的吗?近实时——新文档在一次 refresh(约 1 秒)后可搜,不是即时;而且它是搜索索引,不是主存储。