カスタムオブジェクトをデータセットに保存する方法


149

Sparkデータセットの紹介によると:

Spark 2.0を楽しみにして、特にデータセットにいくつかのエキサイティングな改善を計画します。...カスタムエンコーダー-現在、さまざまなタイプのエンコーダーを自動生成していますが、カスタムオブジェクトのAPIを開きたいと考えています。

カスタムタイプを保存しようとするとDataset、次のようなエラーが発生します:

データセットに保存されているタイプのエンコーダーが見つかりません。プリミティブ型(Int、Stringなど)および製品型(ケースクラス)は、sqlContext.implicits._のインポートによってサポートされます。他の型のシリアル化のサポートは、将来のリリースで追加されます

または:

Java.lang.UnsupportedOperationException:...のエンコーダーが見つかりません

既存の回避策はありますか?


この質問は、コミュニティWiki回答のエントリポイントとしてのみ存在することに注意してください。質問と回答の両方を自由に更新/改善してください。

回答:


240

更新

この回答は物事が今で優れているものの、依然として有効かつ有益であるビルトイン加算エンコーダをサポートするために2.2 / 2.3、以降SetSeqMapDateTimestamp、とBigDecimal。ケースクラスと通常のScala型のみで型を作成することに固執する場合は、暗黙のinだけで問題ありませんSQLImplicits


残念ながら、これを助けるために追加されたものはほとんどありません。検索@since 2.0.0Encoders.scalaまたはSQLImplicits.scalaほとんどのものがプリミティブ型(およびケースクラスのいくつかの調整)を行うことを見つけました。したがって、最初に言うこと:現在、カスタムクラスエンコーダーに対する実際の優れたサポートはありません。それが邪魔にならないうちに、私たちが現在自由に使えるものを考えると、次のことは、私たちが望むことができるほどうまくいくいくつかのトリックです。事前の免責事項として:これは完全に機能しないため、すべての制限を明確かつ事前に明らかにするために最善を尽くします。

正確には何が問題ですか

データセットを作成する場合、Sparkは "タイプTのJVMオブジェクトを内部のSpark SQL表現との間で変換するためのエンコーダーを必要とします。これは通常SparkSession、からの暗黙によって自動的に作成されるか、静的メソッドを呼び出すことによって明示的に作成できますEncoders"(上のドキュメントcreateDatasetから取得)。エンコーダは、フォームかかりますあなたがエンコードされているタイプです。最初の提案は(これらの暗黙的なエンコーダーを提供する)追加することであり、2番目の提案は、この一連のエンコーダー関連関数を使用して暗黙的なエンコーダーを明示的に渡すことです。Encoder[T]Timport 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!

またはほとんど。問題は、kryoSpark を使用すると、データセットのすべての行がフラットバイナリオブジェクトとして格納されることです。十分であるが、同様の操作のために、スパークは本当にこれらの列に分離することが必要です。またはのスキーマを調べると、バイナリ列が1つしかないことがわかります。mapfilterforeachjoind2d3

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型のフィールドを持ちIntjava.util.UUIDSet[String]。1つ目は自分で処理します。2つ目は、kryoとして使用してシリアル化することができますが、として保存されている場合はより便利ですStringUUIDsは通常、私が参加したいものなので)。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)

ExpressionEncoderJSONシリアル化を使用してカスタムクラスを作成することは可能ですか?私の場合..私はタプルで逃げることができない、とkryoは私に、バイナリ列を与える
アレクセイSvyatkovskiy

1
@AlexeyS私はそうは思いません。しかし、なぜそれが必要なのでしょうか。なぜ私が提案する最後の解決策を回避できないのですか?あなたはJSONでデータを置くことができる場合は、フィールドを抽出し、ケースクラスでそれらを置くことができる必要があります...
アレック

1
残念ながら、この回答の要点は、機能する解決策がないということです。
baol 2017年

@baolソート。しかし、Sparkの処理がいかに難しいか覚えておいてください。Scalaの型システムは、フィールドを再帰的に通過するエンコーダーを「導出する」には十分強力ではありません。率直に言って、これについて注釈マクロを作成した人がいないことに驚いています。自然な(しかし難しい)解決策のようです。
アレック2017年

