AACsearch

Планы и лимиты

Каноничная матрица планов AACsearch, определение Search Unit, каталог квот, semantics soft/hard и код, контролирующий это.

AACsearch контролирует ёмкость из одного источника конфига — @repo/payments/lib/entitlements. Те же данные питают страницу цен, карточку плана в панели, документацию Биллинг → Планы и гейты, срабатывающие на каждом запросе. Эта страница — каноничный справочник: что значат цифры и как работают гейты.

Если вы клиент и выбираете план — Биллинг → Планы удобнее. Эта страница — инженерный deep-dive: читайте, когда нужно точно знать, что считается, когда гейт срабатывает и какая форма ответа.

Матрица планов

ПланSearch units / месДокументыИндексыМестаAPI-ключи/индексСинки коннект./месRetention аналитикиПоддержка
Free10 0001 000133307 днейCommunity
Starter100 00010 000310530030 днейEmail
Pro1 000 000100 0001025103 00090 днейPriority email
Business5 000 0001 000 000501002030 000365 дней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 (любые фильтры/сортировка/пагинация)1SearchUsageEvent { type: "search" }
POST /multi-search с N запросамиNПо строке на каждый запрос
POST /search/suggest / autocomplete1SearchUsageEvent { type: "suggest" }
Только-агрегатный запрос (per_page: 0)1Считается как поиск
PUT /indexes/{id}/documents (одиночный)1SearchUsageEvent { type: "ingest" }
POST /indexes/{id}/documents:batch (N доков)NПо строке на каждую успешно обработанную строку
DELETE /indexes/{id}/documents/{external_id}1SearchUsageEvent { 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 Units1-й день биллинг-периода в 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:

  1. Найти последний активный Purchase для org.
  2. Маппинг priceIdplanId через packages/payments/config.ts.
  3. Кэш 60 сек в процессе (per-server).
  4. invalidatePlanCache(orgId) вызывается из webhook провайдера — изменения за секунды.
  5. 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 matrixpackages/payments/lib/entitlements.ts
Quota middlewarepackages/api/modules/entitlements/middleware/quota-check.ts
Feature-gate middlewarepackages/api/modules/entitlements/middleware/feature-gate.ts
Резолв планаpackages/payments/lib/entitlements.ts (resolveOrgPlan)
Маппинг provider price → planpackages/payments/lib/provider-price-ids.ts
Модель rate-limit bucketpackages/database/prisma/schema.prisma (SearchRateLimitBucket)
Модель usage eventpackages/database/prisma/schema.prisma (SearchUsageEvent)
Overage transactionspackages/database/prisma/schema.prisma (OverageTransaction)
Wallet ledgerpackages/database/prisma/schema.prisma (WalletLedgerEntry)

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

On this page