Neon Postgres et dédup par sujet pour le Quiz HP
- hpq
- neon
- postgres
- adr
- drizzle
Le script Gemini produit des questions — il faut les persister, les organiser par film et difficulté, et rejeter les doublons avant insertion. Deux décisions : quelle base, et quelle mécanique de dédup sans sur-coût.
Articles précédents : vision · intégration · design · Gemini.
Neon Postgres via Vercel
Le hub tourne déjà sur Vercel. Neon s'intègre via la marketplace : connexion automatique, scale-to-zero, free tier largement suffisant pour du texte vrai/faux (0,5 Go stockage, 100 h compute/mois).
ORM : Drizzle — schéma TypeScript, migrations légères, client serverless compatible avec @neondatabase/serverless.
Pourquoi pas Supabase ou Turso ?
- Supabase — free tier généreux, mais projet mis en pause après 7 jours d'inactivité. Pour un quiz perso visité par intermittence, risque de trouver la base éteinte un dimanche soir.
- Turso (SQLite edge) — intéressant, moins natif dans l'écosystème Vercel + Next.js que Neon pour ce cas.
Neon : pas de pause auto, intégration directe, amendement assumé de l'ADR hub « sans base de données ».
Variable prod
En local : DATABASE_URL. Sur Vercel, Neon injecte parfois POSTGRES_URL ou des vars PGHOST/PGUSER — le code lit HPQ_DATABASE_URL en priorité, puis les fallbacks. Route health API pour diagnostiquer si la connexion manque en prod.
Schéma minimal
Une table questions :
- film — entier 1 à 8
- difficulty — easy, medium, hard
- statement — énoncé vrai/faux
- answer — boolean
- sujet — tag snake_case (clé de dédup)
- created_at — horodatage
- consumed_at — legacy, plus utilisé au gameplay (questions réutilisables)
Index unique sur film + difficulty + sujet — Postgres refuse deux lignes avec le même tag dans le même combo.
Dédup au populate : le tag sujet
Plutôt qu'embeddings ou un 3ᵉ appel LLM « est-ce un doublon ? », le prompt de génération demande un champ sujet normalisé : maison_harry, mort_dumbledore, etc.
Avant insertion :
- Le script charge les sujets existants pour le combo film/difficulté
- Gemini génère statement + answer + sujet
- Si le sujet existe déjà → rejet, nouvelle génération (jusqu'à N tentatives)
- Sinon → validation (2ᵉ passe LLM) → insert
Périmètre : dédup par combo film/difficulté, pas sur tout le catalogue. Deux combos différents peuvent partager un sujet sans conflit.
Limite assumée
La qualité dépend de la cohérence des tags LLM. maison_harry vs maison_de_harry pourrait passer — à surveiller empiriquement, ajuster le prompt si besoin. Migration embeddings possible plus tard sans refondre le schéma.
Le prompt envoie aussi la liste des sujets existants pour orienter Gemini vers des sujets neufs.
Dédup au jeu : autre couche
Ne pas confondre :
| Moment | Règle |
|---|---|
| Populate | Pas deux fois le même sujet par combo |
| Partie (10 questions) | Pas deux fois le même énoncé dans la manche |
| Entre parties | Les questions reviennent — plus de consommation |
La pioche SQL utilise DISTINCT ON sur l'énoncé normalisé, puis un filtre JS de sécurité. Le compteur de stock compte les énoncés distincts, pas les lignes brutes si jamais deux sujets différents produisaient la même phrase.
Flux populate en chiffres
Le script npm run hpq:populate expose des stats :
- Insérées — nouvelles lignes validées
- Doublons — sujet déjà présent
- Invalides — rejetées par la passe validation
- Échecs — abandon après tentatives max
Option --target=N vise un stock total en base (top-up), pas un batch fixe.
Script hpq:clear pour vider un film ou une difficulté avant repopulate propre.
Ce que Neon ne fait pas
- Pas de cache LLM
- Pas de file d'attente de repopulation (cron à venir, HPQ-005)
- Pas de full-text search sur les énoncés en v1
Juste du stock fiable, requêtable, cheap.
Suite de la série
- Stratégie de repopulation et seuils de stock (article dédié)
- Format JSON et double passe de validation (article dédié)
Quiz : andrewchicout.dev.