web-dev-qa-db-ger.com

Wie werden benutzerdefinierte Objekte in Dataset gespeichert?

Gemäß Einführung von Spark-Datensätzen :

Da wir uns auf Spark 2.0 freuen, planen wir einige aufregende Verbesserungen an Datensätzen, insbesondere: ... Benutzerdefinierte Encoder - Während wir derzeit Encoder für eine Vielzahl von Typen automatisch generieren, möchten wir eine API für benutzerdefinierte Objekte öffnen.

und Versuche, einen benutzerdefinierten Typ in einer Dataset zu speichern, führen zu folgendem Fehler wie:

Kodierer für Typ, der in einem Datensatz gespeichert ist, kann nicht gefunden werden. Primitive Typen (Int, String usw.) und Produkttypen (Fallklassen) werden durch den Import von sqlContext.implicits unterstützt. Die Unterstützung für die Serialisierung anderer Typen wird in zukünftigen Versionen hinzugefügt

oder:

Java.lang.UnsupportedOperationException: Kein Encoder für ... gefunden.

Gibt es bestehende Problemumgehungen?


Beachten Sie, dass diese Frage nur als Einstiegspunkt für eine Community-Wiki-Antwort existiert. Fühlen Sie sich frei, um Fragen und Antworten zu aktualisieren.

121
zero323

Aktualisieren

Diese Antwort ist immer noch gültig und informativ, obwohl die Dinge seit 2.2/2.3 besser sind, wodurch die integrierte Encoder-Unterstützung für Set, Seq, Map, Date, Timestamp und BigDecimal. Wenn Sie sich daran halten, Typen nur mit case-Klassen und den üblichen Scala -Typen zu erstellen, sollten Sie nur das Implizite in SQLImplicits akzeptieren.


Leider wurde dazu so gut wie nichts hinzugefügt. Die Suche nach @since 2.0.0 In Encoders.scala oder SQLImplicits.scala hat hauptsächlich mit primitiven Typen zu tun (und mit einigen Änderungen an der Groß-/Kleinschreibung) Klassen). Also, als erstes zu sagen: Derzeit gibt es keine wirklich gute Unterstützung für benutzerdefinierte Klassencodierer . Aus dem Weg gehen nun einige Tricks hervor, die so gut sind, wie wir es uns je erhoffen können, wenn man bedenkt, was uns derzeit zur Verfügung steht. Als Vorab-Haftungsausschluss: Dies wird nicht perfekt funktionieren und ich werde mein Bestes tun, um alle Einschränkungen klar und deutlich zu machen.

Was genau ist das Problem

Wenn Sie ein Dataset erstellen möchten, erfordert Spark "einen Encoder (zum Konvertieren eines JVM-Objekts vom Typ T in und von der internen Spark SQL-Darstellung), der im Allgemeinen automatisch über Implicits erstellt wird von einem SparkSession oder kann explizit durch Aufrufen statischer Methoden auf Encoders "erstellt werden (entnommen aus dem docs on createDataset ). Ein Encoder hat die Form Encoder[T], Wobei T der Typ ist, den Sie codieren. Der erste Vorschlag ist das Hinzufügen von import spark.implicits._ (Wodurch Sie diese implizite Encoder erhalten) und der zweite Vorschlag ist das explizite Übergeben des impliziten Encoders mit diese set of Encoderbezogene Funktionen.

Für reguläre Klassen ist kein Encoder verfügbar

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

wird Ihnen den folgenden implizit verwandten Kompilierungszeitfehler geben:

Encoder für in einem Datensatz gespeicherten Typ konnte nicht gefunden werden. Primitive Typen (Int, String usw.) und Produkttypen (Fallklassen) werden durch den Import von sqlContext.implicits unterstützt. Die Unterstützung für die Serialisierung anderer Typen wird in zukünftigen Versionen hinzugefügt

Wenn Sie jedoch einen beliebigen Typ umbrechen, den Sie gerade verwendet haben, um den obigen Fehler in einer Klasse zu erhalten, die Product erweitert, wird der Fehler verwirrenderweise zur Laufzeit verzögert

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

