02 déc. 2025·7 min de lecture

Recherche et filtres cassés dans les apps CRUD alimentées par l'IA : comment les réparer

Une recherche et des filtres cassés rendent les apps CRUD IA inutilisables. Apprenez à réparer les query builders, limiter les filtres, prévenir l'injection SQL et accélérer les requêtes avec des index.

Recherche et filtres cassés dans les apps CRUD alimentées par l'IA : comment les réparer

À quoi ressemble une « recherche cassée » dans une app CRUD

Une recherche cassée donne l'impression que l'app ment. Vous tapez un nom que vous savez présent et il n'apparaît pas. Ou vous filtrez par statut et vous obtenez des enregistrements qui ne correspondent clairement pas.

Les symptômes les plus courants sont simples :

  • Résultats manquants
  • Lignes dupliquées
  • Tri qui semble aléatoire

Les doublons viennent souvent des jointures. Un enregistrement devient plusieurs lignes parce que la requête joint une autre table et n'effectue jamais de groupement ou de déduplication. Un tri « aléatoire » survient généralement quand il n'y a pas de tri stable, donc la base de données renvoie les lignes dans l'ordre qui lui convient.

Ces problèmes s'aggravent avec la croissance des données. Avec 50 enregistrements, vous ne remarquerez peut-être pas qu'un filtre est parfois ignoré. Avec 50 000 enregistrements, le même bug se transforme en timeouts, pages partiellement chargées et utilisateurs qui abandonnent parce qu'ils ne trouvent rien.

Quand la recherche paraît peu fiable, les gens cessent de faire confiance à toute l'app. Ils supposent que les données sont fausses, pas la requête. Les tickets support augmentent, et les équipes gardent parfois leurs propres tableurs car le système d'enregistrement ne semble plus sûr.

Un moyen rapide de reproduire le problème est de créer quelques enregistrements faciles à distinguer. Par exemple : un client nommé "Ann Lee" (actif), un nommé "Anne Li" (inactif), et un nommé "Bob" (actif). Ensuite vérifiez :

  • Recherchez "Ann" et confirmez quels enregistrements apparaissent
  • Filtrez status = active et confirmez que "Anne Li" disparaît
  • Triez par date de création deux fois et confirmez que l'ordre reste le même

Si un résultat vous surprend, la recherche est cassée, même si elle ne tombe en panne que « parfois ».

Pourquoi les apps CRUD IA se trompent souvent sur la recherche et les filtres

La plupart des apps CRUD IA démarrent comme des démos rapides. La recherche et les filtres sont ajoutés tard, puis deviennent silencieusement la partie que les utilisateurs manipulent sur chaque page. C'est pourquoi la recherche cassée est si courante : elle est construite sous contrainte de temps, avec des motifs collés-copiés qui semblent corrects jusqu'à l'arrivée de vraies données et de vrais utilisateurs.

Une cause fréquente est un query builder généré par l'IA qui mélange une paramétrisation sûre et de la concaténation de chaînes. Il peut lier les valeurs en toute sécurité à un endroit, puis injecter directement une colonne de tri, un opérateur ou un fragment WHERE brut ailleurs. Cela crée des bugs confus (résultats erronés, lignes manquantes) et un vrai risque (injection SQL via des filtres « astucieux »).

Un autre problème est de laisser l'UI envoyer n'importe quoi : n'importe quel nom de champ, n'importe quel opérateur, n'importe quelle valeur. Ça semble flexible, mais le backend finit par deviner l'intention. Un utilisateur recherche status, un autre utilise createdAt, et quelqu'un d'autre tente contains sur une colonne numérique. Même si rien ne plante, le comportement devient incohérent et difficile à tester.

Les jointures aggravent la situation. Rechercher à travers des tables jointes sans plan conduit à des doublons, des correspondances manquantes et des requêtes lentes. Une page "Customers" peut joindre orders et notes, puis appliquer le terme de recherche aux deux. Sans règles claires pour grouper et dédupliquer, un client avec beaucoup de commandes peut apparaître plusieurs fois, et la pagination devient peu fiable.

