Bagaimana cara menyimpan objek khusus di Dataset?


149

Menurut Introducing Spark Datasets :

Saat kami menantikan Spark 2.0, kami merencanakan beberapa peningkatan yang menarik pada Kumpulan Data, khususnya: ... Penyandi khusus - sementara kami saat ini membuat penyandiaksaraan otomatis untuk berbagai jenis, kami ingin membuka API untuk objek khusus.

dan upaya untuk menyimpan jenis khusus dalam Datasetarahan ke kesalahan berikut seperti:

Tidak dapat menemukan encoder untuk tipe yang disimpan dalam Dataset. Tipe primitif (Int, String, dll) dan tipe Produk (kelas kasus) didukung dengan mengimpor sqlContext.implicits._ Dukungan untuk membuat serial jenis lain akan ditambahkan dalam rilis mendatang

atau:

Java.lang.UnsupportedOperationException: Tidak ditemukan Encoder untuk ....

Apakah ada solusi yang ada?


Perhatikan bahwa pertanyaan ini hanya ada sebagai titik masuk untuk jawaban Wiki Komunitas. Jangan ragu untuk memperbarui / meningkatkan pertanyaan dan jawaban.

Jawaban:


240

Memperbarui

Jawaban ini masih berlaku dan informatif, meskipun hal-hal yang sekarang lebih baik karena 2.2 / 2.3, yang menambahkan dukungan built-in encoder untuk Set, Seq, Map, Date, Timestamp, dan BigDecimal. Jika Anda tetap membuat jenis dengan hanya kelas kasus dan jenis Scala biasa, Anda harus baik-baik saja dengan yang tersirat di SQLImplicits.


Sayangnya, hampir tidak ada yang ditambahkan untuk membantu ini. Mencari @since 2.0.0di Encoders.scalaatau SQLImplicits.scalatemuan hal sebagian besar hubungannya dengan tipe primitif (dan beberapa tweaking kelas kasus). Jadi, hal pertama yang ingin saya katakan: saat ini tidak ada dukungan nyata yang baik untuk pengkodekan kelas khusus . Dengan keluar dari jalan, berikut adalah beberapa trik yang melakukan pekerjaan sebaik yang bisa kita harapkan, mengingat apa yang saat ini kita miliki. Sebagai penafian dimuka: ini tidak akan bekerja dengan sempurna dan saya akan melakukan yang terbaik untuk membuat semua batasan jelas dan dimuka.

Apa sebenarnya masalahnya

Ketika Anda ingin membuat dataset, Spark "memerlukan encoder (untuk mengkonversi objek JVM tipe T ke dan dari representasi SQL Spark internal) yang umumnya dibuat secara otomatis melalui implisit dari SparkSession, atau dapat dibuat secara eksplisit dengan memanggil metode statis pada Encoders"(diambil dari dokumen padacreateDataset ). Encoder akan mengambil bentuk di Encoder[T]mana Tjenis yang Anda encoding. Saran pertama adalah menambahkan import spark.implicits._(yang memberi Anda ini encoders implisit) dan saran kedua adalah untuk secara eksplisit lulus dalam encoder implisit menggunakan ini set fungsi encoder terkait.

Tidak ada encoder yang tersedia untuk kelas reguler, jadi

import spark.implicits._
class MyObj(val i: Int)
// ...
val d = spark.createDataset(Seq(new MyObj(1),new MyObj(2),new MyObj(3)))

akan memberi Anda kesalahan waktu kompilasi terkait implisit berikut:

Tidak dapat menemukan encoder untuk tipe yang disimpan dalam Dataset. Tipe primitif (Int, String, dll) dan tipe Produk (kelas kasus) didukung dengan mengimpor sqlContext.implicits._ Dukungan untuk membuat serial jenis lain akan ditambahkan dalam rilis mendatang

