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

Путь записи

Как запись документа попадает в AACsearch — от POST /v1/indexes/.../documents через SearchIngestBuffer / SearchSyncOutbox и воркер в alias Typesense.

AACsearch работает по принципу DB-first: каждая запись сначала приземляется в PostgreSQL и только затем проецируется в Typesense. HTTP-слой никогда не вызывает Typesense синхронно из клиентского запроса. Это Hard Invariant #2 — от него зависят долговечность, обработка частичных отказов и zero-downtime-переиндексация.

Поток

Описание

Диаграмма показывает синхронную со стороны клиента HTTP-запись, которая аутентифицируется, проходит rate-limit и проверку квоты, а затем долговечно ставится в очередь в PostgreSQL (SearchIngestBuffer / SearchSyncOutbox) — клиент сразу получает 202 Accepted. Фоновый воркер позже забирает ожидающие строки, прикрепляет эмбеддинги и импортирует батч в alias Typesense, сверяя успех или отказ по строкам с экспоненциальным backoff.

sequenceDiagram
    autonumber
    participant Client as Customer / Connector
    participant API as packages/api/v1/documents.ts
    participant Auth as verifySearchApiKey (scope=ingest)
    participant DB as PostgreSQL (SearchIngestBuffer + SearchSyncOutbox)
    participant W as Sync worker (sync-worker.ts)
    participant Embed as autoEmbedDocuments
    participant TS as Typesense alias_name(orgShortId_slug)

    Client->>API: POST /v1/indexes/:indexId/documents:batch
    API->>Auth: Bearer ss_search_* / ss_connector_*
    Auth-->>API: VerifiedSearchKey { organizationId, indexId }
    API->>API: rate-limit (per-key, 1m sliding bucket)
    API->>API: enforceQuota (plan / overage)
    API->>DB: enqueueManySearchIngest() (or SearchSyncOutbox doc_upsert)
    API-->>Client: 202 Accepted (jobId)

    loop worker tick
        W->>DB: claim pending rows (atomic updateMany + lockedBy)
        W->>Embed: autoEmbedDocuments(batch)
        Embed-->>W: vectors attached
        W->>TS: collection.documents().import(batch, action=upsert)
        alt all green
            W->>DB: markIngestRowsSuccess / outbox.status=done
        else partial fail
            W->>DB: markIngestRowsFailure + nextRetryAt (exp. backoff)
        end
    end

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

  1. Auth (verifySearchApiKey). Токен хешируется (sha256) и сравнивается с колонкой SearchApiKey.hash. Коннекторные ключи (ss_connector_*) и поисковые ключи (ss_search_*) разделяют пространство хешей; авторизацию определяет колонка scopes.
  2. Rate limit. Скользящее окно по ключу из SearchRateLimitBucket. Превышение rateLimitPerMinute возвращает 429 ещё до любой записи в БД.
  3. Quota gate. Право плана и overage кошелька проверяются один раз за запрос; квота на запись расходуется атомарно вместе с enqueue.
  4. Долговечный enqueue. Строки пишутся в SearchIngestBuffer (legacy-путь) или SearchSyncOutbox (канонический, идемпотентный путь). HTTP-ответ — 202: документ не обязан быть в Typesense к моменту возврата клиенту.
  5. Проекция воркером. Фоновый процесс забирает строки с lockedBy = WORKER_ID, выполняет авто-эмбеддинг, если в индексе есть векторное поле, вызывает collection.documents().import() и согласует успех / отказ построчно.
  6. Цель alias. Воркер всегда пишет в aliasName(organizationId, slug), который указывает на текущую физическую версию коллекции. Переиндексация переключает alias атомарно; записи в полёте следуют новому указателю.

Почему DB-first

  • Долговечность. Перезапуск сервера или сбои Typesense не теряют записи.
  • Восстановление при частичных отказах. Повторяются только упавшие строки; успешные не дублируются.
  • Изоляция тенантов. Воркер тегирует каждый документ полем tenantId перед импортом; alias принудительно применяет filter_by на чтении.
  • Backpressure. Буфер впитывает пиковые full-sync'и CMS, не перегружая Typesense.

См. также

On this page