AACsearch
Архитектура

Путь чтения

Как поисковый запрос проходит через AACsearch — от клиентского SDK к /search/public/multi, через public-auth и сборку tenant-фильтра, в multi_search Typesense и обратно в виде очищенного ответа.

Путь чтения — stateless и горячий: на синхронном поисковом запросе в PostgreSQL не пишется ни одной строки (события использования записываются fire-and-forget). Вся цепочка — auth, объединение scoped-фильтра, объединение tenant-фильтра, вызов multi_search — спроектирована так, чтобы добавлять минимальные накладные расходы поверх базового вызова Typesense.

Поток

Описание

Диаграмма прослеживает публичный поисковый запрос от клиентского SDK через проверку токена (HMAC-проверка scoped-токена либо поиск parent-ключа по хешу), гейты origin и rate-limit, а затем через combineTenantFilter, который AND-объединяет пользовательский фильтр, фильтр scoped-токена и инжектируемую сервером клаузу tenantId, прежде чем multi_search уйдёт в Typesense. Очищенный ответ возвращается синхронно, тогда как строка SearchUsageEvent асинхронно ставится в очередь для аналитики.

sequenceDiagram
    autonumber
    participant SDK as Customer SDK / Widget
    participant API as packages/api/modules/search/public-handler.ts
    participant Gate as gatePublicSearchRequest
    participant ST as verifyScopedSearchToken (HMAC + exp)
    participant Key as verifySearchApiKey (hash-only)
    participant RL as SearchRateLimitBucket
    participant Filt as combineTenantFilter
    participant TS as Typesense multi_search
    participant Ev as SearchUsageEvent (async)

    SDK->>API: POST /api/search/public/multi
    API->>Gate: Bearer header (ss_search_* OR ss_scoped_*)
    alt ss_scoped_* prefix
        Gate->>ST: decode + HMAC verify + exp check
        ST-->>Gate: { parentRawKey, filterBy (scoped), keyId }
    end
    Gate->>Key: verifySearchApiKey(parentRawKey, scope=search)
    Key-->>Gate: VerifiedSearchKey { organizationId, indexSlug }
    Gate->>Gate: origin allow-list check
    Gate->>RL: increment + compare rateLimitPerMinute
    Gate-->>API: { verified, scopedFilter }
    API->>Filt: combine(userFilter && tenantFilter && scopedFilter)
    Filt-->>API: final filter_by expression
    API->>TS: multi_search(searches[], common_params)
    TS-->>API: hits + facets per search entry
    API->>API: buildSearchResponse (PII strip, highlight tags)
    API-->>SDK: 200 sanitized JSON
    par fire-and-forget
        API->>Ev: recordSearchUsageAsync(search_query)
    end

Что гарантирует каждый шаг

  1. Форма токена. Принимаются только три префикса: ss_search_*, ss_scoped_* и (для записи коннектором) ss_connector_*. Проверка формы выполняется до любого обращения к БД.
  2. Проверка scoped-токена. Токены ss_scoped_* несут подпись HMAC-SHA256 над base64url-кодированным payload-ом { keyId, parentRawKey, filterBy, exp }. Сервер сравнивает подпись за константное время, декодирует родительский ключ изнутри и отказывает, если exp уже истёк. Это Hard Invariant #4.
  3. Проверка родительского ключа. Родительский ss_search_* (или родитель ss_scoped_*) хешируется в sha256 и ищется в SearchApiKey.hash. Открытые ключи никогда не сохраняются — Hard Invariant #3.
  4. Origin allow-list. Если allowedOrigins непуст, заголовок Origin сравнивается строго. На несовпадении возвращается 403 до обращения к Typesense.
  5. Rate limit. Скользящее окно по ключу; 429 при превышении.
  6. Объединение tenant-фильтра. filter_by для Typesense всегда объединяет через AND фильтр от клиента, фильтр scoped-токена (если есть) и серверный tenantId:<org>. Tenant-фильтр добавляется на уровне SQL/Typesense WHERE, а не в заголовке — Hard Invariant #5.
  7. Диспатч multi_search. Один round-trip в Typesense раздаёт записи поиска. Каждая запись ОБЯЗАНА нести один и тот же tenant-фильтр.
  8. Очистка ответа. buildSearchResponse вырезает внутренние поля, применяет настроенные highlight-теги и выдаёт публичную форму ответа.
  9. Аналитика. recordSearchUsageAsync добавляет в очередь строку SearchUsageEvent типа search_query. Promise намеренно не ожидается.

Почему scoped-токены

Scoped-токен — единственный безопасный способ встроить поисковую строку под управлением AACsearch на клиентскую страницу. Он несёт:

  • стабильный HMAC, который клиент не может подделать (серверный BETTER_AUTH_SECRET),
  • ограничение filterBy, которое API будет AND-объединять с каждым запросом (например, user_id:42 && shop_id:abc),
  • короткий exp (обычно 15 минут), чтобы утечка токена имела малый радиус воздействия.

См. также

On this page