编程 Elasticsearch 9 深度实战:列式存储 + 原生向量检索如何重写搜索引擎——从 Lucene 10 内核到 ESQL 全链路拆解

2026-08-16 15:44:29 +0800 CST views 4

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 为例,一条写入请求的完整旅程:

  1. 协调节点接收请求,对文档 _id 做哈希,路由到目标分片(shard = hash(_id) % number_of_shards);
  2. 请求转发给分片的 primary,primary 校验映射、执行 ingest pipeline、写入 Lucene 内存 buffer,同时写 translog(预写日志,保证崩溃恢复);
  3. primary 把请求并行复制给 replica,等副本确认(ack 级别可配 wait_for_active_shards);
  4. 协调节点向客户端返回结果;
  5. 后台每 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 是这次最值得关注的新东西。它的目标场景很明确:日志和时序数据

日志数据的特点是:字段数量多、单条价值低、总量巨大,而且分析时往往只关心少数几个字段(如 levelservice.nameduration)。传统行式存储把整条文档堆在一起,聚合时要读出大量无用字段;Columnar Mode 把高频分析字段按列连续存储,配合 ESQL 只读需要的列,效果是:

  • 存储显著下降:列式编码 + 压缩,重复值多的字段压缩率极高;
  • 聚合查询大幅提速:只扫描涉及的列,跳过无关数据;
  • 与 ESQL 天然契合:ESQL 是管道式按列处理的模型,列存让它如鱼得水。

注意 Columnar Mode 不是替代行存,而是针对日志/时序索引的存储模式选项。业务搜索类的索引(需要全文、高亮、更新)继续用行存,日志分析类索引开启列存,一套集群两种模式共存,各取所长。

3.6 Vector DB 索引模式:开箱即用的向量数据库

9.5 的另一个重磅是 Vector DB 索引模式。过去用 ES 做向量检索,你需要手动调 HNSW 的 mef_constructionef_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_vectorint8_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,单批 515MB,客户端并发 24 路,别一条条发;
  • refresh 降频:批量导数据时 refresh_interval 设为 -1(关闭),导完再恢复,吞吐能差好几倍;日常写入 5~30s 刷新一次完全够「近实时」;
  • translog 异步index.translog.durability: async + sync_interval: 5s,可接受秒级崩溃丢失时用,写入性能显著提升;
  • 副本先降后升:大批量重建索引时临时把副本设 0,灌完再恢复,减少复制开销。

5.2 查询性能

  • filter 优先:所有等值/范围条件放 filter 而非 must,白嫖查询缓存;
  • 深分页用 search_afterfrom+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 清理,核心数据模型没动,迁移成本可控,收益却是实打实的。搜索引擎的内核升级,十年一遇,别错过。

推荐文章

如何在 Linux 系统上安装字体
2025-02-27 09:23:03 +0800 CST
php微信文章推广管理系统
2024-11-19 00:50:36 +0800 CST
2024年微信小程序开发价格概览
2024-11-19 06:40:52 +0800 CST
MCP 测试文章 17812
2026-08-13 06:21:50 +0800 CST
快手小程序商城系统
2024-11-25 13:39:46 +0800 CST
PHP openssl 生成公私钥匙
2024-11-17 05:00:37 +0800 CST
程序员茄子在线接单