Les performances échouent souvent pour la même raison : la base n'est pas indexée pour ce que les gens font réellement. Les équipes indexent id et considèrent le travail fini, alors qu'en production les requêtes filtrent par tenant_id + status, trient par created_at et recherchent par email.

Sécurité d'abord : stoppez les filtres dynamiques dangereux

La recherche et le filtrage cassés commencent souvent par une zone de filtre « flexible » : les utilisateurs peuvent passer n'importe quel champ, n'importe quel opérateur et n'importe quelle valeur. Si votre app assemble ces éléments dans une chaîne SQL, un attaquant peut faire passer du SQL supplémentaire dans la requête. En clair, il ne se contente plus de filtrer les lignes, il change ce que la base exécute.

Un exemple courant est un filtre comme status=active qui devient WHERE status = 'active'. Si quelqu'un soumet active' OR 1=1 --, la requête peut se transformer en « retourner tout ». Dans des cas plus graves, le texte injecté peut lire des tables sensibles ou modifier des données, selon les permissions.

L'échappement n'est pas la même chose que la paramétrisation. L'échappement tente de rendre les caractères dangereux sûrs dans une chaîne. La paramétrisation (prepared statements) garde la structure SQL fixe et envoie les valeurs séparément, de sorte que la base les traite comme des données, pas des instructions.

La partie délicate est que beaucoup de problèmes de « filtres dynamiques » ne concernent pas les valeurs. Ces entrées sont particulièrement risquées car la plupart des bibliothèques SQL ne peuvent pas les paramétrer :

  • Noms de champs (exemple : sortBy=price)
  • Direction de tri (asc/desc)
  • Opérateurs (=, LIKE, >, IN)
  • Extraits SQL bruts (where=..., order=...)
  • Noms de table ou de relation

Pour ceux-ci, n'essayez pas d'échapper et d'espérer. Utilisez des allowlists : définissez exactement quels champs peuvent être filtrés ou triés, quels opérateurs sont autorisés par champ, et comment chacun se mappe à du SQL sûr.

Limitez aussi les dégâts avec des rôles de base de données en moindre privilège. Même si une mauvaise requête passe, le compte utilisé par votre app ne devrait pas pouvoir dropper des tables ou lire des données réservées aux admins.

Créez un contrat de filtres clair (ce qui est autorisé ou non)

La plupart des filtrages cassés arrivent parce que l'app accepte « n'importe quoi » depuis l'UI et essaie de le transformer en requête. Un contrat de filtres règle ça en posant des règles claires sur les filtres existants, leur signification et la façon de traiter une entrée invalide.

Gardez le contrat petit. Commencez par une whitelist courte des champs filtrables et des opérateurs que vous supportez pour chacun. Par exemple : status peut être equals mais pas contains. Une date createdAt peut être before et after, pas du texte libre.

Validez les types avant de construire la requête. Traitez chaque filtre comme une entrée typée, pas une chaîne à coller dans du SQL : chaînes avec longueur max, nombres avec limites de plage, dates strictes, enums qui doivent correspondre aux valeurs autorisées, booléens qui ne sont que true/false.

Ajoutez des contraintes pour qu'une requête ne surcharge pas votre base : taille de page maximale, nombre maximum de filtres par requête, et longueur maximale du texte de recherche.

Enfin, choisissez des règles cohérentes pour les valeurs vides, nulles et inconnues. Si une valeur de filtre est vide, l'ignorez-vous ou la rejetez-vous ? Si un nom de champ est inconnu, retournez-vous une erreur de validation claire ? Ces décisions évitent les bugs du type « pourquoi mes résultats ont disparu ».

Pas à pas : refactorer un query builder qui renvoie de mauvais résultats

La recherche et le filtrage cassés commencent souvent par un query builder qui veut être « flexible » en collant des chaînes SQL. Ça marche pour une démo, puis ça renvoie des résultats bizarres, casse sur les quotes, ou devient un risque de sécurité.

Un chemin de refactorisation pratique

