Parquet, JSONL ou CSV pour le flux produit OpenAI : ce qui compte vraiment

OpenAI accepte parquet (préféré), jsonl.gz, csv.gz et tsv.gz. Le format décide si booléens, identifiants et champs imbriqués survivent à l'export.

Publié le 11 min de lecture

OpenAI accepte quatre formats de flux : parquet, que la documentation préfère « ideally with zstd compression », et JSON Lines, CSV et TSV gzippés. Les règles de contenu sont identiques dans les quatre. Ce qui change, c’est la façon dont chaque format préserve les types (booléens, décimales, identifiants avec zéros initiaux) et les valeurs imbriquées (variant_dict, additional_image_urls). Les petits catalogues se portent bien en jsonl.gz ; les grands tirent profit de parquet.

Ce que la documentation dit réellement

La page de présentation du dépôt de fichiers est brève sur le sujet. Elle énonce la préférence de format (« Prefer parquet », idéalement avec une compression zstd, tandis que « jsonl.gz, csv.gz, and tsv.gz are also supported »), impose l’UTF-8, recommande « Up to 500k items per shard » avec « shard files under ~500MB », demande « a stable file name » écrasé à chaque mise à jour, veut « full snapshots on a predictable cadence (at least daily) », indique qu’OpenAI « retains its most recently processed record for up to 14 days », et confirme que la livraison est un dépôt par SFTP sans marqueur de fin nécessaire.

Il n’existe aucun comparatif publié de vitesse d’ingestion par format, et nous n’en avons pas mesuré. La comparaison honnête n’est donc pas « quel format est le plus rapide » mais « quel format rend le plus difficile l’envoi d’une valeur mal formée ». C’est là que les quatre formats diffèrent.

Les quatre formats côte à côte

parquetjsonl.gzcsv.gztsv.gz
Position de la documentationPréféré, idéalement zstdPris en chargePris en chargePris en charge
Colonnes typéesOui (schéma dans le fichier)Par valeur (types JSON)Non, tout est du texteNon, tout est du texte
BooléensBooléen natiftrue/false JSONChaînes en minuscules true/falseChaînes en minuscules true/false
Valeurs imbriquées (variant_dict, dimensions)Struct ou map natif, ou colonne de chaîne JSONObjet JSON natifJSON sérialisé dans une cellule entre guillemetsJSON sérialisé dans une cellule
Listes (additional_image_urls, target_countries)Liste native, ou chaîne délimitéeTableau JSONChaîne séparée par des virgules, virgules des URL en %2CChaîne séparée par des virgules
Lisible dans un éditeur de texteNonOuiOuiOui
S’ouvre dans un tableurNonNonOui, avec risques d’inférence de typeOui, mêmes risques
Règles de guillemets à respecterAucuneÉchappement JSON seulementVirgules, guillemets, retours à la ligneTabulations et retours à la ligne
Outils nécessaires à l’écritureUne bibliothèque parquet (Arrow, pandas, DuckDB, Spark)N’importe quel langageN’importe quel langage, avec un vrai outil d’écriture CSVN’importe quel langage

Le reste de cet article explique chaque ligne du tableau avec la règle exacte de la spécification.

Parquet : colonnes typées, compression, et un piège

Parquet stocke un schéma avec les données. Une colonne booléenne est un booléen, une colonne de chaînes est une chaîne, une colonne de listes est une liste. C’est cette propriété qui explique la préférence de la documentation : le format lui-même exclut la classe d’erreurs où une colonne texte contient par hasard TRUE ou 1 là où un booléen était attendu.

La compression fait partie du format, colonne par colonne, et zstd est le codec que la page de présentation désigne. Un catalogue aux valeurs répétitives (la même brand, le même seller_name, la même availability sur la plupart des lignes) se compresse bien colonne par colonne.

Le piège est l’inférence de type à l’écriture. La plupart des outils d’écriture parquet déduisent le type d’une colonne à partir des données. Une colonne gtin dont toutes les valeurs ressemblent à des nombres est déduite comme entier, et le zéro initial de 00012345678905 a disparu avant même que le fichier soit écrit. La spécification est directe à ce sujet : « Keep identifiers as strings to preserve leading zeros. » Déclarez le schéma explicitement, avec item_id, group_id, offer_id, gtin et mpn en chaînes, et price et sale_price en chaînes également, puisqu’il s’agit de chaînes monétaires (79.99 USD), pas de décimales.