1
@combinatorist私の理解では、データセットとデータフレーム(エンコーダーを必要としないため、RDDではありません)はパフォーマンスの観点からは同等です。データセットの型安全性を過小評価しないでください!Sparkが大量のリフレクションやキャストなどを内部で使用しているからといって、公開されているインターフェイスのタイプセーフについて気にする必要がないわけではありません。ただし、内部でDataframeを使用する独自のデータセットベースのタイプセーフ関数を作成することについて、気分が良くなります。
アレック

32
  1. 汎用エンコーダーの使用。

    今のところ利用可能な2つの一般的なエンコーダがありますkryoし、javaSerialization後者が明示的と記載されています。

    非常に非効率的であり、最後の手段としてのみ使用する必要があります。

    次のクラスを想定

    class Bar(i: Int) {
      override def toString = s"bar $i"
      def bar = i
    }

    これらのエンコーダーを使用するには、暗黙のエンコーダーを追加します。

    object BarEncoders {
      implicit def barEncoder: org.apache.spark.sql.Encoder[Bar] = 
      org.apache.spark.sql.Encoders.kryo[Bar]
    }

    これは次のように一緒に使用できます。

    object Main {
      def main(args: Array[String]) {
        val sc = new SparkContext("local",  "test", new SparkConf())
        val sqlContext = new SQLContext(sc)
        import sqlContext.implicits._
        import BarEncoders._
    
        val ds = Seq(new Bar(1)).toDS
        ds.show
    
        sc.stop()
      }
    }

    オブジェクトをbinary列として保存するため、変換するDataFrameと次のスキーマが得られます。

    root
     |-- value: binary (nullable = true)

    kryo特定のフィールドのエンコーダーを使用してタプルをエンコードすることも可能です。

    val longBarEncoder = Encoders.tuple(Encoders.scalaLong, Encoders.kryo[Bar])
    
    spark.createDataset(Seq((1L, new Bar(1))))(longBarEncoder)
    // org.apache.spark.sql.Dataset[(Long, Bar)] = [_1: bigint, _2: binary]

    ここでは暗黙のエンコーダーに依存していませんが、エンコーダーを明示的に渡しており、toDSメソッドでは機能しない可能性が高いことに注意してください。

  2. 暗黙的な変換を使用する:

    エンコード可能な表現とカスタムクラス間の暗黙的な変換を提供します。次に例を示します。

    object BarConversions {
      implicit def toInt(bar: Bar): Int = bar.bar
      implicit def toBar(i: Int): Bar = new Bar(i)
    }
    
    object Main {
      def main(args: Array[String]) {
        val sc = new SparkContext("local",  "test", new SparkConf())
        val sqlContext = new SQLContext(sc)
        import sqlContext.implicits._
        import BarConversions._
    
        type EncodedBar = Int
    
        val bars: RDD[EncodedBar]  = sc.parallelize(Seq(new Bar(1)))
        val barsDS = bars.toDS
    
        barsDS.show
        barsDS.map(_.bar).show
    
        sc.stop()
      }
    }

関連する質問:


解決策1は、(少なくともSet)型付けされたコレクションでは機能しないようですException in thread "main" java.lang.UnsupportedOperationException: No Encoder found for Set[Bar]
Victor P.

