Оновлення
Відповідь на це питання залишається в силі і інформативний, хоча речі тепер краще , так як 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
(оскільки UUID
s - це звичайно те, до чого я хочу приєднатися). Третя дійсно просто належить до двійкової колонки.
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 дає мені бінарний стовпчик ..