文档建模

文档建模 #

这一页回答两个问题:应该把什么放进一个文档?字段怎么设计才适合搜索? 这里只讲单个文档层面的建模,跨文档关系放在“数据建模”章节。

什么是文档 #

在 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_idorder_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 统一同时支持全文检索与精确过滤
  • 为标识符与路由预留好字段,方便后续扩展数据建模与多租户策略
  • 适度冗余换取查询简化与性能,但要控制冗余的数量与更新成本

下一步可以继续阅读: