AACsearch
Начало работы

Справочник по схеме индекса

Полный справочник по схеме индекса — обязательные поля, типы полей, флаги searchable / facet / sort, правила slug, ошибки валидации, примеры каталога товаров и каталога контента.

Схема индекса описывает, какие поля хранит поисковый индекс, их типы, и как каждое поле ищется, фильтруется, сортируется и фасетируется. Схема фиксируется в момент создания индекса; добавление или удаление полей в дальнейшем требует переиндексации (без даунтайма через смену alias).

Эта страница — справочник по схеме, которую вы передаёте в search.createIndex и в нижележащий хелпер createPhysicalCollection() из @repo/search. Если вам нужен быстрый старт на 2 минуты — смотрите Создать первый индекс.

Жизненный цикл индекса

draft → created → ingesting → searching → reindexing → searching
                                              ↑                ↓
                                              └──── alias swap ┘
  1. Created — запись индекса появляется в Postgres (модель SearchIndex), а в Typesense создаётся версионированная коллекция (<prefix>_<org>_<slug>_v1) и alias (<prefix>_<org>_<slug>).
  2. Ingesting — документы попадают в SearchIngestBuffer и переносятся в Typesense фоновым воркером. Запись всегда идёт «через БД» (Инвариант 2): публичные клиенты никогда не пишут в Typesense напрямую.
  3. Searching — запросы всегда идут в alias, а не в версионированную коллекцию. Это делает любую будущую переиндексацию прозрачной для клиентов.
  4. 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.

Обязательные поля

Эти поля вы не объявляете — их подставляет система:

ПолеТипЗачем оно нужно
idstringИдентификатор документа в Typesense. Должен быть уникальным в пределах коллекции. Используйте стабильный внешний id (SKU, slug, UUID), чтобы повторный ingest был идемпотентным.
organization_idstringКлюч тенанта. Подставляется внутри createPhysicalCollection() и AND-комбинируется в каждый публичный поисковый вызов (Инвариант 5). Вручную не задаётся.

Если вы передадите organization_id в схеме — оно будет отфильтровано и заменено каноническим полем тенанта. Обойти это нельзя.

Типы полей

Схема поддерживает следующие типы (полный список — searchFieldSchema в packages/api/modules/search/types.ts):

ТипКогда использовать
stringТекстовые значения — названия, описания, идентификаторы, enum-подобные строки
string[]Множественные строки — теги, категории, список брендов
string*Любая строка (string или string[]). Менее строгий вариант; по возможности используйте конкретный тип.
int3232-битный знаковый int. Счётчики, оценки ≤ 2³¹
int6464-битный знаковый int. Unix-таймстампы (в секундах, не мс), большие счётчики
float64-битный 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.
  • Даты — храните как int64 Unix-секунды. created_at, updated_at, published_at — общепринятые имена.
  • Булевы значения — явные true / false. Не пишите строки "yes" / "no".

Флаги полей

Каждое поле принимает небольшой набор флагов. Они определяют, как Typesense строит внутренние индексы — а это напрямую влияет на латентность и память.

ФлагПо умолчаниюЧто делает
facetfalseДелает поле фильтруемым + фасетируемым. Нужно для работы filterBy: и facetBy: на этом поле. Дёшево для строк; дорого для числовых полей с большой кардинальностью.
sorttrue для числовых, false для строковыхПри true поле становится сортируемым. Числовые сортируемы по умолчанию. Для строк нужно явное sort: true, чтобы работал sortBy: "title:asc".
optionalfalseПри true документ без этого поля всё равно принимается. Без флага отсутствующее поле приводит к ошибке валидации ingest.
indextrueПри false поле хранится, но не индексируется. Поиск, фильтр и фасет по нему недоступны. Полезно для полей, которые вы только возвращаете в результате.
storetrueПри false поле индексируется, но не сохраняется. Можно искать/фильтровать, но в документе оно не вернётся. Экономит диск.
range_indexfalseПри true строится явный range-индекс для числовых полей. Ускоряет фильтры вида price:[10..100] на больших индексах. Стоит дополнительной памяти.
stemfalseПри true применяется стемминг при индексировании (для англ.: "running""run"). Используйте вместе с соответствующим стеммером по языку.
truncatefalseПри 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 фасетом.

Как выставлять флаги эффективно

Три правила, которые отлавливают большинство ошибок:

  1. Не фасетируйте то, по чему не фильтруете. Фасетирование на поле с большой кардинальностью (например, свободный текст описания) увеличивает память без пользы.
  2. Явно помечайте сортируемые строки. title не будет сортируемым, пока вы не выставите sort: true.
  3. Используйте 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 dashesSlug не прошёл 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 — возвращается в результате, но не индексируется.
  • categoriesstring[] с facet: true, поэтому работает и filterBy: "categories:=Audio", и facetBy: "categories".
  • descriptionoptional: 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.

Связанные страницы

On this page