Справочник по схеме индекса
Полный справочник по схеме индекса — обязательные поля, типы полей, флаги searchable / facet / sort, правила slug, ошибки валидации, примеры каталога товаров и каталога контента.
Схема индекса описывает, какие поля хранит поисковый индекс, их типы, и как каждое поле ищется, фильтруется, сортируется и фасетируется. Схема фиксируется в момент создания индекса; добавление или удаление полей в дальнейшем требует переиндексации (без даунтайма через смену alias).
Эта страница — справочник по схеме, которую вы передаёте в search.createIndex и в нижележащий хелпер createPhysicalCollection() из @repo/search. Если вам нужен быстрый старт на 2 минуты — смотрите Создать первый индекс.
Жизненный цикл индекса
draft → created → ingesting → searching → reindexing → searching
↑ ↓
└──── alias swap ┘- Created — запись индекса появляется в Postgres (модель
SearchIndex), а в Typesense создаётся версионированная коллекция (<prefix>_<org>_<slug>_v1) и alias (<prefix>_<org>_<slug>). - Ingesting — документы попадают в
SearchIngestBufferи переносятся в Typesense фоновым воркером. Запись всегда идёт «через БД» (Инвариант 2): публичные клиенты никогда не пишут в Typesense напрямую. - Searching — запросы всегда идут в alias, а не в версионированную коллекцию. Это делает любую будущую переиндексацию прозрачной для клиентов.
- Reindexing — при изменении схемы параллельно строится новая версия (
_v2,_v3, …). Когда новая коллекция полностью заполнена, alias атомарно переключается. Старую версионированную коллекцию можно удалить.
Имена коллекций и alias-ов изолированы по организации, поэтому тенанты разделены на уровне движка (Инвариант 5).
Правила slug
У каждого индекса есть slug — идентификатор внутри организации. Slug должен удовлетворять:
| Правило | Значение |
|---|---|
| Длина | 1–64 символа |
| Алфавит | строчные буквы, цифры, дефисы (a-z0-9-) |
| Первый символ | буква или цифра (без ведущего дефиса) |
| Regex | ^[a-z0-9][a-z0-9-]*$ |
Допустимо: products, articles, help-center, catalog-v2.
Недопустимо: Products (верхний регистр), -articles (ведущий дефис), news_2024 (подчёркивание), my products (пробел), 🔥-hot (не-ASCII).
Slug ещё раз санитизируется на уровне коллекций (packages/search/lib/collections.ts — функция sanitize()), но первая ошибка, которую увидит вызывающий код — это API-валидация выше: вернётся BAD_REQUEST с сообщением slug must be lowercase letters, digits, and dashes.
Обязательные поля
Эти поля вы не объявляете — их подставляет система:
| Поле | Тип | Зачем оно нужно |
|---|---|---|
id | string | Идентификатор документа в Typesense. Должен быть уникальным в пределах коллекции. Используйте стабильный внешний id (SKU, slug, UUID), чтобы повторный ingest был идемпотентным. |
organization_id | string | Ключ тенанта. Подставляется внутри createPhysicalCollection() и AND-комбинируется в каждый публичный поисковый вызов (Инвариант 5). Вручную не задаётся. |
Если вы передадите organization_id в схеме — оно будет отфильтровано и заменено каноническим полем тенанта. Обойти это нельзя.
Типы полей
Схема поддерживает следующие типы (полный список — searchFieldSchema в packages/api/modules/search/types.ts):
| Тип | Когда использовать |
|---|---|
string | Текстовые значения — названия, описания, идентификаторы, enum-подобные строки |
string[] | Множественные строки — теги, категории, список брендов |
string* | Любая строка (string или string[]). Менее строгий вариант; по возможности используйте конкретный тип. |
int32 | 32-битный знаковый int. Счётчики, оценки ≤ 2³¹ |
int64 | 64-битный знаковый int. Unix-таймстампы (в секундах, не мс), большие счётчики |
float | 64-битный float. Рейтинги. Деньги в минорных единицах храните как int64, а не float. |
bool | Булево значение |
int32[], int64[], float[], bool[] | Массивные варианты |
object | Вложенный JSON-объект — работает, потому что enable_nested_fields: true включено по умолчанию |
object[] | Массив вложенных объектов |
auto | Тип выводится при первом ingest. В продакшене не используйте — только для исследовательских индексов. |
geopoint | Пара [lat, lng] для гео-поиска |
geopoint[] | Несколько гео-точек на документ |
geopolygon | Полигон GeoJSON для региональных фильтров |
geojson | Произвольная GeoJSON-геометрия |
image | Поле-изображение для image search (на векторной основе) |
Векторные поля — это type: "float[]" плюс num_dim (и опционально hnsw_params, vec_dist). См. AI Search.
Деньги, даты, деньги-как-float
- Деньги — храните как
int64в минорных единицах (копейки, центы). Форматируйте на UI. Инвариант 16 запрещает decimal/float-деньги в выходных схемах oRPC. - Даты — храните как
int64Unix-секунды.created_at,updated_at,published_at— общепринятые имена. - Булевы значения — явные
true/false. Не пишите строки"yes"/"no".
Флаги полей
Каждое поле принимает небольшой набор флагов. Они определяют, как Typesense строит внутренние индексы — а это напрямую влияет на латентность и память.
| Флаг | По умолчанию | Что делает |
|---|---|---|
facet | false | Делает поле фильтруемым + фасетируемым. Нужно для работы filterBy: и facetBy: на этом поле. Дёшево для строк; дорого для числовых полей с большой кардинальностью. |
sort | true для числовых, false для строковых | При true поле становится сортируемым. Числовые сортируемы по умолчанию. Для строк нужно явное sort: true, чтобы работал sortBy: "title:asc". |
optional | false | При true документ без этого поля всё равно принимается. Без флага отсутствующее поле приводит к ошибке валидации ingest. |
index | true | При false поле хранится, но не индексируется. Поиск, фильтр и фасет по нему недоступны. Полезно для полей, которые вы только возвращаете в результате. |
store | true | При false поле индексируется, но не сохраняется. Можно искать/фильтровать, но в документе оно не вернётся. Экономит диск. |
range_index | false | При true строится явный range-индекс для числовых полей. Ускоряет фильтры вида price:[10..100] на больших индексах. Стоит дополнительной памяти. |
stem | false | При true применяется стемминг при индексировании (для англ.: "running" → "run"). Используйте вместе с соответствующим стеммером по языку. |
truncate | false | При true длинные значения обрезаются под токен-лимит Typesense. Используйте для полей, где обрезание допустимо. |
truncate_len | не задано | Per-field обрезание (Typesense v30+). 1–16384. |
num_dim | не задано | Обязательно, если float[] используется как векторное поле. Размерность эмбеддинга. |
hnsw_params | не задано | Параметры HNSW для векторных полей (ef_construction, M). Дефолты безопасны; тюньте только при наличии бенчмарка. |
vec_dist | "cosine" | "cosine" или "ip" (inner product). Выбирайте в зависимости от модели эмбеддингов. |
locale | не задано | На строковом/фасетируемом поле — трактовать значения как иерархические пути. Например, "Electronics/Phones/Smartphones" с locale: "/" станет drill-down фасетом. |
Как выставлять флаги эффективно
Три правила, которые отлавливают большинство ошибок:
- Не фасетируйте то, по чему не фильтруете. Фасетирование на поле с большой кардинальностью (например, свободный текст описания) увеличивает память без пользы.
- Явно помечайте сортируемые строки.
titleне будет сортируемым, пока вы не выставитеsort: true. - Используйте
optional: trueдля разрежённых полей. Sale price, deprecated-флаги, поля для конкретной локали — всё, чего нет на каждом документе.
default_sorting_field
Настройка коллекции (не per-field). Применяется, когда в запросе нет sortBy. Типичные варианты:
"_text_match"— релевантность (большинство поисковых сценариев)"popularity_score:desc"— взвешенная популярность (e-commerce)"created_at:desc"— сначала самое новое (контент / новости)
Поле должно быть сортируемым (числовым или sort: true).
Ошибки валидации схемы
Zod-валидатор (searchFieldSchema и searchIndexSlugSchema в packages/api/modules/search/types.ts) возвращает структурированные ошибки. Самые частые на BAD_REQUEST:
| Ошибка | Что не так |
|---|---|
slug must be lowercase letters, digits, and dashes | Slug не прошёл regex. Только строчные, без подчёркиваний, без ведущего дефиса. |
String must contain at least 1 character(s) at "fields.0.name" | Пустое имя поля. |
String must contain at most 64 character(s) at "fields.0.name" | Имя поля длиннее 64 символов. |
Invalid enum value at "fields.0.type" | Тип не из списка. Проверьте написание — "integer" нельзя; используйте "int32" или "int64". |
Number must be a positive integer at "fields.0.num_dim" | Векторные поля требуют num_dim > 0. |
Number must be greater than or equal to 1 at "fields.0.truncate_len" | truncate_len — это 1–16384. |
Typesense: Field \<name>` should be set as sortable` | Возникает в момент запроса (не на этапе схемы), когда вы делаете sortBy: строкового поля без sort: true. Поправьте схему и переиндексируйте. |
Typesense: default_sorting_field \<f>` is not a sortable type` | Поле для дефолтной сортировки не сортируемое. Возьмите числовое поле или добавьте sort: true. |
Валидация идёт до любого вызова Typesense. Если вы видите ошибку со стороны Typesense (например, кривые hnsw_params), её сообщение нормализуется в public-handler.ts до того, как попадёт к клиенту — сырое сообщение наружу никогда не уходит (Инвариант 6).
Пример: каталог товаров
Схема под e-commerce. Поисковые текстовые поля взвешиваются через queryBy на этапе запроса, а не в схеме (см. Multi-search и querying).
import { orpc } from "@shared/lib/orpc-query-utils";
await orpc.search.createIndex.call({
organizationId: "org_...",
slug: "products",
name: "Каталог товаров",
fields: [
{ name: "id", type: "string" },
{ name: "title", type: "string", sort: true },
{ name: "sku", type: "string" },
{ name: "brand", type: "string", facet: true, sort: true },
{ name: "categories", type: "string[]", facet: true },
{ name: "description", type: "string", optional: true },
{ name: "price", type: "int64", facet: true, range_index: true },
{ name: "sale_price", type: "int64", optional: true, facet: true },
{ name: "currency", type: "string", facet: true },
{ name: "availability", type: "string", facet: true },
{ name: "rating", type: "float", facet: true, optional: true },
{ name: "locale", type: "string", facet: true },
{ name: "created_at", type: "int64", sort: true },
{ name: "image_url", type: "string", index: false },
],
defaultSortingField: "_text_match",
});Заметки:
priceиsale_priceхранятся какint64в минорных единицах.999_99— это 999.99 в валюте отображения.image_urlпомеченindex: false— возвращается в результате, но не индексируется.categories—string[]сfacet: true, поэтому работает иfilterBy: "categories:=Audio", иfacetBy: "categories".description—optional: true, чтобы товары без описания всё равно индексировались.
Пример: каталог контента
Схема под статьи, help-center, блог. Сортируемые строки, полнотекстовый body, иерархические категории, сортировка по таймстампу.
await orpc.search.createIndex.call({
organizationId: "org_...",
slug: "help-center",
name: "Help-center статьи",
fields: [
{ name: "id", type: "string" },
{ name: "title", type: "string", sort: true },
{ name: "excerpt", type: "string", optional: true },
{ name: "body", type: "string", stem: true },
{ name: "author", type: "string", facet: true, sort: true, optional: true },
{ name: "section", type: "string", facet: true, locale: "/" },
{ name: "tags", type: "string[]", facet: true, optional: true },
{ name: "locale", type: "string", facet: true },
{ name: "reading_time", type: "int32", facet: true, optional: true },
{ name: "published_at", type: "int64", sort: true },
{ name: "updated_at", type: "int64", sort: true },
],
defaultSortingField: "published_at",
});Заметки:
bodyпомеченstem: true— запрос"installs"найдёт"installation".sectionиспользуетlocale: "/", поэтому значения вроде"Getting Started/First Index/Schema"становятся drill-down фасетами в дашборде.- Дефолтная сортировка —
published_at, чтобы пустой запрос отдавал сначала самые свежие статьи. - Никакой каши флагов —
optionalстоит явно только на разрежённых полях.
Изменение схемы после создания
Добавление, удаление полей и смена флагов требуют переиндексации: строится новая версионированная коллекция с новой схемой, документы заново загружаются, alias атомарно переключается. Старые данные остаются доступными во время всей операции. Триггер описан в Ingest и reindex, механика — в Reindexing и zero downtime.
Короткое правило: никогда не правьте схему «на лету». Всегда идите через reindex.
Связанные страницы
- Создать первый индекс — быстрый старт на 2 минуты
- Ingest и reindex — массовая загрузка и миграции схемы
- Фильтры, сортировка и пагинация — как флаги полей влияют на синтаксис запросов
- Multi-search и querying —
queryByи веса полей - Reindexing и zero downtime — внутренности alias-swap