AACsearch
Arquitectura

Ruta de escritura

Cómo una escritura de documento entra en AACsearch — desde POST /v1/indexes/.../documents a través de SearchIngestBuffer / SearchSyncOutbox y el worker, hasta el alias de Typesense.

AACsearch es DB-first: cada escritura aterriza primero en PostgreSQL antes de proyectarse a Typesense. La capa HTTP nunca llama a Typesense de forma síncrona desde una petición de cliente. Esto es la Hard Invariant #2 — durabilidad, manejo de fallos parciales y reindex sin tiempo de inactividad dependen de ello.

Flujo

Descripción

El diagrama muestra una escritura HTTP síncrona desde el cliente que es autenticada, limitada por rate-limit y verificada contra cuota, para luego encolarse de forma durable en PostgreSQL (SearchIngestBuffer / SearchSyncOutbox) — el cliente recibe 202 Accepted de inmediato. Un worker en segundo plano reclama después las filas pendientes, adjunta embeddings e importa el batch al alias de Typesense, reconciliando éxito o fallo por fila con backoff exponencial.

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

Qué garantiza cada paso

  1. Auth (verifySearchApiKey). El token se hashea (sha256) y se compara contra la columna SearchApiKey.hash. Las claves de conector (ss_connector_*) y de búsqueda (ss_search_*) comparten espacio de hash; la columna scopes dirige la autorización.
  2. Rate limit. Ventana deslizante por clave desde SearchRateLimitBucket. Sobrepasar rateLimitPerMinute devuelve 429 antes de cualquier escritura en BD.
  3. Gate de cuota. Los entitlements del plan y el overage de wallet se comprueban una vez por petición; la cuota de escritura se consume atómicamente con el enqueue.
  4. Enqueue durable. Las filas se escriben en SearchIngestBuffer (ruta legacy) o SearchSyncOutbox (ruta canónica, idempotente). La respuesta HTTP es 202 — el documento no necesita estar en Typesense antes de que el cliente reciba la respuesta.
  5. Proyección del worker. Un proceso en segundo plano reclama filas con lockedBy = WORKER_ID, ejecuta auto-embedding cuando el índice tiene un campo vectorial, llama a collection.documents().import() y reconcilia éxito / fallo por fila.
  6. Destino del alias. El worker siempre escribe en aliasName(organizationId, slug), que apunta a la versión física actual de la colección. El reindex intercambia el alias de forma atómica; las escrituras en vuelo siguen el nuevo puntero.

Por qué DB-first

  • Durabilidad. Reinicios del servidor o caídas de Typesense no pierden escrituras.
  • Recuperación de fallos parciales. Solo se reintentan las filas fallidas; las exitosas no se duplican.
  • Aislamiento de tenants. El worker etiqueta cada documento con el campo tenantId antes del import; el alias impone filter_by en lectura.
  • Backpressure. El buffer absorbe full-syncs ráfaga del CMS sin sobrecargar Typesense.

Relacionado

On this page