Come archiviare oggetti personalizzati nel set di dati?


149

Secondo l' introduzione di Spark Dataset :

In attesa di Spark 2.0, pianifichiamo alcuni entusiasmanti miglioramenti dei set di dati, in particolare: ... Encoder personalizzati: mentre attualmente generiamo gli encoder per un'ampia varietà di tipi, vorremmo aprire un'API per oggetti personalizzati.

e tenta di memorizzare il tipo personalizzato in un Datasetlead al seguente errore come:

Impossibile trovare l'encoder per il tipo memorizzato in un set di dati. I tipi primitivi (Int, String, ecc.) E i tipi di prodotto (classi di casi) sono supportati importando sqlContext.implicits._ Il supporto per la serializzazione di altri tipi verrà aggiunto nelle versioni future

o:

Java.lang.UnsupportedOperationException: nessun codificatore trovato per ....

Esistono soluzioni alternative esistenti?


Nota che questa domanda esiste solo come punto di ingresso per una risposta Wiki della community. Sentiti libero di aggiornare / migliorare sia le domande che le risposte.

Risposte:


240

Aggiornare

Questa risposta è ancora valido e informativo, anche se le cose sono ora meglio dal 2.2 / 2.3, che aggiunge il supporto integrato per encoder Set, Seq, Map, Date, Timestamp, e BigDecimal. Se continui a creare tipi con solo le classi case e i soliti tipi Scala, dovresti andare bene solo con l'implicito in SQLImplicits.


Sfortunatamente, praticamente nulla è stato aggiunto per aiutare in questo. Ricerca di @since 2.0.0in Encoders.scalao SQLImplicits.scalareperti cose soprattutto a che fare con i tipi primitivi (e qualche ritocco di classi case). Quindi, la prima cosa da dire: al momento non esiste un vero supporto per gli encoder di classe personalizzati . A parte questo, quello che segue sono alcuni trucchi che fanno un buon lavoro come possiamo mai sperare, dato quello che abbiamo attualmente a nostra disposizione. Come disclaimer anticipato: questo non funzionerà perfettamente e farò del mio meglio per rendere chiare e anticipate tutte le limitazioni.

Qual è esattamente il problema

Quando si desidera creare un set di dati, Spark "richiede un codificatore (per convertire un oggetto JVM di tipo T nella e dalla rappresentazione Spark SQL interna) che viene generalmente creato automaticamente tramite impliciti da a SparkSession, oppure può essere creato esplicitamente chiamando metodi statici on Encoders"(tratto dai documenti sucreateDataset ). Un codificatore prenderà la forma in Encoder[T]cui si Ttrova il tipo che stai codificando. Il primo suggerimento è quello di aggiungere import spark.implicits._(che fornisce questi codificatori impliciti) e il secondo suggerimento è quello di passare esplicitamente nell'encoder implicito utilizzando questo set di funzioni correlate all'encoder.

Non esiste un codificatore disponibile per le classi regolari, quindi

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

ti darà il seguente errore implicito relativo alla compilazione:

Impossibile trovare l'encoder per il tipo memorizzato in un set di dati. I tipi primitivi (Int, String, ecc.) E i tipi di prodotto (classi di casi) sono supportati importando sqlContext.implicits._ Il supporto per la serializzazione di altri tipi verrà aggiunto nelle versioni future

Tuttavia, se si avvolge qualsiasi tipo appena utilizzato per ottenere l'errore sopra riportato in una classe che si estende Product , l'errore viene ritardato in modo confuso al runtime, quindi

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

Compilare bene, ma fallisce in fase di esecuzione con

java.lang.UnsupportedOperationException: nessun codificatore trovato per MyObj

La ragione di ciò è che gli encoder che Spark crea con gli impliciti vengono effettivamente realizzati solo in fase di esecuzione (tramite scala relection). In questo caso, tutti i controlli Spark al momento della compilazione è che la classe più esterna si estende Product(cosa che fanno tutte le classi case), e si rende conto solo in fase di esecuzione che non sa ancora cosa fare MyObj(lo stesso problema si verifica se ho provato a fare a Dataset[(Int,MyObj)]- Spark attende che il runtime si attivi MyObj). Questi sono problemi centrali che hanno un disperato bisogno di essere risolti:

  • alcune classi che si estendono Product compilano nonostante si blocchi sempre in fase di esecuzione e
  • non c'è modo di passare codificatori personalizzati per tipi nidificati (non ho modo di alimentare Spark un codificatore solo MyObjper far sì che sappia come codificare Wrap[MyObj]o(Int,MyObj) ).

