# Manticore 中的 UUID：实战指南

在 Manticore Search 28.5.0 中将 UUID 作为文档 ID 使用的实战指南：SQL、JSON API、/bulk、自动生成 ID、错误与复制。

在 [概览文章](/blog/uuid-document-ids/) 中，我们已经说明了为什么要在搜索中使用与主数据库相同的 UUID（如果主数据库存在的话）。这里直接进入实操：创建表、通过 SQL 和 JSON API 执行基本操作，然后通过 `/bulk` 导入几份文档。

所有示例都以 Manticore Search 28.5.0 或更高版本为准。响应中的 `<generated UUID>` 表示 Manticore 在处理请求时生成的 UUID。下一次请求不需要复制这串内容：请用你自己响应里的实际 `id` 替换。

## 所有示例使用的表

要让 UUID 成为文档 ID，首先把字段声明为 `id uuid`。除了 `id` 的类型之外，RT 表的结构不会改变：

```sql
CREATE TABLE products_uuid (
    id uuid,
    title text,
    sku string,
    price int
);
```

`DESC products_uuid` 会显示字段 `id` 的类型是 `uuid`。SQL 和 HTTP API 都会把这个值当作文档 ID 使用，因此不需要把 UUID 复制到字符串属性里。

## 通过 SQL 处理 UUID

先插入一条带有现成 UUID 的商品：

```sql
INSERT INTO products_uuid (id, title, sku, price)
VALUES (
    '550e8400-e29b-41d4-a716-446655440000',
    'Mechanical keyboard',
    'KB-001',
    149
);
```

在 SQL 里，UUID 必须放在单引号中。可以用普通条件 `WHERE id = '...'` 来查找文档：

```sql
SELECT id, title, sku, price
FROM products_uuid
WHERE id = '550e8400-e29b-41d4-a716-446655440000';
```

UUID 也可以由 Manticore 生成。为此，只要不传任何 `id` 值即可：

```sql
INSERT INTO products_uuid (title, sku, price)
VALUES ('USB microphone', 'MIC-001', 89);

SELECT LAST_INSERT_ID();
```

`LAST_INSERT_ID()` 会返回这次请求生成的 UUID：

```text
+--------------------------------------+
| last_insert_id()                     |
+--------------------------------------+
| <generated UUID>                     |
+--------------------------------------+
```