@VictorP。私はこのようなケースでは、特定のタイプ(のためのエンコーダが必要になります怖いです期待されているkryo[Set[Bar]]。クラスはフィールドが含まれている場合と同じ方法Barあなたがオブジェクト全体のためのエンコーダを必要とし、これらは非常に粗製のメソッドです。。
zero323

@ zero323同じ問題に直面しています。プロジェクト全体をエンコードする方法のコード例を記載できますか?どうもありがとう!
ロック

@ロック "プロジェクト全体"の意味がわかりません
zero323

コメントごとに@ zero323、「クラスにフィールドBar全体のエンコーダが必要なフィールドが含まれている場合」。私の質問は、この「プロジェクト全体」をどのようにエンコードするかでしたか?
ロック

9

UDTRegistrationを使用してから、ケースクラス、タプルなどをすべてユーザー定義型で正しく機能させることができます。

カスタムEnumを使用したいとします。

trait CustomEnum { def value:String }
case object Foo extends CustomEnum  { val value = "F" }
case object Bar extends CustomEnum  { val value = "B" }
object CustomEnum {
  def fromString(str:String) = Seq(Foo, Bar).find(_.value == str).get
}

次のように登録します。

// First define a UDT class for it:
class CustomEnumUDT extends UserDefinedType[CustomEnum] {
  override def sqlType: DataType = org.apache.spark.sql.types.StringType
  override def serialize(obj: CustomEnum): Any = org.apache.spark.unsafe.types.UTF8String.fromString(obj.value)
  // Note that this will be a UTF8String type
  override def deserialize(datum: Any): CustomEnum = CustomEnum.fromString(datum.toString)
  override def userClass: Class[CustomEnum] = classOf[CustomEnum]
}

// Then Register the UDT Class!
// NOTE: you have to put this file into the org.apache.spark package!
UDTRegistration.register(classOf[CustomEnum].getName, classOf[CustomEnumUDT].getName)

次に、それを使用してください!

case class UsingCustomEnum(id:Int, en:CustomEnum)

val seq = Seq(
  UsingCustomEnum(1, Foo),
  UsingCustomEnum(2, Bar),
  UsingCustomEnum(3, Foo)
).toDS()
seq.filter(_.en == Foo).show()
println(seq.collect())

ポリモーフィックレコードを使用するとします。

trait CustomPoly
case class FooPoly(id:Int) extends CustomPoly
case class BarPoly(value:String, secondValue:Long) extends CustomPoly

...そしてそれを次のように使用します:

case class UsingPoly(id:Int, poly:CustomPoly)

Seq(
  UsingPoly(1, new FooPoly(1)),
  UsingPoly(2, new BarPoly("Blah", 123)),
  UsingPoly(3, new FooPoly(1))
).toDS

polySeq.filter(_.poly match {
  case FooPoly(value) => value == 1
  case _ => false
}).show()

すべてをバイトにエンコードするカスタムUDTを作成できます(ここではJavaシリアル化を使用していますが、SparkのKryoコンテキストをインスツルメントする方が良いでしょう)。

最初にUDTクラスを定義します。

class CustomPolyUDT extends UserDefinedType[CustomPoly] {
  val kryo = new Kryo()

  override def sqlType: DataType = org.apache.spark.sql.types.BinaryType
  override def serialize(obj: CustomPoly): Any = {
    val bos = new ByteArrayOutputStream()
    val oos = new ObjectOutputStream(bos)
    oos.writeObject(obj)

    bos.toByteArray
  }
  override def deserialize(datum: Any): CustomPoly = {
    val bis = new ByteArrayInputStream(datum.asInstanceOf[Array[Byte]])
    val ois = new ObjectInputStream(bis)
    val obj = ois.readObject()
    obj.asInstanceOf[CustomPoly]
  }

  override def userClass: Class[CustomPoly] = classOf[CustomPoly]
}

次に、それを登録します。

// NOTE: The file you do this in has to be inside of the org.apache.spark package!
UDTRegistration.register(classOf[CustomPoly].getName, classOf[CustomPolyUDT].getName)

その後、それを使用できます!

// As shown above:
case class UsingPoly(id:Int, poly:CustomPoly)

Seq(
  UsingPoly(1, new FooPoly(1)),
  UsingPoly(2, new BarPoly("Blah", 123)),
  UsingPoly(3, new FooPoly(1))
).toDS

polySeq.filter(_.poly match {
  case FooPoly(value) => value == 1
  case _ => false
}).show()

1
kryoが使用されている場所がわかりません(CustomPolyUDT内)
mathieu

プロジェクトでUDTを定義しようとしていますが、「この場所からシンボルUserDefinedTypeにアクセスできません」というエラーが発生します。何か助け?
Rijo Joseph

@RijoJoseph様 プロジェクトにパッケージorg.apache.sparkを作成し、UDTコードをその中に配置する必要があります。
ChoppyTheLumberjack 2018

6

エンコーダはでもほぼ同じように機能しSpark2.0ます。そしてKryo、まだ推奨されるserialization選択です。

あなたはスパークシェルで次の例を見ることができます

scala> import spark.implicits._
import spark.implicits._

scala> import org.apache.spark.sql.Encoders
import org.apache.spark.sql.Encoders

scala> case class NormalPerson(name: String, age: Int) {
 |   def aboutMe = s"I am ${name}. I am ${age} years old."
 | }
defined class NormalPerson

scala> case class ReversePerson(name: Int, age: String) {
 |   def aboutMe = s"I am ${name}. I am ${age} years old."
 | }
defined class ReversePerson

scala> val normalPersons = Seq(
 |   NormalPerson("Superman", 25),
 |   NormalPerson("Spiderman", 17),
 |   NormalPerson("Ironman", 29)
 | )
normalPersons: Seq[NormalPerson] = List(NormalPerson(Superman,25), NormalPerson(Spiderman,17), NormalPerson(Ironman,29))

scala> val ds1 = sc.parallelize(normalPersons).toDS
ds1: org.apache.spark.sql.Dataset[NormalPerson] = [name: string, age: int]

scala> val ds2 = ds1.map(np => ReversePerson(np.age, np.name))
ds2: org.apache.spark.sql.Dataset[ReversePerson] = [name: int, age: string]

scala> ds1.show()
+---------+---+
|     name|age|
+---------+---+
| Superman| 25|
|Spiderman| 17|
|  Ironman| 29|
+---------+---+

scala> ds2.show()
+----+---------+
|name|      age|
+----+---------+
|  25| Superman|
|  17|Spiderman|
|  29|  Ironman|
+----+---------+

scala> ds1.foreach(p => println(p.aboutMe))
I am Ironman. I am 29 years old.
I am Superman. I am 25 years old.
I am Spiderman. I am 17 years old.

scala> val ds2 = ds1.map(np => ReversePerson(np.age, np.name))
ds2: org.apache.spark.sql.Dataset[ReversePerson] = [name: int, age: string]

scala> ds2.foreach(p => println(p.aboutMe))
I am 17. I am Spiderman years old.
I am 25. I am Superman years old.
I am 29. I am Ironman years old.

今までのところ] appropriate encoders現在のスコープには存在しなかったので、私たちの人々はbinary値としてエンコードされませんでした。しかし、シリアライゼーションを使用していくつかのimplicitエンコーダーを提供すると、状況は変わりKryoます。

