데이터 세트에 커스텀 객체를 저장하는 방법?


149

Spark Datasets 소개 에 따르면 :

Spark 2.0을 기대하면서 데이터 집합에 대한 몇 가지 흥미로운 개선 사항을 계획합니다. ... 사용자 지정 인코더 – 현재 다양한 유형의 인코더를 자동 생성하는 동안 사용자 지정 개체에 대한 API를 열고 싶습니다.

Dataset다음과 같은 오류가 발생 하여 사용자 정의 유형을 저장하려고 시도합니다 .

데이터 세트에 저장된 유형의 인코더를 찾을 수 없습니다. sqlContext.implicits를 가져 오면 기본 유형 (Int, String 등) 및 제품 유형 (케이스 클래스)이 지원됩니다. 다른 유형의 직렬화 지원은 다음 릴리스에서 추가 될 예정입니다.

또는:

Java.lang.UnsupportedOperationException : ...에 대한 인코더가 없습니다.

기존 해결 방법이 있습니까?


이 질문은 커뮤니티 위키 답변의 시작점으로 만 존재합니다. 질문과 답변을 자유롭게 업데이트 / 개선하십시오.

답변:


240

최신 정보

이 답변은 상황이 지금보다 나은 있지만, 여전히 유효하고 유익한 내장 추가합니다 인코더 지원을 위해 2.2 / 2.3 이후 Set, Seq, Map, Date, Timestamp,와 BigDecimal. 케이스 클래스와 일반적인 스칼라 유형으로 만 유형을 작성하는 경우 암시 적 in으로 만 괜찮을 것입니다 SQLImplicits.


불행히도, 이것을 돕기 위해 사실상 아무것도 추가되지 않았습니다. 검색 @since 2.0.0에서 Encoders.scala또는 SQLImplicits.scala발견 것들이 대부분 기본 유형 (및 사례 클래스의 일부 조정)을 수행하는. 따라서 가장 먼저 할 말 : 현재 커스텀 클래스 인코더에 대한 실질적인 지원은 없습니다 . 그 길을 따라 가면 다음에 오는 것은 현재 우리가 처분 할 수있는 것을 감안할 때 우리가 기대할 수있는만큼 좋은 일을하는 몇 가지 트릭입니다. 선결제 면책 조항 : 이것은 완벽하게 작동하지 않으며 모든 제한 사항을 명확하고 선결하게하기 위해 최선을 다할 것입니다.

정확히 무엇이 문제입니까

데이터 세트를 만들려면 Spark는 일반적으로의 암시 적을 통해 자동으로 생성 SparkSession되거나 정적 메서드를 호출하여 명시 적으로 생성 할 수 있는 인코더 (T 유형의 JVM 객체를 내부 Spark SQL 표현으로 변환하거나 내부 Spark SQL 표현으로 변환)가 필요 합니다. 에 Encoders"(의 문서에서createDataset 가져온 ). 인코더는 형태 걸릴 것입니다 당신이 인코딩하는 유형입니다. 최초의 제안에 추가한다 (을주는 이러한 암시 인코더) 및 제 제안을 명시 적으로 사용하여 암시 적 인코더에 전달하는 인코더 관련 기능의 세트.Encoder[T]Timport 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 등) 및 제품 유형 (케이스 클래스)이 지원됩니다. 다른 유형의 직렬화 지원은 다음 릴리스에서 추가 될 예정입니다.

그러나 확장 한 일부 클래스에서 위의 오류를 얻는 데 사용한 유형을 래핑 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가 암시 적으로 생성하는 인코더는 실제로 런타임에만 (스칼라 관계를 통해) 만들어지기 때문입니다. 이 경우 컴파일 타임에 모든 Spark 검사는 가장 바깥 쪽 클래스가 확장되고 Product(모든 경우 클래스가 수행하는) 런타임에서만 수행해야 할 작업을 여전히 알지 못한다는 것을 인식합니다 MyObj(내가 만들려고해도 동일한 문제가 발생 함) a Dataset[(Int,MyObj)]-Spark는 런타임시 barf on까지 기다립니다 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리드를 사용 하면 데이터 세트의 모든 행을 플랫 이진 객체로 저장하는 것입니다. 를 들어 map, filter, foreach충분하다,하지만 같은 작업을 위해 join, 스파크 정말이 컬럼으로 분리 될 필요합니다. d2또는 의 스키마를 검사하면 d3이진 열이 하나만있는 것을 알 수 있습니다.

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

튜플 용 부분 솔루션

