Як зберігати власні об’єкти в наборі даних?


149

Відповідно до введення наборів даних Spark :

Очікуючи Spark 2.0, ми плануємо кілька захоплюючих удосконалень наборів даних, зокрема: ... Спеціальні кодери - поки ми автоматично автогенеруємо кодери для найрізноманітніших типів, ми хотіли б відкрити API для спеціальних об'єктів.

і спроби зберігати користувальницький тип Datasetпризводить до наступних помилок, таких як:

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

або:

Java.lang.UnsupportedOperationException: Кодер не знайдено для ....

Чи існують якісь обхідні шляхи?


Зауважте, це питання існує лише як вхідний пункт для відповіді Wiki Wiki. Не соромтеся оновлювати / покращувати питання і відповіді.

Відповіді:


240

Оновлення

Відповідь на це питання залишається в силі і інформативний, хоча речі тепер краще , так як 2.2 / 2.3, який додає вбудовану підтримку енкодера для Set, Seq, Map, Date, Timestamp, і BigDecimal. Якщо ви дотримуєтесь створення типів лише з класів випадків та звичайних типів Scala, вам слід добре ввести лише те, що неявно введено SQLImplicits.


На жаль, практично нічого не додано, щоб допомогти у цьому. Пошук @since 2.0.0в Encoders.scalaабо SQLImplicits.scalaзнахідку речі , в основному , пов'язані з примітивними типами (і деяких настройками тематичних класів). Отже, перше, що потрібно сказати: в даний час немає реальної хорошої підтримки для кодерів спеціальних класів . Якщо це не виходить, то, що випливає, - це кілька хитрощів, які роблять так само хорошу роботу, на яку ми можемо сподіватися, враховуючи те, що ми маємо в своєму розпорядженні. Як попередня відмова від відповідальності: це не спрацює ідеально, і я зроблю все можливе, щоб усі обмеження були чіткими і вперед.

У чому саме проблема

Коли ви хочете створити набір даних, Spark "вимагає кодера (для перетворення об'єкта JVM типу T до внутрішнього представлення Spark SQL і), який, як правило, створюється автоматично через імпліцити з SparkSession, або може бути створений явно за допомогою статичних методів на Encoders"(взято з док. на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 тощо) та типи продуктів (класи випадків) підтримуються імпортом 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: Кодер для MyObj не знайдено

Причиною цього є те, що енкодери, які створює Іскра з імпліцитами, насправді робляться лише під час виконання (за допомогою масштабування). У цьому випадку всі перевірки Spark під час компіляції полягають у тому, що найвіддаленіший клас розширюється Product(що роблять усі класи випадків) і лише під час виконання розуміє, що він все ще не знає, що робити MyObj(така ж проблема виникає, якщо я намагався зробити a Dataset[(Int,MyObj)]- Іскра чекає, поки триває час запуску MyObj). Це основні проблеми, які гостро потребують виправлення:

  • деякі класи, які розширюють Productкомпіляцію, незважаючи на те, що завжди відбувається збій під час виконання та
  • немає способу передачі користувальницьких кодерів для вкладених типів (я не можу подавати іскровий кодер саме для 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!

Або майже. Проблема полягає в тому, що використання kryoприводить до Spark просто зберігає кожен рядок у наборі даних як плоский бінарний об'єкт. Для map, filter, foreachщо досить, але для таких операцій , як 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який має поле типів Int, java.util.UUIDі Set[String]. Перший піклується про себе. Другий, хоча я можу серіалізувати за допомогою, kryoбув би більш корисним, якщо він зберігається як 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 дає мені бінарний стовпчик ..
Олексій Святковський

1
@AlexeyS Я так не думаю. Але чому б ти цього хотів? Чому ви не можете піти з останнього запропонованого нами рішення? Якщо ви можете розмістити свої дані в JSON, ви маєте змогу витягнути поля та помістити їх у клас справ ...
Alec

1
На жаль, суть цієї відповіді полягає в тому, що рішення не працює.
baol

@baol Сортування. Але пам’ятайте, як важко, що робить Іскра. Система типу Scala просто недостатньо потужна для "отримання" кодерів, які рекурсивно проходять через поля. Чесно кажучи, я просто здивований, що ніхто не робив для цього макросу анотації. Схоже, природне (але важке) рішення.
Алек

1
@combinatorist Моє розуміння полягає в тому, що набори даних і фрейми даних (але не RDD, оскільки вони не потребують кодерів!) еквівалентні з точки зору продуктивності. Не занижуйте безпеку типу наборів даних! Тільки тому, що Spark використовує внутрішню тону роздумів, роликів тощо, це не означає, що вам не варто дбати про безпеку типу інтерфейсу, який піддається впливу. Але мені стає краще, коли я створюю власні безпечні функції на основі набору даних, які використовують Dataframes під кришкою.
Алек

32
  1. Використання загальних кодерів.

    Є два загальних кодери , доступні на даний момент 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методом.

  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].
