60 · Transform 数据透视(把明细汇总成实体索引)
阶段:第六阶段 / 进阶专题
ES:Transform(pivot / latest) | PostgreSQL:物化视图(
MATERIALIZED VIEW)+ 定时刷新
1. 概念
明细索引(每笔销售一条)适合检索,但不适合反复做重聚合 (慢、耗资源)。
Transform 能把明细「透视」成一个新的汇总实体索引,例如:
- 从「每笔订单」→ 汇总成「每个客户的总消费、订单数、最近下单时间」。
- 结果写进一个新索引,之后直接查这个索引,秒回。
两种模式:
| 模式 | 作用 | 类比 |
|---|---|---|
| pivot | 按 group_by + 聚合,生成汇总行 | GROUP BY 结果落表 |
| latest | 按 key 取每组最新一条 | DISTINCT ON (key) ORDER BY time DESC |
还能设 continuous(持续):源数据变了,自动增量更新目标索引 ------ 就是「自动刷新的物化视图」。
2. PostgreSQL 对照
sql
-- 物化视图:把明细汇总成客户维度
CREATE MATERIALIZED VIEW customer_summary AS
SELECT customer_id,
SUM(amount) AS total_amount,
COUNT(*) AS order_cnt,
MAX(order_dt) AS last_order_dt
FROM sales
GROUP BY customer_id;
REFRESH MATERIALIZED VIEW customer_summary; -- 定时刷新
Transform(continuous)≈ 会自动增量刷新的物化视图。
3. ES DSL
3.1 pivot:客户维度汇总
PUT _transform/customer_summary
{
"source": { "index": "sales_idx" },
"dest": { "index": "customer_summary_idx" },
"pivot": {
"group_by": {
"customer_id": { "terms": { "field": "customer_id" } }
},
"aggregations": {
"total_amount": { "sum": { "field": "amount" } },
"order_cnt": { "value_count": { "field": "order_no.keyword" } },
"last_order_dt": { "max": { "field": "order_dt" } }
}
},
"sync": {
"time": { "field": "order_dt", "delay": "60s" }
}
}
- 有
sync= continuous,源变了自动增量更新目标索引。 - 去掉
sync= 一次性批处理(batch)。
3.2 启动 / 停止 / 预览
POST _transform/customer_summary/_start
POST _transform/customer_summary/_stop
// 建之前先预览结果长什么样
POST _transform/_preview
{ "source": { "index": "sales_idx" }, "pivot": { ...同上... } }
3.3 latest:每个客户最新一单
PUT _transform/customer_latest
{
"source": { "index": "sales_idx" },
"dest": { "index": "customer_latest_idx" },
"latest": {
"unique_key": ["customer_id"],
"sort": "order_dt"
}
}
4. Spring Boot 实现
java
@Component
public class Doc60Transform {
@Autowired
private ElasticsearchClient elasticsearchClient;
/** 创建并启动一个 pivot transform */
public void createCustomerSummary() throws IOException {
elasticsearchClient.transform().putTransform(t -> t
.transformId("customer_summary")
.source(src -> src.index("sales_idx"))
.dest(d -> d.index("customer_summary_idx"))
.pivot(p -> p
.groupBy("customer_id", g -> g.terms(tm -> tm.field("customer_id")))
.aggregations("total_amount", a -> a.sum(s -> s.field("amount")))
.aggregations("order_cnt", a -> a.valueCount(v -> v.field("order_no.keyword")))
.aggregations("last_order_dt", a -> a.max(m -> m.field("order_dt"))))
.sync(sy -> sy.time(ti -> ti.field("order_dt").delay(dl -> dl.time("60s")))));
elasticsearchClient.transform().startTransform(s -> s.transformId("customer_summary"));
}
/** 停止 */
public void stop() throws IOException {
elasticsearchClient.transform().stopTransform(s -> s.transformId("customer_summary"));
}
}
import:
co.elastic.clients.elasticsearch.transform.*(PutTransformRequest等由 builder 隐式使用)。
5. 坑与最佳实践
- 先
_preview再落库:确认汇总字段和粒度对,再正式建,避免返工。 - continuous 要有时间字段 :
sync.time.field通常用@timestamp/order_dt,delay留出数据到达的缓冲。 - 目标索引是「派生数据」:别直接写它;它由 transform 维护,需要改逻辑就重建。
group_by的text字段用.keyword:同 21/43 篇。- 资源占用 :continuous transform 会持续跑,注意其对集群的负载,必要时调
frequency。 - 和聚合的取舍 :临时分析用聚合(20~24 篇);高频、固定维度的汇总查询才值得用 transform 预计算。