Как сохранить пользовательские объекты в наборе данных?

В соответствии с введением наборов данных Spark:

В преддверии Spark 2.0 мы планируем несколько интересных улучшений в наборах данных, в частности:... Пользовательские кодировщики - в то время как в настоящее время мы автоматически генерируем кодировщики для широкого спектра типов, нам хотелось бы открыть API для пользовательских объектов.

и попытки сохранить пользовательский тип в Dataset приводят к следующей ошибке, такой как:

Невозможно найти кодировщик для типа, хранящегося в наборе данных. Примитивные типы (Int, String и т.д.) И типы Product (классы дел) поддерживаются путем импорта sqlContext.implicits._ Поддержка сериализации других типов будет добавлена в будущих выпусках.

или же:

Java.lang.UnsupportedOperationException: не найден кодировщик для....

Существуют ли обходные пути?


Обратите внимание, что этот вопрос существует только в качестве отправной точки для ответа сообщества Wiki. Не стесняйтесь обновлять/улучшать как вопрос, так и ответ.

Ответ 1

Update

Этот ответ по-прежнему остается актуальным и информативным, хотя теперь все лучше и лучше с 2.2/2.3, что добавляет поддержку встроенного энкодера для Set, Seq, Map, Date, Timestamp и BigDecimal. Если вы придерживаетесь создания типов только для классов case и обычных типов Scala, вы должны быть в порядке с неявным в SQLImplicits.


К сожалению, практически ничего не добавлено, чтобы помочь в этом. Поиск @since 2.0.0 в Encoders.scala или SQLImplicits.scala находит вещи в основном для примитивных типов (и некоторой настройки классов case). Итак, первое, что можно сказать: , в настоящее время нет реальной хорошей поддержки кодировщиков пользовательского класса. Из-за этого следует, что некоторые из трюков, которые делают так же хорошо, как мы можем когда-либо надеяться, учитывая то, что у нас есть в настоящее время. Как предварительный отказ от ответственности: это не сработает отлично, и я сделаю все возможное, чтобы сделать все ограничения понятными и заранее.

В чем именно проблема

Если вы хотите создать набор данных, Spark "требует кодировщика (для преобразования объекта JVM типа T в и из внутреннего представления Spark SQL), который обычно создается автоматически через implicits от SparkSession, или может быть созданный явно путем вызова статических методов на Encoders" (взято из docs на createDataset). Кодер примет форму Encoder[T], где 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)))

предоставит вам следующую неявную связанную ошибку времени компиляции:

Невозможно найти кодировщик для типа, хранящегося в наборе данных. Примитивные типы (Int, String и т.д.) И типы продуктов (классы case) поддерживаются при импорте sqlContext.implicits._ Поддержка сериализации других типов будет добавлена ​​в будущих выпусках

Однако, если вы обертываете любой тип, который вы использовали для получения вышеуказанной ошибки в каком-то классе, который расширяет 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: No Encoder не найден для MyObj

Причина этого в том, что кодеры, создаваемые Spark с implicits, фактически выполняются только во время выполнения (через Scala relfection). В этом случае все проверки Spark во время компиляции заключаются в том, что внешний класс расширяет Product (что все классы классов) и реализует только во время выполнения, что он все еще не знает, что делать с MyObj (та же проблема возникает, если я попытался сделать Dataset[(Int,MyObj)] - Spark ждет, пока время выполнения не будет установлено на MyObj). Это центральные проблемы, которые остро нуждаются в исправлении:

  • некоторые классы, которые расширяют Product, компилируются, несмотря на всегда сбой во время выполнения и
  • В пользовательских кодировщиках нет способов передачи вложенных типов (у меня нет возможности кормить Spark encoder только для MyObj, чтобы он знал, как кодировать Wrap[MyObj] или (Int,MyObj)).

Просто используйте kryo

Решение, предлагаемое всеми, заключается в использовании kryo encoder.

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!

Или почти. Проблема в том, что использование kryo приводит к тому, что Spark просто сохраняет каждую строку в наборе данных как плоский двоичный объект. Для Map, filter, foreach этого достаточно, но для операций, подобных join, Spark действительно нуждается в том, чтобы их разделили на столбцы. Проверяя схему для d2 или d3, вы видите, что есть только один двоичный столбец:

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

Частичное решение для кортежей

Итак, используя магию implicits в 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)

