AACsearch
Architecture

Chemin d'écriture

Comment une écriture de document entre dans AACsearch — depuis POST /v1/indexes/.../documents via SearchIngestBuffer / SearchSyncOutbox et le worker, jusqu'à l'alias Typesense.

AACsearch est DB-first : chaque écriture atterrit d'abord dans PostgreSQL avant d'être projetée vers Typesense. La couche HTTP n'appelle jamais Typesense de manière synchrone depuis une requête client. C'est la Hard Invariant #2 — la durabilité, la gestion des échecs partiels et le reindex sans interruption en dépendent.

Flux

Description

Le diagramme montre une écriture HTTP synchrone côté client qui est authentifiée, soumise au rate-limit et au gate de quota, puis mise en file de manière durable dans PostgreSQL (SearchIngestBuffer / SearchSyncOutbox) — le client reçoit immédiatement 202 Accepted. Un worker en arrière-plan réclame ensuite les lignes en attente, attache les embeddings et importe le batch dans l'alias Typesense, en réconciliant succès ou échec ligne par ligne avec un backoff exponentiel.

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

Ce que garantit chaque étape

  1. Auth (verifySearchApiKey). Le token est haché (sha256) puis comparé à la colonne SearchApiKey.hash. Les clés de connecteur (ss_connector_*) et de recherche (ss_search_*) partagent le même espace de hash ; la colonne scopes pilote l'autorisation.
  2. Rate limit. Fenêtre glissante par clé issue de SearchRateLimitBucket. Dépasser rateLimitPerMinute renvoie 429 avant toute écriture en base.
  3. Gate de quota. Les entitlements du plan et l'overage wallet sont vérifiés une fois par requête ; le quota d'écriture est consommé atomiquement avec l'enqueue.
  4. Enqueue durable. Les lignes sont écrites dans SearchIngestBuffer (chemin legacy) ou SearchSyncOutbox (chemin canonique, idempotent). La réponse HTTP est 202 — le document n'a pas besoin d'être dans Typesense avant que le client ne reçoive sa réponse.
  5. Projection par le worker. Un process en arrière-plan réclame les lignes avec lockedBy = WORKER_ID, exécute l'auto-embedding si l'index a un champ vectoriel, appelle collection.documents().import() et réconcilie succès / échec ligne par ligne.
  6. Cible alias. Le worker écrit toujours dans aliasName(organizationId, slug), qui pointe sur la version physique courante de la collection. Le reindex permute l'alias atomiquement ; les écritures en vol suivent le nouveau pointeur.

Pourquoi DB-first

  • Durabilité. Les redémarrages serveur ou les pannes Typesense ne perdent pas d'écritures.
  • Récupération d'échecs partiels. Seules les lignes en échec sont rejouées ; les succès ne sont pas dupliqués.
  • Isolation tenant. Le worker tague chaque document avec le champ tenantId avant l'import ; l'alias applique filter_by à la lecture.
  • Backpressure. Le buffer absorbe les full-syncs en pic du CMS sans surcharger Typesense.

Liens associés

On this page