索引结构设计
- 文档结构:
- 订单ID:唯一标识每个订单,例如
order_id
,类型为keyword
。
- 用户ID:关联用户,
user_id
,类型为keyword
。
- 地区:购买地区,
region
,类型为keyword
。这是为了处理高基数问题,keyword
类型适合精确匹配和聚合操作。
- 购买金额:
amount
,类型为double
。
- 购买时间:
purchase_time
,类型为date
,格式可以是yyyy - MM - dd HH:mm:ss
。这个字段用于按时间维度(按月)进行分析。
- 映射设置:
{
"mappings": {
"properties": {
"order_id": {
"type": "keyword"
},
"user_id": {
"type": "keyword"
},
"region": {
"type": "keyword"
},
"amount": {
"type": "double"
},
"purchase_time": {
"type": "date",
"format": "yyyy - MM - dd HH:mm:ss"
}
}
}
}
聚合查询策略
- 按月和地区聚合计算百分位数:
{
"aggs": {
"by_month": {
"date_histogram": {
"field": "purchase_time",
"calendar_interval": "month",
"format": "yyyy - MM"
},
"aggs": {
"by_region": {
"terms": {
"field": "region"
},
"aggs": {
"p25_amount": {
"percentiles": {
"field": "amount",
"percents": [25]
}
},
"p50_amount": {
"percentiles": {
"field": "amount",
"percents": [50]
}
},
"p75_amount": {
"percentiles": {
"field": "amount",
"percents": [75]
}
},
"p90_amount": {
"percentiles": {
"field": "amount",
"percents": [90]
}
}
}
}
}
}
}
}
- 外层
date_histogram
按月份对订单数据进行分组。
- 内层
terms
按地区对每个月的数据进行分组。
- 然后针对每个地区每月的数据计算25、50、75、90百分位数的购买金额。
优化手段
- 数据预热:在系统启动或业务低峰期,提前执行一些常见的聚合查询,将结果缓存起来。这样在业务高峰期,直接从缓存获取数据,减少对ElasticSearch的压力。
- 索引优化:
- 分片和副本设置:根据数据量和服务器资源合理设置分片数和副本数。对于高基数数据,可以适当增加分片数,但要注意过多分片会增加管理开销。例如,对于数据量较大的电商订单数据,可以设置较多的分片,如8 - 16个分片,副本数可以根据可用性要求设置为1 - 2个。
- 字段数据存储:对于不需要聚合或排序的字段,设置
doc_values: false
,以减少磁盘占用。但对于region
、purchase_time
和amount
这些用于聚合的字段,需要保留doc_values
。
- 缓存机制:
- 使用ElasticSearch的请求缓存:开启请求缓存,对于相同的聚合查询,ElasticSearch可以直接从缓存返回结果,提高查询效率。可以在
elasticsearch.yml
中配置indices.requests.cache.enable: true
。
- 应用层缓存:在应用程序层面,可以使用Redis等缓存工具,将聚合结果缓存起来,设置合理的过期时间,以减少对ElasticSearch的重复查询。
- 实时性处理:
- 使用ElasticSearch的近实时搜索功能:ElasticSearch默认每隔1秒刷新一次数据到磁盘,这使得数据在1秒内就可以被搜索到。对于实时性要求较高的场景,可以适当缩短刷新间隔,但这会增加I/O开销。可以通过
index.refresh_interval
参数进行设置,例如index.refresh_interval: 5s
,将刷新间隔设置为5秒。
- 采用异步处理:将订单数据写入ElasticSearch的操作放到异步线程或消息队列中处理,以避免影响业务主线程的响应时间,确保系统的实时性。