Namun, jika Anda membungkus tipe apa pun yang baru saja Anda gunakan untuk mendapatkan kesalahan di atas di beberapa kelas yang meluas Product, kesalahan tersebut tertunda hingga runtime, jadi

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))))

Mengkompilasi dengan baik, tetapi gagal saat runtime dengan

java.lang.UnsupportedOperationException: Tidak ditemukan Encoder untuk MyObj

Alasan untuk ini adalah bahwa Spark pembuat enkode dengan implisit sebenarnya hanya dibuat pada saat runtime (melalui scala relfection). Dalam kasus ini, semua pemeriksaan Spark pada waktu kompilasi adalah bahwa kelas terluar meluas Product(yang dilakukan semua kelas kasus), dan hanya menyadari pada saat runtime bahwa ia masih tidak tahu apa yang harus dilakukan dengan MyObj(masalah yang sama terjadi jika saya mencoba membuat a Dataset[(Int,MyObj)]- Spark menunggu sampai runtime untuk muntah pada MyObj). Ini adalah masalah sentral yang sangat perlu diperbaiki:

  • beberapa kelas yang memperpanjang Productkompilasi meskipun selalu crash saat runtime dan
  • tidak ada cara meneruskan encoders khusus untuk tipe bersarang (saya tidak punya cara untuk memberi makan Spark encoder untuk hanya MyObjsehingga ia tahu bagaimana untuk menyandikan Wrap[MyObj]atau (Int,MyObj)).

Gunakan saja kryo

Solusi yang disarankan semua orang adalah dengan menggunakan kryoenkoder.

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)))

Ini menjadi sangat cepat membosankan. Terutama jika kode Anda memanipulasi semua jenis kumpulan data, bergabung, mengelompokkan, dll. Anda akhirnya mengumpulkan banyak implisit tambahan. Jadi, mengapa tidak membuat implisit yang melakukan ini semua secara otomatis?

import scala.reflect.ClassTag
implicit def kryoEncoder[A](implicit ct: ClassTag[A]) = 
  org.apache.spark.sql.Encoders.kryo[A](ct)

Dan sekarang, sepertinya saya bisa melakukan hampir semua yang saya inginkan (contoh di bawah ini tidak akan berfungsi di spark-shelltempat spark.implicits._yang diimpor secara otomatis)

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!

Atau hampir. Masalahnya adalah bahwa menggunakan kryolead ke Spark hanya menyimpan setiap baris dalam dataset sebagai objek biner datar. Untuk map, filter, foreachyang cukup, tetapi untuk operasi seperti join, Spark benar-benar perlu ini untuk dipisahkan ke dalam kolom. Memeriksa skema untuk d2atau d3, Anda melihat hanya ada satu kolom biner:

d2.printSchema
// root
//  |-- value: binary (nullable = true)

Solusi parsial untuk tupel

Jadi, menggunakan keajaiban implisit dalam Scala (lebih banyak dalam 6.26.3 Resolusi Kelebihan Beban ), saya dapat membuat sendiri serangkaian implisit yang akan melakukan pekerjaan sebaik mungkin, setidaknya untuk tupel, dan akan bekerja dengan baik dengan implisit yang ada:

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

Kemudian, dengan berbekal implisit ini, saya dapat membuat contoh di atas berhasil, meskipun dengan penggantian nama kolom

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")

Saya belum menemukan cara untuk mendapatkan nama tuple yang diharapkan (_1 ,, _2...) secara default tanpa mengubah nama mereka - jika orang lain ingin bermain-main dengan ini, ini adalah di mana nama tersebut "value"diperkenalkan dan ini adalah di mana tuple nama biasanya ditambahkan. Namun, kuncinya adalah bahwa saya sekarang memiliki skema terstruktur yang bagus:

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)

