Elasticsearch 从来都不是数据库:一个被“滥用”的神级搜索引擎

本文核心观点源自 James Blackwood-Sewell 的博客,经深度梳理与重构,以更契合中文技术阅读习惯的方式呈现。

在我们开始之前,请先回答一个简单的问题:你团队里的 Elasticsearch(后文简称 ES),主要被用来做什么?

如果你的答案是“全文搜索”,那么恭喜,你正在正确地使用它。但如果你的答案是“作为我们应用的主数据库”,那么这篇文章或许能帮你避开许多未来注定要踩的坑。

一、 引言:被混淆的“身份”

Elasticsearch 从来都不是一个数据库。

它的本质,是在 Apache Lucene 这个强大的全文搜索库之上构建的搜索引擎 API。它生来就是为了解决“搜索”问题,而非作为记录的系统(System of Record)

甚至 Elastic 官方指南也长期建议:你的事实来源(Source of Truth) 应该放在别处(如 PostgreSQL、MySQL),而 ES 只应作为二级索引存在。

然而,在过去十年中,无数团队在尝到搜索的甜头后,开始尝试将这颗“糖衣炮弹”扩展成主数据库,结果往往伴随着各种意想不到的灾难。

二、 正本清源:什么是“数据库”?

在深入探讨之前,我们必须明确“数据库”在此语境下的定义。我们指的是一个能够作为 OLTP(联机事务处理) 工作负载的主要数据存储系统,即应用程序事实的最终仲裁者。

想想 PostgreSQL(连续多年被评为最受欢迎的数据库)、MySQL 甚至 Oracle。它们的核心职责是:

  • 原子性事务:保证相关操作要么全部成功,要么全部失败。
  • 数据一致性:确保数据在任何时候都处于一致的状态。
  • 可靠的持久化:一旦提交,数据绝不会因系统故障而丢失。
  • 丰富的查询能力:支持复杂的关联查询和数据分析。
  • 安全的模式演进:能够平滑地进行表结构变更。

而 ES,从设计之初,就未曾将以上所有特性作为其核心目标。

三、 滑坡之路:搜索引擎是如何“篡位”的?

故事的开端总是美好的。一个团队使用 Postgres 或 MySQL 存储应用数据,但随着业务增长,数据库自带的文本搜索功能变得力不从心。

此时,ES 闪亮登场:它快速、灵活、易于部署。起初,它只是一个安静的二级索引:数据首先写入数据库,然后通过同步机制(如 Logstash、CDC)复制到 ES 中供搜索使用。

然而,危险的诱惑随之而来:“既然数据已经在 ES 里了,为什么还要多写一次数据库?” 那个保持两者同步的管道,逐渐成了系统中最脆弱的一环。于是,为了“简化”架构,有人提议:干掉数据库,让 ES 上位

就这样,在悄无声息中,记录系统完成了转移,麻烦也拉开了序幕。

四、 “篡位”后的代价:当搜索引擎不堪数据库之重

1. 一致性之殇:缺失的“原子事务”

在关系数据库中,一个经典场景是:创建订单并扣减库存。这是一个事务:两个操作必须同时成功或失败。

而 ES 无法在单个文档之外提供这种保证。写入操作各自独立成功,且可能乱序。如果“扣减库存”失败了,系统就会留下一个已创建的订单和未扣减的库存。起初,团队会试图通过重试或对账任务来弥补,但这本身就意味着 ES 已不再是合格的记录系统——一个真正的记录系统,不应让不一致性随时间悄然滋生。

在读取端,问题同样存在:

  • GET by ID:通常返回文档最新确认的版本(尽管在故障时可能读到“脏数据”)。
  • **SEARCH**:查询的是已异步刷新的 Lucene 段,这意味着刚写入的数据可能无法立即被搜索到。

数据库通过事务边界隔离级别优雅地解决了这些问题,而 ES 两者皆无,因为它作为一个搜索引擎,本不需要这些。

2. 模式迁移之痛:需要“重新索引”的ALTER TABLE

应用总是在演进。昨天还是整数的字段,今天可能需要支持小数;一个文本字段可能需要重命名。

在 PostgreSQL 中,这只是一条 ALTER TABLE 语句。而在 ES 中,索引映射一旦设定,基本是不可变的。最常见的解决方案是:创建一个拥有新映射的索引,然后将所有文档重新导入。

当 ES 作为二级索引时,这个过程虽繁琐但安全——你可以从容地从真正的事实来源(数据库)重新全量同步。但当 ES 本身就是唯一存储时,模式迁移就成了一场高风险的豪赌:你需要在线将整个记录系统搬迁到新结构上,一旦失败,只能依赖备份恢复。一个简单的变更,可能演变为一场运维噩梦。