`INSERT` 多个文档以及 `@@session.last_insert_id` 变量在文档的 [向表中添加文档](https://manual.manticoresearch.com/dev/Data_creation_and_modification/Adding_documents_to_a_table/Adding_documents_to_a_real-time_table) 一节里有详细说明。

再添加一条已知 ID 的文档，以便测试 `IN`：

```sql
INSERT INTO products_uuid (id, title, sku, price)
VALUES (
    '550e8400-e29b-41d4-a716-446655440001',
    'USB-C dock',
    'DOCK-001',
    119
);

SELECT id, sku, price
FROM products_uuid
WHERE id IN (
    '550e8400-e29b-41d4-a716-446655440000',
    '550e8400-e29b-41d4-a716-446655440001'
);
```

属性可以用常规的 `UPDATE` 修改。此时 `id` 本身保持不变：

```sql
UPDATE products_uuid
SET price = 139
WHERE id = '550e8400-e29b-41d4-a716-446655440000';
```

`INSERT` 一个已存在 UUID 的文档不会覆盖原文档。Manticore 会返回重复错误：

```sql
INSERT INTO products_uuid (id, title, sku, price)
VALUES (
    '550e8400-e29b-41d4-a716-446655440000',
    'Duplicate keyboard',
    'KB-DUP',
    1
);
```

如果要为同一个 UUID 的文档写入新版本，请使用 `REPLACE`：

```sql
REPLACE INTO products_uuid (id, title, sku, price)
VALUES (
    '550e8400-e29b-41d4-a716-446655440000',
    'Mechanical keyboard, revised',
    'KB-001',
    129
);
```

如果只改价格，用 `UPDATE` 就够了。对于全文字段和列式属性，则需要 `REPLACE`：它会把同一 ID 的旧版本标记为已删除，并写入新版本。如果这个 ID 还不存在，Manticore 会直接添加文档。详情请参阅文档：[UPDATE](https://manual.manticoresearch.com/dev/Data_creation_and_modification/Updating_documents/UPDATE) 和 [REPLACE](https://manual.manticoresearch.com/dev/Data_creation_and_modification/Updating_documents/REPLACE)。

删除文档时使用同一个 UUID：

```sql
DELETE FROM products_uuid
WHERE id = '550e8400-e29b-41d4-a716-446655440001';
```

检查一下刚才插入时指定的那个 UUID 的当前状态：

```sql
SELECT id, title, sku, price
FROM products_uuid
WHERE id = '550e8400-e29b-41d4-a716-446655440000';
```

SQL 客户端发送和接收 UUID 时都使用字符串。传参时请使用字符串；读取结果时，也要把 `id`（由 `SELECT` 返回）当作字符串。无需转换成数字或 `BINARY(16)`。按数字文档 ID 编写的代码需要相应调整。

## 通过 JSON API 执行相同操作

通过 JSON API 写入时，`id` 字段与 `table` 同级传递，而不是放在 `doc` 内部。第一个示例使用大写 UUID：

```bash
curl -sS http://localhost:9308/insert \
  -H 'Content-Type: application/json' \
  -d '{
    "table": "products_uuid",
    "id": "AAAAAAAA-AAAA-4AAA-8AAA-AAAAAAAAAAAA",
    "doc": {
      "title": "Wireless keyboard",
      "sku": "JSON-KB-001",
      "price": 159
    }
  }'
```

Manticore 接受大写 UUID，保存时会转为小写，并且在响应中也以同样形式返回：

```json
{
  "table": "products_uuid",
  "id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
  "created": true,
  "result": "created",
  "status": 201
}
```

自动生成时，只需要把 `id` 字段整个去掉：

```bash
curl -sS http://localhost:9308/insert \
  -H 'Content-Type: application/json' \
  -d '{
    "table": "products_uuid",
    "doc": {
      "title": "Portable speaker",
      "sku": "JSON-SPK-001",
      "price": 79
    }
  }'
```

响应会包含应该保存下来供后续操作使用的 ID：

```json
{
  "table": "products_uuid",
  "id": "<generated UUID>",
  "created": true,
  "result": "created",
  "status": 201
}
```

在 `/search` 中，可以用 `equals` 过滤 UUID。如果请求将 `id` 包含在 `_source` 中，结果会在 `_id` 和 `_source.id` 中返回同一个 UUID：

```bash
curl -sS http://localhost:9308/search \
  -H 'Content-Type: application/json' \
  -d '{
    "table": "products_uuid",
    "query": {
      "equals": {
        "id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
      }
    },
    "_source": ["id", "title", "sku", "price"]
  }'
```

```json
{
  "timed_out": false,
  "hits": {
    "total": 1,
    "total_relation": "eq",
    "hits": [
      {
        "_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
        "_score": 1,
        "_source": {
          "id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
          "title": "Wireless keyboard",
          "sku": "JSON-KB-001",
          "price": 159
        }
      }
    ]
  }
}
```

`_id` 是搜索结果元信息的一部分，而 `_source.id` 是文档字段。对于 UUID 表，这两处包含的是同一串字符串。

现在依次调用其余用于修改数据的端点。`UPDATE` 只会修改价格：

```bash
curl -sS http://localhost:9308/update \
  -H 'Content-Type: application/json' \
  -d '{
    "table": "products_uuid",
    "id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
    "doc": {"price": 149}
  }'
```

如果要整条替换文档，就调用 `/replace`：

```bash
curl -sS http://localhost:9308/replace \
  -H 'Content-Type: application/json' \
  -d '{
    "table": "products_uuid",
    "id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
    "doc": {
      "title": "Wireless keyboard, revised",
      "sku": "JSON-KB-001",
      "price": 139
    }
  }'
```

现在删除刚刚替换的文档：

```bash
curl -sS http://localhost:9308/delete \
  -H 'Content-Type: application/json' \
  -d '{
    "table": "products_uuid",
    "id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
  }'
```

执行 `/delete` 之后，这个 UUID 对应的文档已经不在表里了。再用同一个 ID 做 `/search` 会返回 `total: 0`：

```bash
curl -sS http://localhost:9308/search \
  -H 'Content-Type: application/json' \
  -d '{
    "table": "products_uuid",
    "query": {
      "equals": {
        "id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
      }
    }
  }'
```

```json
{
  "timed_out": false,
  "hits": {
    "total": 0,
    "total_relation": "eq",
    "hits": []
  }
}
```

按 ID 和按条件删除的请求示例，收录在文档的 [删除文档](https://manual.manticoresearch.com/dev/Data_creation_and_modification/Deleting_documents) 一节。

## 在哪里生成 UUID

如果 UUID 已经由主数据库生成，直接把它作为 `id` 传给 Manticore 即可。重复提交同样的请求时，UUID 仍然保持不变。`INSERT` 会报告重复，`REPLACE` 则会用同一个 ID 写入文档的新版本。操作 `insert` 和 `replace` 在 `/bulk` 中也遵循同样的规则。

Manticore 也可以自己生成 UUID，只要不传 `id` 即可。但重复发送这种请求会创建另一条文档，因此在会自动重试的场景里，最好显式指定 UUID。

显式 ID 可使用 v1 到 v8 的任意版本 UUID。自动生成时，Manticore 使用自己的 UUIDv8 结构，但不会把来自客户端的 UUID 转换成这种格式。

## 通过 `/bulk` 批量加载

`/bulk` 接受 NDJSON：每一行都是一个单独操作。UUID 和其他 JSON API 请求一样，通过 `id` 字段传递：

```http
POST /bulk
Content-Type: application/x-ndjson

{"insert":{"table":"products_uuid","id":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb","doc":{"title":"USB hub","sku":"BULK-HUB-001","price":49}}}
{"insert":{"table":"products_uuid","id":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbc","doc":{"title":"Laptop stand","sku":"BULK-STAND-001","price":39}}}
```

在最后一行数据后还需要一个结束换行。对 `curl` 来说，使用 `--data-binary` 传递请求体比较方便，这样它不会丢失换行。

这两个操作属于同一张表，因此 Manticore 会在同一个事务中执行它们。精简响应会显示已添加多少文档，以及整个批次的处理结果：

```json
{
  "items": [
    {
      "bulk": {
        "created": 2,
        "status": 201
      }
    }
  ],
  "current_line": 2,
  "skipped_lines": 0,
  "errors": false
}
```

发生错误时，`current_line` 会指出处理停在哪一行，而 `skipped_lines` 表示被跳过的行数。如果空行或切换表把请求拆成了多个事务，Manticore 不会回滚已经完成的部分。

重复加载文档时，要选择合适的操作。如果 UUID 已存在，`insert` 会以重复错误结束。`replace` 会重新写入文档；如果该 ID 不存在，则会添加新文档。

字段 `id` 在 `/bulk` 请求中也可以省略，Manticore 会生成 UUID。但 `/bulk` 的响应只包含整个事务的总结果，不会给出每一条插入行的独立结果。如果需要保存每个文档的 ID，最好在批量请求前先生成 UUID，或者改为逐条使用 `/insert` 添加文档。

## 校验与常见错误

Manticore 会在添加文档之前校验 UUID。SQL、JSON API 和 `/bulk` 返回错误的方式不同，所以不要把代码绑定到错误消息的精确文本上。

| 传入内容 | Manticore 的处理 |
|---|---|
| `550e8400-e29b-41d4-a716-446655440000` | 添加文档 |
| `AAAAAAAA-AAAA-4AAA-8AAA-AAAAAAAAAAAA` | 添加文档，并将 UUID 以小写形式保存和返回 |
| 没有连字符或包含非法字符的字符串 | 因 UUID 格式不正确而返回错误 |
| `00000000-0000-0000-0000-000000000000` | 返回错误：不能使用零 UUID |
| 数字，包括 `0` | 返回错误：`id` 必须是字符串 |
| 版本不在 1–8 范围内，或 `variant` 不符合 RFC 的 UUID | 返回校验错误 |
| `INSERT` 或 `insert` 在 `/bulk` 中使用已存在的 ID | 返回重复错误 |

几点说明：
- 标准写法由 36 个字符组成，分成 `8-4-4-4-12` 五组。UUID 版本允许 1 到 8；按照 RFC，`variant` 位置必须是以下字符之一：`8`、`9`、`a` 或 `b`。
- Manticore 只检查 ID 的格式。生成是否正确由应用负责：UUIDv4 依赖随机性，而 UUIDv7 依赖时间部分是否正确以及是否符合规则。Manticore 不会把传入的 v4 和 v7 转换成 v8。
- 在使用 UUID 作为文档 ID 的表里，`id = 0` 不会像数字 ID 那样触发自动生成：请求会报错。但如果根本不传 `id`，Manticore 就会生成自己的 UUIDv8 结构并在响应中返回。要注意，它可以作为文档 ID 使用，但不能当作访问令牌之类的秘密值。
- 你可以在接收时用标准库校验 UUID，但要注意，Manticore 仍然会执行自己的校验。
- 在 `/bulk` 响应里检查 `errors`、`current_line` 和 `skipped_lines`：它们会告诉你处理停在哪一步，以及哪一部分批次未能提交。而在 SQL 的批量请求中，一旦发生错误，整个请求都会被取消。

## Columnar RT 与复制

UUID 也可以作为带列式存储的 RT 表中的文档 ID 使用。只需要更改引擎声明：

```sql
CREATE TABLE products_uuid_columnar (
    id uuid,
    title text,
    sku string,
    price int
) engine='columnar';
```

对于普通 RT 表和列式 RT 表，`INSERT`、`REPLACE`、`DELETE`、精确查询以及 `IN` 条件的写法都是一样的。客户端代码不依赖表引擎。但 `UPDATE` 不会修改列式属性，因此例如这类表中的价格只能通过 `REPLACE` 与整条文档一起更新。

复制同样支持 UUID。假设集群 `catalog` 已经创建，节点也已加入，本地表 `products_uuid` 也已存在。现在把它加入集群：

```sql
ALTER CLUSTER catalog ADD products_uuid;
```

在 SQL 中，集群名和表名之间使用冒号：

```sql
INSERT INTO catalog:products_uuid (id, title, sku, price)
VALUES (
    '550e8400-e29b-41d4-a716-446655441000',
    'Replicated keyboard',
    'REPL-KB-001',
    169
);

SELECT id, sku, price
FROM catalog:products_uuid
WHERE id = '550e8400-e29b-41d4-a716-446655441000';
```

在 JSON API 中，表名原样传递，集群则放在单独的字段里。例如，下面这个操作会更新之前通过 SQL 添加的文档：

```bash
curl -sS http://localhost:9308/update \
  -H 'Content-Type: application/json' \
  -d '{
    "cluster": "catalog",
    "table": "products_uuid",
    "id": "550e8400-e29b-41d4-a716-446655441000",
    "doc": {"price": 159}
  }'
```

复制之后，文档会在所有节点上保留相同的 UUID。可以据此执行精确搜索、`UPDATE`、`REPLACE` 和 `DELETE`。

## 落地前需要注意

- `uuid` 类型只能指定给 `id` 字段。它支持 RT 表，包括列式存储和复制场景，但不支持 plain、percolate/PQ 和 shard 表。
- 现有表不能通过 `ALTER TABLE` 从数字 ID 切换到 UUID，反之亦然。这个决定必须在创建新 schema 时做出。
- UUID 可以用于 `=` 和 `IN` 条件。`<`、`<=`、`>`、`>=` 范围查询，以及对 `id` 的算术运算都不受支持。
- UUIDv7 包含时间信息，但仍然不能按范围过滤 `id`。如果要按日期筛选，请添加类似 `created_at timestamp` 的属性，并据此过滤。
- 文档 ID 不能通过 `UPDATE` 修改。若要让对象获得不同的 UUID，需要创建一个带新 ID 的文档，并单独删除旧文档。

SQL 会话的行为和完整限制列表见文档：[UUID document IDs](https://manual.manticoresearch.com/dev/Creating_a_table/Data_types#UUID-document-IDs)。文档 ID 支持 UUID 是从 [Manticore Search 28.5.0](/blog/manticore-search-28-5-0/) 开始提供的。