Kompiliert einwandfrei, schlägt aber zur Laufzeit mit fehl

Java.lang.UnsupportedOperationException: Kein Encoder für MyObj gefunden

Der Grund dafür ist, dass die Encoder, die Spark mit den Implicits erstellt, tatsächlich nur zur Laufzeit erstellt werden (über scala relfection). In diesem Fall prüft Spark zur Kompilierungszeit nur, ob die äußerste Klasse Product ist (was alle case-Klassen tun), und stellt erst zur Laufzeit fest, dass sie immer noch nicht weiß, was zu tun ist mache mit MyObj (das gleiche Problem tritt auf, wenn ich versucht habe, eine Dataset[(Int,MyObj)] - Spark zu machen, die bis zur Laufzeit auf MyObj wartet). Dies sind zentrale Probleme, die dringend behoben werden müssen:

  • einige Klassen, die Product erweitern, werden kompiliert, obwohl sie zur Laufzeit immer abstürzen
  • es gibt keine Möglichkeit, benutzerdefinierte Encoder für verschachtelte Typen weiterzugeben (ich habe keine Möglichkeit, Spark für nur MyObj einen Encoder einzugeben, damit dieser dann weiß, wie man Wrap[MyObj] codiert. oder (Int,MyObj)).

Verwenden Sie einfach kryo

Die Lösung, die jeder vorschlägt, ist die Verwendung des Encoders 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)))

Das wird aber schnell ziemlich langweilig. Vor allem, wenn Ihr Code alle Arten von Datensätzen manipuliert, verknüpft, gruppiert usw. Sie haben am Ende eine Menge zusätzlicher Implikationen. Warum also nicht einfach ein Implizit machen, das das alles automatisch macht?

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

Und jetzt kann ich anscheinend fast alles tun, was ich will (das folgende Beispiel funktioniert nicht in spark-Shell, Wo spark.implicits._ Automatisch importiert wird).

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!

Oder fast. Das Problem ist, dass die Verwendung von kryo dazu führt, dass Spark nur jede Zeile in der Datenmenge als flaches Binärobjekt speichert. Für map, filter, foreach ist das ausreichend, aber für Operationen wie join muss Spark wirklich unterteilt werden Säulen. Wenn Sie das Schema auf d2 Oder d3 Untersuchen, sehen Sie, dass es nur eine binäre Spalte gibt:

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

Teillösung für Tupel

Mit der Magie der Implikationen in Scala (mehr in 6.26.3 Overloading Resolution ) kann ich mir eine Reihe von Implikationen machen, die so gut wie möglich funktionieren. Zumindest für Tupel und wird gut mit bestehenden Implikiten funktionieren:

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

Dann kann ich, bewaffnet mit diesen Implikationen, mein obiges Beispiel zum Funktionieren bringen, wenn auch mit einer gewissen Umbenennung der Spalten

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

Ich habe noch nicht herausgefunden, wie ich die erwarteten Tupelnamen (_1, _2, ...) standardmäßig erhalten kann, ohne sie umzubenennen - wenn jemand anderes damit herumspielen möchte, - this ist der Ort, an dem der Name "value" eingeführt wird, und this ist der Ort, an dem die Tupelnamen normalerweise hinzugefügt werden. Der entscheidende Punkt ist jedoch, dass ich jetzt ein schön strukturiertes Schema habe:

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)

