⚠️ 此页面为自动翻译,翻译可能不完美。
blog-post

Manticore 中的 UUID:实战指南

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

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

所有示例使用的表

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

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 的商品:

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

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

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

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

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

SELECT LAST_INSERT_ID();

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

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

INSERT 多个文档以及 @@session.last_insert_id 变量在文档的 向表中添加文档 一节里有详细说明。

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

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 本身保持不变:

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

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

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

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

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 会直接添加文档。详情请参阅文档:UPDATEREPLACE

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

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

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

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:

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,保存时会转为小写,并且在响应中也以同样形式返回:

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

自动生成时,只需要把 id 字段整个去掉:

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:

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

/search 中,可以用 equals 过滤 UUID。如果请求将 id 包含在 _source 中,结果会在 _id_source.id 中返回同一个 UUID:

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"]
  }'
{
  "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 只会修改价格:

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

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
    }
  }'

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

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

curl -sS http://localhost:9308/search \
  -H 'Content-Type: application/json' \
  -d '{
    "table": "products_uuid",
    "query": {
      "equals": {
        "id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
      }
    }
  }'
{
  "timed_out": false,
  "hits": {
    "total": 0,
    "total_relation": "eq",
    "hits": []
  }
}

按 ID 和按条件删除的请求示例,收录在文档的 删除文档 一节。

在哪里生成 UUID

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

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

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

通过 /bulk 批量加载

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

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 会在同一个事务中执行它们。精简响应会显示已添加多少文档,以及整个批次的处理结果:

{
  "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返回校验错误
INSERTinsert/bulk 中使用已存在的 ID返回重复错误

几点说明:

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

Columnar RT 与复制

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

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

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

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

ALTER CLUSTER catalog ADD products_uuid;

在 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 添加的文档:

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。可以据此执行精确搜索、UPDATEREPLACEDELETE

落地前需要注意

  • 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 。文档 ID 支持 UUID 是从 Manticore Search 28.5.0 开始提供的。

安装Manticore Search

在 Linux 或 macOS 上用一条命令安装 Manticore Search:

curl https://manticoresearch.com | sh

有关高级安装选项,请参阅完整安装指南手册

安装Manticore Search