วิธีการเก็บวัตถุที่กำหนดเองในชุดข้อมูล?


149

ตามที่แนะนำชุดข้อมูล Spark :

ในขณะที่เราตั้งตารอ Spark 2.0 เราวางแผนการปรับปรุงที่น่าตื่นเต้นสำหรับชุดข้อมูลโดยเฉพาะ: ... Custom encoders - ในขณะที่เราสร้าง encoders อัตโนมัติสำหรับประเภทที่หลากหลายเราต้องการเปิด API สำหรับวัตถุที่กำหนดเอง

และพยายามจัดเก็บประเภทที่กำหนดเองDatasetเพื่อนำไปสู่ข้อผิดพลาดต่อไปนี้เช่น:

ไม่พบตัวเข้ารหัสสำหรับประเภทที่เก็บไว้ในชุดข้อมูล ประเภทดั้งเดิม (Int, String, ฯลฯ ) และประเภทผลิตภัณฑ์ (คลาสเคส) ได้รับการสนับสนุนโดยการนำเข้า sqlContext.implicits._ การสนับสนุนสำหรับซีเรียลไลซ์ประเภทอื่น ๆ จะถูกเพิ่มในรุ่นอนาคต

หรือ:

Java.lang.UnsupportedOperationException: ไม่พบตัวเข้ารหัสสำหรับ ....

มีวิธีแก้ไขปัญหาอยู่หรือไม่?


หมายเหตุคำถามนี้มีอยู่เป็นจุดเริ่มต้นสำหรับคำตอบ Community 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 ภายใน) ที่สร้างขึ้นโดยอัตโนมัติผ่าน implicits จาก a 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: ไม่พบ Encoder สำหรับ MyObj

สาเหตุของการทำเช่นนี้คือการเข้ารหัส Spark สร้างด้วย implicits จริง ๆ แล้วทำเฉพาะที่ runtime (ผ่าน scala relfection) ในกรณีนี้การตรวจสอบ Spark ทั้งหมดในเวลาคอมไพล์คือคลาสนอกสุดจะขยายProduct(ซึ่งคลาสเคสทั้งหมดทำ) และรู้ตัวเฉพาะตอนรันไทม์ที่ยังไม่รู้ว่าจะทำอย่างไรMyObj(ปัญหาเดียวกันเกิดขึ้นถ้าฉันพยายามทำ a Dataset[(Int,MyObj)]- Spark รอจนกระทั่งรันไทม์ถึง barf on 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!

หรือเกือบ ปัญหาคือการใช้kryoโอกาสในการสร้าง Spark เพียงแค่จัดเก็บทุกแถวในชุดข้อมูลเป็นวัตถุไบนารีแบบเรียบ สำหรับmap, filter, foreachที่เป็นพอ แต่สำหรับการดำเนินงานเช่นjoin, Spark จริงๆต้องเหล่านี้จะแยกออกเป็นคอลัมน์ ตรวจสอบสคีมาสำหรับd2หรือd3คุณเห็นว่ามีเพียงหนึ่งคอลัมน์ไบนารี:

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

วิธีการแก้ปัญหาบางส่วนสำหรับสิ่งอันดับ

ดังนั้นการใช้ความมหัศจรรย์ของ implicits ใน Scala (เพิ่มเติมใน6.26.3 การแก้ปัญหาการบรรทุกเกินพิกัด ) ฉันสามารถสร้างชุด implicits ที่จะทำงานได้ดีที่สุดอย่างน้อยที่สุดสำหรับ tuples และทำงานได้ดีกับ implicits ที่มีอยู่:

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

ฉันยังไม่ได้คิดหาวิธีรับชื่อ tuple ( _1,, _2... ) ที่เป็นค่าเริ่มต้นโดยไม่ต้องเปลี่ยนชื่อ - ถ้ามีคนอื่นอยากเล่นกับสิ่งนี้นี่คือที่ที่ชื่อ"value"ถูกแนะนำและนี่คือที่ tuple มักจะมีการเพิ่มชื่อ อย่างไรก็ตามประเด็นสำคัญคือตอนนี้ฉันมีโครงสร้างที่ดี:

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)

ดังนั้นโดยสรุปวิธีแก้ปัญหานี้:

  • ช่วยให้เราสามารถรับคอลัมน์แยกสำหรับ tuples (เพื่อให้เราสามารถเข้าร่วมใน tuples อีกครั้ง yay!)
  • เราสามารถพึ่งพา implicits ได้อีกครั้ง (ไม่จำเป็นต้องผ่านkryoทุกที่)
  • เข้ากันได้เกือบทั้งหมดกับimport spark.implicits._(ด้วยการเปลี่ยนชื่อบางอย่างที่เกี่ยวข้อง)
  • ไม่ได้แจ้งให้เราเข้าร่วมในkyroคอลัมน์ไบนารีอันดับให้อยู่คนเดียวในเขตข้อมูลเหล่านั้นอาจมี
  • มีผลข้างเคียงที่ไม่พึงประสงค์ของการเปลี่ยนชื่อคอลัมน์ tuple บางส่วนเป็น "ค่า" (หากจำเป็นสามารถยกเลิกได้โดยการแปลง.toDFระบุชื่อคอลัมน์ใหม่และแปลงกลับเป็นชุดข้อมูล - และชื่อสคีมาจะถูกเก็บรักษาไว้ผ่านการรวม ที่พวกเขาต้องการมากที่สุด)

โซลูชันบางส่วนสำหรับคลาสทั่วไป