// Provide Encoders

scala> implicit val normalPersonKryoEncoder = Encoders.kryo[NormalPerson]
normalPersonKryoEncoder: org.apache.spark.sql.Encoder[NormalPerson] = class[value[0]: binary]

scala> implicit val reversePersonKryoEncoder = Encoders.kryo[ReversePerson]
reversePersonKryoEncoder: org.apache.spark.sql.Encoder[ReversePerson] = class[value[0]: binary]

// Ecoders will be used since they are now present in Scope

scala> val ds3 = sc.parallelize(normalPersons).toDS
ds3: org.apache.spark.sql.Dataset[NormalPerson] = [value: binary]

scala> val ds4 = ds3.map(np => ReversePerson(np.age, np.name))
ds4: org.apache.spark.sql.Dataset[ReversePerson] = [value: binary]

// now all our persons show up as binary values
scala> ds3.show()
+--------------------+
|               value|
+--------------------+
|[01 00 24 6C 69 6...|
|[01 00 24 6C 69 6...|
|[01 00 24 6C 69 6...|
+--------------------+

scala> ds4.show()
+--------------------+
|               value|
+--------------------+
|[01 00 24 6C 69 6...|
|[01 00 24 6C 69 6...|
|[01 00 24 6C 69 6...|
+--------------------+

// Our instances still work as expected    

scala> ds3.foreach(p => println(p.aboutMe))
I am Ironman. I am 29 years old.
I am Spiderman. I am 17 years old.
I am Superman. I am 25 years old.

scala> ds4.foreach(p => println(p.aboutMe))
I am 25. I am Superman years old.
I am 29. I am Ironman years old.
I am 17. I am Spiderman years old.

3

Java Beanクラスの場合、これは役立ちます

import spark.sqlContext.implicits._
import org.apache.spark.sql.Encoders
implicit val encoder = Encoders.bean[MyClasss](classOf[MyClass])

これで、単純にdataFrameをカスタムDataFrameとして読み取ることができます

dataFrame.as[MyClass]

これにより、バイナリではなくカスタムクラスエンコーダーが作成されます。


1

私の例はJavaですが、Scalaへの適応が難しいとは思いません。

単純なJava Beanである限り、spark.createDatasetEncoders.beanの使用RDD<Fruit>への変換はかなり成功しています。Dataset<Fruit>Fruit

ステップ1:単純なJava Beanを作成します。

public class Fruit implements Serializable {
    private String name  = "default-fruit";
    private String color = "default-color";

    // AllArgsConstructor
    public Fruit(String name, String color) {
        this.name  = name;
        this.color = color;
    }

    // NoArgsConstructor
    public Fruit() {
        this("default-fruit", "default-color");
    }

    // ...create getters and setters for above fields
    // you figure it out
}

私は、DataBricksの人々がエンコーダーを強化する前に、プリミティブ型とStringをフィールドとして持つクラスに固執します。ネストされたオブジェクトを持つクラスがある場合は、すべてのフィールドがフラット化された別の単純なJava Beanを作成します。これにより、RDD変換を使用して、複雑なタイプを単純なタイプにマップできます。確かに少し余分な作業ですが、フラットスキーマでのパフォーマンスの向上に大きく役立つと思います。

ステップ2:RDDからデータセットを取得する

SparkSession spark = SparkSession.builder().getOrCreate();
JavaSparkContext jsc = new JavaSparkContext();

List<Fruit> fruitList = ImmutableList.of(
    new Fruit("apple", "red"),
    new Fruit("orange", "orange"),
    new Fruit("grape", "purple"));
JavaRDD<Fruit> fruitJavaRDD = jsc.parallelize(fruitList);


RDD<Fruit> fruitRDD = fruitJavaRDD.rdd();
Encoder<Fruit> fruitBean = Encoders.bean(Fruit.class);
Dataset<Fruit> fruitDataset = spark.createDataset(rdd, bean);

そして出来上がり!泡立て、すすぎ、繰り返します。


単純な構造の場合は、Blobにシリアル化するのではなく、ネイティブのSpark型に格納する方が適切であることを指摘することをお勧めします。それらは、Pythonゲートウェイ全体でよりよく機能し、Parquetでより透過的になり、同じ形状の構造にキャストすることもできます。
metasim

1

私の状況にあるかもしれない人々のために、私もここに私の答えを入れました。

具体的には

  1. SQLContextから「型付きデータの設定」を読み取っていました。したがって、元のデータ形式はDataFrameです。

    val sample = spark.sqlContext.sql("select 1 as a, collect_set(1) as b limit 1") sample.show()

    +---+---+ | a| b| +---+---+ | 1|[1]| +---+---+

  2. 次に、mutable.WrappedArrayタイプでrdd.map()を使用してRDDに変換します。

    sample .rdd.map(r => (r.getInt(0), r.getAs[mutable.WrappedArray[Int]](1).toSet)) .collect() .foreach(println)

    結果:

    (1,Set(1))


0

すでに示した提案に加えて、最近発見した別のオプションは、traitを含むカスタムクラスを宣言できることですorg.apache.spark.sql.catalyst.DefinedByConstructorParams

これは、クラスがExpressionEncoderが理解できる型、つまりプリミティブ値と標準コレクションを使用するコンストラクターを持っている場合に機能します。クラスをケースクラスとして宣言できない場合に便利ですが、データセットに含まれるたびにKryoを使用してエンコードする必要はありません。

たとえば、Breezeベクトルを含むケースクラスを宣言したかったのです。これを処理できる唯一のエンコーダは、通常はKryoです。しかし、Breeze DenseVectorおよびDefinedByConstructorParamsを拡張するサブクラスを宣言した場合、ExpressionEncoderは、それがDoubleの配列としてシリアル化できることを理解しました。

ここに私がそれを宣言した方法があります:

class SerializableDenseVector(values: Array[Double]) extends breeze.linalg.DenseVector[Double](values) with DefinedByConstructorParams
implicit def BreezeVectorToSerializable(bv: breeze.linalg.DenseVector[Double]): SerializableDenseVector = bv.asInstanceOf[SerializableDenseVector]

これSerializableDenseVectorで、Kryoを使用せずに、単純なExpressionEncoderを使用して(直接、または製品の一部として)データセットで使用できます。Breeze DenseVectorと同じように機能しますが、Array [Double]としてシリアル化されます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.