Віктор П.

@VictorP. Очікується, я боюся. У такому випадку вам знадобиться кодер для конкретного типу ( kryo[Set[Bar]]так само, якщо клас містить поле, Barвам потрібен кодер для цілого об'єкта. Це дуже грубі методи.
zero323

@ zero323 Я зіткнувся з тією ж проблемою. Чи можете ви навести приклад коду, як кодувати весь проект? Велике дякую!
Рок

@Rock Я не впевнений, що ти маєш на увазі під "цілим проектом"
zero323

@ zero323 за вашим коментарем, "якщо клас містить поле, Barвам потрібен кодер для цілого об'єкта". моє запитання полягало в тому, як закодувати цей "весь проект"?
Рок

9

Ви можете використовувати UDTRegistration, а потім класи кейсів, кортежі тощо ... і все правильно працює з вашим визначеним користувачем типом!

Скажіть, що ви хочете використовувати спеціальний 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 серіалізацію, але, мабуть, краще зафіксувати контекст Kryo Spark).

Спочатку визначте клас 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
Я не бачу, де використовується ваш крио (у CustomPolyUDT)
mathieu

Я намагаюся визначити UDT у своєму проекті, і я отримую цю помилку "Symbol UserDefinedType недоступний з цього місця". Будь-яка допомога?
Ріхо Йосиф

Привіт @RijoJoseph. Вам потрібно зробити пакет org.apache.spark у своєму проекті і ввести його код UDT.
ChoppyTheLumberjack

6

Енкодери працюють більш-менш однаково в 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.

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.

Я досить успішно перетворення 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);

І вуаля! Натріть, промийте, повторіть.


Я б запропонував зазначити, що для простих структур вам краще послужити, зберігаючи їх у рідних типах Spark, а не серіалізуючи їх на крапку. Вони краще працюють через шлюз Python, більш прозорі в паркеті, і навіть можуть бути відкинуті на конструкції тієї ж форми.
metasim

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. Потім перетворите його в 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))


0

На додаток до вже запропонованих пропозицій, ще один варіант, який я нещодавно відкрив, - це те, що ви можете оголосити свій власний клас, включаючи ознаку org.apache.spark.sql.catalyst.DefinedByConstructorParams.

Це працює, якщо в класі є конструктор, який використовує типи, які ExpressionEncoder може зрозуміти, тобто примітивні значення та стандартні колекції. Це може стати в нагоді, коли ви не в змозі оголосити клас як клас регістру, але не хочете використовувати Kryo для кодування його кожного разу, коли він включений до набору даних.

Наприклад, я хотів оголосити клас регістру, який включав вектор Breeze. Єдиним кодером, який міг би обробити, як правило, це Kryo. Але якщо я оголосив підклас, який розширював Breeze DenseVector та DefinedByConstructorParams, ExpressionEncoder зрозумів, що він може бути серіалізований як масив парних.

Ось як я це заявив:

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, але серіалізується як масив [Double].

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.