# UUID в Manticore: практическое руководство

Практическое руководство по работе с UUID как ID документа в Manticore Search 28.5.0: SQL, JSON API, /bulk, автоматическая генерация ID, ошибки и репликация.

В [обзорной статье](/blog/uuid-document-ids/) мы разобрали, зачем использовать в поиске тот же UUID, что и в основной базе (если таковая имеется). Здесь сразу перейдём к практике: создадим таблицу, выполним основные операции через SQL и JSON API, а затем загрузим несколько документов через `/bulk`.

Все примеры рассчитаны на Manticore Search 28.5.0 или новее. Значение `<generated UUID>` в ответах обозначает UUID, который Manticore создаст при обработке запроса. Копировать эту строку в следующий запрос не нужно: подставьте фактический `id` из своего ответа.

## Таблица для всех примеров

Чтобы UUID стал идентификатором документа, для начала объявите поле как `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 в строковый атрибут не нужно.

## Работа с UUID через SQL

Сначала вставим товар с готовым 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` UUID можно использовать в фильтре `equals`. Если запросить `id` в `_source`, результат будет содержать один и тот же UUID и в `_id`, и в `_source.id`:

```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 в таблице больше нет. `/search` по тому же ID вернёт `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 уже выдаёт основная БД, просто передавайте его в Manticore как `id`. При повторе запроса UUID останется тем же. `INSERT` сообщит о дубликате, а `REPLACE` запишет новую версию документа под тем же ID. Для операций `insert` и `replace` в `/bulk` действует то же правило.

Manticore тоже может сгенерировать UUID самостоятельно — достаточно не передавать `id`. Но повторная отправка такого запроса создаст ещё один документ, поэтому при автоматических повторах лучше задавать UUID явно.

Для явного ID подойдёт UUID любой версии от v1 до v8. При автоматической генерации Manticore использует собственную структуру UUIDv8, но не преобразует в неё UUID, полученные от клиента.

## Пакетная загрузка через `/bulk`

`/bulk` принимает NDJSON: каждая строка содержит отдельную операцию. UUID передаётся в поле `id`, как и в остальных запросах JSON API:

```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` должен быть строкой |
| UUID с версией вне диапазона 1–8 или недопустимым по RFC значением `variant` | Вернёт ошибку валидации |
| `INSERT` или `insert` в `/bulk` с уже существующим ID | Вернёт ошибку дубликата |

Некоторые моменты:
- Каноническая запись состоит из 36 символов, разбитых на группы `8-4-4-4-12`. Допустимы версии UUID от 1 до 8; в позиции `variant` по RFC должен стоять один из символов: `8`, `9`, `a` или `b`.
- Manticore проверяет только формат ID. За корректность генерации отвечает приложение: для UUIDv4 важна случайность, а для UUIDv7 — временная часть и соблюдение правил. Manticore не преобразует переданные v4 и v7 в v8.
- В таблице с UUID айдишниками `id = 0` не запускает автоматическую генерацию в отличие от числовых id: запрос завершится ошибкой. А вот если не передать `id`, Manticore как раз сгенерирует UUIDv8 собственной структуры и вернёт его в ответе. Имейте в виду, что его можно использовать как ID документа, но не как, например, токен доступа.
- Вы можете валидировать UUID стандартной библиотекой при получении, но учитывайте, что Manticore всё равно выполняет собственную проверку. 
- В ответе `/bulk` проверяйте `errors`, `current_line` и `skipped_lines`: они показывают, где остановилась обработка и какую часть пакета не удалось зафиксировать. А в пакетных запросах в SQL формате в случае ошибки отменяется весь запрос.

## Columnar RT и репликация

UUID можно также использовать и как ID документа в RT-таблицах с колоночным хранением. Меняется только объявление движка:

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

Для обычных и колоночных 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-таблицах.
- Существующую таблицу нельзя переключить с числового ID на UUID или обратно через `ALTER TABLE`. Это решение нужно принять при создании новой схемы.
- 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). Поддержка UUID как ID документа появилась в [Manticore Search 28.5.0](/blog/manticore-search-28-5-0/).