D'abord, écrivez ce dont l'UI a réellement besoin, en langage clair. Par exemple : « Rechercher des clients par nom ou email », « Filtrer par statut », « Trier par les plus récents », « Afficher 25 par page ». Si vous ne pouvez pas le décrire clairement, le code ne restera pas propre.

Refactorez ensuite par petites étapes :

  • Verrouillez les entrées autorisées : quels filtres existent, quelles options de tri existent et à quelles colonnes elles correspondent.
  • Construisez le WHERE avec uniquement des paramètres (pas de concaténation de chaînes pour les valeurs).
  • Traitez les clés de filtre comme non sûres : mappez des clés UI comme status à des colonnes connues comme customers.status, et rejetez ou ignorez les clés inconnues.
  • Rendez le tri stable : ajoutez un tie-breaker (exemple : created_at puis id) pour que la pagination n'ignore ni ne répète des lignes.
  • Règlez explicitement la pagination : commencez par limit et offset, ou utilisez plus tard la pagination par curseur, mais ne mélangez pas les styles.

Si l'UI envoie sort=createdAt, ne le passez pas tel quel au SQL. Traduisez-le en un extrait fixe et sûr comme ORDER BY customers.created_at DESC, customers.id DESC.

Tests qui attrapent les bugs « ça avait l'air ok »

