これは、Apache Foundation が提供する Thrift ソースに基づいた、PHP での最初の寄木細工ファイル形式リーダー/ライター実装です。コードと概念の大部分は parquet-dotnet から移植されています (https://github.com/elastacloud/parquet-dotnet および https://github.com/aloneguid/parquet-dotnet を参照)。したがって、Ivan Gavryliuk (https://github.com/aloneguid) に感謝します。
このパッケージを使用すると、特殊な外部拡張機能を使用せずに (特殊な圧縮方法を使用する場合を除いて) Parquet ファイル/ストリームの読み取りと書き込みが可能になります。コア機能に関しては、PHPUnit を介して実行され、parquet-dotnet と (ほぼ?) 100% のテスト互換性があります。
このリポジトリ (および Packagist 上の関連パッケージ) は、 jocoon/parquet
の公式プロジェクトの継続です。さまざまな改善と重要なバグ修正のため、 codename/parquet
では、レガシー パッケージの使用は強くお勧めできません。
このパッケージの一部では、要件を満たす実装が見つからなかったため、いくつかの新しいパターンを考案する必要がありました。ほとんどの場合、利用可能な実装はまったくありませんでした。
いくつかのハイライト:
私がこのライブラリの開発を始めたのは、単純に PHP 用の実装がなかったからです。
私の会社では、データベースから大量のデータを、クエリ可能で、スキーマの観点から拡張可能でフォールト トレラントな形式でアーカイブするための迅速なソリューションを必要としていました。私たちは AWS DMS を介して S3 へのライブ「移行」のテストを開始しましたが、メモリの制限により、特定の量のデータでクラッシュすることになりました。また、以前のロードからのデータを誤って削除しやすいという事実に加えて、単純に DB 指向が強すぎました。当社では SDS 指向が高く、プラットフォームに依存しないアーキテクチャを採用しているため、ダンプのようなデータベースの 1:1 クローンとしてデータを保存する方法は私の好みではありません。代わりに、DMS が S3 にエクスポートしていたのと同じ方法で、動的に構造化されたデータを保存できる機能が必要でした。最終的に、プロジェクトは上記の理由により中止されました。
しかし、寄木細工のフォーマットが頭から離れませんでした。
トップ 1 の検索結果 (https://stackoverflow.com/questions/44780419/how-to-create-orc-or-parquet-files-from-php-code) は、それほど多くの労力を必要としないことが期待できるように見えました。 PHP の実装 - しかし実際には、ある程度の時間がかかりました (連続しない作業で約 2 週間)。 PHP および C# 開発者である私にとって、parquet-dotnet は完璧な出発点でした。それは単にベンチマークが魅力的すぎるという理由だけではありません。しかし、これは初期の実装であり、原理を示しているため、PHP 実装ではこれらのパフォーマンス レベルを満たさないだろうと予想していました。さらに、これまで誰もそれをやったことがありませんでした。
PHP は Web 関連プロジェクトで大きなシェアを占めているため、ビッグ データ アプリケーションやシナリオのニーズが高まる時代には必須の機能となります。私の個人的な動機としては、これは PHP が「スクリプト言語」としての評判を (物理的に、仮想的に?) 超えていることを示す方法です。私は、このパッケージとそれが伝えるメッセージから恩恵を受ける人が世の中にいると思います、あるいは少なくともそう願っています。スリフトオブジェクトだけではありません。ダジャレを意図したものです。
このライブラリを最大限に使用するには、いくつかの拡張機能が必要です。
このライブラリは元々 PHP 7.3 を対象として開発されましたが、PHP 7 以降でも動作するはずであり、リリース時には PHP 8 でテストされる予定です。現時点では、PHP 7.1 および 7.2 でのテストは、DateTime の問題が原因で失敗します。見てみましょう。テストは PHP 7.3 および 7.4 で完全に合格します。この記事の執筆時点では、8.0.0 RC2 も良好に動作しています。
このライブラリは以下に大きく依存しています
v0.2 では、リーダーとライターを使用する実装に依存しないアプローチにも切り替えました。ここでは、基礎となるメカニズムを抽象化する BinaryReader(Interface) および BinaryWriter(Interface) の実装を扱います。 mdurrant/php-binary-readerが遅すぎることに気付きました。 Nelexa の読み取り能力を試すためだけにすべてをリファクタリングしたくありませんでした。代わりに、バイナリの読み取り/書き込みを行うさまざまなパッケージを抽象化するために、上記の 2 つのインターフェイスを作成しました。これにより、最終的に、さまざまな実装をテスト/ベンチマークするための最適な方法が得られ、また、書き込みには Nelexa のパッケージを使用しながら読み取りには wapmorgan のパッケージを使用するなど、混合も可能になります。
v0.2.1 では、パフォーマンス要件を満たす実装がなかったため、バイナリ リーダー/ライターの実装を自分で行いました。特に書き込みの場合、この超軽量実装は Nelexa のバッファの 3 倍* のパフォーマンスを実現します。
* 意図的に、私はこの言葉が大好きです
範囲内の代替サードパーティ製バイナリ読み取り/書き込みパッケージ:
このパッケージをcomposer経由でインストールします。例:
composer require codename/parquet
付属のDockerfile を参照すると、必要なシステム要件がわかります。実行する最も重要なことは、 php-ext-snappy のクローンを作成してインストールすることです。この記事の執筆時点では、 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
...
注意してください: php-ext-snappy は Windows でのコンパイルとインストールが少し独特なので、これは Linux ベースのシステムでのインストールと使用方法に関する簡単な情報にすぎません。読み取りまたは書き込みに高速な圧縮が必要ない限り、php-parquet を自分でコンパイルせずに使用できます。
Mukunku の ParquetViewer (https://github.com/mukunku/ParquetViewer) は、読み取るデータを調べたり、Windows デスクトップ マシン上で何かを検証したりするのに最適な方法であることがわかりました。データを表として表示するだけで多かれ少なかれ視覚的に役立つため、少なくとも、特定のメカニズムを理解するのに役立ちます。
使い方はparquet-dotnetとほぼ同じです。 C# のようなusing ( ... ) { }
がないことに注意してください。したがって、未使用のリソースを自分で閉じる/破棄するか、PHP の GC が参照カウント アルゴリズムによって自動的に処理できるようにする必要があります。 (これが、parquet-dotnet のようなデストラクターを使用しない理由です。)
PHP の型システムは C# とはまったく異なるため、特定のデータ型の処理方法についていくつか追加する必要があります。たとえば、PHP 整数はどういうわけか null 可能です。 C# のintはそうではありません。この点はまだどう対処すればいいのか迷っているところです。現時点では、 int (PHP integer ) を null 可能に設定しました。parquet-dotnet はこれを null 不可として実行しています。 ->hasNulls = true;
を手動で設定することで、いつでもこの動作を調整できます。データフィールド上で。さらに、php-parquet は型を決定する二重の方法を使用します。 PHP では、プリミティブには独自の型 (integer、bool、float/double など) があります。クラス インスタンス (特に DateTime/DateTimeImmutable) の場合、get_type() によって返される型は常に object です。これが、DataTypeHandler の 2 番目のプロパティである phpClass が、それを照合、決定、処理するために存在する理由です。
執筆時点では、parquet-dotnet でサポートされているすべての DataType がここでもサポートされているわけではありません。ここでは Int16、SignedByte などを省略しましたが、完全なバイナリ互換性まで拡張するのはそれほど複雑ではありません。
現時点では、このライブラリは寄木細工のファイル/ストリームの読み取りと書き込みに必要なコア機能を提供します。これには、C# 名前空間Parquet.Data.Rows
の parquet-dotnet のテーブル、行、列挙子/ヘルパーは含まれません。
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
ParquetDataIterator
とParquetDataWriter
使用して、非常に複雑なスキーマ (入れ子になったデータ) を扱うこともできます。執筆時点では実験的ですが、他の Parquet 実装のほとんどには特定の機能や超複雑なネストのケースが欠けているため、単体テストおよび統合テストでは Spark と 100% の互換性があることが示されています。
ParquetDataIterator
とParquetDataWriter
、PHP の型システムと (連想) 配列の「動的性」を利用します。これは、符号なし 64 ビット整数を完全に使用する場合にのみ停止します。これらは、PHP の性質上、部分的にのみサポートされます。
ParquetDataIterator
可能な限り最もメモリ効率の高い方法で、すべての行グループとデータ ページ、および寄木細工ファイルのすべての列を自動的に繰り返します。つまり、すべてのデータセットをメモリにロードするのではなく、データページごと/行グループごとにロードします。
内部では、 DataColumnsToArrayConverter
の機能を活用し、最終的にはDefinition レベルと繰り返しレベルに関するすべての「重労働」を実行します。
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
}
逆に、 ParquetDataWriter
使用すると、PHP 連想配列データを一度に 1 つずつ、またはバッチで渡すことによって、Parquet ファイル (メモリ内またはディスク上) に書き込むことができます。内部的には、 ArrayToDataColumnsConverter
使用して、データ、辞書、定義、繰り返しレベルを生成します。
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 は、 Parquet 形式の完全なネスト機能をサポートします。ネストしているフィールドの種類によっては、何らかの理由でキー名が「失われる」ことに気づくかもしれません。これは仕様によるものです。
一般に、PHP での Parquet 形式の論理型に相当するものは次のとおりです。
寄木細工 | PHP | JSON | 注記 |
---|---|---|---|
データフィールド | 原生的 | 原生的 | fe 文字列、整数など。 |
リストフィールド | 配列 | 配列[] | 要素タイプはプリミティブ、またはリスト、構造体、またはマップにすることもできます |
構造体フィールド | 連想配列 | 物体{} | アソシエーションのキー。配列は StructField 内のフィールド名です。 |
マップフィールド | 連想配列 | 物体{} | 簡略化: array_keys($data['someField']) およびarray_values($data['someField']) 、ただし行ごと |
この形式はspark.conf.set("spark.sql.jsonGenerator.ignoreNullFields", False)
で構成された Spark によって生成された JSON エクスポート データと互換性があります。デフォルトでは、Spark は JSON にエクスポートするときにnull
値を完全に削除します。
注:これらのフィールド タイプはすべて、すべてのネスト レベルで NULL 可能または非 NULL 可能/必須にすることができます(定義レベルに影響します)。一部の NULL 可能性は、空のリストを表し、リストのnull
値と区別するために使用されます。
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.
このパッケージは、parquet-dotnet と同じベンチマークも提供します。これらは私のマシンでの結果です:
Parquet.Net (.NET Core 2.1) | php-parquet (ベアメタル 7.3) | php-parquet (dockerized* 7.3) | ファストパーケット (パイソン) | 寄木細工-mr (Java) | |
---|---|---|---|---|---|
読む | 255ミリ秒 | 1'090ミリ秒 | 1分244ミリ秒 | 154ミリ秒** | 未テスト |
書き込み(非圧縮) | 209ミリ秒 | 1'272ミリ秒 | 1'392ミリ秒 | 237ミリ秒** | 未テスト |
書き込み (gzip) | 1'945ミリ秒 | 3分314ミリ秒 | 3'695ミリ秒 | 1'737ms** | 未テスト |
一般に、これらのテストは、php-parquet の gzip 圧縮レベル 6 を使用して実行されました。 1 (最小圧縮) ではおよそ半分になり、9 (最大圧縮) ではほぼ 2 倍になります。後者の場合、ファイル サイズが最小になるわけではありませんが、圧縮時間は常に最長になることに注意してください。
これはまったく別のプログラミング言語からのパッケージの部分移植であるため、プログラミング スタイルはまったくの混乱です。私は、parquet-dotnet に対する特定の「視覚的な互換性」を保つために、大文字と小文字のほとんどを保持することにしました (例: ->createRowGroup() の代わりに $writer->CreateRowGroup())。少なくとも、これは私の観点からは望ましい状態です。開発の初期段階での比較と拡張がはるかに簡単になるからです。
一部のコード部分と概念は C#/.NET から移植されています。以下を参照してください。
php-parquet は MIT ライセンスに基づいてライセンスされています。ファイル「ライセンス」を参照してください。
ご希望があれば、ぜひPRしてください。これは暇な OSS プロジェクトであるため、貢献はあなた自身を含むこのパッケージのすべてのユーザーに役立ちます。 PR や問題を作成するときは、少し常識を適用してください。テンプレートはありません。