Le second coût est la lisibilité. Vous ne pouvez pas ouvrir un fichier parquet dans un éditeur de texte pour vérifier une ligne. Il vous faut un outil (DuckDB et pandas lisent tous deux le parquet en une ligne), et votre équipe doit être à l’aise avec cela quand un marchand demande pourquoi un produit manque.

JSONL : un objet JSON par ligne

JSON Lines est le format que la spécification utilise pour ses propres exemples. Chaque ligne est un objet JSON complet ; le fichier est gzippé. Les types viennent du JSON : les booléens sont true et false, les chaînes sont entre guillemets, les objets et les tableaux sont natifs. Les champs imbriqués de la spécification s’y projettent directement :

{"item_id":"MUG-350-BLUE","group_id":"MUG-350","listing_has_variations":true,"variant_dict":{"color":"Blue","capacity":"350 mL"},"title":"Blue ceramic mug, 350 mL","description":"Dishwasher-safe glazed ceramic mug with a handle.","url":"https://example.com/products/mug-blue","brand":"Northline","seller_name":"Northline Home","image_url":"https://example.com/images/mug-blue.jpg","additional_image_urls":["https://example.com/images/mug-blue-top.jpg","https://example.com/images/mug-blue-side.jpg"],"availability":"in_stock","price":"18.00 USD","gtin":"00012345678905","is_eligible_search":true,"target_countries":["US"]}

Deux choses à remarquer. gtin est une chaîne entre guillemets bien qu’elle ne contienne que des chiffres, donc les zéros initiaux survivent. variant_dict et additional_image_urls sont un vrai objet et un vrai tableau, sans gymnastique de guillemets.

Le piège du JSONL est l’inverse de celui de parquet : rien n’impose la cohérence entre les lignes. La ligne 1 peut porter "is_eligible_search": true et la ligne 2 "is_eligible_search": "true". Un analyseur JSON accepte les deux ; seule la première est un booléen JSON. La spécification n’autorise la forme chaîne en minuscules que « in delimited files », donc gardez des booléens JSON en JSONL et laissez un contrôle de schéma (ou un validateur) confirmer que chaque ligne a la même forme.

JSONL est aussi le format le plus simple à déboguer. zcat feed.jsonl.gz | grep MUG-350-BLUE vous montre la ligne exactement telle qu’OpenAI la reçoit.

CSV et TSV : compatibles tableur, et le plus de façons de se tromper

Les fichiers délimités n’ont pas de types. Tout est du texte, et la spécification adapte ses règles en conséquence : les booléens sont « the lowercase strings true and false in delimited files » ; « JSON objects in CSV or TSV cells must be serialized as JSON » ; et pour le CSV, « quote a cell containing commas, quotes, or newlines, and double each embedded quote. »

Voici l’en-tête correspondant à la ligne JSONL ci-dessus, puis la ligne elle-même en CSV :

item_id,group_id,listing_has_variations,variant_dict,title,description,url,brand,seller_name,image_url,additional_image_urls,availability,price,gtin,is_eligible_search,target_countries
MUG-350-BLUE,MUG-350,true,"{""color"":""Blue"",""capacity"":""350 mL""}","Blue ceramic mug, 350 mL",Dishwasher-safe glazed ceramic mug with a handle.,https://example.com/products/mug-blue,Northline,Northline Home,https://example.com/images/mug-blue.jpg,"https://example.com/images/mug-blue-top.jpg,https://example.com/images/mug-blue-side.jpg",in_stock,18.00 USD,00012345678905,true,US

Tous les pièges du format sont visibles sur cette seule ligne.

Des virgules dans les valeurs. Le titre Blue ceramic mug, 350 mL contient une virgule, donc la cellule est entre guillemets. Un exportateur maison qui joint les champs avec , sans jamais mettre de guillemets décale toutes les colonnes suivantes d’un cran sur cette ligne : price atterrit dans gtin, availability dans price, et la ligne est mal formée à trois endroits.

Des objets JSON dans les cellules. variant_dict est sérialisé en JSON, puis toute la cellule est mise entre guillemets et chaque guillemet interne est doublé. Le faire correctement à la main est source d’erreurs ; utilisez un vrai outil d’écriture CSV et passez-lui la chaîne JSON.

Des virgules dans les valeurs de liste. additional_image_urls est une cellule qui contient une liste séparée par des virgules, donc la cellule est entre guillemets. Si l’une des URL d’image contient elle-même une virgule (certains CDN utilisent des virgules dans leurs paramètres de transformation), la spécification demande de « Percent-encode commas as %2C in URL », sinon la liste se coupe au milieu de l’URL.

