Il s'agit de la première implémentation de lecteur/graveur de format de fichier Parquet en PHP, basée sur les sources Thrift fournies par la Fondation Apache. De nombreuses parties du code et des concepts ont été portées depuis parquet-dotnet (voir https://github.com/elastacloud/parquet-dotnet et https://github.com/aloneguid/parquet-dotnet). Par conséquent, nos remerciements vont à Ivan Gavryliuk (https://github.com/aloneguid).
Ce package vous permet de lire et d'écrire des fichiers/flux Parquet sans utiliser d'extensions externes exotiques (sauf si vous souhaitez utiliser des méthodes de compression exotiques). Il a (presque ?) 100% de compatibilité de test avec parquet-dotnet, concernant la fonctionnalité de base, réalisée via PHPUnit.
Ce référentiel (et le package associé sur Packagist) est la suite officielle du projet jocoon/parquet
. En raison de diverses améliorations et corrections de bugs essentiels, ici dans codename/parquet
, l'utilisation de l'ancien package est fortement déconseillée.
Pour certaines parties de ce package, de nouveaux modèles ont dû être inventés car je n'ai trouvé aucune implémentation répondant aux exigences. Dans la plupart des cas, aucune implémentation n’était disponible.
Quelques faits marquants :
J'ai commencé à développer cette bibliothèque parce qu'il n'y avait tout simplement aucune implémentation pour PHP.
Dans mon entreprise, nous avions besoin d'une solution rapide pour archiver d'énormes quantités de données d'une base de données dans un format toujours interrogeable, extensible du point de vue du schéma et tolérant aux pannes. Nous avons commencé à tester des « migrations » en direct via AWS DMS vers S3, qui ont fini par planter sur certaines quantités de données, en raison de limitations de mémoire. Et c'était tout simplement trop orienté base de données, à côté du fait qu'il est facile de supprimer accidentellement des données des chargements précédents. Comme nous avons une architecture fortement orientée SDS et indépendante de la plate-forme, ce n'est pas ma manière préférée de stocker des données sous forme de clone 1:1 de base de données, comme un dump. Au lieu de cela, je voulais avoir la possibilité de stocker des données, structurées de manière dynamique, comme je le souhaitais, de la même manière que DMS les exportait vers S3. Finalement, le projet est mort pour les raisons mentionnées ci-dessus.
Mais je n'arrivais pas à me sortir le format du parquet de la tête.
Le résultat de recherche TOP 1 (https://stackoverflow.com/questions/44780419/how-to-create-orc-or-parquet-files-from-php-code) semblait prometteur et il ne faudrait pas beaucoup d'efforts pour avoir une implémentation PHP - mais en fait, cela en a pris (environ 2 semaines de travail non consécutif). Pour moi, en tant que développeur PHP et C#, parquet-dotnet était un point de départ parfait - pas seulement parce que les benchmarks sont tout simplement trop convaincants. Mais je m'attendais à ce que l'implémentation PHP n'atteigne pas ces niveaux de performances, car il s'agit d'une implémentation initiale, montrant le principe. Et en plus, personne ne l’avait fait auparavant.
Comme PHP a une part énorme dans les projets liés au Web, il s'agit d'un MUST-HAVE à une époque de besoin croissant d'applications et de scénarios Big Data. Pour ma motivation personnelle, c'est une façon de montrer que PHP a (physiquement, virtuellement ?) dépassé sa réputation de « langage de script ». Je pense – ou du moins j’espère – qu’il y a des gens qui bénéficieront de ce paquet et du message qu’il véhicule. Pas seulement des objets d’occasion. Jeu de mots intentionnel.
Vous aurez besoin de plusieurs extensions pour utiliser pleinement cette bibliothèque.
Cette bibliothèque a été initialement développée pour/en utilisant PHP 7.3, mais elle devrait fonctionner sur PHP > 7 et sera testée sur 8, lors de sa sortie. Pour le moment, les tests sur PHP 7.1 et 7.2 échoueront en raison de certains problèmes DateTime. Je vais y jeter un œil. Les tests réussissent entièrement sur PHP 7.3 et 7.4. Au moment de la rédaction de cet article, la version 8.0.0 RC2 fonctionne également bien.
Cette bibliothèque dépend fortement de
Depuis la version 0.2, je suis également passé à une approche indépendante de l'implémentation consistant à utiliser des lecteurs et des rédacteurs. Nous avons maintenant affaire à des implémentations de BinaryReader(Interface) et BinaryWriter(Interface) qui font abstraction du mécanisme sous-jacent. J'ai remarqué que mdurrant/php-binary-reader est tout simplement trop lent. Je ne voulais tout simplement pas tout refactoriser juste pour tester les pouvoirs de lecture de Nelexa. Au lieu de cela, j'ai créé les deux interfaces mentionnées ci-dessus pour extraire divers packages fournissant une lecture/écriture binaire. Cela conduit finalement à une manière optimale de tester/évaluer différentes implémentations - et également de mélanger, par exemple en utilisant le package de Wapmorgan pour la lecture tout en utilisant celui de Nelexa pour l'écriture.
Depuis la version 0.2.1, j'ai moi-même réalisé les implémentations du lecteur/enregistreur binaire, car aucune implémentation ne répondait aux exigences de performances. Spécialement pour l'écriture, cette implémentation ultra-légère offre trois fois* les performances du tampon de Nelexa.
* prévu, j'adore ce mot
Packages alternatifs de lecture/écriture binaire tiers concernés :
Installez ce package via composer, par exemple
composer require codename/parquet
Le Dockerfile inclus vous donne une idée de la configuration système requise. La chose la plus importante à faire est de cloner et d'installer php-ext-snappy . Au moment de la rédaction de cet article, il n'a pas encore été publié par PECL .
...
# NOTE: this is a dockerfile snippet. Bare metal machines will be a little bit different
RUN git clone --recursive --depth=1 https://github.com/kjdev/php-ext-snappy.git
&& cd php-ext-snappy
&& phpize
&& ./configure
&& make
&& make install
&& docker-php-ext-enable snappy
&& ls -lna
...
Remarque : php-ext-snappy est un peu bizarre à compiler et à installer sous Windows, il ne s'agit donc que d'une brève information pour l'installation et l'utilisation sur les systèmes basés sur Linux. Tant que vous n'avez pas besoin de la compression rapide pour la lecture ou l'écriture, vous pouvez utiliser php-parquet sans le compiler vous-même.
J'ai trouvé ParquetViewer (https://github.com/mukunku/ParquetViewer) de Mukunku comme étant un excellent moyen d'examiner les données à lire ou de vérifier certaines choses sur un ordinateur de bureau Windows. Au moins, cela aide à comprendre certains mécanismes, car cela aide plus ou moins visuellement en affichant simplement les données sous forme de tableau.
L'utilisation est presque la même que celle de parquet-dotnet. Veuillez noter que nous n'avons pas using ( ... ) { }
, comme en C#. Vous devez donc vous assurer de fermer/éliminer vous-même les ressources inutilisées ou de laisser le GC de PHP les gérer automatiquement par son algorithme de refcounting. (C'est la raison pour laquelle je n'utilise pas de destructeurs comme le fait parquet-dotnet.)
Comme le système de types de PHP est complètement différent de celui de C#, nous devons apporter quelques ajouts sur la façon de gérer certains types de données. Par exemple, un entier PHP est nullable, d'une manière ou d'une autre. Un int en C# ne l’est pas. C'est un point sur lequel je ne sais toujours pas comment le gérer. Pour l'instant, j'ai défini int (PHP integer ) pour qu'il soit nullable - parquet-dotnet le fait comme non nullable. Vous pouvez toujours ajuster ce comportement en définissant manuellement ->hasNulls = true;
sur votre DataField. De plus, php-parquet utilise une double manière de déterminer un type. En PHP, une primitive a son propre type (entier, bool, float/double, etc.). Pour les instances de classe (en particulier DateTime/DateTimeImmutable), le type renvoyé par get_type() est toujours un objet. C'est la raison pour laquelle une deuxième propriété pour les DataTypeHandlers existe pour le faire correspondre, le déterminer et le traiter : phpClass.
Au moment de la rédaction de cet article, tous les types de données pris en charge par parquet-dotnet ne sont pas également pris en charge ici. Fe, j'ai ignoré Int16, SignedByte et quelques autres, mais cela ne devrait pas être trop compliqué d'étendre la compatibilité binaire complète.
Pour le moment, cette bibliothèque sert les fonctionnalités de base nécessaires à la lecture et à l'écriture de fichiers/flux Parquet. Il n'inclut pas les tables, lignes, énumérateurs/assistants de parquet-dotnet de l'espace de noms C# Parquet.Data.Rows
.
use codename parquet ParquetReader ;
// open file stream (in this example for reading only)
$ fileStream = fopen ( __DIR__ . ' /test.parquet ' , ' r ' );
// open parquet file reader
$ parquetReader = new ParquetReader ( $ fileStream );
// Print custom metadata or do other stuff with it
print_r ( $ parquetReader -> getCustomMetadata ());
// get file schema (available straight after opening parquet reader)
// however, get only data fields as only they contain data values
$ dataFields = $ parquetReader -> schema -> GetDataFields ();
// enumerate through row groups in this file
for ( $ i = 0 ; $ i < $ parquetReader -> getRowGroupCount (); $ i ++)
{
// create row group reader
$ groupReader = $ parquetReader -> OpenRowGroupReader ( $ i );
// read all columns inside each row group (you have an option to read only
// required columns if you need to.
$ columns = [];
foreach ( $ dataFields as $ field ) {
$ columns [] = $ groupReader -> ReadColumn ( $ field );
}
// get first column, for instance
$ firstColumn = $ columns [ 0 ];
// $data member, accessible through ->getData() contains an array of column data
$ data = $ firstColumn -> getData ();
// Print data or do other stuff with it
print_r ( $ data );
}
use codename parquet ParquetWriter ;
use codename parquet data Schema ;
use codename parquet data DataField ;
use codename parquet data DataColumn ;
//create data columns with schema metadata and the data you need
$ idColumn = new DataColumn (
DataField:: createFromType ( ' id ' , ' integer ' ), // NOTE: this is a little bit different to C# due to the type system of PHP
[ 1 , 2 ]
);
$ cityColumn = new DataColumn (
DataField:: createFromType ( ' city ' , ' string ' ),
[ " London " , " Derby " ]
);
// create file schema
$ schema = new Schema ([ $ idColumn -> getField (), $ cityColumn -> getField ()]);
// create file handle with w+ flag, to create a new file - if it doesn't exist yet - or truncate, if it exists
$ fileStream = fopen ( __DIR__ . ' /test.parquet ' , ' w+ ' );
$ parquetWriter = new ParquetWriter ( $ schema , $ fileStream );
// optional, write custom metadata
$ metadata = [ ' author ' => ' santa ' , ' date ' => ' 2020-01-01 ' ];
$ parquetWriter -> setCustomMetadata ( $ metadata );
// create a new row group in the file
$ groupWriter = $ parquetWriter -> CreateRowGroup ();
$ groupWriter -> WriteColumn ( $ idColumn );
$ groupWriter -> WriteColumn ( $ cityColumn );
// As we have no 'using' in PHP, I implemented finish() methods
// for ParquetWriter and ParquetRowGroupWriter
$ groupWriter -> finish (); // finish inner writer(s)
$ parquetWriter -> finish (); // finish the parquet writer last
Vous pouvez également utiliser ParquetDataIterator
et ParquetDataWriter
pour travailler même avec des schémas très complexes (par exemple des données imbriquées). Bien qu'expérimentaux au moment de la rédaction, les tests unitaires et d'intégration indiquent que nous avons une compatibilité à 100 % avec Spark, car la plupart des autres implémentations de Parquet manquent de certaines fonctionnalités ou cas d'imbrication super complexe.
ParquetDataIterator
et ParquetDataWriter
exploitent le « dynamisme » du système de types PHP et des tableaux (associatifs) - qui ne s'arrêtent que lors de l'utilisation complète d'entiers 64 bits non signés - ceux-ci ne peuvent être que partiellement pris en charge en raison de la nature de PHP.
ParquetDataIterator
itère automatiquement sur tous les groupes de lignes et pages de données, sur toutes les colonnes du fichier parquet de la manière la plus efficace possible en termes de mémoire. Cela signifie qu'il ne charge pas tous les ensembles de données en mémoire, mais le fait par page de données/par groupe de lignes.
Sous le capot, il exploite la fonctionnalité de DataColumnsToArrayConverter
qui fait finalement tout le « gros du travail » concernant les niveaux de définition et de répétition .
use codename parquet helper ParquetDataIterator ;
$ iterateMe = ParquetDataIterator:: fromFile ( ' your-parquet-file.parquet ' );
foreach ( $ iterateMe as $ dataset ) {
// $dataset is an associative array
// and already combines data of all columns
// back to a row-like structure
}
Vice-versa, ParquetDataWriter
vous permet d'écrire un fichier Parquet (en mémoire ou sur disque) en transmettant des données de tableau associatif PHP, soit une à la fois, soit par lots. En interne, il utilise ArrayToDataColumnsConverter
pour produire des données, des dictionnaires, des niveaux de définition et de répétition.
use codename parquet helper ParquetDataWriter ;
$ schema = new Schema ([
DataField:: createFromType ( ' id ' , ' integer ' ),
DataField:: createFromType ( ' name ' , ' string ' ),
]);
$ handle = fopen ( ' sample.parquet ' , ' r+ ' );
$ dataWriter = new ParquetDataWriter ( $ handle , $ schema );
// add two records at once
$ dataToWrite = [
[ ' id ' => 1 , ' name ' => ' abc ' ],
[ ' id ' => 2 , ' name ' => ' def ' ],
];
$ dataWriter -> putBatch ( $ dataToWrite );
// we add a third, single one
$ dataWriter -> put ([ ' id ' => 3 , ' name ' => ' ghi ' ]);
$ dataWriter -> finish (); // Don't forget to finish at some point.
fclose ( $ handle ); // You may close the handle, if you have to.
php-parquet prend en charge toutes les capacités d'imbrication du format Parquet. Vous remarquerez peut-être qu'en fonction des types de champs que vous imbriquez, vous « perdrez » d'une manière ou d'une autre les noms de clés. C'est par conception :
De manière générale, voici les équivalents PHP des Types Logiques du Format Parquet :
Parquet | PHP | JSON | Note |
---|---|---|---|
Champ de données | primitif | primitif | fe chaîne, entier, etc. |
ChampListe | tableau | tableau [] | le type d'élément peut être une primitive ou même une liste, une structure ou une carte |
ChampStruct | tableau associatif | objet {} | Clés de l'assoc. array sont les noms de champs à l'intérieur du StructField |
ChampCarte | tableau associatif | objet {} | Simplifié : array_keys($data['someField']) et array_values($data['someField']) , mais pour chaque ligne |
Le format est compatible avec les données d'exportation JSON générées par Spark configurées avec spark.conf.set("spark.sql.jsonGenerator.ignoreNullFields", False)
. Par défaut, Spark supprime complètement les valeurs null
lors de l'exportation vers JSON.
Veuillez noter : tous ces types de champs peuvent être rendus nullables ou non nullables/obligatoires à chaque niveau d'imbrication (affecte les niveaux de définition). Certaines valeurs nulles sont utilisées par exemple pour représenter des listes vides et les distinguer d'une valeur null
pour une liste.
use codename parquet helper ParquetDataIterator ;
use codename parquet helper ParquetDataWriter ;
$ schema = new Schema ([
DataField:: createFromType ( ' id ' , ' integer ' ),
new MapField (
' aMapField ' ,
DataField:: createFromType ( ' someKey ' , ' string ' ),
StructField:: createWithFieldArray (
' aStructField '
[
DataField:: createFromType ( ' anInteger ' , ' integer ' ),
DataField:: createFromType ( ' aString ' , ' string ' ),
]
)
),
StructField:: createWithFieldArray (
' rootLevelStructField '
[
DataField:: createFromType ( ' anotherInteger ' , ' integer ' ),
DataField:: createFromType ( ' anotherString ' , ' string ' ),
]
),
new ListField (
' aListField ' ,
DataField:: createFromType ( ' someInteger ' , ' integer ' ),
)
]);
$ handle = fopen ( ' complex.parquet ' , ' r+ ' );
$ dataWriter = new ParquetDataWriter ( $ handle , $ schema );
$ dataToWrite = [
// This is a single dataset:
[
' id ' => 1 ,
' aMapField ' => [
' key1 ' => [ ' anInteger ' => 123 , ' aString ' => ' abc ' ],
' key2 ' => [ ' anInteger ' => 456 , ' aString ' => ' def ' ],
],
' rootLevelStructField ' => [
' anotherInteger ' => 7 ,
' anotherString ' => ' in paradise '
],
' aListField ' => [ 1 , 2 , 3 ]
],
// ... add more datasets as you wish.
];
$ dataWriter -> putBatch ( $ dataToWrite );
$ dataWriter -> finish ();
$ iterateMe = ParquetDataIterator:: fromFile ( ' complex.parquet ' );
// f.e. write back into a full-blown php array:
$ readData = [];
foreach ( $ iterateMe as $ dataset ) {
$ readData [] = $ dataset ;
}
// and now compare this to the original data supplied.
// manually, by print_r, var_dump, assertions, comparisons or whatever you like.
Ce package fournit également le même benchmark que parquet-dotnet. Voici les résultats sur ma machine :
Parquet.Net (.NET Core 2.1) | php-parquet (bare metal 7.3) | php-parquet (dockerisé* 7.3) | Parquet rapide (python) | parquet-mr (Java) | |
---|---|---|---|---|---|
Lire | 255 ms | 1'090ms | 1'244ms | 154 ms** | non testé |
Écrire (non compressé) | 209 ms | 1'272ms | 1'392ms | 237 ms** | non testé |
Écrire (gzip) | 1'945ms | 3'314ms | 3'695ms | 1'737ms** | non testé |
En général, ces tests ont été réalisés avec le niveau de compression gzip 6 pour php-parquet. Il diminuera environ de moitié avec 1 (compression minimale) et presque doublera à 9 (compression maximale). Notez que ce dernier peut ne pas produire la plus petite taille de fichier, mais toujours le temps de compression le plus long.
Comme il s’agit d’un portage partiel d’un package issu d’un langage de programmation complètement différent, le style de programmation est à peu près un pur gâchis. J'ai décidé de conserver la majeure partie du boîtier (par exemple $writer->CreateRowGroup() au lieu de ->createRowGroup()) pour conserver une certaine « compatibilité visuelle » avec parquet-dotnet. À mon avis, c’est au moins un état souhaitable, car il facilite grandement la comparaison et l’extension au cours des premières étapes de développement.
Certaines parties de code et concepts ont été portés depuis C#/.NET, voir :
php-parquet est sous licence MIT. Voir dossier LICENCE.
N'hésitez pas à faire un PR, si vous le souhaitez. Comme il s'agit d'un projet OSS de temps libre, les contributions aideront tous les utilisateurs de ce package, y compris vous-même. Veuillez faire preuve d'une pincée de bon sens lors de la création de PR et/ou de problèmes, il n'y a pas de modèle.