Basta usare kryo

La soluzione che tutti suggeriscono è usare l' kryoencoder.

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

Questo diventa piuttosto noioso in fretta però. Soprattutto se il tuo codice sta manipolando tutti i tipi di set di dati, unendo, raggruppando ecc. Si finisce per accumulare un sacco di implicazioni extra. Quindi, perché non rendere implicito ciò che fa tutto automaticamente?

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

E ora, sembra che posso fare quasi tutto quello che voglio (l'esempio che segue non funzionerà nel punto in spark-shellcui spark.implicits._viene importato automaticamente)

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!

O quasi. Il problema è che l'utilizzo kryodi Spark porta a memorizzare ogni riga del set di dati come un oggetto binario piatto. Per map, filter, foreachche è abbastanza, ma per operazioni come join, Spark ha davvero bisogno questi per essere separati in colonne. Ispezione dello schema per d2od3 , vedi c'è solo una colonna binaria:

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

Soluzione parziale per le tuple

Quindi, usando la magia degli impliciti in Scala (più in 6.26.3 Risoluzione di sovraccarico ), posso farmi una serie di impliciti che faranno il miglior lavoro possibile, almeno per le tuple, e funzioneranno bene con gli impliciti esistenti:

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

Quindi, armato di questi impliciti, posso far funzionare il mio esempio sopra, anche se con qualche rinominazione di colonna

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

Io non ho ancora capito come ottenere i nomi tuple attesi ( _1, _2, ...) per impostazione predefinita senza rinominare - se qualcun altro vuole giocare con questo, questo è dove il nome "value"viene introdotto e questo è dove la tupla i nomi vengono solitamente aggiunti. Tuttavia, il punto chiave è che ora ho un bel schema strutturato:

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)

Quindi, in sintesi, questa soluzione alternativa:

  • ci permette di ottenere colonne separate per le tuple (così possiamo unirci di nuovo sulle tuple, yay!)
  • possiamo ancora fare affidamento sugli impliciti (quindi non c'è bisogno di passare kryodappertutto)
  • è quasi interamente compatibile con le versioni precedenti import spark.implicits._(con alcune ridenominazioni interessate)
  • fa non permette di unirci sulle kyrocolonne binarie serializzate, per non parlare dei campi che potrebbero avere
  • ha lo sgradevole effetto collaterale di rinominare alcune delle colonne di tuple in "valore" (se necessario, questo può essere annullato convertendo .toDF, specificando i nomi di nuove colonne e convertendo nuovamente in un set di dati - e i nomi dello schema sembrano essere conservati attraverso i join , dove sono maggiormente necessari).

Soluzione parziale per le lezioni in generale

Questo è meno piacevole e non ha una buona soluzione. Tuttavia, ora che abbiamo la soluzione di tupla sopra, ho la sensazione che la soluzione di conversione implicita da un'altra risposta sarà anche un po 'meno dolorosa poiché puoi convertire le tue classi più complesse in tuple. Quindi, dopo aver creato il set di dati, probabilmente rinominereste le colonne usando l'approccio dataframe. Se tutto va bene, questo è davvero un miglioramento dal momento che ora posso eseguire join sui campi delle mie lezioni. Se avessi appena usato un binario piattokryo serializzatore che non sarebbe stato possibile.

Ecco un esempio che fa un po 'di tutto: ho una classe MyObjche ha campi di tipi Int, java.util.UUIDe Set[String]. Il primo si prende cura di se stesso. Il secondo, anche se potrei serializzare l'utilizzo kryosarebbe più utile se memorizzato come String(dal momento UUIDche di solito sono qualcosa con cui vorrò unirmi). Il terzo appartiene davvero solo a una colonna binaria.

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)

Ora, posso creare un set di dati con un bel schema usando questo macchinario:

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]

