我们期待Spark 2.0,我们计划对数据集进行一些令人兴奋的改进,特别是: ... 自定义编码器 - 虽然我们目前为各种类型的自动生成编码器,但我们想为自定义对象打开一个API。
并尝试将自定义类型存储在Dataset
导致以下错误,如:
无法找到存储在数据集中的类型的编码器。导入sqlContext.implicits._支持原始类型(Int,String等)和产品类型(case类)。将来版本中将添加对序列化其他类型的支持
或:
Java.lang.UnsupportedOperationException:找不到....的编码器
是否有现成的解决方法?
请注意,此问题仅作为社区Wiki答案的入口点存在。随意更新/改进问题和答案。
答案 0 :(得分:200)
这个答案仍然有效且信息丰富,但自2.2 / 2.3以来现在情况更好,它为Set
,Seq
,Map
添加了内置编码器支持,{{1 },Date
和Timestamp
。如果你只坚持使用case类和通常的Scala类型来制作类型,你应该没有BigDecimal
中隐含的内容。
不幸的是,几乎没有添加任何东西来帮助解决这个问题。在Encoders.scala
或SQLImplicits.scala
中搜索SQLImplicits
会发现与原始类型(以及案例类的一些调整)有关的事情。所以,首先要说的是:目前对自定义类编码器没有真正的好支持。除此之外,接下来的是一些技巧,这些技巧可以做到我们希望的工作,考虑到我们目前拥有的工作。作为前期免责声明:这不会完美地发挥作用,我会尽最大努力使所有限制清晰明确。
当您想要创建数据集时,Spark"需要一个编码器(将T类型的JVM对象转换为内部Spark SQL表示形式),这通常是通过{{1}的含义自动创建的},或者可以通过调用@since 2.0.0
"上的静态方法显式创建; (取自docs on createDataset
)。编码器将采用SparkSession
形式,其中Encoders
是您要编码的类型。第一个建议是添加Encoder[T]
(它为您提供these隐式编码器),第二个建议是使用this一组编码器相关函数显式传入隐式编码器。
常规课程没有编码器,所以
T
将为您提供以下隐式相关编译时错误:
无法找到存储在数据集中的类型的编码器。导入sqlContext.implicits._支持原始类型(Int,String等)和产品类型(case类)。将来版本中将添加对序列化其他类型的支持
但是,如果你在一些扩展import spark.implicits._
的类中包装你刚才用来获取上述错误的任何类型,那么这个错误就会被混淆地延迟到运行时,所以
import spark.implicits._
class MyObj(val i: Int)
// ...
val d = spark.createDataset(Seq(new MyObj(1),new MyObj(2),new MyObj(3)))
编译得很好,但在运行时失败
java.lang.UnsupportedOperationException:找不到MyObj的编码器
原因是Spark使用implicits创建的编码器实际上只在运行时创建(通过scala relfection)。在这种情况下,编译时的所有Spark检查都是最外层的类扩展Product
(所有案例类都这样做),并且只在运行时实现它仍然不知道如何处理{{1 (如果我试图创建一个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))))
,则会出现同样的问题 - Spark等待运行时到Product
上的barf)。这些是迫切需要修复的核心问题:
MyObj
的一些类尽管总是在运行时崩溃并且Dataset[(Int,MyObj)]
提供Spark编码器,以便它知道如何编码MyObj
或{{1} })。Product
每个人建议的解决方案是使用kryo
编码器。
MyObj
但这很快就变得非常繁琐。特别是如果你的代码正在操纵各种数据集,加入,分组等等。你最终会产生一些额外的暗示。那么,为什么不做一个隐含的自动完成这一切呢?
Wrap[MyObj]
现在,似乎我几乎可以做任何我想做的事情(下面的例子在(Int,MyObj)
自动导入kryo
时无法工作
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)))
或差不多。问题是使用import scala.reflect.ClassTag
implicit def kryoEncoder[A](implicit ct: ClassTag[A]) =
org.apache.spark.sql.Encoders.kryo[A](ct)
导致Spark只将数据集中的每一行存储为平面二进制对象。对于spark-shell
,spark.implicits._
,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!
这已经足够了,但对于像kryo
这样的操作,Spark确实需要将它们分成列。检查map
或filter
的架构,您会看到只有一个二进制列:
foreach
所以,使用Scala中的隐含魔法(更多地在6.26.3 Overloading Resolution中),我可以使自己成为一系列的暗示,尽可能做好工作,至少对于元组来说,并且可以很好地与现有的含义:
join
然后,有了这些暗示,我可以让我的例子在上面工作,虽然有一些列重命名
d2
我还没有想出如何在没有重命名的情况下获得预期的元组名称(d3
,d2.printSchema
// root
// |-- value: binary (nullable = true)
,...) - 如果其他人想玩的话这个,this是引入名称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
的地方,this是通常添加元组名称的地方。但是,关键是我现在有一个很好的结构化架构:
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")
总而言之,这个解决方法:
_1
遍布所有地方)_2
(涉及一些重命名)"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)
,指定新列名称和转换回数据集来撤消这一点 - 并且模式名称似乎通过连接保留,最需要它们。)这个不太愉快,没有好的解决方案。但是,现在我们已经有了上面的元组解决方案,我有一个预感,另一个答案的隐式转换解决方案也会有点痛苦,因为你可以将更复杂的类转换为元组。然后,在创建数据集之后,您可能会使用数据框方法重命名列。如果一切顺利,那么真的是一种改进,因为我现在可以在我的类的字段上执行连接。如果我刚刚使用了一个无法实现的平面二进制kryo
序列化器。
这是一个做一些事情的例子:我有一个类import spark.implicits._
,其中包含kyro
,.toDF
和kryo
类型的字段。第一个照顾自己。第二个,虽然我可以使用MyObj
进行序列化,如果存储为Int
会更有用(因为java.util.UUID
通常是我想要加入的东西)。第三个真正属于二进制列。
Set[String]
现在,我可以使用这个机器创建一个具有良好模式的数据集:
kryo
模式向我展示了具有正确名称的列和前两个我可以加入的对象。
String
答案 1 :(得分:28)
使用通用编码器。
目前有两种通用编码器kryo
和javaSerialization
,其中后者明确描述为:
效率极低,只能作为最后的手段使用。
假设有以下课程
class Bar(i: Int) {
override def toString = s"bar $i"
def bar = i
}
您可以通过添加隐式编码器来使用这些编码器:
object BarEncoders {
implicit def barEncoder: org.apache.spark.sql.Encoder[Bar] =
org.apache.spark.sql.Encoders.kryo[Bar]
}
可以按如下方式一起使用:
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()
}
}
它将对象存储为binary
列,因此当转换为DataFrame
时,您将获得以下架构:
root
|-- value: binary (nullable = true)
也可以使用kryo
编码器为特定字段编码元组:
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]
请注意,我们不依赖于隐式编码器,而是明确地传递编码器,因此这很可能不适用于toDS
方法。
使用隐式转换:
提供可编码的表示与自定义类之间的隐式转换,例如:
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()
}
}
相关问题:
答案 2 :(得分:7)
您可以使用UDTRegistration,然后使用Case类,元组等...都可以正确使用您的用户定义类型!
说您要使用自定义枚举:
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
}
像这样注册它:
// 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)
然后使用它!
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())
说您要使用多态记录:
trait CustomPoly
case class FooPoly(id:Int) extends CustomPoly
case class BarPoly(value:String, secondValue:Long) extends CustomPoly
...以及这样的用法:
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()
您可以编写一个自定义的UDT,将所有内容编码为字节(我在这里使用Java序列化,但是最好检测Spark的Kryo上下文)。
首先定义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]
}
然后注册:
// 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)
那么您就可以使用它!
// 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()
答案 3 :(得分:5)
编码器在Spark2.0
中的工作方式大致相同。 Kryo
仍然是推荐的serialization
选项。
您可以使用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.
到目前为止]目前范围内没有appropriate encoders
,因此我们的人员未被编码为binary
值。但是,一旦我们使用implicit
序列化提供了一些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.
答案 4 :(得分:4)
对于Java Bean类,这可能很有用
import spark.sqlContext.implicits._
import org.apache.spark.sql.Encoders
implicit val encoder = Encoders.bean[MyClasss](classOf[MyClass])
现在您只需将dataFrame读取为自定义DataFrame
即可dataFrame.as[MyClass]
这将创建一个自定义类编码器而不是二进制编码器。
答案 5 :(得分:1)
我的示例将使用Java,但我不能想象它很难适应Scala。
只要RDD<Fruit>
是一个简单的spark.createDataset,我就可以使用Encoders.bean和Java Bean将Dataset<Fruit>
转换为Fruit
非常成功。< / p>
第1步:创建简单的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
}
我坚持使用基本类型和字符串作为字段的类,然后DataBricks人员加强他们的编码器。 如果您有一个具有嵌套对象的类,则创建另一个简单的Java Bean,其所有字段都被展平,因此您可以使用RDD转换将复杂类型映射到更简单的类型。确定它是&#39; sa一点点额外的工作,但我认为它对使用平面架构的性能有很大的帮助。
第2步:从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);
瞧!泡沫,冲洗,重复。
答案 6 :(得分:1)
对于那些可能在我情况下的人,我也在这里提出答案。
具体而言,
我正在阅读&#39;设置类型数据&#39;来自SQLContext。所以原始数据格式是DataFrame。
val sample = spark.sqlContext.sql("select 1 as a, collect_set(1) as b limit 1")
sample.show()
+---+---+
| a| b|
+---+---+
| 1|[1]|
+---+---+
然后使用带有mutable.WrappedArray类型的rdd.map()将其转换为RDD。
sample
.rdd.map(r =>
(r.getInt(0), r.getAs[mutable.WrappedArray[Int]](1).toSet))
.collect()
.foreach(println)
结果:
(1,Set(1))
答案 7 :(得分:0)
除了已经给出的建议外,我最近发现的另一个选择是您可以声明自定义类,包括特征org.apache.spark.sql.catalyst.DefinedByConstructorParams
。
如果该类的构造函数使用ExpressionEncoder可以理解的类型(即原始值和标准集合),则此方法有效。当您无法将该类声明为case类,但又不想每次将Kryo包含在数据集中时,都可以使用Kryo对其进行编码。
例如,我想声明一个包含Breeze矢量的案例类。唯一能够处理的编码器通常是Kryo。但是,如果我声明了一个扩展Breeze DenseVector和DefinedByConstructorParams的子类,则ExpressionEncoder可以将其序列化为Doubles数组。
这是我的声明方式:
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]
现在,我可以使用简单的ExpressionEncoder而不用Kryo在数据集中(直接或作为产品的一部分)使用SerializableDenseVector
。它就像Breeze DenseVector一样工作,但是序列化为Array [Double]。
答案 8 :(得分:0)
@Alec的答案很好!只是在他/她的答案的这一部分中添加评论:
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))))
@Alec提到:
无法传递用于嵌套类型的自定义编码器(我无法为Spark仅为MyObj提供编码器,这样它便知道如何编码Wrap [MyObj]或(Int,MyObj))。
似乎是这样,因为如果我为MyObj
添加编码器:
implicit val myEncoder = org.apache.spark.sql.Encoders.kryo[MyObj]
,它仍然失败:
java.lang.UnsupportedOperationException: No Encoder found for MyObj
- field (class: "MyObj", name: "unwrap")
- root class: "Wrap"
at org.apache.spark.sql.catalyst.ScalaReflection$$anonfun$org$apache$spark$sql$catalyst$ScalaReflection$$serializerFor$1.apply(ScalaReflection.scala:643)
但是请注意重要的错误消息:
根类:“包装”
它实际上提示仅对MyObj
进行编码是不够的,您必须对整个链进行编码,其中包括Wrap[T]
。
因此,如果我这样做,它将解决问题:
implicit val myWrapperEncoder = org.apache.spark.sql.Encoders.kryo[Wrap[MyObj]]
因此,@ Alec的评论不是真的:
我无法仅为MyObj馈送Spark编码器,这样它就知道如何编码Wrap [MyObj]或(Int,MyObj)
我们仍然有一种方法可以为MyObj
提供Spark编码器,从而使它知道如何对Wrap [MyObj]或(Int,MyObj)进行编码。