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.0
in Encoders.scala
o SQLImplicits.scala
reperti 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 T
trova 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
MyObj
per far sì che sappia come codificare Wrap[MyObj]
o(Int,MyObj)
).
Basta usare kryo
La soluzione che tutti suggeriscono è usare l' kryo
encoder.
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-shell
cui 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 kryo
di Spark porta a memorizzare ogni riga del set di dati come un oggetto binario piatto. Per map
, filter
, foreach
che è abbastanza, ma per operazioni come join
, Spark ha davvero bisogno questi per essere separati in colonne. Ispezione dello schema per d2
od3
, 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
kryo
dappertutto)
- è quasi interamente compatibile con le versioni precedenti
import spark.implicits._
(con alcune ridenominazioni interessate)
- fa non permette di unirci sulle
kyro
colonne 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 MyObj
che ha campi di tipi Int
, java.util.UUID
e Set[String]
. Il primo si prende cura di se stesso. Il secondo, anche se potrei serializzare l'utilizzo kryo
sarebbe più utile se memorizzato come String
(dal momento UUID
che 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)
ExpressionEncoder
utilizzando la serializzazione JSON? Nel mio caso non riesco a cavarmela con le tuple e Kryo mi dà una colonna binaria ..