E lo schema mi mostra le colonne con i nomi giusti e con le prime due cose a cui posso unirmi.

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

È possibile creare una classe personalizzata ExpressionEncoderutilizzando la serializzazione JSON? Nel mio caso non riesco a cavarmela con le tuple e Kryo mi dà una colonna binaria ..
Alexey Svyatkovskiy,

1
@AlexeyS Non credo. Ma perché lo vorresti? Perché non riesci a cavartela con l'ultima soluzione che ti propongo? Se riesci a inserire i tuoi dati in JSON, dovresti essere in grado di estrarre i campi e inserirli in una classe case ...
Alec,

1
Purtroppo la linea di fondo di questa risposta è che non esiste una soluzione che funzioni.
baol

@baol Sort of. Ma ricorda quanto è difficile ciò che Spark sta facendo. Il sistema di tipi di Scala non è semplicemente abbastanza potente da "derivare" encoder che ricorrono in modo ricorsivo a campi. Francamente, sono solo sorpreso che nessuno abbia creato una macro di annotazioni per questo. Sembra la soluzione naturale (ma difficile).
Alec,

1
@combinatorist La mia comprensione è che i set di dati e i frame di dati (ma non i RDD, poiché non hanno bisogno di encoder!) sono equivalenti dal punto di vista delle prestazioni. Non sottovalutare la sicurezza dei tipi di set di dati! Solo perché Spark utilizza internamente una tonnellata di riflessi, cast, ecc. Non significa che non dovresti preoccuparti della sicurezza del tipo dell'interfaccia che è esposta. Ma mi fa sentire meglio nel creare le mie funzioni di sicurezza dei tipi basate su Dataset che utilizzano Dataframe sotto il cofano.
Alec,

32
  1. Utilizzo di codificatori generici.

    Ci sono due codificatori generici disponibili per ora kryoe in javaSerializationcui quest'ultimo è esplicitamente descritto come:

    estremamente inefficiente e dovrebbe essere usato solo come ultima risorsa.

    Supponendo la seguente lezione

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

    puoi usare questi codificatori aggiungendo un codificatore implicito:

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

    che può essere usato insieme come segue:

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

    Memorizza gli oggetti come binarycolonna, quindi quando convertiti in DataFrameottieni il seguente schema:

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

    È anche possibile codificare le tuple usando l' kryoencoder per un campo specifico:

    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]

    Si noti che qui non dipendiamo dagli encoder impliciti ma passiamo esplicitamente all'encoder, quindi molto probabilmente non funzionerà con il toDSmetodo.

  2. Utilizzo delle conversioni implicite:

    Fornire conversioni implicite tra rappresentazione che può essere codificata e classe personalizzata, ad esempio:

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

Domande correlate:


La soluzione 1 non sembra funzionare per le raccolte tipizzate (almeno Set) Exception in thread "main" java.lang.UnsupportedOperationException: No Encoder found for Set[Bar].
Victor P.

@VictorP. Temo che in questo caso avrai bisogno di un codificatore per un tipo specifico (allo kryo[Set[Bar]]stesso modo se la classe contiene un campo Barhai bisogno di un codificatore per un intero oggetto. Questi sono metodi molto rozzi.
zero323

@ zero323 Sto affrontando lo stesso problema. Puoi fornire un esempio di codice su come codificare l'intero progetto? Grazie molto!
Rock

@ Rock Non sono sicuro di cosa intendi per "intero progetto"
zero323

@ zero323 per il tuo commento, "se la classe contiene un campo Barhai bisogno dell'encoder per un intero oggetto". la mia domanda era: come codificare questo "intero progetto"?
Rock

9

Puoi usare UDTRegistration e quindi Classi di casi, Tuple, ecc ... funzionano tutti correttamente con il tuo Tipo definito dall'utente!

Supponi di voler utilizzare un Enum personalizzato:

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
}

Registralo in questo modo:

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

Quindi USARLO!

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

Supponiamo che tu voglia utilizzare un record polimorfico:

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

... e usalo così:

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

Puoi scrivere un UDT personalizzato che codifica tutto in byte (sto usando la serializzazione Java qui, ma probabilmente è meglio strumentare il contesto Kryo di Spark).

