文档建模 #
这一页回答两个问题:应该把什么放进一个文档?字段怎么设计才适合搜索? 这里只讲单个文档层面的建模,跨文档关系放在“数据建模”章节。
什么是文档 #
在 Easysearch 中,一个 文档(Document) 是被序列化为 JSON 的最顶层对象,指定了唯一 ID 并存储到 Easysearch 中。例如:
{
"name": "John Smith",
"age": 42,
"confirmed": true,
"join_date": "2014-06-01",
"home": {
"lat": 51.5,
"lon": 0.1
},
"accounts": [
{ "type": "facebook", "id": "johnsmith" },
{ "type": "twitter", "id": "johnsmith" }
]
}
文档可以包含字符串、数字、布尔、日期、嵌套对象、数组等多种类型。
文档元数据 #
每个文档都有三个核心元数据:
| 元数据 | 说明 |
|---|---|
_index | 文档存放的索引,是逻辑命名空间 |
_id | 文档的唯一标识符,可自定义或自动生成 |
_source | 文档的原始 JSON 内容 |
此外,每个文档还有 _version 字段——每次对文档修改(包括删除)时版本号递增,用于并发控制。
一个“好文档”的几个特征 #
- 字段含义清晰、类型正确(text/keyword/数值/日期等)
- 能支撑主要的搜索与聚合需求,而不是只反映存储表结构
- 重要的过滤/排序/聚合维度都有对应字段
- 更新策略是可控的(全量更新、部分更新、幂等写入等)
可以先从“业务查询需求”倒推字段设计,而不是从数据库表结构直接平铺。
字段类型与多字段(multi-fields) #
同一份业务信息,往往需要不同的查询方式,例如“商品名称”:
- 既要支持全文检索(模糊匹配)
- 又要支持精确匹配/去重/聚合(关键词级别)
推荐做法是使用多字段(multi-fields),例如:
name(text):用于全文检索name.keyword(keyword):用于精确过滤、排序、聚合
实务建议:
- 人类可读文本:通常需要 text + keyword 两个视角
- ID、编码、枚举:直接用 keyword/数值/布尔类型即可
- 金额、计数、比例:用适当的数值类型,并为排序/聚合做好 doc_values 支持
标识符与路由字段 #
_id 与业务字段的考虑:
_id:内部唯一标识,用于幂等写入、精确读取/删除- 业务 ID(如
user_id、order_id):通常也会单独做成字段,用于过滤、聚合、权限控制等
在多租户/分库分表等场景下,可以考虑额外的路由字段(如 tenant_id),后续在索引设计和路由策略中使用(见“数据建模”“分布式基础”章节)。
规范化与冗余 #
与传统数据库“强规范化”不同,面向搜索的文档往往会有适度冗余:
- 预先把常用的派生信息存进文档(如标准化后的地区名、拼音、缩写)
- 把查询高频的外键信息“带过来”,减少查询时的 join 需求
但冗余也要有边界:
- 冗余会放大存储与更新成本
- 冗余字段过多,会让 mapping 变得臃肿、难以维护
经验做法:
- 只冗余“确实会被高频查询/排序/聚合”的字段
- 对变动频率极高的冗余信息,要慎重评估更新成本
更新模式与幂等性 #
常见的文档更新模式:
- 全量替换(put index):一次写入整个文档
- 简单可靠,适合文档相对较小、更新频率不高的场景
- 部分更新(update + doc/script):只更新部分字段
- 适合字段很多但部分字段频繁变更的场景
从建模角度,需要考虑:
- 是否有字段可以晚一点异步更新(例如离线计算得出的统计值)
- 是否需要为“最终一致”的字段准备单独的更新通道
在“写入与 Bulk”“并发控制与版本”章节中会更详细讨论更新与幂等的实现,这里只强调:文档结构要能支撑你想要的更新模式。
并发控制:乐观锁 #
Easysearch 使用乐观并发控制:不会阻塞操作,而是利用版本号来避免冲突。
在更新文档时指定版本号,只有版本匹配时更新才会成功:
PUT /website/_doc/1?if_seq_no=5&if_primary_term=1
{
"title": "My first blog entry",
"text": "Starting to get the hang of this..."
}
如果版本冲突(其他进程已更新),Easysearch 返回 409 Conflict,应用程序可以选择重试或通知用户。
常见场景:
- 不关心冲突:直接写入覆盖(如从主数据库同步数据)
- 需要避免冲突:使用
if_seq_no+if_primary_term做乐观锁 - 外部版本号:如果主数据库已有版本号(如时间戳),可通过
version_type=external复用
小结 #
- 文档是 JSON 对象,包含
_index、_id、_source等元数据 - 以"搜索/过滤/聚合需求"驱动字段设计,而不是简单照搬表结构
- 合理利用 multi-fields 统一同时支持全文检索与精确过滤
- 为标识符与路由预留好字段,方便后续扩展数据建模与多租户策略
- 适度冗余换取查询简化与性能,但要控制冗余的数量与更新成本
下一步可以继续阅读: