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 ..Utilisation d'encodeurs génériques.
Il existe deux encodeurs génériques disponibles pour l'instant
kryo
etjavaSerialization
où le dernier est explicitement décrit comme:En supposant la classe suivante
vous pouvez utiliser ces encodeurs en ajoutant un encodeur implicite:
qui peuvent être utilisés ensemble comme suit:
Il stocke les objets sous forme de
binary
colonne, de sorte qu'une fois converti,DataFrame
vous obtenez le schéma suivant:Il est également possible d'encoder des tuples en utilisant un
kryo
encodeur pour un champ spécifique:Veuillez noter que nous ne dépendons pas des encodeurs implicites ici, mais transmettons explicitement l'encodeur, donc cela ne fonctionnera probablement pas avec la
toDS
méthode.Utilisation de conversions implicites:
Fournissez des conversions implicites entre la représentation qui peut être encodée et la classe personnalisée, par exemple:
Questions connexes:
la source
Set
) que j'obtiensException in thread "main" java.lang.UnsupportedOperationException: No Encoder found for Set[Bar]
.kryo[Set[Bar]]
. De la même manière si la classe contient un champ,Bar
vous avez besoin d'un encodeur pour un objet entier. Ce sont des méthodes très grossières.Bar
vous avez besoin d'un encodeur pour un objet entier". ma question était de savoir comment encoder ce "projet entier"?Vous pouvez utiliser UDTRegistration, puis les classes de cas, les tuples, etc. fonctionnent tous correctement avec votre type défini par l'utilisateur!
Supposons que vous souhaitiez utiliser un Enum personnalisé:
Enregistrez-le comme ceci:
Alors UTILISEZ-LE!
Supposons que vous souhaitiez utiliser un enregistrement polymorphe:
... et utilisez-le comme ceci:
Vous pouvez écrire un UDT personnalisé qui encode tout en octets (j'utilise la sérialisation java ici mais il est probablement préférable d'instrumenter le contexte Kryo de Spark).
Définissez d'abord la classe UDT:
Puis enregistrez-le:
Ensuite, vous pouvez l'utiliser!
la source
Les encodeurs fonctionnent plus ou moins de la même manière dans
Spark2.0
. EtKryo
c'est toujours le recommandéserialization
choix .Vous pouvez regarder l'exemple suivant avec spark-shell
Jusqu'à présent] il n'y avait pas
appropriate encoders
de portée actuelle, donc nos personnes n'étaient pas encodées en tant quebinary
valeurs. Mais cela changera une fois que nous fournirons certainsimplicit
encodeurs utilisant laKryo
sérialisation.la source
Dans le cas de la classe Java Bean, cela peut être utile
Vous pouvez maintenant simplement lire le dataFrame en tant que DataFrame personnalisé
Cela créera un encodeur de classe personnalisé et non un encodeur binaire.
la source
Mes exemples seront en Java, mais je n'imagine pas qu'il soit difficile de s'adapter à Scala.
J'ai réussi à convertir
RDD<Fruit>
enDataset<Fruit>
utilisant spark.createDataset et Encoders.bean tant qu'ilFruit
s'agit d'un simple Java Bean .Étape 1: Créez le simple Java Bean.
Je m'en tiendrais aux classes avec des types primitifs et String comme champs avant que les gens de DataBricks renforcent leurs encodeurs. Si vous avez une classe avec un objet imbriqué, créez un autre Java Bean simple avec tous ses champs aplatis, de sorte que vous puissiez utiliser les transformations RDD pour mapper le type complexe vers le plus simple.Bien sûr, c'est un peu de travail supplémentaire, mais j'imagine que cela aidera beaucoup sur les performances de travailler avec un schéma plat.
Étape 2: Obtenez votre ensemble de données à partir du RDD
Et voila! Faire mousser, rincer, répéter.
la source
Pour ceux qui peuvent dans ma situation, je mets ma réponse ici aussi.
Pour être précis,
Je lisais «Définir les données typées» de SQLContext. Le format de données original est donc DataFrame.
val sample = spark.sqlContext.sql("select 1 as a, collect_set(1) as b limit 1") sample.show()
+---+---+ | a| b| +---+---+ | 1|[1]| +---+---+
Puis convertissez-le en RDD en utilisant rdd.map () avec le type mutable.WrappedArray.
sample .rdd.map(r => (r.getInt(0), r.getAs[mutable.WrappedArray[Int]](1).toSet)) .collect() .foreach(println)
Résultat:
(1,Set(1))
la source
En plus des suggestions déjà données, une autre option que j'ai récemment découverte est que vous pouvez déclarer votre classe personnalisée en incluant le trait
org.apache.spark.sql.catalyst.DefinedByConstructorParams
.Cela fonctionne si la classe a un constructeur qui utilise des types que ExpressionEncoder peut comprendre, c'est-à-dire des valeurs primitives et des collections standard. Cela peut être utile lorsque vous ne pouvez pas déclarer la classe en tant que classe de cas, mais que vous ne voulez pas utiliser Kryo pour l'encoder chaque fois qu'il est inclus dans un ensemble de données.
Par exemple, je voulais déclarer une classe de cas qui incluait un vecteur Breeze. Le seul encodeur capable de gérer cela serait normalement Kryo. Mais si je déclarais une sous-classe qui étendait le Breeze DenseVector et DefinedByConstructorParams, le ExpressionEncoder comprenait qu'il pouvait être sérialisé comme un tableau de Doubles.
Voici comment je l'ai déclaré:
Maintenant, je peux utiliser
SerializableDenseVector
dans un ensemble de données (directement ou dans le cadre d'un produit) en utilisant un simple ExpressionEncoder et pas de Kryo. Il fonctionne exactement comme un Breeze DenseVector mais se sérialise comme un Array [Double].la source