Jadi, secara ringkas, solusi ini:

  • memungkinkan kita mendapatkan kolom terpisah untuk tupel (jadi kita bisa bergabung di tupel lagi, yay!)
  • kita bisa lagi hanya mengandalkan implisit (jadi tidak perlu lewat di kryosemua tempat)
  • hampir seluruhnya kompatibel dengan import spark.implicits._(dengan beberapa penggantian nama terlibat)
  • tidak tidak mari kita bergabung pada kyrokolom biner serial, apalagi di bidang-bidang mungkin memiliki
  • memiliki efek samping yang tidak menyenangkan dari penggantian nama beberapa kolom tuple menjadi "nilai" (jika perlu, ini dapat dibatalkan dengan mengonversi .toDF , menentukan nama kolom baru, dan mengonversi kembali ke dataset - dan nama skema tampaknya dipertahankan melalui penggabungan). , di mana mereka paling dibutuhkan).

Solusi parsial untuk kelas secara umum

Yang ini kurang menyenangkan dan tidak memiliki solusi yang baik. Namun, sekarang kita memiliki solusi tuple di atas, saya punya firasat bahwa solusi konversi implisit dari jawaban lain juga akan sedikit lebih menyakitkan karena Anda dapat mengonversi kelas Anda yang lebih kompleks menjadi tupel. Kemudian, setelah membuat dataset, Anda mungkin akan mengganti nama kolom menggunakan pendekatan dataframe. Jika semuanya berjalan dengan baik, ini benar - benar - perbaikan karena saya sekarang dapat melakukan gabung di bidang kelas saya. Jika saya baru saja menggunakan satu kryoserializer biner datar yang tidak akan mungkin terjadi.

Berikut adalah contoh yang tidak sedikit semuanya: Aku punya kelas MyObjyang memiliki bidang jenis Int, java.util.UUIDdan Set[String]. Yang pertama mengurus dirinya sendiri. Yang kedua, meskipun saya bisa membuat serial menggunakan kryoakan lebih berguna jika disimpan sebagai String(karena UUIDs biasanya adalah sesuatu yang saya ingin bergabung melawan). Yang ketiga benar-benar hanya berada di kolom biner.

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)

Sekarang, saya bisa membuat dataset dengan skema yang bagus menggunakan mesin ini:

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]

Dan skema menunjukkan saya kolom dengan nama yang tepat dan dengan dua hal pertama yang saya dapat bergabung.

d.printSchema
// root
//  |-- i: integer (nullable = false)
//  |-- u: string (nullable = true)
//  |-- s: binary (nullable = true)

Apakah mungkin untuk membuat kelas khusus ExpressionEncodermenggunakan serialisasi JSON? Dalam kasus saya, saya tidak bisa lolos dengan tupel, dan kryo memberi saya kolom biner ..
Alexey Svyatkovskiy

1
@ Alexey Saya tidak berpikir begitu. Tetapi mengapa Anda menginginkannya? Mengapa Anda tidak bisa lolos dengan solusi terakhir yang saya usulkan? Jika Anda bisa memasukkan data Anda di JSON, Anda harus dapat mengekstraksi bidang dan menempatkannya dalam kelas kasus ...
Alec

1
Sayangnya, garis bawah dari jawaban ini adalah tidak ada solusi yang berfungsi.
baol

@baol Sortir dari. Tetapi ingat betapa sulitnya apa yang dilakukan Spark. Sistem tipe Scala sama sekali tidak cukup kuat untuk "memperoleh" pembuat enkode yang secara rekursif melewati bidang. Terus terang, saya hanya terkejut tidak ada yang membuat makro penjelasan untuk ini. Sepertinya solusi alami (tapi sulit).
Alec

1
@combinatorist Pemahaman saya adalah bahwa Dataset dan Dataframe (tetapi bukan RDD, karena mereka tidak memerlukan encoders!) Setara dengan perspektif kinerja. Jangan meremehkan keamanan tipe dataset. Hanya karena Spark secara internal menggunakan satu ton refleksi, gips, dll. Tidak berarti Anda tidak perlu peduli dengan keamanan jenis antarmuka yang terbuka. Tapi itu membuat saya merasa lebih baik tentang membuat fungsi tipe-aman berbasis Dataset saya sendiri yang menggunakan Dataframe di bawah tenda.
Alec