Also, zusammenfassend, diese Problemumgehung:

  • erlaubt es uns, getrennte Spalten für Tupel zu erhalten (damit wir wieder Tupel hinzufügen können, yay!)
  • wir können uns wieder einfach auf die Implikationen verlassen (es ist also nicht notwendig, überall kryo einzugeben)
  • ist fast vollständig abwärtskompatibel mit import spark.implicits._ (mit einigem Umbenennen)
  • tut nicht Lassen Sie uns an den serialisierten binären Spalten kyro teilnehmen, geschweige denn an Feldern, die diese möglicherweise haben
  • hat den unangenehmen Nebeneffekt, dass einige der Tupel-Spalten in "value" umbenannt werden (dies kann bei Bedarf rückgängig gemacht werden, indem .toDF konvertiert, neue Spaltennamen angegeben und zurück in ein Dataset konvertiert werden - und die Schemanamen scheinen durch Joins erhalten zu werden, wo sie am dringendsten benötigt werden.

Teillösung für Klassen im Allgemeinen

Dieser ist weniger angenehm und hat keine gute Lösung. Da wir jedoch die Tuple-Lösung oben haben, ist die implizite Konvertierungslösung aus einer anderen Antwort weniger schmerzhaft, da Sie Ihre komplexeren Klassen in Tupel konvertieren können. Nach dem Erstellen des Datasets würden Sie die Spalten wahrscheinlich mit dem Dataframe-Ansatz umbenennen. Wenn alles gut geht, ist dies wirklich eine Verbesserung, da ich jetzt Joins auf den Feldern meiner Klassen ausführen kann. Wenn ich nur einen flachen binären Serialisierer kryo verwendet hätte, wäre das nicht möglich gewesen.

Hier ist ein Beispiel, das ein bisschen von allem macht: Ich habe eine Klasse MyObj mit Feldern der Typen Int, Java.util.UUID Und Set[String]. Der erste kümmert sich um sich. Die zweite, obwohl ich mit kryo serialisieren könnte, wäre nützlicher, wenn sie als String gespeichert würde (da UUID normalerweise etwas sind, gegen das ich mich aussprechen möchte). Das dritte gehört wirklich nur in eine binäre Spalte.

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)

Jetzt kann ich mit dieser Maschine einen Datensatz mit einem Nice-Schema erstellen:

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]

Und das Schema zeigt mir, dass ich Spalten mit den richtigen Namen und mit den ersten beiden Dingen, mit denen ich mich verbinden kann, habe.

d.printSchema
// root
//  |-- i: integer (nullable = false)
//  |-- u: string (nullable = true)
//  |-- s: binary (nullable = true)
208
Alec
  1. Generische Encoder verwenden.

    Es gibt zwei generische Encoder für jetzt kryo und javaSerialization , wobei der letztere explizit als beschrieben wird:

    extrem ineffizient und sollte nur als letztes Mittel verwendet werden.

    Angenommen, folgende Klasse

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

    sie können diese Encoder verwenden, indem Sie einen impliziten Encoder hinzufügen:

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

    die zusammen wie folgt verwendet werden können:

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

    Es speichert Objekte als binary-Spalte. Wenn Sie also in DataFrame konvertieren, erhalten Sie folgendes Schema:

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

    Es ist auch möglich, Tupel mit kryo encoder für ein bestimmtes Feld zu codieren:

    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]
    

    Bitte beachten Sie, dass wir hier nicht auf implizite Encoder angewiesen sind, sondern den Encoder explizit übergeben, sodass dies höchstwahrscheinlich nicht mit der toDS-Methode funktioniert. 

  2. Implizite Konvertierungen verwenden:

    Stellen Sie implizite Konvertierungen zwischen der darstellbaren Repräsentation und benutzerdefinierten Klassen bereit, zum Beispiel:

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

Verwandte Fragen:

28
zero323

Sie können die UDTRegistration und dann Case Classes, Tupel usw. verwenden. Alle funktionieren korrekt mit Ihrem benutzerdefinierten Typ!

Angenommen, Sie möchten eine benutzerdefinierte Enumeration verwenden:

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
}

Registrieren Sie es so:

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

Dann USE IT!

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

Angenommen, Sie möchten einen polymorphen Datensatz verwenden:

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

... und das so benutzen:

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

Sie können eine benutzerdefinierte UDT schreiben, die alles in Bytes codiert (ich verwende hier Java-Serialisierung, aber es ist wahrscheinlich besser, den Kryo-Kontext von Spark zu verwenden).