Итак, вкратце, это обходное решение:

  • позволяет нам получить отдельные столбцы для кортежей (так что мы снова можем присоединиться к кортежам, yay!)
  • мы снова можем просто полагаться на имплициты (поэтому нет необходимости проходить в kryo по всему месту)
  • почти полностью обратно совместим с import spark.implicits._ (с некоторым переименованием)
  • не позволяет нам присоединяться к сериализованным двоичным столбцам kyro, не говоря уже о полях, которые могут иметь
  • имеет неприятный побочный эффект переименования некоторых столбцов кортежа в значение "значение" (при необходимости это можно отменить, преобразование .toDF, определение новых имен столбцов и преобразование обратно в набор данных - и имена схем похоже, сохраняются через соединения, где они наиболее необходимы).

Частичное решение для классов вообще

Этот менее приятный и не имеет хорошего решения. Однако теперь, когда у нас есть решение кортежа выше, у меня есть догадка, что неявное решение для преобразования из другого ответа будет немного менее болезненным, так как вы можете преобразовать более сложные классы в кортежи. Затем, создав набор данных, вы, вероятно, переименуете столбцы, используя подход dataframe. Если все идет хорошо, это действительно улучшается, так как теперь я могу выполнять объединения в полях моих классов. Если бы я просто использовал один плоский двоичный сериализатор kryo, который не был бы возможен.

Вот пример, который выполняет немного всего: у меня есть класс MyObj, который имеет поля типов Int, java.util.UUID и Set[String]. Первый заботится о себе. Второй, хотя я мог бы сериализовать использование kryo, был бы более полезен, если бы он был сохранен как String (так как UUID, как правило, я хочу присоединиться к нему). Третий действительно просто принадлежит двоичному столбцу.

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]

И схема показывает мне столбцы с правильными именами и двумя первыми, с чем я могу присоединиться.

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

Ответ 2

  • Использование универсальных кодеров.

    На данный момент доступны два общих кодера 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 encoder для определенного поля:

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

Похожие вопросы:

Ответ 3

Вы можете использовать UDTRegistration, а затем Case Classes, Tuples и т.д.... все правильно работает с вашим Пользовательским типом!

Предположим, вы хотите использовать пользовательское Enum:

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

Ответ 4

Кодеры работают более или менее одинаково в Spark2.0. И Kryo по-прежнему является рекомендуемым выбором serialization.

Вы можете посмотреть следующий пример с искровой оболочкой

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.

Ответ 5

В случае класса 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]

Это создаст собственный кодировщик классов, а не двоичный.

Ответ 6

Мои примеры будут на Java, но я не думаю, что это трудно адаптироваться к Scala.

Мне удалось успешно преобразовать RDD<Fruit> в Dataset<Fruit> с помощью spark.createDataset и Encoders.bean, пока Fruit является простым Java Bean.

Шаг 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
}

Я бы придерживался классов с примитивными типами и String как поля перед тем, как люди DataBricks повышают свои кодеры. Если у вас есть класс с вложенным объектом, создайте еще один простой 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);

И вуаля! Намочите, промойте, повторите.

Ответ 7

Для тех, кто может в моей ситуации, я тоже здесь отвечу.

Чтобы быть конкретным,

  1. Я читал "Set typed data" из 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. Затем преобразуйте его в RDD, используя rdd.map() с mutable.WrappedArray.

    sample.rdd.map(r => (r.getInt(0), r.getAs[mutable.WrappedArray[Int]](1).toSet)).collect().foreach(println)

    Результат:

    (1,Set(1))

Ответ 8

В дополнение к уже представленным предложениям, еще один вариант, который я недавно обнаружил, заключается в том, что вы можете объявить свой собственный класс, включая черту org.apache.spark.sql.catalyst.DefinedByConstructorParams.

Это работает, если класс имеет конструктор, который использует типы, которые может понимать ExpressionEncoder, то есть примитивные значения и стандартные коллекции. Это может пригодиться, когда вы не можете объявить класс как класс case, но не хотите использовать Kryo для его кодирования каждый раз, когда он входит в набор данных.

Например, я хотел объявить класс case, который включал вектор Бриза. Единственный кодировщик, который мог бы обрабатывать это, как правило, 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 и no Kryo. Он работает так же, как Breeze DenseVector, но сериализуется как массив [Double].