32
  1. Menggunakan encoders generik.

    Ada dua encoders generik yang tersedia untuk saat ini kryodan di javaSerializationmana yang terakhir secara eksplisit digambarkan sebagai:

    sangat tidak efisien dan hanya digunakan sebagai pilihan terakhir.

    Dengan asumsi kelas berikut

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

    Anda dapat menggunakan encoders ini dengan menambahkan encoder implisit:

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

    yang dapat digunakan bersama sebagai berikut:

    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()
      }
    }

    Ini menyimpan objek sebagai binarykolom sehingga ketika dikonversi ke DataFrameAnda mendapatkan skema berikut:

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

    Dimungkinkan juga untuk menyandikan tupel menggunakan kryoenkoder untuk bidang tertentu:

    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]

    Harap perhatikan bahwa kami tidak bergantung pada enkode tersirat di sini, tetapi lulus enkode secara eksplisit sehingga kemungkinan besar ini tidak akan berfungsi dengan toDSmetode.

  2. Menggunakan konversi tersirat:

    Berikan konversi implisit antara representasi yang dapat disandikan dan kelas khusus, misalnya:

    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()
      }
    }

Pertanyaan-pertanyaan Terkait:


Solusi 1 tampaknya tidak berfungsi untuk koleksi yang diketik (setidaknya Set) saya dapatkan Exception in thread "main" java.lang.UnsupportedOperationException: No Encoder found for Set[Bar].
Victor P.

@ Viktor. Diharapkan saya takut Dalam kasus seperti ini Anda akan memerlukan encoder untuk tipe tertentu ( kryo[Set[Bar]]. Cara yang sama jika kelas berisi bidang yang BarAnda butuhkan encoder untuk seluruh objek. Ini adalah metode yang sangat kasar.
zero323

@ zero323 Saya menghadapi masalah yang sama. Bisakah Anda memberikan contoh kode tentang cara menyandikan seluruh proyek? Terimakasih banyak!
Rock

@ Rock Saya tidak yakin apa yang Anda maksud dengan "seluruh proyek"
zero323

@ zero323 per komentar Anda, "jika kelas berisi bidang, BarAnda perlu encoder untuk seluruh objek". pertanyaan saya adalah bagaimana menyandikan "seluruh proyek" ini?
Rock

9

Anda dapat menggunakan UDTRegistration dan kemudian Kelas Kasus, Tuple, dll ... semua berfungsi dengan benar dengan Tipe Buatan Pengguna Anda!

Katakanlah Anda ingin menggunakan Enum khusus:

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
}

Daftarkan seperti ini:

// 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)

Lalu GUNAKAN!

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())

Katakanlah Anda ingin menggunakan Catatan Polimorfik:

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

... dan gunakan seperti ini:

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()

Anda dapat menulis UDT khusus yang mengkodekan segalanya ke byte (Saya menggunakan serialisasi java di sini tapi mungkin lebih baik untuk instrumen konteks Kryo Spark).

Pertama-tama tentukan kelas 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]
}

Kemudian daftarkan:

// 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)

Maka Anda bisa menggunakannya!

// 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
Saya tidak melihat di mana kryo Anda digunakan (dalam CustomPolyUDT)
mathieu

Saya mencoba mendefinisikan UDT dalam proyek saya dan saya mendapatkan kesalahan ini "Simbol UserDefinedType tidak dapat diakses dari tempat ini". Ada bantuan?
Rijo Joseph

Hai @RijoJoseph. Anda perlu membuat paket org.apache.spark di proyek Anda dan memasukkan kode UDT Anda ke dalamnya.
ChoppyTheLumberjack

6

Encoder bekerja kurang lebih sama di Spark2.0. Dan Kryomasih merupakan serializationpilihan yang disarankan .