อันนี้น่าพอใจน้อยกว่าและไม่มีทางออกที่ดี อย่างไรก็ตามตอนนี้เรามีวิธีแก้ปัญหา tuple ด้านบนฉันมีลางสังหรณ์ว่าวิธีแก้ปัญหาการแปลงโดยนัยจากคำตอบอื่นจะค่อนข้างเจ็บปวดน้อยลงเช่นกันเนื่องจากคุณสามารถแปลงคลาสที่ซับซ้อนมากขึ้นเป็น tuples จากนั้นหลังจากสร้างชุดข้อมูลแล้วคุณอาจเปลี่ยนชื่อคอลัมน์โดยใช้วิธีดาต้าเฟรม ถ้าทุกอย่างเป็นไปด้วยดีนี่เป็นการปรับปรุงจริง ๆเพราะตอนนี้ฉันสามารถเข้าร่วมได้ในสาขาการเรียนของฉัน ถ้าฉันเพิ่งใช้เลขฐานสองแบบแบน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]

และสคีมาแสดงให้ฉันเห็นฉันด้วยชื่อที่ถูกต้องและสองสิ่งแรกที่ฉันสามารถเข้าร่วมได้

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

เป็นไปได้หรือไม่ที่จะสร้างคลาสที่กำหนดเองExpressionEncoderโดยใช้การทำให้เป็นอันดับ JSON? ในกรณีของฉันฉันไม่สามารถออกไปกับสิ่งอันดับและ kryo ให้คอลัมน์แบบไบนารีกับฉัน ..
Alexey Svyatkovskiy

1
@ Alexey ฉันไม่คิดอย่างนั้น แต่ทำไมคุณต้องการที่ ทำไมคุณไม่ไปกับทางออกสุดท้ายที่ฉันเสนอ? หากคุณสามารถใส่ข้อมูลของคุณใน JSON คุณควรจะสามารถแยกเขตข้อมูลและใส่ไว้ในระดับกรณี ...
อเล็กซ์

1
น่าเสียดายที่บรรทัดล่างของคำตอบนี้คือไม่มีวิธีแก้ปัญหาที่ใช้งานได้
baol

@ baol เรียงจาก แต่จำไว้ว่าการทำ Spark นั้นทำได้ยากแค่ไหน ระบบประเภทของสกาล่านั้นไม่ทรงพลังพอที่จะเข้ารหัส "รับ" ที่ผ่านช่องแบบวนซ้ำ ตรงไปตรงมาฉันแค่ประหลาดใจว่าไม่มีใครทำแมโครคำอธิบายประกอบสำหรับสิ่งนี้ ดูเหมือนว่าวิธีการแก้ปัญหาธรรมชาติ (แต่ยาก)
Alec

1
@combinatorist ความเข้าใจของฉันคือชุดข้อมูลและ Dataframes (แต่ไม่ใช่ RDDs เนื่องจากไม่จำเป็นต้องใช้ตัวเข้ารหัส!) เทียบเท่าจากมุมมองประสิทธิภาพ อย่าประเมินความปลอดภัยของชุดข้อมูลต่ำกว่ามาตรฐาน! เพียงเพราะ 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)

    นอกจากนี้ยังเป็นไปได้ที่จะเข้ารหัส tuples โดยใช้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

@ รอกฉันไม่แน่ใจว่าคุณหมายถึงอะไรโดย "โครงการทั้งหมด"
zero323

@ zero323 ต่อความคิดเห็นของคุณ "ถ้าคลาสมีเขตข้อมูลที่Barคุณต้องการเข้ารหัสสำหรับวัตถุทั้งหมด" คำถามของฉันคือจะเข้ารหัส "โครงการทั้งหมด" นี้ได้อย่างไร
Rock

9

คุณสามารถใช้ UDTR การลงทะเบียนจากนั้น Case Classes, Tuples และอื่น ๆ ... ทำงานได้อย่างถูกต้องกับ User Defined Type ของคุณ!

สมมติว่าคุณต้องการใช้ 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())

สมมติว่าคุณต้องการใช้เร็กคอร์ด Polymorphic:

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
ฉันไม่เห็นว่าจะใช้ kryo ของคุณที่ใด (ใน CustomPolyUDT)
mathieu

ฉันกำลังพยายามกำหนด 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.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
}

ฉันจะยึดคลาสที่มีประเภทดั้งเดิมและสตริงเป็นฟิลด์ก่อน DataBricks folks เนื้อขึ้น Encoders ของพวกเขา หากคุณมีคลาสที่มีวัตถุซ้อนอยู่ให้สร้าง 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);

และ voila! นวดแล้วล้างซ้ำ


ฉันขอแนะนำให้ชี้ให้เห็นว่าสำหรับโครงสร้างที่เรียบง่ายคุณจะได้รับการบริการที่ดีขึ้นโดยการจัดเก็บไว้ในประเภท Spark ดั้งเดิมแทนที่จะจัดลำดับให้เป็นหยด มันทำงานได้ดีกว่าในเกตเวย์ Python ความโปร่งใสใน Parquet และสามารถส่งไปยังโครงสร้างที่มีรูปร่างเดียวกัน
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นอกเหนือไปจากข้อเสนอแนะที่ได้รับแล้วตัวเลือกที่ฉันเพิ่งค้นพบก็คือว่าคุณสามารถประกาศคลาสที่กำหนดเองของคุณรวมถึงลักษณะ

ใช้งานได้ถ้าคลาสมี Constructor ที่ใช้ประเภท 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 แต่ทำให้อนุกรมเป็น Array [Double]

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.