Mettre à jour
Cette réponse est toujours valide et informatif, bien que les choses sont maintenant mieux depuis 2.2 / 2.3, qui ajoute le support intégré de codeur pour Set
, Seq
, Map
, Date
, Timestamp
et BigDecimal
. Si vous vous en tenez à créer des types avec uniquement des classes de cas et les types Scala habituels, vous devriez vous contenter de l'implicite in SQLImplicits
.
Malheureusement, pratiquement rien n'a été ajouté pour y remédier. Rechercher @since 2.0.0
dans Encoders.scala
ou SQLImplicits.scala
trouver des choses principalement à voir avec les types primitifs (et quelques ajustements des classes de cas). Donc, première chose à dire: il n'y a actuellement pas vraiment de bon support pour les encodeurs de classe personnalisés . Avec cela à l'écart, ce qui suit est quelques trucs qui font un aussi bon travail que nous pouvons jamais espérer, étant donné ce que nous avons actuellement à notre disposition. En guise d'avertissement initial: cela ne fonctionnera pas parfaitement et je ferai de mon mieux pour clarifier toutes les limitations.
Quel est le problème exactement
Lorsque vous souhaitez créer un ensemble de données, Spark "nécessite un encodeur (pour convertir un objet JVM de type T vers et à partir de la représentation SQL Spark interne) qui est généralement créé automatiquement via des implicits à partir d'un SparkSession
, ou peut être créé explicitement en appelant des méthodes statiques on Encoders
"(extrait de la documentationcreateDataset
). Un encodeur prendra la forme Encoder[T]
où T
est le type que vous encodez. La première suggestion est d'ajouter import spark.implicits._
(ce qui vous donne ces encodeurs implicites) et la deuxième suggestion est de passer explicitement l'encodeur implicite en utilisant cet ensemble de fonctions liées à l'encodeur.
Il n'y a pas d'encodeur disponible pour les classes régulières, donc
import spark.implicits._
class MyObj(val i: Int)
// ...
val d = spark.createDataset(Seq(new MyObj(1),new MyObj(2),new MyObj(3)))
vous donnera l'erreur de compilation implicite suivante:
Impossible de trouver le codeur pour le type stocké dans un ensemble de données. Les types primitifs (Int, String, etc.) et les types de produit (classes de cas) sont pris en charge par l'importation de sqlContext.implicits._ La prise en charge de la sérialisation d'autres types sera ajoutée dans les versions futures
Cependant, si vous encapsulez le type que vous venez d'utiliser pour obtenir l'erreur ci-dessus dans une classe qui s'étend Product
, l'erreur est retardée de manière déroutante à l'exécution, donc
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))))
Compile très bien, mais échoue à l'exécution avec
java.lang.UnsupportedOperationException: aucun encodeur trouvé pour MyObj
La raison en est que les encodeurs créés par Spark avec les implicits ne sont en fait créés qu'au moment de l'exécution (via la relfection de scala). Dans ce cas, toutes les vérifications Spark au moment de la compilation sont que la classe la plus externe s'étend Product
(ce que font toutes les classes de cas), et ne se rend compte qu'au moment de l'exécution qu'elle ne sait toujours pas quoi faire MyObj
(le même problème se produit si j'essaye de faire a Dataset[(Int,MyObj)]
- Spark attend que l'exécution soit activée MyObj
). Ce sont des problèmes centraux qui doivent absolument être résolus:
- certaines classes qui s'étendent se
Product
compilent malgré toujours des plantages à l'exécution
- il n'y a aucun moyen de passer des encodeurs personnalisés pour les types imbriqués (je n'ai aucun moyen de fournir à Spark un encodeur pour
MyObj
qu'il sache ensuite encoder Wrap[MyObj]
ou (Int,MyObj)
).
Juste utiliser kryo
La solution que tout le monde suggère est d'utiliser l' kryo
encodeur.
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)))
Cela devient vite assez fastidieux. Surtout si votre code manipule toutes sortes d'ensembles de données, jointures, regroupements, etc. Vous finissez par accumuler un tas d'implicits supplémentaires. Alors, pourquoi ne pas simplement faire un implicite qui fait tout cela automatiquement?
import scala.reflect.ClassTag
implicit def kryoEncoder[A](implicit ct: ClassTag[A]) =
org.apache.spark.sql.Encoders.kryo[A](ct)
Et maintenant, il semble que je puisse faire presque tout ce que je veux (l'exemple ci-dessous ne fonctionnera pas dans l' spark-shell
endroit où spark.implicits._
est automatiquement importé)
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!
Ou presque. Le problème est que l'utilisation kryo
de Spark conduit à stocker simplement chaque ligne de l'ensemble de données sous la forme d'un objet binaire plat. Pour map
, filter
, foreach
cela suffit, mais pour des opérations telles que join
, Spark a vraiment besoin de ceux - ci soient séparés en colonnes. En inspectant le schéma pour d2
ou d3
, vous voyez qu'il n'y a qu'une seule colonne binaire:
d2.printSchema
// root
// |-- value: binary (nullable = true)
Solution partielle pour les tuples
Donc, en utilisant la magie des implicits dans Scala (plus en 6.26.3 Overloading Resolution ), je peux me créer une série d'implicits qui feront le meilleur travail possible, au moins pour les tuples, et fonctionnera bien avec les implicits existants:
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
Ensuite, armé de ces implicits, je peux faire fonctionner mon exemple ci-dessus, mais avec un changement de nom de colonne
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")
Je ne l' ai pas encore compris comment obtenir les noms de tuple attendus ( _1
, _2
...) par défaut sans les renommer - si quelqu'un d' autre veut jouer avec cela, c'est où le nom se présente et c'est là tuple les noms sont généralement ajoutés. Cependant, le point clé est que j'ai maintenant un joli schéma structuré:"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)
Donc, en résumé, cette solution de contournement:
- nous permet d'obtenir des colonnes séparées pour les tuples (afin que nous puissions rejoindre à nouveau les tuples, yay!)
- on peut à nouveau se fier aux implicits (donc pas besoin de passer
kryo
partout)
- est presque entièrement rétrocompatible avec
import spark.implicits._
(avec certains changements de nom impliqués)
- ne nous permet pas de nous joindre sur les
kyro
colonnes binaires sérialisées, encore moins sur les champs
- a pour effet secondaire désagréable de renommer certaines des colonnes de tuple en «valeur» (si nécessaire, cela peut être annulé en convertissant
.toDF
, en spécifiant de nouveaux noms de colonnes et en les reconvertissant en un ensemble de données - et les noms de schéma semblent être préservés grâce aux jointures , là où ils sont le plus nécessaires).
Solution partielle pour les classes en général
Celui-ci est moins agréable et n'a pas de bonne solution. Cependant, maintenant que nous avons la solution de tuple ci-dessus, j'ai le sentiment que la solution de conversion implicite d'une autre réponse sera également un peu moins pénible puisque vous pouvez convertir vos classes plus complexes en tuples. Ensuite, après avoir créé l'ensemble de données, vous renommeriez probablement les colonnes en utilisant l'approche dataframe. Si tout se passe bien, c'est vraiment une amélioration puisque je peux désormais effectuer des jointures sur les champs de mes cours. Si j'avais juste utilisé un kryo
sérialiseur binaire plat, cela n'aurait pas été possible.
Voici un exemple qui fait un peu de tout: j'ai une classe MyObj
qui a des champs de types Int
, java.util.UUID
et Set[String]
. Le premier prend soin de lui-même. Le second, bien que je puisse sérialiser en utilisant kryo
serait plus utile s'il est stocké en tant que String
(puisque les UUID
s sont généralement quelque chose contre lequel je voudrais me joindre). Le troisième appartient vraiment à une colonne binaire.
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)
Maintenant, je peux créer un ensemble de données avec un joli schéma en utilisant cette machine:
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]
Et le schéma me montre des colonnes avec les bons noms et avec les deux premières choses contre lesquelles je peux me joindre.
d.printSchema
// root
// |-- i: integer (nullable = false)
// |-- u: string (nullable = true)
// |-- s: binary (nullable = true)
ExpressionEncoder
aide de la sérialisation JSON? Dans mon cas, je ne peux pas m'en sortir avec des tuples, et kryo me donne une colonne binaire ..