Anda dapat melihat contoh berikut dengan cangkang

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.

Sampai sekarang] tidak ada appropriate encodersdalam ruang lingkup saat ini sehingga orang-orang kami tidak dikodekan sebagai binarynilai. Tapi itu akan berubah setelah kami menyediakan beberapa implicitencoders menggunakan Kryoserialisasi.

// 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

Dalam kasus kelas Java Bean, ini bisa bermanfaat

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

Sekarang Anda cukup membaca dataFrame sebagai DataFrame kustom

dataFrame.as[MyClass]

Ini akan membuat encoder kelas khusus dan bukan yang biner.


1

Contoh saya akan di Jawa, tapi saya tidak membayangkan itu sulit beradaptasi dengan Scala.

Saya telah cukup berhasil mengkonversi RDD<Fruit>ke Dataset<Fruit>menggunakan spark.createDataset dan Encoders.bean selama Fruitadalah sederhana Java Bean .

Langkah 1: Buat Java Bean sederhana.

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
}

Saya akan tetap berpegang pada kelas dengan tipe primitif dan String sebagai bidang sebelum orang-orang DataBricks menambah Encoders mereka. Jika Anda memiliki kelas dengan objek bersarang, buat Java Bean lain sederhana dengan semua bidangnya diratakan, sehingga Anda dapat menggunakan transformasi RDD untuk memetakan tipe kompleks ke yang lebih sederhana. Tentu ini sedikit kerja ekstra, tapi saya membayangkan itu akan banyak membantu kinerja bekerja dengan skema datar.

Langkah 2: Dapatkan Dataset Anda dari 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);

Dan voila! Busa, bilas, ulangi.


Saya sarankan menunjukkan bahwa untuk struktur sederhana Anda akan lebih baik dilayani dengan menyimpannya dalam tipe Spark asli, daripada membuat serial mereka ke gumpalan. Mereka bekerja lebih baik di gateway Python, lebih transparan di Parket, dan bahkan dapat dilemparkan ke struktur dengan bentuk yang sama.
metasim

1

Bagi mereka yang mungkin dalam situasi saya, saya juga meletakkan jawaban saya di sini.

Untuk lebih spesifik,

  1. Saya sedang membaca 'Set data yang diketik' dari SQLContext. Jadi format data asli adalah DataFrame.

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

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

  2. Kemudian konversikan ke RDD menggunakan rdd.map () dengan tipe mutable.WrappedArray.

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

    Hasil:

    (1,Set(1))


0

Selain saran yang sudah diberikan, opsi lain yang baru-baru ini saya temukan adalah bahwa Anda dapat mendeklarasikan kelas khusus Anda termasuk sifat tersebut org.apache.spark.sql.catalyst.DefinedByConstructorParams .

Ini berfungsi jika kelas memiliki konstruktor yang menggunakan jenis ExpressionEncoder dapat mengerti, yaitu nilai-nilai primitif dan koleksi standar. Ini bisa berguna ketika Anda tidak dapat mendeklarasikan kelas sebagai kelas kasus, tetapi tidak ingin menggunakan Kryo untuk menyandikannya setiap kali itu termasuk dalam Dataset.

Sebagai contoh, saya ingin mendeklarasikan kelas kasus yang menyertakan vektor Breeze. Satu-satunya pembuat kode yang dapat menangani itu adalah Kryo. Tetapi jika saya mendeklarasikan subclass yang memperpanjang Breeze DenseVector dan DefinedByConstructorParams, ExpressionEncoder mengerti bahwa itu bisa diserialisasi sebagai array Doubles.

Begini cara saya menyatakannya:

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]

Sekarang saya dapat menggunakan SerializableDenseVectorDataset (secara langsung, atau sebagai bagian dari Produk) menggunakan ExpressionEncoder sederhana dan tanpa Kryo. Ia bekerja seperti Breeze DenseVector tetapi membuat serial sebagai Array [Double].

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.