따라서 Scala ( 6.26.3 Overloading Resolution ) 의 암시 적 마술을 사용하여 최소한 튜플에 대해 가능한 한 잘 작동하고 기존 암시 적과 잘 작동 하는 일련의 암시 적을 만들 수 있습니다.

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것이기 때문에) 더 유용합니다 . 세 번째는 실제로 이진 열에 속합니다.StringUUID

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)

ExpressionEncoderJSON 직렬화를 사용하여 사용자 정의 클래스를 작성할 수 있습니까? 내 경우에는 튜플을 피할 수 없으며 kryo는 나에게 이진 열을 제공합니다.
Alexey Svyatkovskiy

1
@AlexeyS 나는 그렇게 생각하지 않습니다. 근데 왜 그런걸 원해? 내가 제안한 마지막 솔루션으로 벗어날 수없는 이유는 무엇입니까? 데이터를 JSON에 넣을 수 있다면 필드를 추출하여 사례 클래스에 넣을 수 있어야합니다.
Alec

1
불행히도이 답변의 결론은 작동하는 솔루션이 없다는 것입니다.
baol

@baol 일종의. 그러나 Spark가하는 일이 얼마나 어려운지 기억하십시오. 스칼라 타입 시스템은 재귀 적으로 필드를 통과하는 인코더를 "유도"할만큼 강력하지 않다. 솔직히, 아무도 이것을 위해 주석 매크로를 만들지 않은 것에 놀랐습니다. 자연스러운 (그러나 어려운) 솔루션처럼 보입니다.
Alec

1
@combinatorist 필자의 이해는 데이터 세트와 데이터 프레임 (엔코더가 필요하지 않기 때문에 RDD는 아님)은 성능 측면에서 동등하다는 것입니다. 데이터 세트의 유형 안전성을 과소 평가하지 마십시오! Spark가 내부적으로 많은 반사, 캐스트 등을 사용한다고해서 노출되는 인터페이스의 유형 안전성에 신경 쓰지 않아도됩니다. 그러나 그것은 후드 아래에서 데이터 프레임을 사용하는 자체 데이터 세트 기반 유형 안전 함수를 만드는 것에 대해 더 나은 느낌을줍니다.
Alec

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]

    여기서는 암시 적 인코더에 의존하지 않지만 인코더를 명시 적으로 전달 하므로이 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].
Victor P.

@VictorP. 내가 두려워 할 것으로 예상된다 이와 같은 경우 특정 유형에 대한 인코더가 필요하다 ( kryo[Set[Bar]]. 클래스에 필드가 포함되어있는 경우와 같은 방법 Bar으로 전체 객체에 대한 인코더가 필요하다
zero323

@ zero323 나는 같은 문제에 직면하고있다. 전체 프로젝트를 인코딩하는 방법에 대한 코드 예제를 넣을 수 있습니까? 많은 감사합니다!
Rock

@Rock "전체 프로젝트"의 의미를 잘 모르겠습니다
zero323

귀하의 의견에 따라 @ zero323, "클래스에 필드가 포함되어 있으면 Bar전체 객체에 대한 인코더가 필요합니다". 내 질문은이 "전체 프로젝트"를 인코딩하는 방법이었습니다.
Rock

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 직렬화를 사용하고 있지만 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에서) 사용되는 경우 표시되지 않습니다
마티유

프로젝트에서 UDT를 정의하려고하는데 "Symbol 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에 적응하기가 어렵다고 생각하지 않습니다.

나는 변환 매우 성공적이었다 RDD<Fruit>Dataset<Fruit>사용 spark.createDatasetEncoders.bean을 만큼으로 Fruit간단 자바 콩 .

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 변환을 사용하여 복잡한 유형을 더 간단한 유형으로 맵핑 할 수 있습니다. 물론 약간의 추가 작업이지만 플랫 스키마로 작업하는 데 많은 도움이 될 것입니다.

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

그리고 짜잔! 오히려 헹구고 반복하십시오.


간단한 구조의 경우 blob으로 직렬화하는 대신 기본 Spark 유형으로 저장하면 더 나은 서비스를 제공 할 수 있다고 지적합니다. Python 게이트웨이에서 더 잘 작동하고 Parquet에서 더 투명하며 같은 모양의 구조로 캐스트 할 수도 있습니다.
metasim

1

내 상황에 처한 사람들을 위해 나는 여기에도 내 대답을 넣었습니다.

구체적으로 말하면

  1. SQLContext에서 'Set typed data'를 읽고있었습니다. 따라서 원래 데이터 형식은 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가 이해할 수있는 유형 (예 : 기본 값 및 표준 콜렉션)을 사용하는 생성자가있는 경우 작동합니다. 클래스를 사례 클래스로 선언 할 수는 없지만 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]로 직렬화됩니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.