如何在数据集中存储自定义对象?


149

根据介绍Spark数据集

当我们期待Spark 2.0时,我们计划对数据集进行一些激动人心的改进,特别是:...自定义编码器–虽然我们目前可以自动生成多种类型的编码器,但我们希望为自定义对象打开一个API。

并尝试在Dataset导致以下错误的情况下存储自定义类型:

找不到用于存储在数据集中的类型的编码器。导入sqlContext.implicits。支持基本类型(Int,String等)和产品类型(案例类)。_在将来的版本中将添加对序列化其他类型的支持。

要么:

Java.lang.UnsupportedOperationException:未找到...的编码器。

是否有任何现有的解决方法?


请注意,此问题仅作为社区Wiki回答的切入点存在。随时更新/改善问题和答案。

Answers:


240

更新资料

这个答案仍然是有效的和翔实的,但事已至此更好的是,因为2.2 / 2.3,它加入了内置编码器的支持SetSeqMapDateTimestamp,和BigDecimal。如果您坚持只使用case类和常用的Scala类型来创建类型,那么只使用隐式in就可以了SQLImplicits


不幸的是,几乎没有任何东西可以帮助您。@since 2.0.0在in中搜索Encoders.scalaSQLImplicits.scala发现与原始类型(以及对case类的一些调整)有关的事情。因此,首先要说的是:当前没有对自定义类编码器的真正好的支持。鉴于此,考虑到我们目前掌握的一切,接下来的一些技巧将尽我们所能完成。作为预先的免责声明:这将无法完美运行,并且我会尽力使所有限制都明确并预先提出。

到底是什么问题

当您要创建数据集时,Spark“需要一个编码器(以将T类型的JVM对象与内部Spark SQL表示形式相互转换),该编码器通常是通过的隐式自动创建的SparkSession,或者可以通过调用静态方法来显式创建的在上Encoders(取自上的文档createDataset)。编码器的格式为Encoder[T]where T是您要编码的类型。第一个建议是添加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)))

将为您提供以下隐式相关的编译时错误:

找不到用于存储在数据集中的类型的编码器。导入sqlContext.implicits。支持基本类型(Int,String等)和产品类型(案例类)。_在将来的版本中将添加对序列化其他类型的支持。

但是,如果将任何用于包装上述错误的类型包装在某个extends类中Product,则该错误会很容易地延迟到运行时,因此

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

编译很好,但是在运行时失败

java.lang.UnsupportedOperationException:找不到MyObj的编码器

这样做的原因是,Spark使用隐式创建的编码器实际上仅在运行时(通过scala relfection)制成。在这种情况下,所有Spark在编译时的检查都是最外层的类扩展Product(所有大小写类都这样做),并且仅在运行时才意识到它仍然不知道如何处理MyObj(如果我尝试执行此操作,则会出现相同的问题a Dataset[(Int,MyObj)]-Spark等待,直到运行时发出声音MyObj。这些是亟需解决的核心问题:

  • Product尽管扩展始终在运行时崩溃,但某些扩展了编译的类并
  • 没有办法为嵌套类型传入自定义编码器(我没有办法为Spark提供编码器,MyObj以便它随后知道如何编码Wrap[MyObj](Int,MyObj))。

只需使用 kryo

每个人都建议的解决方案是使用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-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!

或差不多。问题在于,使用kryoLead导致Spark仅将数据集中的每一行存储为平面二进制对象。对于mapfilterforeach那就足够了,但对于像操作join,星火真的需要这些被分隔成列。检查d2或的架构d3,您会看到只有一个二进制列:

d2.printSchema
// root
//  |-- value: binary (nullable = true)

元组的部分解决方案

因此,使用Scala中的隐式魔术(在6.26.3重载分辨率中有更多信息),我可以使自己成为一系列隐式,它们至少在元组上会做得更好,并且可以与现有隐式一起很好地工作:

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

然后,使用这些隐式函数,尽管可以重命名某些列,但我可以使上面的示例正常工作

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,其具有的类型的字段Intjava.util.UUID以及Set[String]。首先照顾自己。第二,尽管我可以序列化使用,kryo如果将其存储为a则将更加有用String(因为UUIDs通常是我想加入的对象)。第三个实际上只是属于一个二进制列。

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)

现在,我可以使用以下机制创建具有良好架构的数据集:

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]

该模式向我显示了具有正确名称的I列,以及前两个可以加入的内容。

d.printSchema
// root
//  |-- i: integer (nullable = false)
//  |-- u: string (nullable = true)
//  |-- s: binary (nullable = true)

是否可以ExpressionEncoder使用JSON序列化创建自定义类?在我的情况,我不能逃脱元组,并KRYO给了我一个二进制列...
阿列克谢Svyatkovskiy