3. 查询之困:在“无联接”的世界里挣扎

一旦成为主存储,开发者自然会想用它做更多事,而不仅仅是搜索。这时,你就会撞上另一堵墙。

ES 基于 JSON 的查询 DSL 在全文搜索和聚合方面非常强大,但对于关系型查询则显得力不从心。记录系统所期望的基本功能,如 表关联(JOIN),在 ES 中是缺失或仅被部分支持的。

考虑这个简单的 SQL 查询:

1
2
3
4
5
6
7
8
-- 查询平均评分最高的前十款产品,且仅考虑拥有至少50条评论的产品
SELECT p.id, p.name, AVG(r.rating) AS avg_rating
FROM products p
JOIN reviews r ON r.product_id = p.id
GROUP BY p.id, p.name
HAVING COUNT(r.id) >= 50
ORDER BY avg_rating DESC
LIMIT 10;

在 Postgres 中,这是基础操作。在 ES 中,你的选择却很尴尬:

  • 数据非规范化:将评论数据扁平化到产品文档中,每次新增评论都需重写整个产品文档。
  • 嵌套文档:将评论作为子文档嵌入产品,查询和更新同样复杂。
  • 应用层拼接:分别查询产品和评论索引,然后在代码里手动关联,效率低下。

Elastic 公司也意识到了这个问题,并推出了 ES|QL(支持类似查找联接的功能)和 Elastic SQL。但这些功能仍受限于 Lucene 底层的索引模型,并且导致了查询语法体系的混乱(查询 DSL、ES|QL、SQL、EQL、KQL…)。这是进步,但与成熟关系数据库的能力仍不可同日而语。

4. 可靠性之虞:当故障发生时

所有系统最终都会失败。索引和数据库的关键区别在于如何恢复

数据库使用 预写日志(WAL)重做日志(Redo Log) 来保证:一旦事务提交,其所有更改都是持久的,并能在崩溃后干净利落地重放恢复。

在正常操作下,ES 在单个文档级别也是持久的。其 事务日志(translog) 确保已确认的文档在主分片上持久化,能在崩溃中幸存并可恢复。但正如前文所述,这种持久性无法跨越多个文档。没有事务边界来保证相关写入同生共死。因此,故障很可能留下“半吊子”操作,而恢复机制并不会像数据库那样自动回滚它们。

当 ES 只是数据库之上的一个索引时,这个假设可以接受。但当它是你唯一的存储时,事务持久性的缺口,就会直接转化为数据正确性的缺口。

5. 运维之累:为“弹性”付出的稳定性代价

大规模运维 ES 本身就是一个挑战。数据库被期望是一个稳定的基石:你运行它、监控它,并相信它能保障数据安全。

ES 的设计优先考虑的是 弹性(Elasticity) 而非绝对的稳定。分片可以移动,集群可以伸缩,数据可以重新平衡。这种灵活性强大,但也带来了运维的复杂性:分片可能失衡、JVM 堆需要精细调优、重新索引会消耗大量集群资源、滚动升级可能引发流量中断。

许多团队确实成功运维着大型 ES 集群,但基线期望是不同的。关系数据库为稳定性和正确性而设计,因为它默认自己是你的记录系统。ES 则“为速度和相关性做了优化”,将其作为记录系统运行,意味着你需要承担比传统数据库更高的运维风险。

五、 结论:回归本源,各司其职

试图让 ES 承担主数据库的角色,看似简化了架构,实则将两种不同的优化目标(高效的搜索 vs. 可靠的事务)强加于一个系统之上,导致工程复杂度不降反升。你最终得到的是:激增的开发工作量、高昂的运维成本,以及一个无法提供事实来源保证的“伪”数据库。

那么,ES 正确的归宿是哪里?

答案就是它出发的地方:一个无与伦比的搜索引擎。

Elasticsearch 和其底层的 Apache Lucene 是一项了不起的成就,只要你不试图把它当作记录系统,它就能完美地完成其本职工作。

当然,即便“正确”使用,最棘手的部分往往也不是搜索本身,而是其周边的数据同步管道、ETL 作业和摄入层,这些部分极易成为系统的脆弱点。

这也正是 ParadeDB 等新一代数据库试图解决的问题:将 OLTP 事务能力与原生、高性能的全文搜索深度融合在同一个引擎中。你可以选择它作为主数据库,也可以让它作为现有 Postgres 的逻辑从库,从而彻底消除脆弱的 ETL 过程。

技术选型的智慧,在于让合适的工具做它最擅长的事。 当你需要兼具正确性、简洁性和世界级性能的开源搜索时,是时候重新审视你的架构,并考虑像 ParadeDB 这样的现代化解决方案了。