Планы и лимиты
Каноничная матрица планов AACsearch, определение Search Unit, каталог квот, semantics soft/hard и код, контролирующий это.
AACsearch контролирует ёмкость из одного источника конфига — @repo/payments/lib/entitlements. Те же данные питают страницу цен, карточку плана в панели, документацию Биллинг → Планы и гейты, срабатывающие на каждом запросе. Эта страница — каноничный справочник: что значат цифры и как работают гейты.
Если вы клиент и выбираете план — Биллинг → Планы удобнее. Эта страница — инженерный deep-dive: читайте, когда нужно точно знать, что считается, когда гейт срабатывает и какая форма ответа.
Матрица планов
| План | Search units / мес | Документы | Индексы | Места | API-ключи/индекс | Синки коннект./мес | Retention аналитики | Поддержка |
|---|---|---|---|---|---|---|---|---|
| Free | 10 000 | 1 000 | 1 | 3 | 3 | 30 | 7 дней | Community |
| Starter | 100 000 | 10 000 | 3 | 10 | 5 | 300 | 30 дней | |
| Pro | 1 000 000 | 100 000 | 10 | 25 | 10 | 3 000 | 90 дней | Priority email |
| Business | 5 000 000 | 1 000 000 | 50 | 100 | 20 | 30 000 | 365 дней | Dedicated |
| Enterprise | По договорённости | По дог. | По дог. | По дог. | По дог. | По дог. | По дог. | SLA 99.95% |
Точные цифры — в PLAN_LIMITS в packages/payments/lib/entitlements.ts. Маркетинг-страница, панель и billing/plans.mdx тянут из одних констант. Меняете в одном — правьте источник; остальное подтянется на следующем релизе.
Search Units
Главная квота — Search Unit. Определение умышленно простое:
Один Search Unit = один поисковый запрос ИЛИ одна запись документа.
Оба идут из одного ведра, потому что у них схожая нагрузка на инфраструктуру и схожий клиентский профиль. Единый cap даёт клиенту свободу гибко переходить между query-heavy и ingest-heavy внутри одного бюджета.
Что эмитирует Search Unit?
| Действие | Юнитов | Запись события |
|---|---|---|
POST /search (любые фильтры/сортировка/пагинация) | 1 | SearchUsageEvent { type: "search" } |
POST /multi-search с N запросами | N | По строке на каждый запрос |
POST /search/suggest / autocomplete | 1 | SearchUsageEvent { type: "suggest" } |
Только-агрегатный запрос (per_page: 0) | 1 | Считается как поиск |
PUT /indexes/{id}/documents (одиночный) | 1 | SearchUsageEvent { type: "ingest" } |
POST /indexes/{id}/documents:batch (N доков) | N | По строке на каждую успешно обработанную строку |
DELETE /indexes/{id}/documents/{external_id} | 1 | SearchUsageEvent { type: "ingest_delete" } |
DELETE …documents:byFilter, под фильтр M документов | M | По строке на каждый сматченный |
Полный реиндекс (POST /indexes/{id}/sync) | M | По строке на каждый ре-эмитированный документ |
| Delta-синк коннектора, M документов | M | По строке на доку + 1 connector-sync unit (отдельная квота) |
Неудачные запросы (4xx, 5xx, 429) не тратят юниты. Удачные запросы с нулевыми хитами — тратят (для движка это та же работа).
Что НЕ эмитирует?
- AI-вызовы (Knowledge / RAG, embeddings, rerank, summarize, chat) — идут по кошельку, отдельный реестр.
- Heartbeats токенов коннекторов.
- Чтение в панели (server-side, не метерится).
- Admin-операции (правка схемы, создание ключей, инвайты).
Каталог квот
В AACsearch шесть квот. Пять идут в план; шестая (кошелёк) — независимая.
1. Search Units (per month)
Описано выше. Сброс в 00:00:00 UTC 1-го числа биллинг-периода (месячный) или на годовщину (годовой). Клиентский вид: Биллинг → Квоты.
2. Индексируемые документы (steady-state)
Каплит общее число документов во всех индексах в любой момент. Проверяется на ингесте, не периодическим батчем. В отличие от Search Units, это steady-state cap — месячного сброса нет, потому что счётчик — снимок, а не поток.
При достижении новые upsert-ы → quota_exceeded. Чтение работает. Удаления освобождают сразу.
Документ — одна строка вне зависимости от количества полей или длины, но очень большие документы (>1 МБ) упрутся в per-document size limit движка. Цельтесь в <64 КБ.
3. Индексы на организацию
Hard cap на количество строк SearchIndex. При попадании в потолок — index_limit_reached на create. Существующие работают; удаление одного освобождает слот сразу.
4. Синхронизации коннекторов (per month)
Каждый full/delta-ран токена ss_connector_* стоит один connector-sync unit + N search units (по одному на каждый тронутый документ).
Heartbeats бесплатны. Этот cap чаще всего становится ограничителем для высокочастотных коннекторов (пятиминутный поллинг PrestaShop = 8 640 синков/мес, что выше Pro 3 000). Решайте: длиннее интервал, выше план, или webhook-driven push из CMS (без поллинга).
5. Места
Активные участники в org. Owner / admin / member / viewer все считаются; удалённые освобождают место сразу; pending-инвайты не считаются.
Капы — в PLAN_LIMITS.maxSeats. На потолке кнопка Invite неактивна с подсказкой.
6. AI-использование (кошелёк, независим от плана)
Кошелёк — предоплаченный баланс в микро-USD (или копейках), списывается per-AI-вызов. План не каплит AI-использование — каплит баланс кошелька. План только гейтит, доступны ли AI-функции в принципе (Pro+ для Knowledge и AI rerank).
Полная механика: Биллинг → Кошелёк и AI-кредиты.
Retention аналитики (не квота, а атрибут плана)
Селектор периода в Аналитике гейтится retention плана — Free / Starter не могут 90d / 365d. Контролируется на процедурах агрегации через featureGate("analyticsRetention", "30d") и т.д.
Включено для полноты — это plan limit, хотя не счётчик, который можно «истратить».
Soft caps и hard caps
Soft cap — порог, дающий предупреждение без блока трафика. Hard cap — отклонение запроса. AACsearch использует оба:
- 80% от квоты → soft cap. Баннер в панели; advisory-заголовок на каждом ответе; однократный email биллинг-контактам.
- 100% от квоты → hard cap. API → 429 (
search_quota_exceeded); ингест блокируется; новые синки — в очередь. - Выше 100% → overage. По умолчанию off. На Pro/Business при включении — запросы продолжают по metered post-paid-ставке. См. Биллинг → Квоты → Overage.
Код:
// packages/api/modules/entitlements/middleware/quota-check.ts
const result = await checkQuota(orgId, "search");
// result.allowed — true, если ниже 100%
// result.isSoftCap — true, если между 80% и 100%
// result.isHardCap — true, если на/выше 100% (и overage off)
// result.percentUsed — точный процент (для UI)
// result.remaining — юнитов осталось в периоде
// result.overageRateUsdMicrosPerSearch — присутствует только если overage onПри isHardCap === true и overage off — запрос отклонён:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
{
"error": "search_quota_exceeded",
"detail": "Месячная квота поиска исчерпана. Обновите план или дождитесь сброса.",
"quota": "search",
"limit": 1000000,
"used": 1000000,
"resetsAt": "2025-11-01T00:00:00Z"
}При isHardCap === true и overage on — запрос проходит; строка OverageTransaction фиксирует юнит по настроенной per-unit цене.
Зачем soft + hard, а не одно?
- Только soft позволял бы runaway-виджету разорить клиента; по умолчанию hard.
- Только hard удивлял бы клиента без предупреждения — плохо для churn.
- Баннер 80% — шанс обновить план или подрезать до остановки трафика.
Поведение сброса квот
Сбросы:
| Квота | Триггер сброса | Замечания |
|---|---|---|
| Search Units | 1-й день биллинг-периода в 00:00:00 UTC | Счётчик в 0; overage прошлого периода в следующий счёт |
| Синки коннекторов | Тот же якорь | — |
| Индексируемые документы | Сброса нет — steady-state | Подрезайте или обновляйте план |
| Индексы | Сброса нет — steady-state | Удалите индекс — освободится слот |
| Места | Сброса нет — steady-state | Удалите участников — освободятся места |
| Spending limit кошелька | 1-е число календарного месяца | Сам баланс кошелька не сбрасывается |
Rollover нет. Неиспользованные Search Units не переходят дальше.
Для годовых клиентов якорь сброса — месячный anniversary первого платежа, не календарный месяц. Это убирает ловушку, когда оплативший 15-го находит счётчик сброшенным 1-го (с 14 днями «потерянного» использования).
Последовательность гейтов
Каждый поисковый/ингест-запрос проходит стек гейтов в packages/api/modules/search/public-handler.ts:
Запрос
↓
Auth — public-auth.ts. Префикс ключа + хэш + scopes.
↓
Origin — Origin совпадает с allowedOrigins (если задано).
↓
Tenant — ключ принадлежит заявленной org.
↓
Feature — quotaCheck.ts → resolveOrgPlan(orgId) → featureGate("synonyms")
↓
Quota — quotaCheck.ts → checkQuota(orgId, "search")
↓ allowed=false → 429 quota_exceeded (return)
isSoftCap=true → ставим X-Aacsearch-Quota-Warning
↓
Rate — rate-limit.ts → sliding window на SearchRateLimitBucket
↓ exceeded → 429 rate_limit_exceeded (return)
↓
Search / ingest — клиент Typesense
↓
Эмит события — SearchUsageEvent (best-effort; падение не блокирует ответ)
↓
ОтветПадения на любом гейте — типизированные JSON-ошибки, не сырое от upstream (Invariant 6). Падение на auth/tenant — без события; падение на feature/quota/rate — событие SearchUsageEvent { type: "{gate}_block" }, видно в Аналитике → Ошибки.
Резолв плана
resolveOrgPlan(orgId) в @repo/payments:
- Найти последний активный
Purchaseдля org. - Маппинг
priceId→planIdчерезpackages/payments/config.ts. - Кэш 60 сек в процессе (per-server).
invalidatePlanCache(orgId)вызывается из webhook провайдера — изменения за секунды.- Fail open — провайдер недоступен → лимиты Free, не блокируем трафик.
60-секундный кэш — потолок задержки распространения. Клиент после апгрейда увидит новые лимиты на следующем тике webhook + следующем cache miss по серверу — обычно секунды.
oRPC процедура entitlements
entitlements.getPlanInfo возвращает live-снимок плана + использования:
const planInfo = await orpc.entitlements.getPlanInfo.call({ organizationId });
// {
// planId: "pro",
// planName: "Pro",
// searchUnitsUsed: "847352", // BigInt как строка (Invariant 7)
// searchUnitsLimit: "1000000",
// percentUsed: 84.7,
// isSoftCap: true,
// isHardCap: false,
// indexCount: 7,
// indexLimit: 10,
// seatCount: 18,
// seatLimit: 25,
// features: { synonyms: true, curations: true, scopedTokens: true, ... },
// resetsAt: "2025-11-01T00:00:00.000Z"
// }Используется в панели и кастомной admin-обвязке. BigInt-поля идут строками (Invariant 7) — конвертируйте обратно при нужде в арифметике.
Rate limiting (отдельно от квоты)
Rate limit — per API key, квота — per organization. Оба могут упасть на одном запросе:
- Per-key: sliding-window bucket в
SearchRateLimitBucket, по умолчанию 600 req/min (per-key конфигурируется). - Per-org: месячная квота Search Units.
Превышение лимита — 429 с rate_limit_exceeded + Retry-After. Bucket — (keyId, windowStart), окно ротируется каждые 60 секунд.
Rate-limit-фейлы не тратят Search Units. Quota check — раньше, rate gate — последний фильтр перед движком.
См. Search API → Ошибки и лимиты — заголовки, ответы, тюнинг.
Примеры биллинга
Малый магазин
- 5 000 SKU, один индекс.
- 30 000 посетителей/мес × 4 поиска = 120 000 Search Units.
- Daily sync = 30 синков/мес.
- Без AI.
Проверка cap:
- Search Units: 120k → больше Starter (100k), меньше Pro (1M) → Pro.
- Документы: 5k → Starter (10k) и Pro (100k) подходят.
- Синки: 30 → подходит любому платному.
Pro ($99/мес по драфтовой цене). Кошелёк: ноль.
База знаний с AI Q&A
- 2 000 статей.
- 50 000 поисков/мес (значительно ниже Pro 1M).
- 5 000 Knowledge-запросов/мес × 1 500 токенов = 7,5M токенов.
- Смешанная ставка ~$0,005 / 1k токенов → ~$37,50/мес из кошелька.
План: Pro. Кошелёк: $40/мес, автозачисление при пороге $5.
Маркетплейс со scoped-токенами
- 8 тенантов изолированы scoped-токенами (Pro+).
- 80 000 документов в одном общем индексе.
- 2M поисков/мес.
План: Pro (scoped-токены, влезает в search-юниты). Кошелёк: ноль, если нет AI rerank.
Пик Black Friday на Business
- Обычный месяц: 4M поисков.
- Неделя BF: +2M → 6M → 1M выше Business cap.
- Overage on по $0,00008 за Search Unit → 1M × $0,00008 = $80 overage в счёт.
Дешевле, чем апгрейд на Enterprise на одну неделю; spending limit $200 ограничивает худший сценарий.
Override администратора
- Org admins видят план + использование в
/[orgSlug]/settings/billing. - Платформенные admin на
/admin/organizationsинспектируют права любой org и могут вручную править назначение плана — для миграций и edge-cases.
Ручные правки эмитят admin_override_plan в аудит (одно из действий AuditLog). См. Безопасность → Аудит — retention и экспорт.
Где цифры в коде
| Сущность | Файл |
|---|---|
Матрица планов (PLAN_LIMITS) | packages/payments/lib/entitlements.ts |
| Feature matrix | packages/payments/lib/entitlements.ts |
| Quota middleware | packages/api/modules/entitlements/middleware/quota-check.ts |
| Feature-gate middleware | packages/api/modules/entitlements/middleware/feature-gate.ts |
| Резолв плана | packages/payments/lib/entitlements.ts (resolveOrgPlan) |
| Маппинг provider price → plan | packages/payments/lib/provider-price-ids.ts |
| Модель rate-limit bucket | packages/database/prisma/schema.prisma (SearchRateLimitBucket) |
| Модель usage event | packages/database/prisma/schema.prisma (SearchUsageEvent) |
| Overage transactions | packages/database/prisma/schema.prisma (OverageTransaction) |
| Wallet ledger | packages/database/prisma/schema.prisma (WalletLedgerEntry) |
Связанные страницы
- Биллинг → Планы — клиентская матрица с подбором.
- Биллинг → Единицы потребления — справочник единиц.
- Биллинг → Квоты — клиентский взгляд на soft/hard.
- Биллинг → Кошелёк и AI-кредиты — pay-as-you-go AI.
- Search API → Ошибки и лимиты — каталог HTTP-ошибок.
- Troubleshooting → Billing limits — что делать при срабатывании квоты.
- Безопасность → Аудит — записанные события смены плана.