Blog

  • Conception orientée données et caches du processeur

    Conception orientée données et caches du processeur

    La plus grande leçon de performance que j’aie apprise, c’est que les processeurs modernes ne sont pas lents à calculer, ils sont lents à attendre la mémoire. Un défaut de cache vers la mémoire principale peut coûter quelques centaines de cycles. Pendant ce temps, le processeur aurait pu faire des centaines d’additions. Une fois cet écart intériorisé, on commence à concevoir les programmes autour de la façon dont les données circulent, pas seulement des opérations qu’on exécute dessus. C’est le cœur de la conception orientée données.

    La hiérarchie mémoire est la vraie machine

    Entre le processeur et la mémoire principale s’intercalent plusieurs niveaux de cache : L1 est minuscule et presque aussi rapide que les registres, L2 est plus grand et plus lent, L3 plus grand et plus lent encore. Quand le processeur a besoin d’un octet, il n’en cherche pas un seul. Il récupère une ligne de cache entière, typiquement 64 octets, et la range en cache. Si votre accès suivant est dans cette ligne, il est quasi gratuit. S’il est ailleurs au loin, vous payez la pénalité complète du défaut.

    Cela veut dire que la disposition de vos données en mémoire contrôle directement votre performance. Deux programmes faisant l’arithmétique identique peuvent différer d’un ordre de grandeur uniquement à cause des schémas d’accès.

    Tableau de structures contre structure de tableaux

    L’exemple classique est la façon de stocker une collection d’enregistrements. La disposition intuitive, orientée objet, est un tableau de structures :

    struct Particle {
        float x, y, z;     // position
        float vx, vy, vz;  // vitesse
        float mass;
        char name[32];
    };
    struct Particle particles[100000];
    
    // mise à jour des positions
    for (int i = 0; i < 100000; i++) {
        particles[i].x += particles[i].vx;
    }

    Cette boucle ne touche que x et vx, mais chaque ligne de cache chargée est pleine de mass, name et des autres champs dont vous n'avez pas besoin. Vous traînez des données froides à travers le cache pour rien. La disposition orientée données scinde les champs en tableaux parallèles, une structure de tableaux :

    struct Particles {
        float x[100000], y[100000], z[100000];
        float vx[100000], vy[100000], vz[100000];
        float mass[100000];
    };
    
    for (int i = 0; i < 100000; i++) {
        p.x[i] += p.vx[i];
    }

    Maintenant le tableau x et le tableau vx sont chacun densément empaquetés. Chaque octet tiré en cache est un octet utilisé. Sur du vrai matériel, ce genre de changement donne couramment des accélérations de 3x à 10x sur les boucles chaudes, sans aucun changement d'algorithme.

    Pourquoi la poursuite de pointeurs fait mal

    Les listes chaînées et les arbres à nœuds sont l'opposé d'amicaux pour le cache. Chaque nœud est une allocation de tas séparée qui peut vivre n'importe où, donc parcourir la liste saute partout en mémoire et rate le cache à presque chaque étape. Si vous comprenez pile contre tas et comment les allocations de tas s'éparpillent, cela découle naturellement. Un tableau plat parcouru linéairement est nettement plus rapide qu'une liste chaînée du même nombre d'éléments, même si les deux sont en O(n), car le préchargeur matériel peut prédire et précharger un accès séquentiel.

    • Préférez les tableaux contigus aux structures à nœuds quand vous itérez souvent.
    • Regroupez les champs auxquels vous accédez ensemble, et séparez ceux que vous n'utilisez pas.
    • Traitez les données dans l'ordre où elles sont en mémoire dès que possible.
    • Gardez les données chaudes petites pour qu'il en tienne plus en cache à la fois.

    C'est la même idée qu'un bon allocateur

    Voilà pourquoi la stratégie d'allocation compte autant. Si vous allouez dix mille objets un par un, ils finissent éparpillés. Si vous les allouez en un seul bloc, ils sont voisins et itèrent vite. L'allocateur sur mesure de écrire un allocateur mémoire simple vous donne exactement ce contrôle : vous décidez la disposition au lieu de la laisser au hasard.

    Quand y recourir

    Je ne restructure pas tout autour du cache. Pour du code qui tourne une fois ou rarement, la clarté l'emporte. Mais pour les boucles chaudes, les noyaux internes qui tournent des millions de fois, la disposition des données est généralement la première chose que je règle et souvent le meilleur retour. Profilez d'abord, trouvez où sont les défauts de cache, puis disposez les données comme le matériel veut les lire. La machine est ravie d'être rapide si vous cessez de la faire attendre.

  • Bonnes pratiques de schema de base de donnees

    Bonnes pratiques de schema de base de donnees

    Le schema est la partie d’un systeme la plus difficile a changer une fois pleine de donnees. Vous pouvez reecrire un service en un week-end. Migrer une table d’un milliard de lignes sans interruption est un projet. Je passe donc du temps reel sur le schema en amont, car le cout d’une erreur ne fait que grandir. Voici les decisions que je ne regrette pas.

    Laissez la base imposer les regles

    Le code applicatif n’est pas l’endroit pour garantir l’integrite des donnees, car il y a toujours un autre chemin d’entree : un script de migration, une correction manuelle, un second service, un developpeur dans une console. La base est le seul point de passage par lequel tout transite, c’est donc la que les regles appartiennent. J’utilise NOT NULL agressivement, les cles etrangeres pour imposer les relations, les contraintes d’unicite pour empecher les doublons, et les contraintes de verification pour les plages de valeurs.

    CREATE TABLE orders (
      id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
      customer_id bigint NOT NULL REFERENCES customers(id),
      status      text   NOT NULL DEFAULT 'pending'
                  CHECK (status IN ('pending','paid','shipped','cancelled')),
      total_cents integer NOT NULL CHECK (total_cents >= 0),
      created_at  timestamptz NOT NULL DEFAULT now()
    );

    Chaque contrainte ici est un bug qui ne pourra jamais atteindre la production. Un statut « shippd » est rejete au moment de l’ecriture au lieu de casser un rapport trois semaines plus tard.

    Normalisez d’abord, denormalisez sur preuve

    Je commence normalise : chaque fait vit a un seul endroit. Une donnee dupliquee est une verite dupliquee, et les copies divergent des que quelqu’un en met une a jour et oublie l’autre. La normalisation garde les ecritures simples et la justesse peu couteuse.

    La denormalisation est une optimisation de performance, et comme toute optimisation je veux une mesure avant de la faire. Quand une requete precise est vraiment trop lente et que le profil pointe les jointures, alors je vais cacher une valeur calculee ou dupliquer une colonne, sachant que je prends en charge la synchronisation des copies. Le faire par anticipation, c’est ainsi qu’on obtient un schema plein de champs auxquels personne ne fait confiance.

    Choisissez cles et types deliberement

    Chaque table recoit une cle primaire synthetique, en general un bigint identite ou un UUID. J’evite les cles naturelles comme les adresses e-mail en cle primaire, car la seule chose que vous pouvez promettre d’une cle naturelle, c’est qu’elle changera, et changer une cle primaire referencee partout est un calvaire. Utilisez des types qui veulent dire quelque chose :

    • Stockez l’argent en centimes entiers, jamais en flottants. Le flottant et la monnaie sont de vieux ennemis.
    • Utilisez toujours un horodatage avec fuseau pour le temps, et stockez en UTC.
    • Prenez un enum natif ou une contrainte de verification au lieu de champs de statut en texte libre.
    • Utilisez le vrai type JSON de la base pour des donnees vraiment non structurees, pas un bloc de texte.

    Indexez pour vos lectures, mais connaissez le cout

    Un index rend les lectures rapides et les ecritures un peu plus lentes, et il prend de la place. J’indexe les cles etrangeres, les colonnes que je filtre regulierement, et les colonnes par lesquelles je trie. Je n’indexe pas tout, car un index inutilise n’est que du poids sur chaque insertion. La facon de savoir, c’est de regarder : la plupart des bases vous diront quels index restent inutilises, et ceux-la sont candidats au retrait.

    Les index composites valent la peine d’etre compris, car l’ordre des colonnes compte. Un index sur (customer_id, created_at) aide une requete qui filtre par client et trie par date, mais ne fait rien pour une requete qui ne filtre que par date. Le meme etat d’esprit d’observabilite que j’ai decrit dans les bonnes pratiques d’observabilite s’applique ici : mesurez les vrais schemas de requete avant de deviner.

    Traitez les migrations comme du code

    Chaque changement de schema passe par un fichier de migration, verse dans la gestion de versions, relu comme tout autre changement. Aucune instruction ALTER manuelle lancee a la main en production, jamais, car l’environnement suivant ne l’aura pas et vous passerez une journee a chasser la difference. La meme discipline de relecture que dans les bonnes pratiques de revue de code s’applique, avec un soin supplementaire, puisqu’une mauvaise migration peut verrouiller une table ou perdre des donnees.

    • Faites des migrations vers l’avant et additives quand vous le pouvez. Ajoutez une colonne, remplissez-la, puis retirez l’ancienne plus tard dans une etape separee.
    • Evitez les changements qui prennent un long verrou exclusif sur une grande table en service.
    • Testez la migration sur une copie de donnees a la taille de la production avant de la lancer pour de vrai.

    Un schema que vous pouvez faire evoluer en securite vaut plus qu’un schema parfait que vous avez peur de toucher. Construisez pour le changement, car le changement est la seule certitude.

  • Publier une application Flutter sur l App Store et le Play Store

    Publier une application Flutter sur l App Store et le Play Store

    Ecrire l application est la moitie du travail. La mettre dans les deux stores est l autre moitie, et c est celle qui surprend. Apple et Google ont chacun leur propre signature, leurs propres metadonnees et leurs propres humeurs de revue. J ai publie sur les deux plus de fois que je ne peux compter, et la difference entre une publication fluide et une stressante est la preparation. Voici le chemin que je suis.

    Reglez les bases avant de builder

    Avant de generer le moindre binaire de release, reglez bien les champs ennuyeux. Un identifiant d application unique que vous ne changerez jamais, un numero de version et de build sense, le nom de l application, et les versions d OS minimales supportees. Changer l identifiant d application apres le lancement equivaut a une nouvelle application, alors decidez le avec soin. Ils vivent dans la config Gradle sur Android et le projet Xcode sur iOS.

    # pubspec.yaml controle la version et le numero de build
    version: 1.0.0+1
    # 1.0.0 est la version vue par les utilisateurs, +1 est le numero de build

    Signature Android et app bundle

    Google Play veut un app bundle plutot qu un APK maintenant, et il le veut signe avec une cle d upload que vous creez une fois et gardez precieusement. Perdez cette cle et vous etes parti pour un processus de recuperation penible. Je stocke le keystore en dehors du depot et je le reference via un fichier de proprietes jamais commit. Builder le bundle de release est une seule commande une fois la signature branchee.

    flutter build appbundle --release
    # produit build/app/outputs/bundle/release/app-release.aab

    Uploadez ce fichier vers la Play Console, remplissez la fiche du store, configurez le questionnaire de classification de contenu, et choisissez une piste de release. Je pousse toujours vers les tests internes d abord pour pouvoir installer l artefact exact que les utilisateurs auront avant qu il devienne public.

    Signature iOS et archivage

    La signature Apple est plus impliquee. Il vous faut un compte Apple Developer, des certificats, un identifiant d application et des profils de provisionnement. Xcode peut gerer la plupart de cela automatiquement si vous vous connectez, ce que je recommande plutot que de lutter avec les profils a la main. Vous buildez la release iOS via Flutter puis archivez et uploadez via Xcode ou l outil transporter.

    flutter build ipa --release
    # puis ouvrez Xcode, archivez et uploadez vers App Store Connect
    • Enregistrez l identifiant d application dans le portail developpeur
    • Laissez Xcode gerer les certificats et profils quand c est possible
    • Uploadez vers TestFlight avant de soumettre pour revue
    • Preparez des captures d ecran pour chaque taille d appareil requise

    Les fiches de store prennent plus de temps qu on ne le croit

    Les deux stores ont besoin de captures d ecran a des tailles precises, d une icone, d une description, de mots cles, d une URL de politique de confidentialite et d une declaration de collecte de donnees. Apple est stricte sur le questionnaire de confidentialite et rejettera les reponses vagues. Reservez du vrai temps pour cela, car une fiche a moitie faite bloque toute la publication. Je garde une checklist pour que rien ne soit oublie a la derniere minute.

    Survivre a la revue

    La revue Apple peut rejeter pour des raisons qui paraissent arbitraires jusqu a ce que vous lisiez les directives de pres. Les plus courantes sont l absence d identifiants de compte de demonstration, des liens casses, des plantages sur leur appareil de test, et un usage flou des permissions. Donnez leur un identifiant fonctionnel si votre application en a besoin, testez sur un vrai appareil, et expliquez pourquoi vous demandez chaque permission. La revue Google est generalement plus rapide et plus automatisee mais se soucie toujours des permissions et du respect des politiques.

    Planifiez aussi les mises a jour

    Livrer la version un est le debut. Incrementez le numero de build a chaque upload, gardez un changelog, et utilisez les deploiements progressifs sur Android pour qu une mauvaise release atteigne d abord une petite tranche d utilisateurs. Les habitudes de performance de l optimisation des performances Flutter comptent ici car les metriques de plantage et de saccade affectent votre classement dans le store. Et si votre application s appuie sur des fonctionnalites natives, testez les avec soin sur les versions d OS comme je le decris dans les canaux de plateforme Flutter. Une publication calme est une publication preparee. Faites le travail ennuyeux tot et le jour du lancement cesse d etre effrayant.

  • Comment optimiser les images pour le web

    Comment optimiser les images pour le web

    Si une page semble lente, les images sont la première chose que je vérifie, et c’est presque toujours le coupable. Le texte et le code sont minuscules. Une seule photo de bannière non compressée peut peser plus que tout votre bundle JavaScript. La bonne nouvelle, c’est que l’optimisation d’images est surtout mécanique, et vous pouvez tout automatiser.

    Choisissez le bon format

    Le choix du format est la décision la plus rentable que vous prenez. Le JPEG convient aux photographies mais il est vieux et peu efficace. Le PNG est pour les images qui ont besoin de transparence ou de bords nets, comme les logos et les captures d’écran, et il est catastrophique pour les photos. La réponse moderne pour presque tout est le WebP, qui donne des photos de qualité JPEG à une fraction de la taille, et l’AVIF quand vous voulez pousser la compression encore plus loin. Je sers de l’AVIF avec un repli WebP et un repli JPEG en dessous.

    • Photographies : AVIF ou WebP, jamais du JPEG brut si vous pouvez l’éviter.
    • Logos et icônes : SVG si possible, il s’agrandit à l’infini et ne pèse rien.
    • Captures avec du texte : PNG ou WebP sans perte pour que le texte reste net.

    Redimensionnez avant de compresser

    C’est l’erreur que je vois sans cesse. Les gens prennent une photo d’appareil de 4000 pixels de large, la compressent, et l’affichent dans une colonne de 600 pixels. Le navigateur télécharge tous ces pixels gaspillés et les jette. Redimensionnez d’abord l’image à la plus grande taille à laquelle elle sera réellement affichée, puis compressez. Le seul redimensionnement réduit souvent la taille du fichier de 80 pour cent avant même de toucher aux réglages de qualité.

    Automatisez avec sharp

    Je n’édite pas les images à la main dans une application. Je les passe par un script utilisant la bibliothèque sharp, qui est rapide et produit un excellent résultat. Un court pipeline redimensionne et convertit tout un dossier :

    const sharp = require('sharp');
    
    sharp('input.jpg')
      .resize({ width: 1200, withoutEnlargement: true })
      .webp({ quality: 75 })
      .toFile('output.webp')
      .then(() => console.log('done'));

    La qualité 75 en WebP est mon réglage par défaut. La différence entre 75 et 90 est invisible à l’écran mais double la taille du fichier. Glissez-le dans votre build et chaque image reçoit le même traitement automatiquement, ce qui s’intègre bien à un pipeline lancé via le CI/CD avec GitHub Actions.

    Servez des tailles adaptatives

    Un téléphone et un ordinateur de bureau ne devraient pas télécharger la même image. Générez quelques largeurs et laissez le navigateur choisir avec srcset. Le balisage indique au navigateur quelles tailles existent et la largeur d’affichage de l’image, et il prend la plus petite qui reste nette :

    <img
      src="photo-800.webp"
      srcset="photo-400.webp 400w, photo-800.webp 800w, photo-1200.webp 1200w"
      sizes="(max-width: 600px) 100vw, 600px"
      alt="Une légende descriptive"
    >

    Chargez en paresseux tout ce qui est sous la ligne de flottaison

    Ajoutez loading="lazy" aux images qui ne sont pas visibles au premier rendu de la page. Le navigateur diffère alors leur chargement jusqu’à ce que l’utilisateur s’en approche en faisant défiler. C’est un changement d’un seul attribut au gros retour, car il empêche les images hors écran de se disputer la bande passante avec le contenu que les gens voient vraiment. Laissez-le toutefois sur votre image de bannière, car vous voulez qu’elle se charge immédiatement.

    Définissez toujours largeur et hauteur

    Définissez des attributs de largeur et de hauteur explicites, ou un ratio d’aspect en CSS, sur chaque image. Sans eux, le navigateur ne sait pas combien d’espace réserver, donc la page sautille pendant le chargement des images. Ce saut est le décalage de mise en page, et c’est l’une des métriques sur lesquelles Google vous note. Cela donne aussi une impression de site cassé aux utilisateurs. Réserver l’espace ne coûte rien et corrige le problème complètement.

    Le gain

    Mettez tout cela ensemble et une page typique chargée d’images passe de plusieurs mégaoctets à quelques centaines de kilooctets sans perte de qualité visible. Cela se voit directement dans vos temps de chargement et vos Core Web Vitals. Des images plus légères donnent un site plus vif partout, y compris au moment où vous le publiez via Cloudflare Pages et que vos visiteurs le chargent depuis la périphérie. Optimisez une fois dans le build, et vous n’avez plus jamais à y penser image par image.

  • Conception de bases de données et normalisation (et quand dénormaliser)

    Conception de bases de données et normalisation (et quand dénormaliser)

    J’ai hérité d’assez de schémas cassés pour avoir des avis tranchés. Les pires incidents que j’ai dû gérer ne venaient presque jamais d’un index manquant ou d’un disque lent. Ils venaient d’un modèle de données qui mentait. Une colonne qui voulait dire trois choses différentes selon la ligne. Un champ « statut » qui servait en secret de dépotoir à texte libre. Une clé étrangère qui existait dans la tête de quelqu’un mais jamais dans la base. Une bonne conception est l’assurance la moins chère que vous achèterez jamais, et vous l’achetez avant d’écrire la moindre requête.

    Cet article explique comment j’aborde réellement la normalisation, ce que les formes normales apportent en pratique, et la poignée de situations où je dénormalise délibérément. Si vous voulez le volet collecte des besoins, je l’ai traité à part dans mon guide sur la méthodologie de modélisation des données. Ici, on parle du schéma lui-même.

    Contre quoi la normalisation vous protège vraiment

    On parle de la normalisation comme d’un exercice académique. Elle ne l’est pas. Chaque forme normale existe pour empêcher une classe précise de bugs qui finira par vous réveiller à 3h du matin. Retirez le jargon formel et l’objectif est simple. Stocker chaque fait une seule fois, à l’endroit où il appartient, pour qu’il soit impossible que deux copies du même fait se contredisent.

    Quand la même donnée vit à deux endroits, ces deux endroits finiront par diverger. Pas peut-être. Ils divergeront. Quelqu’un met à jour l’e-mail du client dans une table et oublie l’autre. Un batch touche la moitié des lignes. Vous avez maintenant deux vérités et aucun moyen de savoir laquelle est correcte. La normalisation supprime la seconde copie pour que la contradiction devienne impossible plutôt que simplement improbable.

    Les formes normales, à ma façon

    Je ne récite pas les définitions formelles, mais je garde leur intention en tête quand j’esquisse des tables.

    • La première forme normale signifie pas de groupes répétés ni de colonnes multivaluées. Si vous nommez des colonnes telephone1, telephone2, telephone3, une table séparée attend de naître. Une liste séparée par des virgules dans un varchar est le même crime déguisé.
    • La deuxième forme normale signifie que chaque colonne non-clé dépend de toute la clé primaire, pas seulement d’une partie. Cela ne mord qu’avec des clés composites, mais quand ça mord, ça laisse une marque.
    • La troisième forme normale signifie que les colonnes non-clés dépendent de la clé et de rien d’autre que la clé. Si une colonne dépend d’une autre colonne non-clé, elle a sa propre table. L’exemple classique est de stocker une ville et son code postal ensemble alors que l’un détermine l’autre.

    Au quotidien, si j’atteins la troisième forme normale je suis généralement en bonne posture. Boyce-Codd et les formes supérieures comptent pour des cas précis de clés qui se chevauchent, mais la troisième forme normale attrape la grande majorité des vraies erreurs de modélisation que je vois en revue de code.

    Un exemple concret

    Disons que nous stockons des commandes. La version naïve entasse tout dans une seule table large, en répétant le nom et l’e-mail du client sur chaque ligne de commande. Voici la version normalisée que je livrerais réellement.

    -- Les clients possèdent leurs propres faits, une seule fois
    CREATE TABLE customers (
        id          BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
        email       CITEXT NOT NULL UNIQUE,
        full_name   TEXT NOT NULL,
        created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
    );
    
    -- Les commandes référencent le client, elles ne le copient pas
    CREATE TABLE orders (
        id           BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
        customer_id  BIGINT NOT NULL REFERENCES customers(id),
        status       TEXT NOT NULL
                     CHECK (status IN ('pending','paid','shipped','cancelled')),
        placed_at    TIMESTAMPTZ NOT NULL DEFAULT now()
    );
    
    -- Les lignes ont leur propre granularité : une ligne par produit par commande
    CREATE TABLE order_items (
        order_id     BIGINT NOT NULL REFERENCES orders(id),
        product_id   BIGINT NOT NULL REFERENCES products(id),
        quantity     INT NOT NULL CHECK (quantity > 0),
        unit_price   NUMERIC(12,2) NOT NULL,
        PRIMARY KEY (order_id, product_id)
    );

    Remarquez quelques choix qui ne relèvent pas strictement des formes normales mais qui accompagnent une bonne conception. La colonne status a une contrainte CHECK pour que la base elle-même impose les valeurs autorisées. Le unit_price vit sur la ligne, pas sur le produit, parce que le prix au moment de la vente est un fait différent du prix actuel. Cette distinction est exactement le genre de chose que la normalisation vous oblige à voir. Est-ce la valeur actuelle ou la valeur telle qu’elle était ? Ce ne sont pas le même fait et ils n’appartiennent pas à la même colonne.

    Les contraintes font partie de la conception, pas de la décoration

    Un schéma sans contraintes est une suggestion. Je pousse autant d’invariants que raisonnablement possible dans la base, parce que le code applicatif est le mauvais endroit pour garantir l’intégrité des données. Il y aura toujours un second écrivain un jour. Un script de migration, un outil d’admin, un collègue qui fouille dans une session psql. La base est la seule couche qu’ils partagent tous.

    J’utilise donc NOT NULL agressivement, des clés étrangères sans m’excuser, des contraintes UNIQUE sur tout ce qui doit être unique, et des contraintes CHECK pour les plages de valeurs et les énumérations. Si vous ne retenez qu’une habitude de cet article, prenez celle-là. La plupart des « données mystérieusement corrompues » que j’ai déboguées auraient été impossibles avec une contrainte qui prend trente secondes à écrire. J’approfondis ce point dans mes notes sur les bonnes pratiques de schéma.

    Quand je dénormalise volontairement

    Maintenant l’hérésie. Je dénormalise régulièrement, et je ne culpabilise pas, parce que la dénormalisation faite avec intention est une optimisation, pas une erreur. L’astuce est que vous ne dénormalisez qu’après avoir compris le motif d’accès, jamais avant. Une dénormalisation prématurée n’est qu’un modèle de données avec des bugs en plus.

    Voici les cas où j’y ai recours.

    • Agrégats à forte lecture. Si un tableau de bord lit le total d’une commande mille fois pour chaque fois où la commande change, recalculer ce total à chaque lecture est du gaspillage. Je stockerai une colonne total mise en cache et je la maintiendrai à jour avec un trigger ou dans la même transaction que l’écriture.
    • Tables de reporting et d’analyse. La normalisation transactionnelle et les motifs de requêtes analytiques tirent dans des sens opposés. Une table large dénormalisée ou un schéma en étoile peut transformer une jointure brutale à huit tables en un seul parcours. Je les garde séparées de la source de vérité et je les reconstruis à partir d’elle.
    • Jointures coûteuses sur le chemin chaud. Parfois une jointure est réellement le goulot même après indexation. Copier une colonne souvent lue pour éviter une jointure peut en valoir la peine, tant que vous maîtrisez le chemin de mise à jour.

    La règle non négociable dans chacun de ces cas est que la version normalisée reste la source de vérité. La copie dénormalisée est dérivée, jetable et reconstructible. Dès l’instant où vous avez deux sources de vérité indépendantes, vous revenez au péché originel que la normalisation servait à empêcher.

    Comment je garde la dénormalisation sûre

    Si je stocke une valeur dérivée, je rends la dérivation explicite et automatique. Une colonne en cache est mise à jour dans la même transaction que sa source, ou par un trigger, jamais par un commentaire optimiste disant « penser à mettre à jour ceci ». Une vue matérialisée a un calendrier de rafraîchissement documenté. Une table de reporting est reconstruite par un job que je peux lancer à la demande et vérifier contre la source.

    J’écris aussi un contrôle, même lent et exécuté la nuit, qui compare la valeur dérivée à un calcul frais et hurle s’ils diffèrent. La dérive est le mode de défaillance de la dénormalisation, et la seule défense est de la détecter tôt. Une fois que je peux prouver que la copie correspond à la source, le gain de performance arrive avec la conscience tranquille. Quand les copies commencent à diverger, c’est presque toujours parce qu’un plan de requête a changé ou qu’un index a disparu, exactement le terrain que je couvre dans mon guide de l’indexation.

    L’ordre dans lequel je procède

    Ma séquence par défaut n’a pas changé depuis des années. Normaliser d’abord, jusqu’à la troisième forme normale, avec de vraies contraintes. Rendre le modèle correct et le laisser correct. Puis mesurer. Ce n’est que lorsqu’un motif d’accès précis et mesuré l’exige que j’introduis une copie dénormalisée, et uniquement comme un artefact dérivé avec un chemin de mise à jour imposé. La correction d’abord, puis la vitesse, et jamais une vitesse achetée au prix d’un mensonge dans les données.

    Cet ordre compte parce qu’il est bien plus facile de dénormaliser un modèle propre que de nettoyer un modèle vaseux dès la naissance. Commencez strict. Relâchez délibérément. Votre futur vous, réveillé à 3h du matin, vous remerciera.

  • Une méthodologie pratique de modélisation des données, des besoins au schéma

    Une méthodologie pratique de modélisation des données, des besoins au schéma

    Presque tous les schémas vraiment pénibles avec lesquels j’ai dû vivre ont commencé de la même façon. Quelqu’un a ouvert un fichier de migration et s’est mis à taper CREATE TABLE avant que personne n’ait convenu de ce que les données étaient réellement. Les tables sont venues en premier et la compréhension ensuite, ce qui est exactement à l’envers. Un schéma est le dernier artefact de la modélisation, pas le premier. Au moment où j’écris du SQL, la réflexion difficile est déjà faite.

    Voici la méthodologie que j’utilise pour passer d’une demande floue à un schéma que j’accepte de signer. Elle n’est pas lourde. Elle ne demande pas d’outillage spécial. Elle demande surtout de ralentir le temps d’un après-midi pour avancer vite les deux années suivantes. Pour les principes de conception qui gouvernent le schéma final, associez ceci à mon article sur la normalisation et quand dénormaliser.

    Étape un, rassembler les noms et les règles

    Je commence par lire ou écouter comment les gens qui font réellement le travail le décrivent. Pas les ingénieurs. Les gens du métier. Je chasse deux choses. Les noms, qui deviennent des entités candidates, et les règles, qui deviennent des contraintes et des relations.

    Quand un coordinateur logistique dit « une expédition peut avoir plusieurs colis mais chaque colis appartient à exactement une expédition », il vient de me tendre une relation un-à-plusieurs et une clé étrangère NOT NULL, gratuitement, en langage clair. Les experts métier font déjà la modélisation. Mon travail est de l’écrire fidèlement et de remarquer quand leurs phrases se contredisent.

    Je tiens un glossaire au fur et à mesure. L’outil de modélisation le plus sous-estimé est une définition convenue de chaque terme. Quand deux personnes utilisent le mot « compte » pour dire deux choses différentes, vous ne le découvrirez qu’en production, à moins d’avoir forcé la définition tôt.

    Étape deux, trouver les entités et leur identité

    À partir des noms, je dégage les vraies entités. Le test que j’applique est l’identité. Cette chose a-t-elle une existence propre que je dois référencer dans le temps ? Un client en a une. Une commande en a une. Une ligne de commande en a une. La couleur « bleu » généralement non, c’est un attribut, jusqu’au jour où le métier a besoin d’un catalogue de couleurs avec ses propres règles, et là elle gagne son statut d’entité.

    Pour chaque entité je pose immédiatement une question. Qu’est-ce qui rend une ligne unique ? Parfois il existe une clé naturelle, comme un code pays ISO. Plus souvent non, et j’ajoute une clé de substitution, une colonne d’identité générée sans signification métier. Je penche vers les clés de substitution pour la plupart des entités parce que les clés naturelles ont la fâcheuse habitude de changer, et une clé primaire qui change est une clé primaire qui gâche votre semaine.

    Étape trois, cartographier les relations

    Maintenant je connecte les entités, et je suis précis sur la cardinalité parce que c’est là que le schéma se décide.

    • Un-à-plusieurs est le cas courant. Le côté « plusieurs » porte une clé étrangère pointant vers le côté « un ». Une commande a plusieurs lignes, donc order_items porte le order_id.
    • Plusieurs-à-plusieurs devient toujours une table de jonction. Il n’y a pas d’autre façon honnête de le représenter. Étudiants et cours se rencontrent dans une table inscriptions qui porte les deux clés étrangères.
    • Un-à-un est rare et mérite la méfiance. Cela signifie souvent soit une entité que vous avez scindée sans raison, soit une extension optionnelle qui appartient vraiment à sa propre table. Je m’oblige à justifier chaque un-à-un.

    Pour chaque relation je fixe aussi les règles de participation. La clé étrangère est-elle obligatoire ou optionnelle ? Que doit-il se passer à la suppression ? Ce ne sont pas des détails après coup. Le comportement ON DELETE est une vraie décision métier déguisée en décision technique, et le métier devrait avoir voix au chapitre.

    Étape quatre, les attributs et la question de la granularité

    Avec les entités et les relations en place, j’attache les attributs, et pour chacun je demande ce qu’il est vraiment. Est-il atomique, ou cache-t-il plusieurs faits entassés ensemble ? Un champ « nom » que tout le monde veut chercher par prénom et nom de famille, c’est deux colonnes qui se font passer pour une. Une adresse en est presque toujours plusieurs.

    La question la plus importante à ce stade est la granularité. Que représente une ligne de cette table, exactement, en une phrase ? Si je ne peux pas le dire proprement, la table est confuse et les requêtes le seront aussi. « Une ligne par commande » est une granularité claire. « Une ligne par commande, sauf parfois par expédition » est un futur rapport d’incident.

    Étape cinq, écrire le schéma et laisser la base aider

    Ce n’est que maintenant que j’écris du SQL, et à ce stade il s’écrit presque tout seul, parce que la réflexion est terminée. Voici le genre de chose qui découle des étapes ci-dessus pour un simple domaine d’inscription aux cours.

    -- Une ligne par étudiant
    CREATE TABLE students (
        id          BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
        email       CITEXT NOT NULL UNIQUE,
        full_name   TEXT NOT NULL,
        enrolled_on DATE NOT NULL DEFAULT CURRENT_DATE
    );
    
    -- Une ligne par cours proposé
    CREATE TABLE courses (
        id        BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
        code      TEXT NOT NULL UNIQUE,     -- clé naturelle, stable par catalogue
        title     TEXT NOT NULL,
        capacity  INT NOT NULL CHECK (capacity > 0)
    );
    
    -- La table de jonction : une ligne par étudiant par cours
    CREATE TABLE enrollments (
        student_id BIGINT NOT NULL REFERENCES students(id) ON DELETE CASCADE,
        course_id  BIGINT NOT NULL REFERENCES courses(id)  ON DELETE RESTRICT,
        grade      TEXT CHECK (grade IN ('A','B','C','D','F') OR grade IS NULL),
        PRIMARY KEY (student_id, course_id)
    );

    Regardez combien du modèle est désormais imposé par la base plutôt que laissé à l’espoir. Le plusieurs-à-plusieurs devient une clé primaire composite, ce qui empêche aussi un étudiant de s’inscrire deux fois au même cours sans aucun code applicatif. Les deux choix ON DELETE différents encodent une vraie règle : supprimer un étudiant retire ses inscriptions, mais vous ne pouvez pas supprimer un cours qui a encore des étudiants.

    Étape six, valider contre les requêtes que vous exécuterez

    Un modèle ne vaut que par les questions auxquelles il sait répondre. Avant de le déclarer terminé, je prends les cinq ou dix requêtes que l’application exécutera le plus souvent et je les écris contre le schéma sur papier. Si une question courante exige une jointure tordue à cinq tables ou une sous-requête imbriquée trois niveaux de profondeur, le modèle se bat contre la charge et je reviens une étape en arrière.

    C’est aussi là que les motifs d’accès commencent à éclairer l’indexation, même si je garde cela comme une préoccupation distincte. Rendez le modèle honnête d’abord, puis rendez-le rapide. Je détaille le volet performance dans mon guide de l’indexation des bases de données.

    Étape sept, prévoir le changement

    Aucun modèle ne survit intact au contact d’une feuille de route, alors je conçois pour l’évolution dès le départ. J’évite les colonnes qui veulent dire des choses différentes selon les lignes. Je préfère ajouter une colonne nullable ou une nouvelle table plutôt que de surcharger une colonne existante. Je tiens une discipline de migration où chaque changement de schéma est un fichier versionné et révisable, jamais une édition manuelle d’une base en production.

    L’état d’esprit qui m’a le mieux servi est celui-ci. Modéliser, c’est écrire ce qui est vrai du monde, avec assez de soin pour que la base puisse l’imposer. Le SQL n’est que la transcription. Faites la réflexion d’abord, écrivez le schéma en dernier, validez-le contre les vraies questions, et vous obtenez des tables ennuyeuses dans le meilleur sens du terme. Les schémas ennuyeux ne vous réveillent pas.

  • Comment fonctionnent vraiment les pointeurs

    Comment fonctionnent vraiment les pointeurs

    Les pointeurs m’ont fait peur pendant un temps embarrassant. La syntaxe n’aidait pas : des astérisques avec deux sens différents, des esperluettes, des flèches. Mais le concept est simple une fois la syntaxe écartée. Un pointeur est une variable dont la valeur est une adresse mémoire. C’est tout. C’est un nombre qui indique où vit quelque chose.

    La mémoire est un seul gigantesque tableau

    Imaginez l’espace d’adressage de votre processus comme un énorme tableau d’octets, indexé de zéro jusqu’à un nombre colossal. Chaque variable que vous déclarez vit à un certain indice de ce tableau. Un pointeur stocke simplement l’un de ces indices. Quand vous « déréférencez » un pointeur, vous dites « va à cet indice et lis ce qui s’y trouve ».

    int x = 42;
    int *p = &x;      // p contient l'adresse de x
    printf("%d\n", *p);   // déréférencement : affiche 42
    *p = 7;           // écriture à travers le pointeur
    printf("%d\n", x);    // affiche 7

    L’esperluette signifie « adresse de » et l’astérisque dans une expression signifie « la chose à cette adresse ». Le même astérisque dans une déclaration signifie « cette variable est un pointeur ». Deux rôles pour un seul symbole, ce qui est la principale raison pour laquelle les pointeurs semblent déroutants au début.

    Le type compte plus que vous ne le pensez

    Un pointeur n’est pas qu’une adresse, c’est une adresse plus un type. Le type indique au compilateur deux choses : combien d’octets lire au déréférencement, et de combien sauter lors d’une arithmétique. Un pointeur sur int et un pointeur sur char peuvent contenir exactement la même adresse numérique et se comporter tout à fait différemment.

    int arr[4] = {10, 20, 30, 40};
    int *p = arr;     // pointe sur arr[0]
    p++;              // pointe maintenant sur arr[1], déplacé de 4 octets, pas de 1
    printf("%d\n", *p);   // affiche 20

    Voilà l’idée clé de l’arithmétique des pointeurs. Ajouter un à un pointeur sur int le déplace de sizeof(int) octets, pas d’un octet. Le compilateur met à l’échelle pour vous selon le type. C’est aussi pourquoi tableaux et pointeurs semblent si interchangeables en C : indexer arr[i] est défini comme prendre l’adresse de arr, ajouter i fois la taille d’un élément, puis déréférencer.

    Où pointent les pointeurs

    Un pointeur peut contenir l’adresse d’une variable de pile, d’une allocation sur le tas, d’une fonction, ou de rien du tout. Le « où » compte parce qu’il décide si le déréférencement est sûr. Si les durées de vie pile contre tas restent floues pour vous, c’est la base ici, et je l’ai couverte dans pile contre tas : comment la mémoire fonctionne vraiment. Un pointeur vers une variable de pile devient pendouillant dès que la trame retourne. Un pointeur vers de la mémoire du tas libérée est un use-after-free qui n’attend que de planter.

    • Pointeur NULL : contient l’adresse zéro, un « ne pointe vers rien » délibéré. Le déréférencer plante, ce qui est en fait le résultat le plus aimable.
    • Pointeur pendouillant : contient une adresse autrefois valide. Le déréférencer est un comportement indéfini et peut corrompre des données silencieusement.
    • Pointeur sauvage : jamais initialisé, contient n’importe quoi. Le pire, car il peut pointer partout.

    Pointeurs sur pointeurs

    Dès qu’un pointeur n’est qu’une variable, un pointeur sur pointeur cesse d’être mystérieux. C’est l’adresse d’une variable qui contient elle-même une adresse. Il en faut un chaque fois qu’une fonction doit changer où un pointeur pointe, pas seulement ce vers quoi il pointe.

    void allocate(int **out) {
        *out = malloc(sizeof(int));   // écrit une nouvelle adresse dans le pointeur de l'appelant
        **out = 99;                   // écrit une valeur dans cette mémoire
    }
    
    int *p = NULL;
    allocate(&p);     // passe l'adresse de p pour que la fonction puisse le modifier
    printf("%d\n", *p);   // affiche 99

    Pourquoi ça vaut l’effort

    Les pointeurs sont le mécanisme derrière presque tout ce qui est intéressant en code système : structures chaînées, mémoire dynamique, passage de gros objets sans copie, dialogue avec le matériel à des adresses fixes. Ils sont aussi la source de la plupart des plantages en C. L’allocateur que je décris dans écrire un allocateur mémoire simple n’est rien d’autre qu’une manipulation soigneuse de pointeurs sur un bloc brut d’octets. Dès qu’on voit un pointeur comme un entier typé dans le grand tableau d’octets, la peur s’en va et la puissance apparaît.

  • Les bases de donnees vectorielles expliquees : embeddings et recherche par similarite

    Les bases de donnees vectorielles expliquees : embeddings et recherche par similarite

    La premiere fois que j’ai mis en production une fonctionnalite reposant sur une base vectorielle, j’ai passe un apres-midi a ne rien comprendre a mes resultats. Je l’avais traitee comme un index de mots-cles. Ce n’en est pas un. Une base vectorielle stocke du sens, ou du moins une approximation numerique du sens, et elle repond a un type de question different de celui des bases que j’utilisais depuis des annees. Cet article est l’explication que j’aurais aime qu’on me donne avant de commencer.

    Ce qu’est reellement un embedding

    Un embedding est une liste de nombres a virgule flottante qui represente un contenu. Vous prenez du texte, une image ou un extrait audio, vous le passez dans un modele, et il en ressort un tableau de taille fixe. Un embedding de texte typique peut avoir 384, 768 ou 1536 dimensions. Chaque nombre pris isolement ne signifie rien d’interpretable. Ensemble, ils placent le contenu en un point precis d’un espace a haute dimension.

    La propriete utile est que des contenus similaires se retrouvent a des positions proches. La phrase « mon ordinateur portable ne s’allume plus » et la phrase « mon pc est mort » produisent des vecteurs proches, alors qu’elles ne partagent presque aucun mot. Une recherche par mots-cles raterait completement ce lien. Un embedding le capte parce que le modele qui a produit les vecteurs a appris, a partir d’enormes quantites de texte, que ces phrases veulent dire a peu pres la meme chose.

    C’est tout l’interet. On convertit la notion humaine floue de « ces deux choses parlent du meme sujet » en un probleme de geometrie. Une fois que c’est de la geometrie, un ordinateur le resout vite.

    Comment se mesure la similarite

    La proximite dans cet espace se mesure generalement avec la similarite cosinus ou, de facon equivalente pour des vecteurs normalises, le produit scalaire. La similarite cosinus regarde l’angle entre deux vecteurs et ignore leur longueur. Deux vecteurs pointant dans la meme direction ont un score proche de 1.0, des vecteurs a angle droit un score proche de 0, et des vecteurs opposes un score proche de -1.0. Certains systemes utilisent plutot la distance euclidienne, qui mesure la distance en ligne droite. Le choix depend de la facon dont le modele d’embedding a ete entraine, et se tromper degrade discretement vos resultats.

    Voici la partie qui surprend ceux qui viennent des bases relationnelles. Il n’y a pas de correspondance exacte. Chaque requete renvoie une liste classee d’elements approximativement pertinents avec un score attache. C’est vous qui decidez ou couper la liste. Ce changement mental, de « la ligne qui correspond » vers « les lignes les plus similaires », est le plus gros ajustement.

    Generer des embeddings en pratique

    Vous entrainez rarement un modele d’embedding vous-meme. Vous en appelez un. Voici un petit exemple qui transforme quelques phrases en vecteurs et mesure leur proximite.

    from sentence_transformers import SentenceTransformer
    import numpy as np
    
    model = SentenceTransformer("all-MiniLM-L6-v2")
    
    phrases = [
        "mon ordinateur portable ne s'allume plus",
        "mon pc est mort",
        "a quelle heure ferme le magasin",
    ]
    
    vecteurs = model.encode(phrases, normalize_embeddings=True)
    
    def cosinus(a, b):
        return float(np.dot(a, b))
    
    print("portable vs pc:", cosinus(vecteurs[0], vecteurs[1]))
    print("portable vs magasin:", cosinus(vecteurs[0], vecteurs[2]))
    

    Lancez ceci et la premiere paire obtient un score eleve tandis que la seconde obtient un score bas. Le modele n’a jamais vu ces phrases exactes pendant l’entrainement. Il a appris les relations a partir du contexte, et cette generalisation est ce qui fait fonctionner toute l’approche.

    Pourquoi il faut une base specialisee

    Vous pourriez stocker les vecteurs dans une colonne normale et comparer votre requete a chaque ligne. Cela s’appelle une recherche exacte ou par force brute, et cela fonctionne bien jusqu’a peut-etre cent mille vecteurs. Au-dela, ca s’effondre, car comparer une requete a dix millions de vecteurs a chaque appel est trop lent pour quoi que ce soit d’interactif.

    Les bases vectorielles resolvent cela avec des index de plus proches voisins approximatifs. Le plus courant est HNSW, pour Hierarchical Navigable Small World. Il construit un graphe en couches ou chaque vecteur est relie a ses voisins, et une requete parcourt le graphe au lieu de tout balayer. Vous echangez un peu de precision contre un gain de vitesse enorme. D’autres types d’index comme IVF et la quantification de produit font d’autres compromis entre memoire, vitesse et rappel.

    • Le rappel est la fraction des vrais plus proches voisins que votre index renvoie reellement. Vous l’ajustez selon la latence que vous pouvez vous permettre.
    • La latence est le temps que prend une requete. HNSW repond typiquement en quelques millisecondes meme sur des millions de vecteurs.
    • La memoire compte car les graphes HNSW sont volumineux. La quantification reduit les vecteurs au prix d’un peu de precision.

    Le filtrage par metadonnees, la ou ca devient concret

    La recherche par similarite pure suffit rarement a elle seule. Dans une vraie application, vous voulez « trouver les documents similaires a cette requete, mais uniquement dans l’espace de cet utilisateur, ecrits ces 90 derniers jours. » Cela suppose de combiner recherche vectorielle et filtres classiques sur les metadonnees. La qualite avec laquelle une base gere cette combinaison, appelee recherche filtree ou hybride, est l’un des criteres qui me tiennent le plus a coeur quand j’evalue les options, et c’est un theme central de mes notes sur comment choisir une base vectorielle.

    Mal fait, le filtrage force le moteur soit a tout balayer, soit a renvoyer trop peu de resultats parce que le filtre a elimine la plupart des candidats trouves par l’index. Bien fait, le filtre est applique pendant le parcours du graphe, et vous gardez des resultats rapides et precis a l’interieur du sous-ensemble qui vous interesse.

    Un modele mental qui tient

    Je vois une base vectorielle comme un moteur de recherche du sens plutot que des mots. Un index de mots-cles repond a « quels documents contiennent ces termes. » Un index vectoriel repond a « quels documents parlent de la meme chose que cette requete. » Les deux sont complementaires, pas concurrents, et c’est pour cela que les systemes les plus solides que j’ai construits utilisent les deux a la fois et fusionnent les classements.

    Les embeddings portent la semantique. L’index rend la recherche rapide a grande echelle. Les filtres de metadonnees gardent les resultats pertinents pour l’utilisateur reel. Une fois ces trois pieces assimilees, le reste du domaine a cesse de ressembler a de la magie pour devenir de l’ingenierie.

    Vers quoi cela mene

    Si les bases vectorielles ont explose en popularite, c’est qu’elles sont la couche de stockage sous la generation augmentee par recuperation. Quand vous voulez qu’un modele de langage reponde a des questions sur vos propres documents, vous encodez ces documents, vous stockez les vecteurs, et vous recuperez les pertinents au moment de la requete. Je deroule ce schema complet dans construire des systemes RAG avec des bases vectorielles, et il s’appuie directement sur tout ce qui precede. Pour la vue d’ensemble de la mise en production de ces systemes, mon article sur l’ingenierie IA pratique couvre les decisions qui l’entourent.

    Commencez par generer quelques embeddings et afficher les scores de similarite vous-meme. Voir des phrases liees obtenir un score eleve et des phrases sans rapport un score bas fait plus pour l’intuition que n’importe quel schema. Tout le reste n’est que detail par-dessus cette seule idee.

  • Ingénierie IA pratique : livrer des fonctionnalités LLM qui tiennent

    Ingénierie IA pratique : livrer des fonctionnalités LLM qui tiennent

    Il y a un grand fossé entre une démo qui marche devant un public et une fonctionnalité qui survit à de vrais utilisateurs pendant un mois. J’ai livré quelques fonctionnalités LLM, et presque tout ce que j’ai appris à la dure vit dans ce fossé.

    La démo, c’est les 80 % faciles

    Brancher un modèle et obtenir une bonne réponse prend un après-midi. Le reste, c’est tout ce qui arrive quand l’entrée est bizarre, que le modèle se trompe avec assurance, ou que l’utilisateur demande une chose jamais testée. Cette partie prend les trois semaines restantes, et c’est elle qui décide si quelqu’un continue à utiliser le produit.

    Alors prévoyez-la. Réservez plus de temps à l’évaluation et aux garde-fous qu’au chemin heureux, car le chemin heureux se construit presque tout seul.

    RAG : le difficile, c’est la recherche, pas la génération

    La plupart des fonctionnalités LLM utiles ont besoin de vos données, pas seulement de l’entraînement du modèle. La génération augmentée par la recherche est la réponse standard : trouver les passages pertinents, les mettre dans le prompt, laisser le modèle répondre à partir d’eux. Simple à décrire, délicat à réussir.

    La qualité d’un système RAG, c’est presque entièrement la qualité de sa recherche. Si vous récupérez les mauvais passages, aucune astuce de prompt ne vous sauve. Passez votre temps sur la stratégie de découpage, sur le fait de savoir si vous avez vraiment besoin d’embeddings ou si une recherche par mots-clés suffit pour vos données, et sur la mesure : le contexte récupéré contient-il la réponse, avant même de regarder la génération.

    Un conseil concret : journalisez les passages récupérés pour chaque requête en développement. La moitié de mes bugs RAG étaient évidents dès que je voyais ce que la recherche tirait vraiment.

    On n’améliore pas ce qu’on ne mesure pas

    « Ça a l’air mieux » n’est pas une métrique. Avant de régler quoi que ce soit, constituez un petit jeu d’évaluation : trente à cinquante entrées réelles avec les bonnes sorties connues. Lancez-le à chaque changement. Ça semble excessif jusqu’au jour où un ajustement de prompt « qui améliorait évidemment les choses » casse discrètement un tiers de vos cas.

    Les évaluations n’ont pas besoin d’être sophistiquées. Un tableur d’entrées, de comportements attendus et d’un succès/échec vérifié à l’œil vaut mieux qu’aucune évaluation. Automatisez plus tard, une fois que vous savez ce que vous mesurez.

    Traitez la sortie du modèle comme non fiable

    C’est la leçon qui rejoint la sécurité. La sortie d’un modèle n’est que du texte, et si vous l’injectez dans une requête de base, une commande shell ou un autre système, elle peut faire des dégâts comme une entrée utilisateur. Si un agent lit du contenu non fiable, ce contenu peut porter des instructions, le problème d’injection de prompt que j’aborde dans l’IA agentique en cybersécurité.

    Validez les sorties structurées contre un schéma. Ne passez jamais du texte brut du modèle dans quoi que ce soit qui s’exécute. L’état d’esprit « l’entrée est hostile » de ma checklist de sécurité s’applique directement à ce qui sort du modèle, pas seulement à ce qui entre.

    Coût et latence sont des décisions produit

    Le plus gros modèle est rarement le bon choix par défaut. Un modèle plus petit qui répond en 400 millisecondes bat souvent un plus gros qui prend quatre secondes, car l’utilisateur ressent la latence tout de suite et juge la qualité lentement. Mettez en cache agressivement. Routez les requêtes faciles vers des modèles bon marché et gardez le coûteux pour les cas difficiles.

    Choisissez votre niveau de modèle exprès. Je pars du modèle le plus capable pendant le développement, puis je descends une fois que je sais quels appels ont vraiment besoin de puissance.

    Où cela vous mène

    Livrer des fonctionnalités IA, c’est surtout de l’ingénierie normale avec une composante probabiliste greffée. Le modèle est la partie amusante et la plus petite. La recherche, l’évaluation, la validation et la plomberie autour, c’est le vrai travail. Si vous construisez le système autour de zéro, les patterns de l’architecture full-stack moderne sont là où le modèle doit réellement vivre.

  • Debuter avec Flutter pour Android et iOS

    Debuter avec Flutter pour Android et iOS

    Je suis venu a Flutter apres des annees a maintenir des bases de code Android et iOS separees qui divergeaient peu importe la discipline de l equipe. Un seul framework, un seul langage, deux stores. Cette promesse paraissait trop belle, alors j ai livre une vraie application avec avant de decider. Ca a tenu. Voici comment je lance un projet et ce que j aurais aime savoir des le premier jour.

    Installer la chaine d outils

    L installation est plus lourde qu on ne l avoue. Il faut le SDK Flutter, mais aussi une installation complete d Android Studio pour le SDK Android et un emulateur, et sur un Mac il faut Xcode plus les outils en ligne de commande pour iOS. Si vous sautez l un d eux, vous n avez que la moitie de la plateforme. Apres l installation, lancez la commande doctor et corrigez chaque avertissement avant d ecrire une ligne de code.

    flutter doctor -v
    flutter create my_app
    cd my_app
    flutter run -d all

    La commande doctor est la chose la plus utile du SDK. Elle verifie vos licences Android, votre configuration Xcode, votre version de CocoaPods, et si un appareil est connecte. Je la lance des que quelque chose se comporte bizarrement, car neuf fois sur dix le probleme est environnemental plutot que dans mon code.

    Comprendre l arbre de widgets

    Tout dans Flutter est un widget. Le padding est un widget. L alignement est un widget. Cela parait absurde la premiere semaine puis ca s eclaire. Au lieu de regler des proprietes sur une vue, vous enveloppez des widgets dans d autres widgets, et l imbrication decrit votre mise en page. Le framework reconstruit des parties de l arbre quand l etat change, et c est rapide car il compare a l arbre precedent plutot que de toucher directement aux vues natives.

    import 'package:flutter/material.dart';
    
    void main() => runApp(const MyApp());
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: Scaffold(
            appBar: AppBar(title: const Text('Hello')),
            body: const Center(child: Text('Tourne sur les deux plateformes')),
          ),
        );
      }
    }

    StatelessWidget sert aux elements qui ne changent jamais apres leur construction. Des que vous avez besoin d un compteur, d un interrupteur ou d une valeur qui se met a jour, vous passez a StatefulWidget ou a une vraie solution d etat. J en parle en detail dans mon article sur la gestion d etat Flutter, car choisir la bonne approche tot evite un refactor penible plus tard.

    Le hot reload change la facon de travailler

    Le hot reload est la fonctionnalite qui m a convaincu. Vous sauvegardez un fichier et l application en cours se met a jour en moins d une seconde tout en gardant son etat. Ajuster une couleur, modifier un padding, corriger un bug de mise en page devient une boucle serree sans reconstruction. Il y a une difference entre le hot reload, qui preserve l etat, et le hot restart, qui jette l etat et relance depuis zero. Quand l interface parait fausse apres un reload, un hot restart la remet souvent en ordre.

    • Hot reload: garde l etat, injecte le code modifie, quasi instantane
    • Hot restart: reinitialise l etat, plus lent, necessaire apres un changement de code de haut niveau
    • Reconstruction complete: necessaire apres un changement de config native ou l ajout de plugins

    Gerer les deux plateformes honnetement

    Une seule base de code ne veut pas dire que vous pouvez ignorer les plateformes. Les utilisateurs iOS attendent un glissement de retour et une certaine sensation au defilement. Les utilisateurs Android attendent les ondulations material et un bouton retour materiel. Flutter vous donne les deux langages de design, Material et Cupertino, et vous pouvez brancher selon la plateforme quand c est utile. Je garde ce branchement petit et centralise pour qu il ne s eparpille pas dans toute l application.

    import 'dart:io' show Platform;
    import 'package:flutter/material.dart';
    
    Widget adaptiveSpinner() {
      if (Platform.isIOS) {
        return const CupertinoActivityIndicator();
      }
      return const CircularProgressIndicator();
    }

    La structure de projet que j adopte

    Le modele par defaut met tout dans un seul fichier. C est bien pour une demo et terrible pour une application a maintenir. Je decoupe le code en dossiers par fonctionnalite plutot que par type, ainsi une fonctionnalite possede ses ecrans, ses modeles et sa logique au meme endroit. Cela paie le jour ou vous supprimez une fonctionnalite et voulez qu elle disparaisse proprement.

    Une fois la structure en place, les sujets suivants sont la facon dont l application parle aux API natives et comment elle reste fluide sous charge. Je creuse les ponts natifs dans les canaux de plateforme Flutter, et le maintien de temps de trame bas dans l optimisation des performances Flutter. Posez d abord de bonnes fondations, puis ajoutez ces sujets par dessus.

    Quoi construire en premier

    Ne commencez pas par l application de vos reves. Construisez quelque chose de petit qui touche un appel reseau, une liste, un ecran de detail et un stockage local. Cela couvre la plupart de ce dont une vraie application a besoin et expose les asperites de votre configuration tant que les enjeux sont faibles. Quand vous l aurez fait deux fois, le framework cesse de vous combattre et commence a disparaitre en arriere plan, exactement la ou un bon outil doit etre.