Definieren Sie zuerst die UDT-Klasse:

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]
}

Dann registriere es:

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

Dann kannst du es benutzen!

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

Encoder arbeiten in Spark2.0 mehr oder weniger gleich. Und Kryo ist immer noch die empfohlene serialization-Wahl.

Folgendes Beispiel können Sie mit spark-Shell betrachten

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.

Bis jetzt] gab es im derzeitigen Umfang keinen appropriate encoders, so dass unsere Personen nicht als binary-Werte codiert wurden. Das wird sich jedoch ändern, sobald wir einige implicit-Encoder mit Kryo-Serialisierung zur Verfügung stellen.

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

Im Falle einer Java Bean-Klasse kann dies nützlich sein

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

Jetzt können Sie den dataFrame einfach als benutzerdefinierten DataFrame lesen 

dataFrame.as[MyClass]

Dies erstellt einen benutzerdefinierten Klassencodierer und keinen binären. 

4
Akash Mahajan

Für diejenigen, die möglicherweise in meiner Situation sind, stelle ich hier auch meine Antwort.

Um genau zu sein,

  1. Ich las "Set typed data" von SQLContext. Das ursprüngliche Datenformat ist also DataFrame.

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

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

  2. Dann konvertieren Sie es in RDD mit rdd.map () mit mutable.WrappedArray-Typ.

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

    Ergebnis:

    (1,Set(1))

1
Taeheon Kwon

Meine Beispiele werden in Java sein, aber ich kann mir nicht vorstellen, dass es schwierig ist, sich an Scala anzupassen.

Ich habe recht erfolgreich Konvertierung von RDD<Fruit> in Dataset<Fruit> mit spark.createDataset und Encoders.bean durchgeführt, solange Fruit eine einfache Java Bean ist.

Schritt 1: Erstellen Sie das einfache 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
}

Ich würde mich an Klassen mit primitiven Typen und String als Feldern halten, bevor die DataBricks-Leute ihre Encoder auffrischen. Wenn Sie über eine Klasse mit verschachtelten Objekten verfügen, erstellen Sie ein weiteres einfaches Java Bean mit reduzierten Feldern, sodass Sie den komplexen Typ mithilfe von RDD-Transformationen dem einfacheren zuordnen können. Aber ich kann mir vorstellen, dass es bei der Performance mit einem flachen Schema viel helfen wird.

Schritt 2: Holen Sie sich Ihren Datensatz von der 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);

Und voila! Spülen, abspülen, wiederholen.

1
Jimmy Da

Neben den bereits gegebenen Vorschlägen habe ich kürzlich entdeckt, dass Sie Ihre benutzerdefinierte Klasse einschließlich des Merkmals org.Apache.spark.sql.catalyst.DefinedByConstructorParams deklarieren können.

Dies funktioniert, wenn die Klasse über einen Konstruktor verfügt, der Typen verwendet, die der ExpressionEncoder verstehen kann, d. H. Grundwerte und Standardsammlungen. Es kann nützlich sein, wenn Sie die Klasse nicht als Fallklasse deklarieren können, Kryo jedoch nicht jedes Mal verwenden möchten, wenn sie in ein Dataset aufgenommen wird.

Ich wollte zum Beispiel eine Fallklasse deklarieren, die einen Breeze-Vektor enthielt. Der einzige Encoder, der damit umgehen könnte, wäre normalerweise Kryo. Wenn ich jedoch eine Unterklasse deklarierte, die Breeze DenseVector und DefinedByConstructorParams erweiterte, erkannte der ExpressionEncoder, dass er als Array von Doubles serialisiert werden könnte.

So habe ich es erklärt:

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]

Jetzt kann ich SerializableDenseVector in einem Datensatz (direkt oder als Teil eines Produkts) verwenden, wobei ein einfacher ExpressionEncoder und kein Kryo verwendet wird. Es funktioniert wie ein Breeze DenseVector, serialisiert jedoch als Array [Double].

0
Matt