Quelques tests ciblés évitent les régressions :

  • Noms avec apostrophes (O'Connor)
  • Emojis et noms non latins
  • Casse mixte (alex vs Alex)
  • Recherche vide avec filtres appliqués
  • Clés de filtres inconnues (ignorées ou rejetées, de façon cohérente)

Choisir le bon comportement de recherche (et le garder prévisible)

Refactorer le code de requêtes généré par l'IA
Nous refactorisons des query builders brouillons en code sûr et testable que vous pouvez déployer.

Beaucoup de recherches cassées viennent d'un manque de règles claires. Si les utilisateurs ne savent pas ce que signifie la boîte de recherche, chaque résultat paraît faux, même si le SQL fait exactement ce que vous lui avez demandé.

Choisissez un mode de recherche par défaut et appliquez-le partout dans l'app. Mélanger correspondance exacte sur un écran et « contains » sur un autre est une manière courante de confondre les utilisateurs.

La correspondance exacte est préférable pour les IDs et les emails. Le préfixe (prefix match) est bon pour les noms et codes et peut être rapide avec le bon index. Le contains est utile mais facile à rendre lent sur de grosses tables. Le fuzzy est pertinent quand on attend des fautes de frappe, mais il faut le signaler.

Si vous utilisez la recherche contains, faites attention aux motifs comme LIKE '%term%' sur de grandes tables. Ce wildcard en tête force souvent un scan complet.

Quoi que vous choisissiez, normalisez l'entrée toujours de la même manière : supprimez les espaces en début/fin, uniformisez la casse quand cela n'altère pas le sens, et décidez comment traiter la ponctuation. Chercher " Acme, Inc " devrait se comporter comme "acme inc", mais chercher "C++" ne devrait pas silencieusement devenir "c".

Accélérez-la : ajoutez les bons index pour vos requêtes réelles

Si les utilisateurs trouvent la recherche lente, commencez par repérer les quelques requêtes qui coûtent le plus. Ne devinez pas. Extrayez les pires offenders des logs de la base, des traces API, ou même d'un simple log horodaté autour du endpoint de recherche.

Les index fonctionnent mieux quand ils correspondent aux motifs réels : les colonnes dans votre WHERE, plus la façon dont vous triez. Si votre UI filtre par status et date et trie par newest, indexer seulement status n'aidera pas beaucoup.

Évitez d'indexer tout « au cas où ». Chaque index ajoute un coût : écritures plus lentes, migrations plus lourdes, et plus d'éléments à maintenir. Ajoutez un petit nombre d'index ciblés, puis retestez les performances avec une taille de données réaliste.

Pagination et tri qui tiennent sous charge

Mettre votre app CRUD en production
Nous nettoyons les problèmes de sécurité et de production pour que votre app soit prête à lancer.

Les bugs de pagination apparaissent comme « la page 2 répète des éléments de la page 1 » ou « certaines lignes disparaissent ». La cause racine est généralement un tri instable. Si vous triez seulement par created_at, beaucoup de lignes partagent le même timestamp, donc la base est libre de retourner les ex aequo dans n'importe quel ordre. Quand de nouvelles lignes sont insérées entre les requêtes, l'ordre change et des éléments sont sautés ou répétés.

Utilisez un tri stable avec un tie-breaker, par exemple ORDER BY created_at DESC, id DESC. L'id rend la position de chaque ligne unique, donc la « page suivante » reste prévisible.

La pagination par offset (LIMIT 50 OFFSET 5000) est simple mais devient plus lente quand l'offset grandit. Pour les grandes tables, la pagination par curseur (keyset) est souvent un meilleur choix : au lieu de demander « page 101 », vous demandez « les 50 lignes suivantes après ce dernier vu (created_at, id)».

Les totaux peuvent silencieusement devenir votre requête la plus lente. Un COUNT(*) filtré sur une grosse table peut faire beaucoup de travail, et le faire à chaque requête nuit aux performances. Les alternatives courantes sont d'afficher les totaux seulement quand nécessaire, de mettre en cache des totaux courants, ou de renvoyer hasNextPage avec LIMIT pageSize + 1 au lieu de tout compter.

Comment déboguer rapidement une recherche lente

Quand la recherche cassée se manifeste par « ça marche, mais c'est lent », traitez d'abord ça comme un problème de mesure. Deviner mène à des changements d'index aléatoires et à de nouveaux bugs.

Commencez par capturer le SQL réel des seules requêtes lentes. Gardez-le limité à un ID de requête ou une courte fenêtre temporelle, et évitez de logger les entrées brutes si elles peuvent contenir des emails, tokens ou autres données sensibles. Logger la forme des filtres (quels champs ont été utilisés) suffit souvent.

Ensuite exécutez EXPLAIN (ou l'équivalent de votre base) sur le SQL exact exécuté. Vous cherchez à savoir si la base utilise un index ou scanne toute la table et trie de larges ensembles de résultats.

Tueurs de performance courants :

  • N+1 queries (une requête pour les lignes, puis une par ligne pour les données liées)
  • Jointures non bornées (joindre de grandes tables sans filtre sélectif)
  • LIMIT absent (ou pagination qui trie quand même toute la table)
  • Filtres qui empêchent l'usage d'index (fonctions sur colonnes, wildcard en tête comme %term)
  • Tri par une colonne non indexée

Si vous ne pouvez pas reproduire la lenteur sur votre machine, construisez un jeu de données minimal qui la montre encore. Si la lenteur disparaît, le problème vient souvent de la distribution des données, pas seulement du code.

Erreurs communes à éviter quand on corrige la recherche

L'échappement des entrées utilisateur est bien, mais ça ne rend pas toute la requête sûre. Un bug très courant dans les apps CRUD IA est d'échapper les valeurs tout en laissant le client envoyer des noms de colonnes bruts comme ?sort=users.email ou ?filter[field]=status. Si quelqu'un peut contrôler les noms de colonnes, les opérateurs ou les fragments SQL, il peut toujours casser votre requête.

Laisser le client choisir n'importe quelle colonne de tri est un autre piège. Cela provoque des erreurs (trier par une colonne non sélectionnée), des fuites de données (trier par un champ interne), et des problèmes de perf (trier par une colonne non indexée sur une grosse table). Gardez le tri sur une petite allowlist que vous supportez réellement.

Filtrer sur des champs calculés vous mordra aussi. Filtrer par full_name quand il est construit depuis first_name + last_name, ou "jours depuis la dernière connexion" calculé en code, tend à devenir lent ou incohérent. Si vous devez filtrer dessus, envisagez de le stocker, l'indexer ou le mettre en cache.

Faites attention à ne pas corriger la vitesse en changeant les résultats (ou corriger les résultats en rendant tout lent). Passer d'un LEFT JOIN à un INNER JOIN peut accélérer les choses mais supprimer silencieusement des enregistrements. Ajouter DISTINCT pour cacher les doublons peut masquer un bug de jointure et rendre la pagination et les totaux confus.

Checklist rapide avant de déployer le correctif

Supprimer les doublons issus des jointures
Si votre liste affiche des doublons, nous corrigeons la logique de jointure et les comptes.

Testez les cas ennuyeux. La plupart des bugs se cachent dans des entrées limites, des combinaisons inattendues et des performances avec des données réelles.

Vérifications de correction : quotes, signes pourcentage, underscores, emojis, texte très long, espaces multiples, et recherche vide combinée à des filtres. Un bon résultat est prévisible, même quand l'entrée est sale.

Vérifications de sécurité : le backend n'accepte qu'un petit ensemble de champs et d'opérateurs, et chaque valeur est passée comme paramètre (placeholders), jamais collée dans du SQL.

Vérifications de performance : mesurez les requêtes lentes avant et après vos changements en utilisant le même dataset et les mêmes entrées. Documentez les index dont vous dépendez pour que les modifications futures n'annulent pas le travail.

Vérifications de stabilité : le tri est déterministe (avec un tie-breaker comme id), et la pagination n'ignore ni ne répète des éléments quand de nouvelles lignes arrivent.

Exemple : corriger une recherche "Customers" cassée dans une app CRUD IA

Un cas courant apparaît sur une page "Customers" : filtre par statut (active, paused), plan (Free, Pro), plage de date d'inscription, et une recherche rapide par nom.

Les symptômes paraissent aléatoires. "Active + Pro" renvoie des clients qui ne sont pas Pro, la recherche par nom manque des correspondances évidentes, et l'ordre change à chaque rafraîchissement. Sous charge, la page devient suffisamment lente pour expirer.

Ce qui a généralement mal tourné :

  • Une jointure vers plans ou subscriptions multiplie les lignes, donc un client apparaît plusieurs fois et les comptes sont faux.
  • Le tri est construit depuis l'entrée brute (non sûre et instable).
  • La base scanne trop car il n'y a pas d'index correspondant au vrai motif filtre+tri.

Une correction propre commence par rendre les filtres ennuyeux et stricts : n'autoriser que des champs connus, valider les types, et construire les requêtes depuis un petit contrat (status est une des X valeurs, plan est une des Y, dates sont de vraies dates, name est une chaîne simple).

Rendez ensuite les résultats prévisibles : appliquez d'abord les filtres, traduisez les clés de tri via une allowlist, et ajoutez un tie-breaker stable (par exemple created_at puis id) pour que la pagination ne réorganise pas les éléments.

Enfin, ajoutez des index ciblés qui correspondent à la façon dont les gens recherchent réellement. Pour cet écran, cela signifie souvent un index composite couvrant le combo filtre+tri le plus courant, plus une approche séparée pour la recherche par nom.

Si vous avez hérité d'une base générée par l'IA (Lovable, Bolt, v0, Cursor, Replit) avec des query builders emmêlés et des filtres dynamiques dangereux, FixMyMess (fixmymess.ai) peut commencer par un audit de code gratuit pour pointer les jointures incorrectes, le SQL risqué et les requêtes spécifiques à refactorer en premier. Beaucoup de projets peuvent être réparés et prêts au déploiement en 48–72 heures une fois le contrat de filtres verrouillé.

Questions Fréquentes

Comment savoir si la recherche de mon app est vraiment cassée ou juste « bizarre » ?

Commencez par vérifier trois choses : des correspondances manquantes que vous attendez, des lignes dupliquées et un ordre incohérent. Créez un petit jeu de données évident (des noms similaires et des statuts différents), puis répétez les mêmes recherches, filtres et tris. Si les résultats changent ou vous surprennent, la recherche est cassée, même si elle ne tombe en panne que parfois.

Pourquoi je vois des lignes dupliquées après avoir ajouté la recherche sur des tables liées ?

Les jointures multiplient souvent les lignes. Un enregistrement parent (par exemple un client) peut correspondre à de nombreuses lignes liées (par exemple des commandes), et la requête renvoie une ligne par correspondance de jointure à moins que vous ne fassiez un groupement ou une déduplication correctement. Cela peut aussi casser la pagination car la base paginate des lignes, pas des entités uniques.

Pourquoi l'ordre trié semble aléatoire même quand je trie par date ?

Cela signifie généralement que vous n'avez pas de tri stable. Si vous triez seulement par une colonne non unique (comme created_at), de nombreuses lignes peuvent avoir la même valeur et la base est libre de retourner les ex aequo dans n'importe quel ordre. Ajoutez un tie-breaker comme created_at puis id pour que les résultats restent déterministes au rechargement et entre les pages.

Pourquoi laisser l'UI envoyer n'importe quel champ ou opérateur de filtrage est une mauvaise idée ?

Parce que la flexibilité est risquée. Si le backend laisse le client envoyer des noms de champ arbitraires, des opérateurs ou des fragments SQL bruts, vous obtenez un comportement incohérent et un vrai risque d'injection. L'approche sûre est une allowlist : définissez exactement quels champs peuvent être filtrés/triés et traduisez les clés UI en colonnes SQL connues.

Quelle est la manière la plus sûre de construire des filtres dynamiques sans injection SQL ?

Paramétrez les valeurs partout où c'est possible, et ne collez jamais l'entrée utilisateur directement dans des chaînes SQL. Pour les parties qu'on ne peut pas paramétrer (colonne de tri, direction, opérateur), utilisez des allowlists et mappez chaque option autorisée à un snippet SQL fixe et sûr. L'échappement seul ne remplace pas la paramétrisation.

Qu'est-ce qu'un « contrat de filtres » et que doit-il inclure ?

C'est un contrat simple et explicite. Choisissez une courte liste de filtres supportés, définissez quels opérateurs chaque filtre autorise, validez les types (énums, dates, nombres, booléens), et décidez d'un comportement cohérent pour les valeurs vides ou inconnues. Un contrat strict empêche les bugs du type « parfois ça marche » et facilite les tests.

Comment déboguer une recherche lente sans deviner ?

Capturez ou consignez le SQL exact des requêtes lentes (sans divulguer d'entrées sensibles), puis exécutez EXPLAIN sur cette requête. Cherchez les scans de table complets, les gros tris, les jointures non limitées et les motifs qui empêchent l'utilisation d'index (comme les recherches avec wildcard en tête %term%). Corrigez la pire requête d'abord avant d'ajouter des index au hasard.

Quels index aident le plus pour les écrans CRUD de recherche ?

Indexez les colonnes que vous filtrez et triez ensemble. Par exemple, si en production vous filtrez par tenant_id et status et triez par created_at, un index composite correspondant à ce motif aide souvent. Ne créez pas des index partout : ajoutez quelques index ciblés et re-testez avec des données réalistes.

Pourquoi la pagination répète ou saute des éléments sous charge ?

La pagination par offset devient lente quand l'offset grandit, et un tri instable provoque répétitions ou omissions entre les pages. Utilisez un tri stable avec un tie-breaker et envisagez la pagination par curseur (keyset) pour les grandes tables. Aussi, faites attention à COUNT(*) sur chaque requête : il peut devenir la partie la plus lente. En alternative, renvoyez hasNextPage en utilisant LIMIT pageSize + 1 plutôt que de compter tout.

Comment savoir si mon app CRUD générée par l'IA a besoin d'aide pro pour corriger la recherche ?

Si le code a été généré par des outils comme Lovable, Bolt, v0, Cursor ou Replit, surveillez les concaténations de chaînes SQL, les clés de tri contrôlées par le client, des règles de recherche incohérentes entre écrans, et des jointures qui créent des doublons. FixMyMess peut démarrer par un audit de code gratuit pour repérer les filtres dangereux et les requêtes à refactorer, et beaucoup de projets peuvent être réparés et prêts à déployer en 48–72 heures.