全文启动,我自己在想,把“索引”这两个字拆开读,实际上挺有意思的。 索引(index)在 ES 里,本质上就是个庞大的、开放的、所有东西都能放进去的仓库。想象一下,你站在一个图书馆门口,里面堆满了书,你只管把书扔进去,地板湿不湿、走廊挤不挤、书会不会被老鼠咬,全看你的。ES 里的 index 就是这块地板。它资源消耗庞大,数据量大了之后,你得定期清理,否则整个仓库就爆满,就连把服务器都撑爆。 这种“敞开式”的设计,是 ES 早期的核心逻辑。你希望数据能无限扩展,故此它不赞成像关系型数据库那样锁定单条记录。

要是我把某条数据删了,文件只要没删干净利落,另一台机器看到的还是全量数据。

这听起来可能有点悬,但在别的地方,比如我们的互联网,数据更新频率高了,删不干净利落多难受;但在搜索引擎里,工夫就是票子。

要是删错了,用户查不到结局,那就是个灾难。

故此 ES 选择了“要么一辈子保存,要么一辈子干净利落”的极端模式。 这就引出了它另一个大特征:不用写 SQL。 SQL 是那种既把数据存进硬盘,又专门修好数据库结构的语言。它处理的是结构化的、有明确约束的数据,比如一张几千人的员工表,如何查某个部门的人,如何按工资排序。ES 就不如此走心,它更像是一个大坛子。你扔进去任何格式的数据,它都能“喝”了——JSON、XML、就连是你手写的脏兮兮差文本。哪位管它是有序的、乱序的、有空的、有间隙的呢,只要能读,它就能读。 这就好比菜市场。你手里拿着一个 ListBox 里的苹果,要么一堆散乱的鸡蛋,ES 都不管你是如何装的,它只管把筐子里的东西倒进数据库,然后让你挑。

这就是为啥我们要用 Lucene 要么 teradata 这种预置好的结构存数据,而不是让 ES 自己建表。ES 就是那个超级大肚皮的收纳箱,负责存和给,不负责管你东西长啥样。 这种“不管你如何放”的本事,造就了它作为搜索引擎的“万能狂魔”身份。它不关心你数据是结构化还是非结构化,也不关心它是存了才删还是删了就放。它只管“有”就行。 这种“不区分数据形态”的设计,带来了一些意想不到的副功能,也就是大家常说的“时空永存”(time-space eternity)。 举个例子,假设你在 2021 年 1 月 1 日创建了一个名为 `logs2021` 的索引,当时只存了当天的日志。2022 年 1 月 1 日你突然想找回 2021 年的旧日志,却发现 ES 根本帮你找不到,出于它默认这个索引是“只读”的、只针对 2021 年,并且数据全体不可变。 这个逻辑有点硬,但它确实存有。

要是你不想让数据“过期”,要么想保留历史记录,你只能另开一个新索引。

比如新索引叫 `logs2022`,新建索引时,要是开启了“永久保存”(permanent),那么所有创建它之前的旧数据都会自动追加进来,成为新索引的一局部,不再视为“删除”。 这就害得了一个怪现象:你实际上是在不断创建新索引来保存旧数据。想象一下,你每天往一个本子上记字,明天早上发现昨天写的字被划掉了,你该如何办?你不会直接重写,你只会把昨天本子上剩下的字抄到新本子上。每天抄一次,直到你记满为止。出于今天新写的字,明天早上可能会变成“昨天”的旧数据。 ES 的“时空永存”机制,就是把这种“抄字”操作无限放大了。

要是索引从 2020 年启动到目前,每天新索引一个,每天数据量假设是 10 万行,那目前就有 1000 万个索引。

不是每个索引都存数据,但肯定有几十个。

这些索引分别存着 2020 年 1 月 1 日的快照、2 月 1 日的快照……一直到今天的状态。 查 2021 年的日志,你就得翻遍整个 2021 年的所有快照,去对应的索引里找。

要是查错了,那个索引里的数据可能早就被覆盖了(比如 2021 年 1 月 1 日的日志在 2 月 1 日的快照里被新日志吞掉了)。

