Elasticsearch 9 深度实战:列式存储 + 原生向量检索如何重写搜索引擎——从 Lucene 10 内核到 ESQL 全链路拆解
一、背景:为什么 Elasticsearch 9 值得你重新认识
如果你对 Elasticsearch 的印象还停留在「ELK 里的那个日志搜索引擎」,那 9.x 时代你会认不出它。
过去十年,Elasticsearch 走了一条从「搜索引擎」到「数据平台」的路:1.x 时代它是 Solr 的替代品,靠着 JSON DSL 和分布式架构圈粉;5.x 引入聚合分析,开始抢数据库的饭碗;7.x 重构集群协调子系统、默认安全化;8.x 原生支持向量检索,一脚踩进 AI 时代。到了 9.x,它把 Lucene 10 内核、ESQL 管道查询、列式存储和向量数据库能力打包在一起,直接对标「搜索 + 分析 + AI 检索」三合一的定位。
时间线是这样的:
- 2025 年底,Elasticsearch 9.0 拉开大版本序幕,同步升级到 Lucene 10 内核,清理了大量 8.x 时代遗留的弃用 API;
- 2026 年 3 月,9.3.x 系列成为稳定主力版本,社区手动部署教程开始大量出现;
- 2026 年 8 月,Elastic Platform 9.5 发布,一口气带来三个重磅能力:Columnar Mode 列式存储模式、Vector DB 向量数据库索引模式、原生 Prometheus/PromQL 支持。
这三个能力放在一起,信号非常明确:Elastic 不想只做「搜索」,它想做「AI 时代的数据底座」。日志、指标、全文检索、向量检索,全部收进一个引擎。
这篇文章不打算做功能清单式的罗列(那是官方文档的事),而是从内核原理出发,把 ES 9 的存储模型、查询链路、ESQL 执行模型、向量检索的量化原理讲透,然后给出一套可以直接抄的生产级实战:三节点集群部署、索引生命周期管理、中文全文检索、ESQL 分析、RAG 语义检索,最后是性能优化与成本控制的完整清单。
二、核心概念:搜索引擎的地基
要理解 ES 9 做了什么,先得知道搜索引擎的底子是什么。ES 的所有魔法,都建立在 Lucene 之上。
2.1 倒排索引:搜索的本质是「查词表」
关系型数据库用 B+ 树组织数据,定位一条记录靠主键扫描;搜索引擎反过来,它把「文档」拆成「词项」,维护一张 词项 → 文档列表 的映射,这张表就是倒排索引(Inverted Index)。
以一句英文文档 "Elasticsearch is fast" 为例,分词后得到三个词项,倒排表长这样:
elasticsearch -> [doc1]
fast -> [doc1, doc3]
is -> [doc1]
查询 "fast" 时,直接在词项词典(Term Dictionary)里二分查找,拿到 posting list(文档 ID 链表),再通过跳表加速合并。整个过程不扫描任何文档内容——这就是为什么全文搜索比 LIKE '%xx%' 快几个数量级。
ES 的倒排索引在 Lucene 里是这么落盘的:
- Term Dictionary + FST:词项词典用 FST(有限状态转换器)编码,前缀共享,内存占用极低,支持前缀查询和模糊查询的高效遍历;
- Posting List:文档 ID 用差分编码 + 变长整型压缩(VInt),相邻文档 ID 差值很小,压缩率惊人;
- Skip List:在每个 block 上挂跳表指针,做多词项合并(AND/OR)时直接跳过无关 block。
2.2 正排结构:Doc Values 与 BKD 树
倒排索引擅长「词 → 文档」,但聚合、排序、范围查询需要「文档 → 值」的正向访问。ES 为此准备了两套结构:
- Doc Values:列式存储的文档级字段值,按 docID 顺序排列,排序和聚合时顺序扫描,无需随机读;
- BKD 树:数值和地理坐标字段的平衡 k-d 树,做范围查询(
range)和地理距离查询时,可以在树里剪枝,复杂度远低于全扫。
注意一个关键点:Doc Values 是列式存储。一个字段的所有值连续存放在一起,而不是跟着文档走。这为 9.x 的 Columnar Mode 埋下了伏笔——ES 骨子里早就有列存的基因,只是过去只用在 Doc Values 和聚合上。
2.3 分段(Segment)与近实时(NRT)
Lucene 的索引由不可变的分段组成。写入时数据先进内存 buffer,每隔 refresh_interval(默认 1 秒)落成一个 segment,从此可被搜索——这就是「近实时」的由来。后台线程不断把小的 segment 合并成大 segment(merge),控制 segment 数量,回收删除文档的空间。
不可变分段带来一个巨大的工程红利:读路径无锁。segment 一旦落盘就不再修改,可以安全地并发读;查询时只需合并多个 segment 的结果。代价是更新和删除只能标记(tombstone),空间要等 merge 才真正释放。
2.4 Lucene 10:9.x 的内核升级
ES 9 最底层的改变是换上了 Lucene 10。Lucene 10 的优化方向可以概括为「更小的索引、更快的查询、更少的内存」:
- 数值编码重做:long/double 的存储格式被重新设计,整数用更紧凑的可变长编码,浮点用新的二进制布局。同样一批数据,索引体积明显下降,范围查询和聚合的 cache 命中率更高;
- 术语词典与倒排表优化:block tree 结构继续改进,posting 编码更紧凑,高频词项的合并查询更快;
- 内存占用下降:索引加载时 mmap 的元数据更少,堆外内存压力降低,单节点能承载的数据量更大。
对使用者来说,Lucene 10 的收益是「无感升级」——同样的查询,索引小了、速度快了,但你不需要改任何业务代码。这正是内核升级的正确姿势。
2.5 从 8 到 9:破坏性变更要心里有数
升级大版本前必须知道砍掉了什么。ES 9 的主要破坏性变更包括:
- 遗留 REST API 清理:8.x 标记弃用的旧端点(如
_type相关路径、部分旧参数)在 9.x 被移除; - 客户端要求:
RestHighLevelClient早在 7.15 就弃用、8.x 移除,9.x 只支持官方 Java API Client 和各类语言的新客户端; - 安全默认开启:延续 8.x 策略,默认启用安全认证,生产环境必须配证书和账号,不能再裸奔;
- Java 版本基线:9.x 基于 Java 21 构建,部署环境的 JDK 版本需要对齐。
这些变更对老集群是「迁移成本」,对新项目是「干净的起点」。下文实战部分会直接按 9.x 的新姿势来。
三、架构分析:一次查询的旅程
3.1 节点角色:让每一台机器干一件事
ES 集群的节点按职责分为几类,9.x 延续了这套模型:
| 角色 | 职责 | 典型配置 |
|---|---|---|
| Master-eligible | 集群元数据管理、分片分配决策 | 3 台小规格,奇数 |
| Data | 存数据、执行读写 | 主力机器,磁盘大 |
| Ingest | 写入前的管道处理(解析、清洗、切分) | 按需 |
| Coordinating | 只转发请求、合并结果 | 高并发入口可单独部署 |
生产环境推荐角色分离:master 节点不存数据,coordinating 节点扛查询并发,data 节点按热温冷分层。这样任何一个角色的抖动都不会拖垮整个集群。
3.2 集群协调:从 Zen 到 Raft
ES 7.0 起,集群协调子系统从老旧的 Zen Discovery 换成了基于 Raft 共识的新实现:master 选举、集群状态(cluster state)的发布与确认,全部走 Raft 日志。9.x 里这套机制已经非常成熟:
- 选举:候选节点通过 Raft 投票选出 leader,避免脑裂;
- 状态发布:cluster state(索引映射、分片分配、节点列表)作为 Raft 日志条目复制到所有节点,每个节点本地应用;
- 高可用:3 个 master-eligible 节点可容忍 1 台宕机,5 个可容忍 2 台。
对应用无感知,但对运维是重大利好:再也不用纠结 discovery.zen.minimum_master_nodes 这种玄学参数了。
3.3 写入链路:一次 index 请求发生了什么
以 POST /products/_doc 为例,一条写入请求的完整旅程:
- 协调节点接收请求,对文档
_id做哈希,路由到目标分片(shard = hash(_id) % number_of_shards); - 请求转发给分片的 primary,primary 校验映射、执行 ingest pipeline、写入 Lucene 内存 buffer,同时写 translog(预写日志,保证崩溃恢复);
- primary 把请求并行复制给 replica,等副本确认(ack 级别可配
wait_for_active_shards); - 协调节点向客户端返回结果;
- 后台每
refresh_interval把 buffer 刷成 segment(可搜索),每flush把 translog 落盘并清空。
三个关键参数决定了写入性能:refresh_interval(刷段频率)、index.translog.durability(同步还是异步刷盘)、index.number_of_replicas(副本数)。后面性能优化章节会细说。
3.4 查询链路:两阶段查询的真相
搜索请求同样先到协调节点,但它会广播到所有相关分片(含副本),每个分片本地执行,分两阶段汇总:
- Query Phase:每个分片返回满足条件的 Top N 文档的
_id、得分(score)和排序值; - Fetch Phase:协调节点合并各分片的 Top N,去重排序后,再向相关分片取
_source原文,拼装返回。
这就是为什么 ES 的 from + size 深分页很贵——from=10000 意味着每个分片都要把前 10000+size 条都捞出来排序。正确的深分页姿势是 search_after + PIT(下文实战会写)。
3.5 Columnar Mode:把日志按「列」存
9.5 的 Columnar Mode 是这次最值得关注的新东西。它的目标场景很明确:日志和时序数据。
日志数据的特点是:字段数量多、单条价值低、总量巨大,而且分析时往往只关心少数几个字段(如 level、service.name、duration)。传统行式存储把整条文档堆在一起,聚合时要读出大量无用字段;Columnar Mode 把高频分析字段按列连续存储,配合 ESQL 只读需要的列,效果是:
- 存储显著下降:列式编码 + 压缩,重复值多的字段压缩率极高;
- 聚合查询大幅提速:只扫描涉及的列,跳过无关数据;
- 与 ESQL 天然契合:ESQL 是管道式按列处理的模型,列存让它如鱼得水。
注意 Columnar Mode 不是替代行存,而是针对日志/时序索引的存储模式选项。业务搜索类的索引(需要全文、高亮、更新)继续用行存,日志分析类索引开启列存,一套集群两种模式共存,各取所长。
3.6 Vector DB 索引模式:开箱即用的向量数据库
9.5 的另一个重磅是 Vector DB 索引模式。过去用 ES 做向量检索,你需要手动调 HNSW 的 m、ef_construction、ef_search 等参数,参数不对召回率就崩。Vector DB 模式把这些封装成自动校准:
- 建索引时自动评估向量分布,选择合适的图结构和量化策略;
- 支持 int8 标量量化,把 4 字节 float 压缩成 1 字节整数,内存直接降到 1/4;
- 与全文检索、过滤条件、聚合无缝结合,一个查询里同时跑关键词 + 向量。
这意味着 ES 9 可以名正言顺地当向量数据库用:RAG 场景里,文档切片、embedding、向量索引、混合检索、rerank,全部在一个引擎内闭环,不用再拼 Milvus + ES 两套系统。
3.7 ESQL:用管道思考数据
ESQL(Elasticsearch Query Language)是 ES 面向分析场景的查询语言,8.11 引入,9.x 全面成熟。它的设计哲学是 Unix 管道:
FROM logs-*
| WHERE status >= 500
| STATS count = COUNT(*) BY service.name
| SORT count DESC
| LIMIT 10
每个 | 是一个操作符,数据像水流一样流过管道:FROM 取数 → WHERE 过滤 → STATS 聚合 → SORT 排序 → LIMIT 截断。相比 JSON DSL,ESQL 的优势是:
- 表达力强:复杂的多级聚合、时间窗口分析,DSL 要写一大坨嵌套 JSON,ESQL 几行搞定;
- 类型安全:字段类型在编译期推断,类型不匹配直接报错,而不是运行时静默失败;
- 下推优化:
WHERE里的条件会下推到 Lucene 执行,只扫必要数据; - 跨索引统一:一个
FROM可以带通配符扫多个索引,日志场景尤其好用。
ESQL 不会取代 DSL(DSL 在搜索场景仍然最强),但它会是 9.x 时代分析场景的默认选择。
四、代码实战:从零搭一套 ES 9 生产环境
下面进入实战。我们用 Docker Compose 部署一套三节点 ES 9 集群,然后走完「建索引 → 写入 → 搜索 → 聚合 → ESQL → 向量检索」全流程。所有代码都在 ES 9.3+ 验证过(API 层面兼容 9.x 系列)。
4.1 三节点集群部署
新建 docker-compose.yml:
services:
es01:
image: docker.elastic.co/elasticsearch/elasticsearch:9.3.2
container_name: es01
environment:
- node.name=es01
- cluster.name=es-cluster
- discovery.seed_hosts=es02,es03
- cluster.initial_master_nodes=es01,es02,es03
- xpack.security.enabled=false
- ES_JAVA_OPTS=-Xms1g -Xmx1g
volumes:
- es01-data:/usr/share/elasticsearch/data
ports:
- "9200:9200"
networks:
- esnet
es02:
image: docker.elastic.co/elasticsearch/elasticsearch:9.3.2
container_name: es02
environment:
- node.name=es02
- cluster.name=es-cluster
- discovery.seed_hosts=es01,es03
- cluster.initial_master_nodes=es01,es02,es03
- xpack.security.enabled=false
- ES_JAVA_OPTS=-Xms1g -Xmx1g
volumes:
- es02-data:/usr/share/elasticsearch/data
networks:
- esnet
es03:
image: docker.elastic.co/elasticsearch/elasticsearch:9.3.2
container_name: es03
environment:
- node.name=es03
- cluster.name=es-cluster
- discovery.seed_hosts=es01,es02
- cluster.initial_master_nodes=es01,es02,es03
- xpack.security.enabled=false
- ES_JAVA_OPTS=-Xms1g -Xmx1g
volumes:
- es03-data:/usr/share/elasticsearch/data
networks:
- esnet
volumes:
es01-data:
es02-data:
es03-data:
networks:
esnet:
启动并验证:
docker compose up -d
# 等待集群变绿
curl -s http://localhost:9200/_cluster/health?pretty
输出 "status" : "green"、"number_of_nodes" : 3 即集群就绪。注意:生产环境必须开启安全认证(xpack.security.enabled=true 并配置证书和账号),上面为了本地演示关闭了安全。9.x 默认安全是开启的,生产不要关。
4.2 索引模板与生命周期管理(ILM)
日志类数据的最佳实践是「按天建索引 + ILM 自动滚动和清理」。先建索引模板:
PUT /_index_template/app-logs
{
"priority": 100,
"index_patterns": ["app-logs-*"],
"template": {
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"index.lifecycle.name": "logs-policy",
"index.lifecycle.rollover_alias": "app-logs"
},
"mappings": {
"properties": {
"@timestamp": { "type": "date" },
"level": { "type": "keyword" },
"service": { "type": "keyword" },
"message": { "type": "text" },
"duration_ms": { "type": "integer" },
"trace_id": { "type": "keyword" }
}
}
}
}
再建 ILM 策略:hot 阶段保留 7 天,warm 阶段压缩分片并降副本,冷阶段 30 天后转只读,删除阶段 90 天后清掉:
PUT /_ilm/policy/logs-policy
{
"policy": {
"phases": {
"hot": {
"actions": {
"rollover": { "max_size": "50gb", "max_age": "1d" },
"set_priority": { "priority": 100 }
}
},
"warm": {
"min_age": "7d",
"actions": {
"forcemerge": { "max_num_segments": 1 },
"shrink": { "number_of_shards": 1 },
"set_priority": { "priority": 50 }
}
},
"cold": {
"min_age": "30d",
"actions": {
"searchable_snapshot": { "snapshot_repository": "backup" },
"set_priority": { "priority": 0 }
}
},
"delete": {
"min_age": "90d",
"actions": { "delete": {} }
}
}
}
}
这套配置是日志平台的标准姿势:索引自动滚动、7 天前压缩、30 天前转可搜索快照(省 90% 存储)、90 天自动删除,全程无人值守。
4.3 建商品索引:全文 + 数值 + 向量三合一
接下来建一个电商商品索引,同时包含全文检索字段、数值过滤字段和向量字段(用于后续 RAG/相似推荐):
PUT /products
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"analysis": {
"analyzer": {
"ik_smart_plus": {
"type": "custom",
"tokenizer": "ik_max_word"
}
}
}
},
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "ik_max_word",
"fields": { "keyword": { "type": "keyword", "ignore_above": 256 } }
},
"description": { "type": "text", "analyzer": "ik_max_word" },
"category": { "type": "keyword" },
"price": { "type": "double" },
"stock": { "type": "integer" },
"tags": { "type": "keyword" },
"embedding": {
"type": "dense_vector",
"dims": 768,
"index": true,
"index_options": {
"type": "int8_hnsw",
"m": 32,
"ef_construction": 200,
"confidence_interval": 0.95
}
}
}
}
}
几个关键点:
- 中文搜索必须配 IK 分词器(
ik_max_word细粒度分词),需要安装对应 ES 9.x 版本的 IK 插件(elasticsearch-plugin install或离线 zip,注意版本必须与 ES 严格匹配); name字段同时保留keyword子字段,供精确匹配、排序、聚合使用;embedding字段用int8_hnsw:HNSW 图做 ANN 检索,int8 量化把 768 维 float 向量从 3KB 压缩到 768 字节,内存直降 4 倍。confidence_interval是量化误差的控制旋钮,追求召回率就调高到 0.99,追求极致内存可以降到 0.9。
4.4 批量写入
用 _bulk 批量写入,单次 5~15MB 是吞吐最优区间:
curl -s -X POST "http://localhost:9200/_bulk?refresh=false" -H 'Content-Type: application/x-ndjson' --data-binary @products.ndjson
products.ndjson 格式(每两行一条文档):
{"index":{"_index":"products","_id":"1"}}
{"name":"无线机械键盘 87键 热插拔","description":"Gasket 结构,三模连接,RGB 背光,适合程序员长时间码字","category":"外设","price":399.0,"stock":120,"tags":["键盘","外设","办公"],"embedding":[0.012,-0.045,0.118,/* ...768维向量... */]}
{"index":{"_index":"products","_id":"2"}}
{"name":"4K 27寸显示器 100Hz","description":"IPS 面板,Type-C 65W 反向充电,护眼低蓝光","category":"显示","price":1299.0,"stock":45,"tags":["显示器","办公"],"embedding":[-0.031,0.072,0.005,/* ...768维向量... */]}
生产写入建议用官方客户端(Python/Go/Java)的 bulk helper,自动做分批和重试,比手搓 curl 稳得多。
4.5 全文搜索:bool 查询组合拳
搜索「程序员用的机械键盘,500 以内」:
GET /products/_search
{
"query": {
"bool": {
"must": [
{ "match": { "name": "机械键盘" } }
],
"filter": [
{ "range": { "price": { "lte": 500 } } },
{ "term": { "category": "外设" } }
],
"should": [
{ "match": { "description": "程序员 码字" } }
]
}
},
"highlight": {
"fields": { "name": {}, "description": {} }
},
"sort": [
{ "price": "asc" }
]
}
要点:
must参与打分,filter只过滤不计分(会被缓存,高频过滤条件务必放 filter);should加分项,命中越多得分越高;highlight返回命中片段,前端直接渲染;- 用
sort指定排序时,_score失效,需要相关性就删掉 sort。
4.6 聚合分析:一次请求出报表
按分类统计商品数和价格分布:
GET /products/_search
{
"size": 0,
"aggs": {
"by_category": {
"terms": { "field": "category", "size": 10 },
"aggs": {
"price_stats": { "stats": { "field": "price" } },
"price_histogram": {
"histogram": { "field": "price", "interval": 200 }
}
}
}
}
}
size: 0 表示不返回文档只返回聚合结果。stats 一次给出 min/max/avg/sum/count,histogram 做价格分桶。这套嵌套聚合在 DSL 里已经算复杂了——这正是 ESQL 的用武之地,看下一节。
4.7 ESQL:管道式分析实战
同样的分析用 ESQL 写:
FROM products
| WHERE price > 0
| STATS avg_price = AVG(price), max_price = MAX(price), cnt = COUNT(*) BY category
| SORT cnt DESC
| LIMIT 10
通过 _query API 执行:
curl -s -X POST "http://localhost:9200/_query" \
-H 'Content-Type: application/json' \
-d '{"query":"FROM products | STATS avg_price = AVG(price) BY category | SORT avg_price DESC | LIMIT 10"}'
再看一个时间序列分析——统计最近 1 小时每 5 分钟的错误日志数:
FROM app-logs-*
| WHERE @timestamp >= NOW() - 1 hour AND level == "ERROR"
| EVAL bucket = DATE_TRUNC(5 minutes, @timestamp)
| STATS errors = COUNT(*) BY bucket
| SORT bucket
一个 ESQL 里同时用到了 EVAL(计算列)、DATE_TRUNC(时间分桶)、STATS(聚合),在 DSL 里这要写三层嵌套,可读性天差地别。日志分析、监控报表、临时探查,直接用 ESQL,别再用 DSL 硬凑了。
ESQL 还有几个实用操作符值得记住:
DISSECT/GROK:在管道里直接解析日志文本提取字段;ENRICH:关联 enrich policy 补全字段(如 IP → 归属地);MV_EXPAND:把多值字段展开成多行;RENAME/DROP/KEEP:投影和裁剪列。
4.8 向量检索与 RAG 实战
最后是最有时代感的部分:用 ES 9 做 RAG 语义检索。流程是:文档切片 → embedding 模型生成向量 → 写入 dense_vector 字段 → 查询时把问题向量化 → kNN 检索 → 可选混合检索(全文 + 向量 RRF 融合)→ 把 Top K 片段喂给大模型。
第一步:Python 客户端批量写入切片向量(embedding 用任意开源 embedding 模型生成,这里用 embed 函数占位示意):
from elasticsearch import Elasticsearch, helpers
es = Elasticsearch("http://localhost:9200")
def embed(text: str) -> list[float]:
# 调用本地/云端 embedding 模型,返回 768 维向量
# 示例:sentence-transformers 或 OpenAI-compatible API
return model.encode(text).tolist()
docs = [
{"title": "ES 9 列式存储", "content": "Columnar Mode 面向日志场景,按列存储高频字段,显著降低存储并加速聚合。"},
{"title": "ES 9 向量检索", "content": "dense_vector 支持 int8 量化,HNSW 图索引实现毫秒级 ANN 检索。"},
{"title": "ESQL 管道查询", "content": "ESQL 用管道操作符组织分析逻辑,FROM/WHERE/STATS/SORT 一目了然。"},
]
actions = []
for i, d in enumerate(docs):
actions.append({
"_index": "kb_docs",
"_id": str(i),
"_source": {
**d,
"embedding": embed(d["title"] + " " + d["content"]),
},
})
helpers.bulk(es, actions)
print("indexed", len(actions), "docs")
先建好 kb_docs 索引(映射同 4.3 的 products,dense_vector 用 int8_hnsw)。
第二步:kNN 语义检索:
question = "ES 9 怎么做向量数据库"
q_vec = embed(question)
resp = es.search(
index="kb_docs",
knn={
"field": "embedding",
"query_vector": q_vec,
"k": 5,
"num_candidates": 100,
},
source=["title", "content"],
)
for hit in resp["hits"]["hits"]:
print(f"{hit['_score']:.3f}", hit["_source"]["title"])
num_candidates 是 ANN 的召回池大小,越大召回越准但越慢;生产上根据延迟预算调,一般 100~500。
第三步:混合检索(全文 + 向量,RRF 融合)——这是 RAG 召回质量的关键:
GET /kb_docs/_search
{
"retriever": {
"rrf": {
"rank_window_size": 50,
"retrievers": [
{
"standard": {
"query": { "match": { "content": "列式存储 日志" } }
}
},
{
"knn": {
"field": "embedding",
"query_vector": [0.012, -0.045, 0.118],
"k": 10,
"num_candidates": 100
}
}
]
}
}
}
RRF(Reciprocal Rank Fusion)把两个检索器的排名融合:score = Σ 1/(k + rank),k 通常取 60。它不需要调权重,对「关键词命中和语义命中」各打五十大板,效果稳定,是 9.x 推荐的混合检索姿势。拿到 Top K 片段后拼进 prompt 喂给 LLM,就是一个完整的 RAG 管线,全程只依赖 ES 一个引擎。
五、性能优化:把每一分硬件花在刀刃上
5.1 写入性能
- 批量:用 bulk helper,单批 5
15MB,客户端并发 24 路,别一条条发; - refresh 降频:批量导数据时
refresh_interval设为-1(关闭),导完再恢复,吞吐能差好几倍;日常写入 5~30s 刷新一次完全够「近实时」; - translog 异步:
index.translog.durability: async+sync_interval: 5s,可接受秒级崩溃丢失时用,写入性能显著提升; - 副本先降后升:大批量重建索引时临时把副本设 0,灌完再恢复,减少复制开销。
5.2 查询性能
- filter 优先:所有等值/范围条件放
filter而非must,白嫖查询缓存; - 深分页用 search_after:
from+size超过 1 万条就开始吃力,用 PIT +search_after游标翻页:
POST /products/_pit?keep_alive=1m
GET /products/_search
{
"size": 100,
"pit": { "id": "PIT_ID", "keep_alive": "1m" },
"query": { "match_all": {} },
"sort": [ { "price": "asc" }, { "_shard_doc": "asc" } ],
"search_after": [399.0, 12345]
}
- 聚合与列存:日志分析索引开 Columnar Mode,聚合只读需要的列;
- 向量检索:
int8_hnsw量化 + 合适的num_candidates,在延迟和召回之间取平衡;高基数过滤条件下,先 filter 再 kNN(ES 支持在 knn 内嵌 filter)。
5.3 存储成本
- ILM 冷热分层:hot 用 SSD,warm 转 HDD + forcemerge 单段,cold 转可搜索快照(数据放对象存储,本地只留缓存),日志类数据存储成本降 80%+ 是常态;
- _source 裁剪:不需要原文检索的字段用
_source.includes/excludes裁剪;分析型索引甚至可以关_source; - 字段瘦身:不参与聚合/排序的 text 字段关
doc_values关不掉就关norms;keyword 字段设ignore_above,防超长垃圾值撑爆词典; - 压缩:
index.codec: best_compression,日志场景可省 30% 左右空间,代价是查询稍慢,冷数据索引很划算。
5.4 监控:Prometheus 原生接入
9.5 原生支持 Prometheus 和 PromQL,可以直接把 ES 指标接进现有监控体系,不用再折腾 exporter:
# prometheus.yml
scrape_configs:
- job_name: elasticsearch
metrics_path: /_prometheus/metrics
static_configs:
- targets: ["es01:9200", "es02:9200", "es03:9200"]
重点盯的指标:集群状态、分片数、JVM 堆使用率、查询延迟分位数(search latency)、写入拒绝数(threadpool rejected)、段合并队列。
六、总结与展望
回到开头的问题:ES 9 到底是什么?
它是一台多模数据引擎:全文检索的看家本领还在,倒排索引 + FST + 跳表的组合依然能打;分析能力被 ESQL 和 Columnar Mode 拉到了新高度,日志场景可以正面刚 ClickHouse;向量检索在 Vector DB 模式和 int8 量化加持下,具备了当生产级向量数据库的资格。
给团队的技术选型建议:
- 需要全文检索 + 复杂过滤 + 聚合 → ES 9 是默认答案;
- 日志/可观测性分析为主 → ES 9 的 ESQL + Columnar Mode 够用;如果日志量到 PB 级且分析模式极其固定,再考虑专用 OLAP;
- RAG 应用 → 中小规模直接用 ES 9 一个引擎闭环(切分 + 向量 + 混合检索),省掉 Milvus/pgvector 的运维负担;超大规模(亿级向量以上)再评估专用向量库;
- 与 MySQL/Redis 的分工:MySQL 管事务,Redis 管缓存,ES 管「搜索和分析」——三驾马车各司其职,别让 ES 干事务的活,也别用 MySQL 的 LIKE 硬扛搜索。
ES 9 的进化方向也很清晰:AI 原生。ESQL 正在变成 AI Agent 查数据的标准接口,向量检索 + RAG 会成为内置能力而非插件,可观测性数据直接喂给 AI 做根因分析。搜索引擎这个古老的品类,正在被重写成 AI 时代的数据底座——而 ES 9,就是这场重写的第一批成果。
如果你还在 7.x/8.x 观望,我的建议是:新项目直接用 9.x,老集群排期迁移——破坏性变更主要是 API 清理,核心数据模型没动,迁移成本可控,收益却是实打实的。搜索引擎的内核升级,十年一遇,别错过。