更新
この回答は物事が今で優れているものの、依然として有効かつ有益であるビルトイン加算エンコーダをサポートするために2.2 / 2.3、以降Set
、Seq
、Map
、Date
、Timestamp
、とBigDecimal
。ケースクラスと通常のScala型のみで型を作成することに固執する場合は、暗黙のinだけで問題ありませんSQLImplicits
。
残念ながら、これを助けるために追加されたものはほとんどありません。検索@since 2.0.0
中Encoders.scala
またはSQLImplicits.scala
ほとんどのものがプリミティブ型(およびケースクラスのいくつかの調整)を行うことを見つけました。したがって、最初に言うこと:現在、カスタムクラスエンコーダーに対する実際の優れたサポートはありません。それが邪魔にならないうちに、私たちが現在自由に使えるものを考えると、次のことは、私たちが望むことができるほどうまくいくいくつかのトリックです。事前の免責事項として:これは完全に機能しないため、すべての制限を明確かつ事前に明らかにするために最善を尽くします。
正確には何が問題ですか
データセットを作成する場合、Sparkは "タイプTのJVMオブジェクトを内部のSpark SQL表現との間で変換するためのエンコーダーを必要とします。これは通常SparkSession
、からの暗黙によって自動的に作成されるか、静的メソッドを呼び出すことによって明示的に作成できますEncoders
"(上のドキュメントcreateDataset
から取得)。エンコーダは、フォームかかりますあなたがエンコードされているタイプです。最初の提案は(これらの暗黙的なエンコーダーを提供する)追加することであり、2番目の提案は、この一連のエンコーダー関連関数を使用して暗黙的なエンコーダーを明示的に渡すことです。Encoder[T]
T
import spark.implicits._
通常のクラスで使用できるエンコーダーがないため、
import spark.implicits._
class MyObj(val i: Int)
// ...
val d = spark.createDataset(Seq(new MyObj(1),new MyObj(2),new MyObj(3)))
次の暗黙的な関連コンパイル時エラーが表示されます。
データセットに保存されているタイプのエンコーダーが見つかりません。プリミティブ型(Int、Stringなど)および製品型(ケースクラス)は、sqlContext.implicits._のインポートによってサポートされます。他の型のシリアル化のサポートは、将来のリリースで追加されます
ただし、上記のエラーを取得するために使用したタイプを、拡張するいくつかのクラスでラップするProduct
と、エラーが混乱してランタイムに遅延するため、
import spark.implicits._
case class Wrap[T](unwrap: T)
class MyObj(val i: Int)
// ...
val d = spark.createDataset(Seq(Wrap(new MyObj(1)),Wrap(new MyObj(2)),Wrap(new MyObj(3))))
正常にコンパイルされますが、実行時に失敗します
java.lang.UnsupportedOperationException:MyObjのエンコーダーが見つかりません
これは、Sparkが暗黙的に作成するエンコーダーは、実際には(scala relfectionを介して)実行時にのみ作成されるためです。この場合、コンパイル時のすべてのSparkチェックは、最も外側のクラスが拡張することProduct
(すべてのケースクラスが行う)であり、実行時にまだ何をすべきかが分からないことだけを認識しますMyObj
(同じ問題がa Dataset[(Int,MyObj)]
-Sparkは、実行時にbarfを実行するまで待機しますMyObj
)。これらは、修正する必要がある緊急の中心的な問題です。
Product
実行時に常にクラッシュするにもかかわらず、コンパイルを拡張するいくつかのクラスと
- 入れ子にされた型のカスタムエンコーダに渡す方法はありません(私はスパークにちょうどのためのエンコーダ送りの手立てがない
MyObj
ことが、その後どのようにエンコードを知っていることなどWrap[MyObj]
かを(Int,MyObj)
)。
使うだけ kryo
誰もが提案する解決策は、kryo
エンコーダを使用することです。
import spark.implicits._
class MyObj(val i: Int)
implicit val myObjEncoder = org.apache.spark.sql.Encoders.kryo[MyObj]
// ...
val d = spark.createDataset(Seq(new MyObj(1),new MyObj(2),new MyObj(3)))
ただし、これは非常に手間がかかります。特に、コードがあらゆる種類のデータセットの操作、結合、グループ化などを行っている場合は、余分な暗黙の要素が大量に発生します。それで、これをすべて自動的に行う暗黙的なものを作成しないのはなぜですか?
import scala.reflect.ClassTag
implicit def kryoEncoder[A](implicit ct: ClassTag[A]) =
org.apache.spark.sql.Encoders.kryo[A](ct)
そして今、私は私がやりたいことがほとんど何でもできるようです(以下の例は自動的にインポートされるspark-shell
場所でspark.implicits._
は機能しません)
class MyObj(val i: Int)
val d1 = spark.createDataset(Seq(new MyObj(1),new MyObj(2),new MyObj(3)))
val d2 = d1.map(d => (d.i+1,d)).alias("d2") // mapping works fine and ..
val d3 = d1.map(d => (d.i, d)).alias("d3") // .. deals with the new type
val d4 = d2.joinWith(d3, $"d2._1" === $"d3._1") // Boom!
またはほとんど。問題は、kryo
Spark を使用すると、データセットのすべての行がフラットバイナリオブジェクトとして格納されることです。十分であるが、同様の操作のために、スパークは本当にこれらの列に分離することが必要です。またはのスキーマを調べると、バイナリ列が1つしかないことがわかります。map
filter
foreach
join
d2
d3
d2.printSchema
// root
// |-- value: binary (nullable = true)
タプルの部分的なソリューション
したがって、Scalaの暗黙のマジック(詳細は6.26.3オーバーロードの解決を参照)を使用して、少なくともタプルに対して可能な限りうまく機能し、既存の暗黙でうまく機能する一連の暗黙を作成できます。
import org.apache.spark.sql.{Encoder,Encoders}
import scala.reflect.ClassTag
import spark.implicits._ // we can still take advantage of all the old implicits
implicit def single[A](implicit c: ClassTag[A]): Encoder[A] = Encoders.kryo[A](c)
implicit def tuple2[A1, A2](
implicit e1: Encoder[A1],
e2: Encoder[A2]
): Encoder[(A1,A2)] = Encoders.tuple[A1,A2](e1, e2)
implicit def tuple3[A1, A2, A3](
implicit e1: Encoder[A1],
e2: Encoder[A2],
e3: Encoder[A3]
): Encoder[(A1,A2,A3)] = Encoders.tuple[A1,A2,A3](e1, e2, e3)
// ... you can keep making these
次に、これらの暗黙的要素を利用して、列の名前を変更しても、上記の例を機能させることができます
class MyObj(val i: Int)
val d1 = spark.createDataset(Seq(new MyObj(1),new MyObj(2),new MyObj(3)))
val d2 = d1.map(d => (d.i+1,d)).toDF("_1","_2").as[(Int,MyObj)].alias("d2")
val d3 = d1.map(d => (d.i ,d)).toDF("_1","_2").as[(Int,MyObj)].alias("d3")
val d4 = d2.joinWith(d3, $"d2._1" === $"d3._1")
私はまだ(予想タプル名を取得する方法を考え出したていない_1
、_2
彼らの名前を変更せずに、デフォルトでは、...) -他の誰かがこれで遊ぶしたい場合、これは名前がどこで"value"
導入されますと、これはどこタプルです名前は通常追加されます。ただし、重要な点は、これで素晴らしい構造化スキーマができたことです。
d4.printSchema
// root
// |-- _1: struct (nullable = false)
// | |-- _1: integer (nullable = true)
// | |-- _2: binary (nullable = true)
// |-- _2: struct (nullable = false)
// | |-- _1: integer (nullable = true)
// | |-- _2: binary (nullable = true)
つまり、要約すると、この回避策は次のとおりです。
- タプルの個別の列を取得できるようにします(これで、タプルを再び結合できるようになりました!)
- 私たちは再び暗黙に頼ることができます(そのため、あちこちに渡される必要はありません
kryo
)
- とほぼ完全に下位互換性があります
import spark.implicits._
(一部の名前の変更を伴います)
- んではない私たちが上で参加しましょう
kyro
連載バイナリ列、それらが持つかもしれフィールド上おろか
- いくつかのタプル列の名前を「値」に変更するという不愉快な副作用があります(必要に応じて、これを変換し
.toDF
、新しい列名を指定して、データセットに戻すことで元に戻すことができます-スキーマ名は結合によって保持されているようです、最も必要とされる場所)。
一般的なクラスの部分的なソリューション
これはあまり快適ではなく、良い解決策はありません。ただし、上記のタプルソリューションがあるので、より複雑なクラスをタプルに変換できるため、別の回答からの暗黙の変換ソリューションのほうが少し苦痛になるのではないかと思います。次に、データセットを作成した後、おそらくデータフレームアプローチを使用して列の名前を変更します。すべてがうまくいけば、クラスのフィールドで結合を実行できるようになったので、これは本当に改善です。フラットバイナリシリアkryo
ライザーを1つだけ使用した場合、それは不可能だったでしょう。
私はクラスがあります。ここでは、すべてのビットを行う例であるMyObj
型のフィールドを持ちInt
、java.util.UUID
とSet[String]
。1つ目は自分で処理します。2つ目は、kryo
として使用してシリアル化することができますが、として保存されている場合はより便利ですString
(UUID
sは通常、私が参加したいものなので)。3番目は本当にバイナリ列に属しています。
class MyObj(val i: Int, val u: java.util.UUID, val s: Set[String])
// alias for the type to convert to and from
type MyObjEncoded = (Int, String, Set[String])
// implicit conversions
implicit def toEncoded(o: MyObj): MyObjEncoded = (o.i, o.u.toString, o.s)
implicit def fromEncoded(e: MyObjEncoded): MyObj =
new MyObj(e._1, java.util.UUID.fromString(e._2), e._3)
今、私はこの機構を使用して素敵なスキーマを持つデータセットを作成することができます:
val d = spark.createDataset(Seq[MyObjEncoded](
new MyObj(1, java.util.UUID.randomUUID, Set("foo")),
new MyObj(2, java.util.UUID.randomUUID, Set("bar"))
)).toDF("i","u","s").as[MyObjEncoded]
そして、スキーマは私に正しい名前の列と私が結合できる最初の2つと両方を示しています。
d.printSchema
// root
// |-- i: integer (nullable = false)
// |-- u: string (nullable = true)
// |-- s: binary (nullable = true)
ExpressionEncoder
JSONシリアル化を使用してカスタムクラスを作成することは可能ですか?私の場合..私はタプルで逃げることができない、とkryoは私に、バイナリ列を与える