Des booléens en chaînes. listing_has_variations et is_eligible_search sont les chaînes en minuscules true. Un tableur qui affiche une case à cocher ou le résultat d’une formule écrit TRUE à l’export, ce qui n’est aucune des deux graphies acceptées.

Des zéros initiaux. 00012345678905 est ici du texte, mais ouvrez ce CSV dans un tableur, enregistrez-le, et la colonne devient un nombre : 12345678905, 11 chiffres, longueur invalide, GTIN invalide. C’est la façon la plus courante pour un flux CSV valide de devenir invalide, et cela se produit sans que personne n’ait modifié la cellule.

Des retours à la ligne dans les descriptions. Une description avec des sauts de paragraphe doit être entre guillemets, et un lecteur qui découpe sur les retours à la ligne avant d’analyser les guillemets casse le fichier. La plupart des bibliothèques CSV gèrent ce cas ; la plupart des scripts rapides ne le gèrent pas.

Le TSV partage tous ces pièges sauf la gestion des virgules : les tabulations sont plus rares que les virgules dans les données produit, donc moins de cellules réclament de l’attention, mais une tabulation égarée dans une description casse quand même la ligne, et les objets JSON doivent toujours être sérialisés. Pour cet usage, le TSV est par ailleurs interchangeable avec le CSV.

L’avantage des fichiers délimités est réel, cependant : une équipe merchandising peut ouvrir un CSV de 2 000 lignes, trier par availability et voir le catalogue. Aucun autre format ne le permet sans outillage. La contrepartie est que ce même tableur est l’outil le plus susceptible de corrompre le fichier.

Les règles identiques dans tous les formats

Le format change la façon d’encoder les valeurs, pas ce qu’elles doivent être. Ces règles s’appliquent aux quatre.

  • Découpage en fragments. « Up to 500k items per shard is recommended; target shard files under ~500MB ». Sous ces limites, un seul fichier suffit. Au-dessus, découpez en plusieurs fragments.
  • Noms de fichiers stables. « Keep the same file name on every update and overwrite it with the latest snapshot instead of creating a new name each run. » Les noms de fichiers datés s’accumulent au lieu de se remplacer.
  • Instantanés complets, au moins quotidiens. Les livraisons incrémentales ne sont pas décrites. Chaque livraison est le catalogue entier.
  • La queue de 14 jours. Un produit absent d’un instantané est conservé « for up to 14 days ». Pour le retirer plus vite, gardez sa ligne et passez is_eligible_search=false.
  • UTF-8, URL HTTP ou HTTPS absolues, monnaie en amount CURRENCY, aucune valeur de remplissage, identifiants en chaînes.
  • Livraison par SFTP, sans marqueur de fin à envoyer.
  • Commencez petit. La page de présentation demande de « Start with a small sample (around 100 items) ». C’est sur l’échantillon que vous découvrez que votre outil CSV n’a pas mis de guillemets, ou que votre schéma parquet a déduit un entier.

Si un produit est absent de ChatGPT et que le format semble correct, les causes au niveau de la ligne sont listées dans Pourquoi vos produits n’apparaissent pas dans les résultats shopping de ChatGPT.

Une recommandation honnête selon la taille du catalogue

Nous n’avons pas mesuré l’ingestion d’OpenAI selon les formats et la documentation ne publie aucun chiffre de ce type ; cette recommandation porte donc sur la justesse et l’exploitabilité, pas sur la vitesse.

Jusqu’à quelques milliers de produits. Utilisez jsonl.gz. Le fichier est petit quel que soit le format, donc la compression et les colonnes typées n’apportent rien de mesurable, tandis que pouvoir faire zcat et grep sur une ligne quand un marchand pose une question vaut beaucoup. Les booléens et les champs imbriqués sont natifs, et la seule discipline nécessaire est de garder les identifiants entre guillemets.

Quelques dizaines de milliers de produits. Toujours jsonl.gz si vous générez le fichier depuis du code et que vous le validez. Envisagez parquet si le fichier est produit par un pipeline de données qui parle déjà Arrow ou Spark, parce que le schéma typé vient alors gratuitement et que vous évitez la dérive entre booléens et chaînes d’une ligne à l’autre.

Des centaines de milliers de produits et au-delà. Parquet avec zstd, comme la documentation le préfère. À cette taille, vous découpez de toute façon en fragments, la compression en colonnes compte pour le temps de transfert par SFTP, et un schéma explicite est le seul moyen fiable de garantir qu’un demi-million de valeurs gtin sont toutes des chaînes. Déclarez le schéma ; ne laissez pas l’outil d’écriture le déduire.