1
@AlexeyS我不这么认为。但是你为什么要那样?为什么您无法摆脱我提出的最后一个解决方案?如果您可以将数据放入JSON中,则应该能够提取字段并将其放入案例类中……
Alec

1
不幸的是,这个答案的底线是没有可行的解决方案。
baol

@baol之类的。但是请记住,Spark的工作有多么困难。Scala的类型系统根本不够强大,无法“递归”遍历字段的编码器。坦白说,我很惊讶没有人为此创建注释宏。似乎是自然(但困难)的解决方案。
亚历克

1
@combinatorist我的理解是,从性能角度来看,数据集和数据帧(而不是RDD,因为它们不需要编码器!)是等效的。不要低估数据集的类型安全性!仅仅因为Spark内部使用了大量反射,强制转换等,并不意味着您不应该在乎所公开接口的类型安全性。但这确实使我对创建自己的基于Dataset的类型安全函数感到更好,该函数在后台使用Dataframes。
亚历克

32
  1. 使用通用编码器。

    有现在两个可用通用的编码器kryojavaSerialization其中后者明确地描述为:

    效率极低,只能作为不得已的手段。

    假设下课

    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]

    请注意,这里我们不依赖于隐式编码器,而是显式地传递编码器,因此这很可能无法与toDSmethod一起使用。

  2. 使用隐式转换:

    在可以编码的表示形式和自定义类之间提供隐式转换,例如:

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

相关问题:


解决方案1似乎不适用于Set我至少获得的类型化集合Exception in thread "main" java.lang.UnsupportedOperationException: No Encoder found for Set[Bar]
维克多·P。16年

@VictorP。预计恐怕如果这样你所需要的特定类型的编码器(kryo[Set[Bar]]如果类包含一个字段相同的方式。Bar你需要编码器以用于整个对象这是非常野蛮的方法。
zero323

@ zero323我面临着同样的问题。您可以举一个如何对整个项目进行编码的代码示例吗?非常感谢!
摇滚

@Rock我不确定您所说的“整个项目”是什么意思
zero323 '16

根据您的评论@ zero323,“如果类包含一个字段Bar,则需要整个对象的编码器”。我的问题是如何对这个“整个项目”进行编码?
摇滚

9

您可以使用UDTRegistration,然后使用Case类,元组等...都可以与您的User Defined Type一起正常使用!

假设您要使用自定义枚举:

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

1
我看不到您的Kryo在哪里使用(在CustomPolyUDT中)
Mathieu

我正在尝试在我的项目中定义UDT,并且收到此错误“无法从此位置访问符号UserDefinedType”。有什么帮助吗?
Rijo Joseph

嗨@RijoJoseph。您需要在项目中制作一个包org.apache.spark并将UDT代码放入其中。
ChoppyTheLumberjack

6

编码器在中的工作原理大致相同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.

3

如果是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]

这将创建一个自定义类编码器,而不是二进制编码器。


1

我的示例将使用Java,但是我不认为要适应Scala很难。

只要是简单的Java Bean,我就已经非常成功地转换RDD<Fruit>Dataset<Fruit>使用spark.createDatasetEncoders.beanFruit

步骤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人员加强他们的编码器之前,我会坚持使用具有原始类型和String作为字段的类。如果您有一个带有嵌套对象的类,请创建另一个所有字段都展平的简单Java Bean,以便可以使用RDD转换将复杂类型映射到更简单的类型。当然,这需要一些额外的工作,但是我想这将对使用平面模式的性能有很大帮助。

步骤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);

瞧!泡沫,冲洗,重复。


我建议指出,对于简单结构,最好将它们存储在本机Spark类型中,而不是将它们序列化为Blob。它们在Python网关上的工作效果更好,在Parquet中更透明,甚至可以转换为相同形状的结构。
metasim '18

1

对于那些可能遇到我的人,我也将我的答案放在这里。

再具体一点,

  1. 我正在从SQLContext中读取“设置类型的数据”。因此原始数据格式为DataFrame。

    val sample = spark.sqlContext.sql("select 1 as a, collect_set(1) as b limit 1") sample.show()

    +---+---+ | a| b| +---+---+ | 1|[1]| +---+---+

  2. 然后使用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))


0

除了已经给出的建议之外,我最近发现的另一个选择是您可以声明包括trait的自定义类org.apache.spark.sql.catalyst.DefinedByConstructorParams

如果该类的构造函数使用ExpressionEncoder可以理解的类型,即原始值和标准集合,则此方法有效。当您无法将该类声明为case类,但又不想每次将Kryo包含在Dataset中时,都不想使用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]

现在,我可以使用SerializableDenseVector简单的ExpressionEncoder而不使用Kryo在数据集中使用(直接或作为产品的一部分)。它就像Breeze DenseVector一样工作,但是序列化为Array [Double]。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.