Per prima cosa definire la classe 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]
}

Quindi registralo:

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

Quindi puoi usarlo!

// 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
Non vedo dove viene utilizzato il tuo kryo (in CustomPolyUDT)
mathieu,

Sto cercando di definire un UDT nel mio progetto e visualizzo questo errore "Simbolo UserDefinedType non è accessibile da questo luogo". Qualsiasi aiuto ?
Rijo Joseph,

Ciao @RijoJoseph. Devi creare un pacchetto org.apache.spark nel tuo progetto e inserire il tuo codice UDT.
ChoppyTheLumberjack,

6

Gli encoder funzionano più o meno allo stesso modo in Spark2.0. Ed Kryoè ancora la serializationscelta consigliata .

Puoi guardare l'esempio seguente con spark-shell

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.

Fino ad ora] non erano presenti appropriate encodersnel campo di applicazione attuale, quindi le nostre persone non erano codificate come binaryvalori. Ma questo cambierà quando forniremo alcuni implicitencoder usando la Kryoserializzazione.

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

In caso di classe Java Bean, questo può essere utile

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

Ora puoi semplicemente leggere dataFrame come DataFrame personalizzato

dataFrame.as[MyClass]

Ciò creerà un codificatore di classe personalizzato e non binario.


1

I miei esempi saranno in Java, ma non immagino che sia difficile adattarsi a Scala.

Ho avuto abbastanza successo convertendomi RDD<Fruit>a Dataset<Fruit>usare spark.createDataset e Encoders.bean purché Fruitsia un semplice Java Bean .

Passaggio 1: creare il semplice 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
}

Attaccherei alle classi con tipi primitivi e String come campi prima che la gente di DataBricks rinforzi i propri Encoder. Se si dispone di una classe con oggetto nidificato, creare un altro Java Bean semplice con tutti i suoi campi appiattiti, quindi è possibile utilizzare le trasformazioni RDD per mappare il tipo complesso a quello più semplice. Certo è un po 'di lavoro extra, ma immagino che aiuterà molto sulle prestazioni lavorando con uno schema piatto.

Passaggio 2: ottieni il tuo set di dati dall'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);

E voilà! Raccogliere, sciacquare, ripetere.


Suggerirei di sottolineare che per le strutture semplici sarebbe meglio servirle archiviandole nei tipi Spark nativi, piuttosto che serializzarle in un BLOB. Funzionano meglio attraverso il gateway Python, più trasparenti in Parquet e possono anche essere proiettati su strutture della stessa forma.
metasim,

1

Per coloro che possono nella mia situazione, ho inserito anche qui la mia risposta.

Essere specifici,

  1. Stavo leggendo "Imposta dati digitati" da SQLContext. Quindi il formato dati originale è DataFrame.

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

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

  2. Quindi convertilo in RDD usando rdd.map () con il tipo mutable.WrappedArray.

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

    Risultato:

    (1,Set(1))


0

Oltre ai suggerimenti già forniti, un'altra opzione che ho scoperto di recente è che puoi dichiarare la tua classe personalizzata incluso il tratto org.apache.spark.sql.catalyst.DefinedByConstructorParams.

Funziona se la classe ha un costruttore che utilizza tipi che ExpressionEncoder può comprendere, ovvero valori primitivi e raccolte standard. Può tornare utile quando non sei in grado di dichiarare la classe come case class, ma non vuoi usare Kryo per codificarla ogni volta che è inclusa in un set di dati.

Ad esempio, volevo dichiarare una classe case che includeva un vettore Breeze. L'unico codificatore che sarebbe in grado di gestire normalmente sarebbe Kryo. Ma se avessi dichiarato una sottoclasse che estendeva Breeze DenseVector e DefinedByConstructorParams, ExpressionEncoder avrebbe capito che poteva essere serializzato come un array di doppi.

Ecco come l'ho dichiarato:

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]

Ora posso utilizzare SerializableDenseVectorun set di dati (direttamente o come parte di un prodotto) utilizzando un semplice ExpressionEncoder e nessun Kryo. Funziona proprio come un Breeze DenseVector ma serializza come un array [doppio].

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.