web-dev-qa-db-fra.com

Comment stocker des objets personnalisés dans Dataset?

Selon Introduction aux jeux de données Spark :

Dans l’attente de Spark 2.0, nous prévoyons des améliorations intéressantes pour les jeux de données, notamment: ... Encodeurs personnalisés: bien que nous générions automatiquement des encodeurs pour une grande variété de types, nous aimerions ouvrir une API pour les objets personnalisés.

et les tentatives de stockage de type personnalisé dans un Dataset entraînent une erreur suivante, telle que:

Impossible de trouver le codeur pour le type stocké dans un jeu de données. Les types primitifs (Int, String, etc.) et les types Product (classes de cas) sont pris en charge lors de l'importation de sqlContext.implicits._ La prise en charge de la sérialisation d'autres types sera ajoutée dans les prochaines versions.

ou:

Java.lang.UnsupportedOperationException: aucun encodeur trouvé pour ....

Existe-t-il des solutions de contournement existantes?


Notez que cette question existe uniquement en tant que point d’entrée pour une réponse au wiki de communauté. N'hésitez pas à mettre à jour/améliorer les questions et les réponses.

121
zero323
  1. Utiliser des encodeurs génériques.

    Il existe actuellement deux codeurs génériques disponibles kryo et javaSerialization , ce dernier étant explicitement décrit comme suit:

    extrêmement inefficace et ne devrait être utilisé qu'en dernier recours.

    En supposant que le cours suivant

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

    vous pouvez utiliser ces encodeurs en ajoutant un encodeur implicite:

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

    qui peuvent être utilisés ensemble comme suit:

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

    Il stocke les objets sous la forme d'une colonne binary. Ainsi, lors de la conversion en DataFrame, vous obtenez le schéma suivant:

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

    Il est également possible de coder des n-uplets en utilisant le codeur kryo pour un champ spécifique:

    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]
    

    Veuillez noter que nous ne dépendons pas des encodeurs implicites ici, mais passons explicitement l'encodeur afin que cela ne fonctionne probablement pas avec la méthode toDS

  2. Utilisation de conversions implicites:

    Fournissez des conversions implicites entre des représentations pouvant être codées et des classes personnalisées, par exemple:

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

Questions connexes:

28
zero323

Vous pouvez utiliser UDTRegistration puis les classes de cas, les tuples, etc. fonctionnent correctement avec votre type défini par l'utilisateur!

Disons que vous voulez utiliser un Enum personnalisé:

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
}

Enregistrez-le comme ceci:

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

Alors UTILISEZ-LE!

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

Disons que vous voulez utiliser un disque polymorphe:

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

... et l'utilise comme ceci:

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

Vous pouvez écrire un UDT personnalisé qui code tout en octets (j'utilise la sérialisation Java ici, mais il vaut probablement mieux instrumenter le contexte Kryo de Spark).

Définissez d'abord 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]
}

Puis enregistrez-le:

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

Ensuite, vous pouvez l'utiliser!

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

Les encodeurs fonctionnent plus ou moins de la même manière dans Spark2.0. Et Kryo est toujours le choix serialization recommandé.

Vous pouvez regarder l'exemple suivant avec 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.

Jusqu'à présent] il n'y avait pas de appropriate encoders dans la portée actuelle, donc nos personnes n'étaient pas codées comme des valeurs binary. Mais cela changera une fois que nous aurons fourni des encodeurs implicit utilisant la sérialisation Kryo.

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

En cas de classe Java Bean, cela peut être utile

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

Maintenant, vous pouvez simplement lire le dataFrame en tant que DataFrame personnalisé 

dataFrame.as[MyClass]

Cela créera un encodeur de classe personnalisé et non un encodeur binaire. 

4
Akash Mahajan

Pour ceux qui peuvent dans ma situation, je mets ma réponse ici aussi.

Pour être précis,

  1. Je lisais 'Set typed data' dans SQLContext. Le format de données d'origine 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]| +---+---+

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

1
Taeheon Kwon

_ {Mes exemples seront en Java, mais je ne pense pas qu'il soit difficile de s'adapter à Scala.}

J'ai réussi à convertir RDD<Fruit> en Dataset<Fruit> en utilisant spark.createDataset et Encoders.bean tant que Fruit est un simple Java Bean .

Étape 1: Créez le bean Java simple.

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
}

Je me contenterais des classes avec des types primitifs et String en tant que champs avant que les utilisateurs de DataBrick ne renforcent leurs encodeurs. Si vous avez une classe avec un objet imbriqué, créez un autre bean Java simple avec tous ses champs aplati afin de pouvoir utiliser les transformations RDD pour mapper le type complexe au type le plus simple. Bien sûr, c'est un travail supplémentaire, mais j'imagine que cela aidera beaucoup sur la performance de travailler avec un schéma plat.

Étape 2: Obtenez votre jeu de données à partir du 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);

Et le tour est joué! Faire mousser, rincer, répéter.

1
Jimmy Da

En plus des suggestions déjà données, une autre option que j'ai découverte récemment est que vous pouvez déclarer votre classe personnalisée, y compris le trait org.Apache.spark.sql.catalyst.DefinedByConstructorParams.

Cela fonctionne si la classe a un constructeur qui utilise des types qu'ExpressionEncoder peut comprendre, c'est-à-dire des valeurs primitives et des collections standard. Cela peut s'avérer utile lorsque vous n'êtes pas en mesure de déclarer la classe en tant que classe de cas, mais que vous ne souhaitez pas utiliser le kryo pour l'encoder à chaque fois qu'il est inclus dans un jeu 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 Breeze DenseVector et DefinedByConstructorParams, ExpressionEncoder comprenait qu’elle pouvait être sérialisée en tant que tableau de Doubles.

Voici comment je l'ai déclaré:

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]

Maintenant, je peux utiliser SerializableDenseVector dans un jeu de données (directement ou dans le cadre d'un produit) en utilisant un simple ExpressionEncoder et aucun Kryo. Cela fonctionne comme un Breeze DenseVector mais se sérialise comme un tableau [Double].

0
Matt