Trouve-KA — Décisions d'architecture
Author: Simon-Pierre Boucher — Contact: contact@spboucher.ai
D1 — Backend de recherche : OpenSearch 2.x
Candidats évalués : OpenSearch, Elasticsearch, Typesense, Meilisearch, Vespa, Tantivy, Quickwit (CLAUDE.md §3).
Choix : OpenSearch 2.17, parce que :
- BM25 réel et paramétrable par champ (multi_match, boosts), critère n°1 du MVP.
- Indexation incrémentale near-real-time :
refresh_interval: 1s→ une page indexée est cherchable en ~1 s (principe cardinal §0.3). function_score: la fonction de ranking Québec-first custom (§9) s'exprime nativement, poids ajustables sans réindexation.- Analyzers FR/EN +
synonym_graphsearch-time (bilinguisme sans traduction à l'ingestion). - Highlighting natif pour les snippets, facettes pour les filtres, k-NN natif pour l'hybride sémantique futur (§17.7) sans changer de moteur.
- Licence Apache 2.0, image ARM64, tourne en 2 Go de heap sur un seul node m2m32.
Rejetés : Meilisearch/Typesense (ranking custom trop rigide pour Score(d,q) pondéré; c'était le choix « facile » interdit par la spec), Tantivy (librairie Rust, il faudrait construire le serveur), Quickwit (orienté logs append-only, pas de mise à jour de documents), Vespa (excellent mais complexité opérationnelle démesurée pour un node unique), Elasticsearch (équivalent fonctionnel d'OpenSearch, licence moins permissive).
D2 — Backend Python unique (FastAPI + pipeline)
Crawler, parser, classifier, indexer, ranking, scheduler, enrichment et API partagent un seul langage (Python 3.12) et un seul packaging (pyproject.toml racine, namespace trouveka.* mappé sur packages/ et services/). Réduit la complexité (§3 : « le crawler peut rester Python même si l'API est TS » — ici tout le backend est Python, seul le web est TS).
D3 — Frontier dans Postgres, coordination dans Redis
- Frontier = Postgres : état relationnel (priorités, retries, scheduling), claims multi-workers via
FOR UPDATE SKIP LOCKED— N workers sans coordinateur. - Redis : politesse par hôte (
SET NX PX, un fetch par hôte par fenêtre, tous workers confondus), drapeau pause, Redis Streams pour l'enrichissement asynchrone (consumer groups, ack explicite). - Pas de Celery/RQ : la file d'enrichissement est un stream nu, observable (
XLEN), sans dépendance lourde.
D4 — Pipeline inline (fetch→parse→score→index) dans le crawler-worker
Le chemin rapide est exécuté inline par le worker : c'est la garantie la plus simple du « cherchable en secondes ». La scalabilité passe par le nombre de workers (--scale crawler-worker=N), pas par une séparation prématurée fetch/parse/index (§13 : pas de complexité distribuée prématurée). Les étapes 2-3 (§4) passent par le stream d'enrichissement et ne bloquent jamais.
D5 — apps/admin fusionné dans apps/web (route /admin)
Un seul runtime Next.js sur m2m32 au lieu de deux (~150 Mo RSS économisés), même design system, même proxy API. La séparation reste possible plus tard (le dashboard est un groupe de composants isolés sous components/admin/). Justification prévue par §2 (« Claude peut améliorer cette structure sur justification technique solide »).
D6 — Graphe de liens au niveau domaine d'abord
domain_links agrégé (from, to, count) plutôt que page→page : suffisant pour l'autorité de domaine et la découverte au MVP, volume contrôlé. Le graphe page→page viendra quand le ranking l'exigera (§8).
D7 — Réseau : le web proxifie /api
Next.js rewrite /api/* → API interne. Derrière ngrok (www.trouve-ka.com), un seul port exposé (3000), pas d'URL absolues côté client, pas de CORS en prod.