属性越来越多,不想开发 —— EAV 异构优化
需求背景
EAV 模型引入之后,扩展属性的存储问题解决了。但随着搜索标签越来越多,一个新的痛点开始显现:每次新增一个扩展属性,都需要写代码、上线。
问题现状
典型的开发流程是这样的:
产品提需求:新增"字数"搜索属性
↓
开发写异构代码:把 EAV 表里的 word_count 字段同步到 ES
↓
开发写查询接口:支持按字数范围搜索
↓
代码审查 → 上线 → 等待发布窗口
↓
总耗时:2-3 天
问题不在于单次开发量大,而在于每次都要重复这个流程。EAV 模型的灵活性让扩展属性的种类越来越多,但每次新增都要走一遍完整的开发上线流程,上线等待的时间往往比开发时间还长。
核心矛盾
EAV 表里的数据结构是统一的(entity_id + attribute_key + attribute_value),但异构到 ES 时,每个属性的处理逻辑却是分散的——不同属性有不同的类型,需要写不同的代码。
能不能把这个过程也配置化,让新增扩展属性不再需要写代码?
方案设计
问题的根源在于 ES 是类型敏感的:同一个字段,存字符串和存整数的 mapping 不一样,查询方式也不一样。所以之前每次新增属性都要手动处理类型转换。
解决思路是:改造 ES 文档结构,用 nested 类型统一存储所有扩展属性,通过不同的字段名区分不同的值类型。
新的 ES 文档结构
{
"mappings": {
"extension_attribute": {
"type": "nested",
"properties": {
"key": { "type": "keyword" },
"strValue": { "type": "keyword" },
"intValue": { "type": "integer" },
"longValue": { "type": "long" },
"doubleValue":{ "type": "double" },
"floatValue": { "type": "float" },
"dateValue": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss" }
}
}
}
}
每个扩展属性存为一个 nested 对象,key 是属性名,值根据类型存入对应的字段(strValue、intValue 等)。
配置化异构
维护一张元数据配置表,记录每个扩展属性的类型信息:
create table extension_attribute_config (
attribute_key varchar(255) primary key comment '属性键',
attribute_name varchar(255) comment '属性名称',
attribute_type varchar(32) comment '属性类型',
is_searchable boolean comment '是否可搜索',
display_order int comment '展示顺序'
)
异构逻辑变成通用的:
public List<ExtensionAttribute> buildExtensionAttributes(Long bookId) {
List<BookExtensionAttribute> eavList = eavRepository.findByBookId(bookId);
return eavList.stream().map(eav -> {
ExtensionAttribute attr = new ExtensionAttribute();
attr.setKey(eav.getAttributeKey());
switch (eav.getAttributeType()) {
case "Integer": attr.setIntValue(Integer.parseInt(eav.getAttributeValue())); break;
case "Long": attr.setLongValue(Long.parseLong(eav.getAttributeValue())); break;
case "Double": attr.setDoubleValue(Double.parseDouble(eav.getAttributeValue())); break;
case "String": attr.setStrValue(eav.getAttributeValue()); break;
case "Date": attr.setDateValue(parseDate(eav.getAttributeValue())); break;
}
return attr;
}).collect(Collectors.toList());
}
这段代码是通用的,不需要针对每个属性单独写。新增一个扩展属性,只需要在 extension_attribute_config 里插入一条配置记录,异构逻辑自动生效。
查询方式
扩展属性的查询改用 ES nested 查询:
{
"query": {
"nested": {
"path": "extension_attribute",
"query": {
"bool": {
"must": [
{ "term": { "extension_attribute.key": "author_dynasty" } },
{ "term": { "extension_attribute.strValue": "唐朝" } }
]
}
}
}
}
}
根据属性类型不同,值字段也不同:String/Date 用 strValue,Integer 用 intValue,以此类推。
性能影响
Nested 查询比普通字段查询略慢,性能上会有轻微损耗。不过在长期的对标签指标的监控发现,稀疏属性相关的标签,普遍都是非热点搜索项,相较于书名,作者,品类,评分这些热点搜索项,这些非热点搜索项的相关查询性能略有下降完全可以接受。
当然,并非所有的 EAV 我们都一股脑塞到这个 nested 字段里面,当然可以做一些特殊配置化,让一些虽然稀疏但热点的字段作为 ES 字段查询
成本对比
优化前(新增"字数"属性):写异构代码 + 写查询接口 + 代码审查 + 上线,耗时 2-3 天。
优化后:插入一条元数据配置,耗时 30 分钟,无需上线。
| 维度 | 优化前 | 优化后 |
|---|---|---|
| 新增属性周期 | 2-3 天 | 30 分钟 |
| 代码上线 | 必须 | 无需 |
| 性能影响 | 快 | 部分场景下较慢,可接受 |
这一步解决了什么
从此以后,新增扩展属性不需要写代码了。EAV 模型的灵活性终于被完整地释放出来。
但系统的熵增并没有停止。标签嵌套规则的需求越来越多,硬编码的代码越堆越高,下一个大问题已经在路上了。
这引出了下一篇:标签嵌套规则的挑战,以及系统走向大迭代的转折点。