第一阶段迭代:非模型自有属性的搜索扩展
需求背景
系统上线后,业务方提了一个新需求:用户想根据作者朝代来搜索中国古籍,比如《三国演义》、《战国策》这类。
听起来很简单,但仔细一想,这个需求暗藏了一个设计问题。
核心矛盾
"作者朝代"这个属性,并不属于图书本身。大多数现代图书根本没有"朝代"这个概念,它只对一小部分古籍有意义。
最直觉的做法是在 book_base 主表上加一列 author_dynasty。但这样做有明显的问题:这个字段对 99% 的图书来说都是空值,白白占用存储空间,而且每次新增类似的稀疏属性都要改表结构,长期下去主表会越来越臃肿。
换一个思路:单独建一张扩展表,每种稀疏属性用一列来存。但如果扩展属性越来越多,这张表同样会出现大量空值,问题没有本质改变。
核心矛盾在于:如何存储那些只属于部分实体、且种类会持续增长的稀疏属性?
方案设计
这是一个经典的稀疏数据存储问题,业界有成熟的解法:EAV 模型(Entity-Attribute-Value,实体-属性-值)。
EAV 的核心思想是把"属性"本身也当作数据来存储,而不是固化在表结构里。每一行记录一个"某实体的某属性值是多少",三元组设计天然支持稀疏数据——没有这个属性的实体,直接不存这一行就行了。
create table book_extension_attribute (
entity_id bigint comment '实体ID',
attribute_key varchar(255) comment '属性键',
attribute_value text comment '属性值',
attribute_type int comment '属性类型枚举code'
)
这个设计和 HBase、Cassandra 等 NoSQL 数据库的列存储思路很相似——不同的行可以拥有完全不同的列,对稀疏数据极其友好。
ES 文档的处理
ES 的文档是 JSON 格式,天然支持动态扩展字段,稀疏字段对它来说不是问题。新增一类扩展属性,只需要在 mapping 里加一个字段:
{
"mappings": {
"author_dynasty": {
"type": "keyword"
}
}
}
ES 甚至支持动态映射(dynamic mapping),新字段写入时会自动推断类型,不需要提前声明。这让扩展属性的接入成本非常低。
不过这里也埋了一个坑:每次新增一类扩展属性,都需要手动修改 ES 的 mapping 和异构代码。当前阶段属性种类还少,这个成本可以接受,但随着业务发展,这个问题会越来越突出——这也是后续 [06] 篇要解决的核心问题。
这一步解决了什么
EAV 模型让我们可以灵活地扩展实体属性,而不需要频繁改动主表结构。新增一类稀疏属性,只需要往扩展表里写数据,主表完全不受影响。
但新的问题随之而来:业务开始接入外部系统的数据,这些数据不归我们管,一致性怎么保障?
这引出了下一篇:外部系统接入的设计。