这就变成了“垃圾进,垃圾出”(GILO)的噩梦,要不就你贼小心维护。 这种设计让 ES 在构建大规模文档索引时,务必贼谨慎。你知道数据在变,便你得把数据分成不与此同工夫片存,每个工夫片一个索引。但这又带来了另一个难题:工夫片越多,查询越慢。 举个例子,你需求查 2021 年 1 月 1 日某个公司的所有订单。 - 方案一:只查 2021 年的一个索引,查得快,但要是 2021 年 1 月 1 日后面形成了事故,数据丢了,你查不到。 - 方案二:查所有包含 2021 年 1 月 1 日数据的索引。

要是每个索引都存了当天的数据,你就要打开几十个索引,比对几十个字段,才能拼凑出结局。

这就像你要查 2021 年 1 月 1 日某个公司的所有订单,你得打开 2021 年 1 月 1 日、2 月 1 日、3 月 1 日……直到今天的所有索引,逐一比对。 这就是 ES 在处理大范围查询时的痛点。它精通“最近的东西”(最近 100 行),但拖家带口查“挺久那会儿的东西”就挺费事。

这就是为啥 Elasticsearch 官方推荐用分词器(分词索引)来处理大局部查询,分词索引内部也是基于工夫切片管理的。 并且,ES 的查询本事也受限于它的底层存逻辑。它本质上是一个“文档数据库”,不是“关系数据库”。别看它赞成复杂的查询,比如“找到所有 A 和 B 都存有的记录”,但它的引擎底层是基于文件的,不像关系型数据库那样靠索引树和连接算法。

故此它的查询速度、复杂度的上限,和关系型数据库是一脉相承的,只是它跳过了那层抽象,直接面对的是物理文件。 这就害得了一个有趣的场景:在某些复杂的业务需求下,ES 可能会“卡”。

比如你要写一个关联查询,要么需求跨多个表做贼复杂的过滤。别看 ES 的 API 挺强大,能处理 100 万级别的数据,但在某些边缘情况下,要是数据量特别大要么逻辑忒复杂,它确实会执行挺久。 再加上,ES 本身就是一个“服务层”。它通过一个 HTTP 接口(主要是 RESTful API)来暴露这些逻辑。

这意味着你每次想删数据、查数据,都得花几分钟去 connection 那个端口,建立连接,发送请求,接收结局。

这中间的工夫延迟,在高频写入的场景下,可能就变得明显了。 这就害得了一个悖论:ES 为了追求极致的写入速度和灵活性,牺牲了一定的查询复杂度和极致的低延迟。它让你能省事地把垃圾数据随意丢进去,但要是你非要争个高(比方说需求毫秒级延迟查数据),就得小心翼翼,依赖分词索引和分片策略。 还有,ES 的“写入”和“查询”在架构上是分离的。写入时,数据被压入磁盘,当时不感知到任何查询请求。直到你查询时,磁盘搜索到数据,再写回内存,序列化,再回写磁盘。

这个过程有延迟,特别是在磁盘 IO 瓶颈的时候。为了优化这个,ES 默认会把数据分成多个分片(shard),每个分片独立写入,读的时候也从对应的分片取。但这又增添了路由和管理的复杂度。 故此,当你回头看 ES 的原理图,你会发现它实际上是个挺有意思的混合体。它既相关系型数据库的“数据清洗”本事(别看有内存中的 pipeline 来做),又有搜索引擎的“海量文档”处理本事。它准数据无限增长,准格式任意变化,准数据“永生”,但这种永生是以“数据膨胀”和“查询延迟”为代价的。 没有它,搜索引擎就是“数据垃圾场”;有了它,数据能够省事入库,但查询就要精打细算。

这就是 Elasticsearch 的魅力与无奈。它不追求完美的平衡,它追求的是在数据无限增长的前提下,尽可能快地还你搜索结局。 我有时候会想,要是 ES 能像关系型数据库一样,准你锁定单条数据,准你创建真正有意义的“关系”(表、字段、外键约束),那世界会不会不一样?或许,数据就能确实“不朽”,查询也能确实“秒开”。

可惜,它还是那个“扔进去就能喝,喝完了就没了”的容器。 这就是为啥我们在使用 ES 时,要格外注意数据的生命周期管理,要时刻警惕“时空永存”带来的数据污染风险。

毕竟,再好的仓库,要是里面塞满了本不该存有的旧书,也读不到拿得出手的新书。