CSV ou TSV, à toute taille. Choisissez-les uniquement si le fichier doit être modifié ou relu dans un tableur par des personnes, et traitez alors le tableur en lecture seule : exportez depuis le système de référence, ne réenregistrez jamais depuis le tableur vers le flux. Si vous maintenez déjà un CSV Google Shopping, les différences de correspondance sont traitées dans Flux Google Shopping ou flux OpenAI.

Erreurs fréquentes

  • Laisser un tableur toucher le CSV entre l’export et le téléversement (zéros initiaux perdus, TRUE écrit pour les booléens).
  • Écrire du parquet sans schéma explicite, si bien que gtin devient un entier.
  • Mélanger booléens JSON et booléens en chaînes entre les lignes d’un même fichier JSONL.
  • Joindre les champs CSV avec une virgule et sans guillemets.
  • Nommer le fichier avec une date, si bien que chaque exécution ajoute un fichier au lieu d’en remplacer un.
  • Envoyer seulement les produits modifiés au lieu de l’instantané complet.
  • Sauter l’échantillon de 100 articles et déboguer la première livraison sur le catalogue complet.

Ce que Convrail fait des formats

Convrail exporte les quatre formats : parquet, jsonl.gz, csv.gz et tsv.gz. Le choix est un réglage ; la validation est la même. Chaque ligne est contrôlée face à la spécification avant d’être encodée, puis écrite avec l’encodage que le format exige : booléens JSON en JSONL et en parquet, chaînes en minuscules en CSV et en TSV ; identifiants toujours en chaînes ; variant_dict sérialisé en JSON dans des cellules entre guillemets pour les fichiers délimités ; virgules protégées par des guillemets ; UTF-8 partout. Les fragments sont découpés à 500 000 articles ou environ 450 Mo, sous les limites recommandées, nommés feed-organic-000.<ext>, feed-organic-001.<ext> et ainsi de suite, et les mêmes noms sont écrasés à chaque livraison SFTP quotidienne, à l’heure que vous choisissez. Chaque exécution consigne les articles lus, acceptés et rejetés ainsi que les erreurs ligne par ligne, si bien qu’une erreur de format n’a jamais à être trouvée en ouvrant le fichier. Dans notre test automatisé, un catalogue de 10 000 produits est validé, exporté et livré par SFTP en moins d’une minute.

Prochaine étape

Choisissez le format dans un réglage et laissez le validateur appliquer les règles d’encodage à chaque ligne : voir la page flux produit.

Sources

Questions fréquentes

Quels formats de fichier OpenAI accepte-t-il pour le flux produit ?

Quatre : parquet, que la documentation préfère, idéalement avec une compression zstd, plus JSON Lines gzippé (jsonl.gz), CSV gzippé (csv.gz) et TSV gzippé (tsv.gz). Tous doivent être en UTF-8 et sont déposés par SFTP.

Parquet est-il obligatoire pour un flux produit OpenAI ?

Non, il est préféré. Un petit catalogue livré en jsonl.gz est parfaitement conforme ; parquet devient le choix raisonnable quand le catalogue est assez grand pour que les colonnes typées et la compression comptent.

Quelle taille peut atteindre un fichier de flux ?

La documentation recommande jusqu'à 500 000 articles par fragment et des fichiers de fragment sous environ 500 Mo. Découpez les catalogues plus grands en plusieurs fichiers aux noms stables et écrasez-les à chaque instantané.

Puis-je envoyer seulement les produits modifiés depuis la veille ?

Non. La documentation demande des instantanés complets à une cadence prévisible, au moins quotidienne. Un produit absent d'un instantané est conservé jusqu'à 14 jours, puis expire.

Pourquoi mes zéros initiaux disparaissent-ils dans le flux ?

Parce que l'outil d'export a typé la colonne comme un nombre. Les GTIN et les autres identifiants doivent être des chaînes dans tous les formats ; en CSV, l'approche la plus sûre est un outil d'écriture qui ne déduit jamais les types.

Mesurez le canal ChatGPT dès cette semaine

Gratuit pendant l'accès anticipé, le temps d'intégrer les premières boutiques. Laissez votre email et vous recevez le lien d'installation dès qu'une place s'ouvre.

Pas de newsletter. Un seul email avec le lien d'installation, rien d'autre.