ปรับปรุง
คำตอบนี้ยังคงถูกต้องและให้ข้อมูลถึงแม้ว่าสิ่งที่มีตอนนี้ดีขึ้นตั้งแต่ 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
(เนื่องจาก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]
และสคีมาแสดงให้ฉันเห็นฉันด้วยชื่อที่ถูกต้องและสองสิ่งแรกที่ฉันสามารถเข้าร่วมได้
d.printSchema
// root
// |-- i: integer (nullable = false)
// |-- u: string (nullable = true)
// |-- s: binary (nullable = true)
ExpressionEncoder
โดยใช้การทำให้เป็นอันดับ JSON? ในกรณีของฉันฉันไม่สามารถออกไปกับสิ่งอันดับและ kryo ให้คอลัมน์แบบไบนารีกับฉัน ..