---
title: "文档建模"
date: 0001-01-01
description: "面向搜索的数据建模：字段设计、范式取舍、标识符选择与更新策略。"
summary: "文档建模 #  这一页回答两个问题：应该把什么放进一个文档？字段怎么设计才适合搜索？ 这里只讲单个文档层面的建模，跨文档关系放在“数据建模”章节。
什么是文档 #  在 Easysearch 中，一个 文档（Document） 是被序列化为 JSON 的最顶层对象，指定了唯一 ID 并存储到 Easysearch 中。例如：
{ &#34;name&#34;: &#34;John Smith&#34;, &#34;age&#34;: 42, &#34;confirmed&#34;: true, &#34;join_date&#34;: &#34;2014-06-01&#34;, &#34;home&#34;: { &#34;lat&#34;: 51.5, &#34;lon&#34;: 0.1 }, &#34;accounts&#34;: [ { &#34;type&#34;: &#34;facebook&#34;, &#34;id&#34;: &#34;johnsmith&#34; }, { &#34;type&#34;: &#34;twitter&#34;, &#34;id&#34;: &#34;johnsmith&#34; } ] } 文档可以包含字符串、数字、布尔、日期、嵌套对象、数组等多种类型。
文档元数据 #  每个文档都有三个核心元数据：
   元数据 说明     _index 文档存放的索引，是逻辑命名空间   _id 文档的唯一标识符，可自定义或自动生成   _source 文档的原始 JSON 内容    此外，每个文档还有 _version 字段——每次对文档修改（包括删除）时版本号递增，用于并发控制。"
---


# 文档建模

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

在 Easysearch 中，一个 **文档（Document）** 是被序列化为 JSON 的最顶层对象，指定了唯一 ID 并存储到 Easysearch 中。例如：

```json
{
    "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 使用**乐观并发控制**：不会阻塞操作，而是利用版本号来避免冲突。

在更新文档时指定版本号，只有版本匹配时更新才会成功：

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

下一步可以继续阅读：

- [文档操作](../features/document-operations/)
- [映射基础](../features/mapping-and-analysis/mapping-basics.md)
- [数据建模](../best-practices/data-modeling/)



