Làm thế nào để lưu trữ các đối tượng tùy chỉnh trong Dataset?


149

Theo Giới thiệu Bộ dữ liệu Spark :

Khi chúng tôi mong đợi Spark 2.0, chúng tôi lên kế hoạch một số cải tiến thú vị cho Bộ dữ liệu, cụ thể: ... Bộ mã hóa tùy chỉnh - trong khi chúng tôi hiện đang tự động mã hóa cho nhiều loại, chúng tôi muốn mở API cho các đối tượng tùy chỉnh.

và cố gắng lưu trữ loại tùy chỉnh Datasetdẫn đến lỗi sau như:

Không thể tìm thấy bộ mã hóa cho loại được lưu trữ trong Bộ dữ liệu. Các kiểu nguyên thủy (Int, String, v.v.) và các loại Sản phẩm (các lớp trường hợp) được hỗ trợ bằng cách nhập sqlContext.implicits._ Hỗ trợ để tuần tự hóa các loại khác sẽ được thêm vào trong các bản phát hành trong tương lai

hoặc là:

Java.lang.UnsupportedOperationException: Không tìm thấy Bộ mã hóa cho ....

Có bất kỳ cách giải quyết hiện có?


Lưu ý câu hỏi này chỉ tồn tại như một điểm vào cho câu trả lời của Wiki cộng đồng. Hãy cập nhật / cải thiện cả câu hỏi và câu trả lời.

Câu trả lời:


240

Cập nhật

Câu trả lời này vẫn còn hiệu lực và thông tin, mặc dù những điều bây giờ tốt hơn là từ 2.2 / 2.3, trong đó cho biết thêm built-in hỗ trợ mã hóa cho Set, Seq, Map, Date, Timestamp, và BigDecimal. Nếu bạn dính vào việc tạo các kiểu chỉ với các lớp trường hợp và các kiểu Scala thông thường, bạn sẽ ổn với chỉ ẩn SQLImplicits.


Thật không may, hầu như không có gì được thêm vào để giúp với điều này. Tìm kiếm @since 2.0.0trong Encoders.scalahoặc SQLImplicits.scalatìm thấy những thứ chủ yếu để làm với các kiểu nguyên thủy (và một số điều chỉnh các lớp trường hợp). Vì vậy, điều đầu tiên cần nói: hiện tại không có hỗ trợ tốt thực sự cho các bộ mã hóa lớp tùy chỉnh . Theo cách đó, một số thủ thuật sau đây là một công việc tốt như chúng ta có thể hy vọng, đưa ra những gì chúng ta hiện đang có. Như một tuyên bố từ chối trách nhiệm trước: điều này sẽ không hoạt động hoàn hảo và tôi sẽ cố gắng hết sức để làm cho mọi giới hạn rõ ràng và thẳng thắn.

Chính xác thì vấn đề là gì

Khi bạn muốn tạo tập dữ liệu, Spark "yêu cầu bộ mã hóa (để chuyển đổi một đối tượng JVM loại T sang và từ biểu diễn Spark SQL bên trong) thường được tạo tự động thông qua ẩn SparkSessionhoặc có thể được tạo một cách rõ ràng bằng cách gọi các phương thức tĩnh trên Encoders"(lấy từ các tài liệu trêncreateDataset ). Một bộ mã hóa sẽ mang hình thức Encoder[T]Tlà loại bạn được mã hóa. Các gợi ý đầu tiên là thêm import spark.implicits._(mang đến cho bạn những bộ mã hóa tuyệt đối) và gợi ý thứ hai là phải vượt qua một cách rõ ràng trong bộ mã hóa ngầm sử dụng này tập hợp các hàm mã hóa liên quan.

Không có bộ mã hóa có sẵn cho các lớp thông thường, vì vậy

import spark.implicits._
class MyObj(val i: Int)
// ...
val d = spark.createDataset(Seq(new MyObj(1),new MyObj(2),new MyObj(3)))

sẽ cung cấp cho bạn lỗi thời gian biên dịch ngầm liên quan sau đây:

Không thể tìm thấy bộ mã hóa cho loại được lưu trữ trong Bộ dữ liệu. Các kiểu nguyên thủy (Int, String, v.v.) và các loại Sản phẩm (các lớp trường hợp) được hỗ trợ bằng cách nhập sqlContext.implicits._ Hỗ trợ để tuần tự hóa các loại khác sẽ được thêm vào trong các bản phát hành trong tương lai

Tuy nhiên, nếu bạn bọc bất kỳ loại nào bạn vừa sử dụng để nhận lỗi ở trên trong một số lớp kéo dài Product, thì lỗi sẽ bị chậm trễ trong thời gian chạy, vì vậy

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

Biên dịch tốt, nhưng thất bại trong thời gian chạy với

java.lang.UnsupportedOperationException: Không tìm thấy Bộ mã hóa cho MyObj

Lý do cho điều này là các bộ mã hóa mà Spark tạo ra với các ẩn ý thực sự chỉ được thực hiện khi chạy (thông qua việc chuyển đổi scala). Trong trường hợp này, tất cả các kiểm tra Spark tại thời gian biên dịch là lớp ngoài cùng mở rộng Product(mà tất cả các lớp trường hợp làm) và chỉ nhận ra khi chạy mà nó vẫn không biết phải làm gì MyObj(vấn đề tương tự xảy ra nếu tôi cố gắng thực hiện a Dataset[(Int,MyObj)]- Spark chờ cho đến khi thời gian chạy để bật barf MyObj). Đây là những vấn đề trung tâm đang rất cần được khắc phục:

  • một số lớp mở rộng Productbiên dịch mặc dù luôn bị lỗi khi chạy và
  • không có cách nào chuyển qua các bộ mã hóa tùy chỉnh cho các loại lồng nhau (Tôi không có cách nào cho Spark một bộ mã hóa chỉ MyObjđể nó biết cách mã hóa Wrap[MyObj]hoặc (Int,MyObj)).

Chỉ dùng kryo

Giải pháp mà mọi người gợi ý là sử dụng kryobộ mã hóa.

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

Điều này khá nhanh chóng mặc dù. Đặc biệt là nếu mã của bạn đang thao túng tất cả các loại bộ dữ liệu, tham gia, nhóm, v.v ... Bạn sẽ kết thúc với một loạt các ẩn ý bổ sung. Vì vậy, tại sao không làm cho một ẩn mà thực hiện tất cả điều này tự động?

import scala.reflect.ClassTag
implicit def kryoEncoder[A](implicit ct: ClassTag[A]) = 
  org.apache.spark.sql.Encoders.kryo[A](ct)

Và bây giờ, có vẻ như tôi có thể làm hầu hết mọi thứ tôi muốn (ví dụ dưới đây sẽ không hoạt động ở spark-shellnơi spark.implicits._được nhập tự động)

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!

Hoặc gần như. Vấn đề là việc sử dụng kryokhách hàng tiềm năng để Spark chỉ lưu trữ mọi hàng trong tập dữ liệu dưới dạng đối tượng nhị phân phẳng. Đối với map, filter, foreachđó là đủ, nhưng đối với các hoạt động như join, Spark thực sự cần những để được tách ra thành các cột. Kiểm tra lược đồ cho d2hoặc d3, bạn thấy chỉ có một cột nhị phân:

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

Giải pháp một phần cho bộ dữ liệu

Vì vậy, bằng cách sử dụng phép thuật ẩn ý trong Scala (nhiều hơn trong 6.26.3 Độ phân giải quá tải ), tôi có thể tạo cho mình một loạt các hàm ý sẽ làm tốt nhất có thể, ít nhất là cho các bộ dữ liệu và sẽ hoạt động tốt với các ẩn ý hiện có:

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

Sau đó, được trang bị những ẩn ý này, tôi có thể làm cho ví dụ của mình ở trên hoạt động, mặc dù với một số đổi tên cột

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

Tôi vẫn chưa tìm ra làm thế nào để có được các tên tuple dự kiến ​​( _1,, _2...) mà không đổi tên chúng - nếu ai đó muốn chơi xung quanh nó, đây là nơi tên "value"được giới thiệu và đây là nơi đặt tên tên thường được thêm vào. Tuy nhiên, điểm quan trọng là bây giờ tôi có một lược đồ có cấu trúc đẹp:

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)

Vì vậy, tóm lại, cách giải quyết này:

  • cho phép chúng tôi nhận các cột riêng biệt cho các bộ dữ liệu (vì vậy chúng tôi có thể tham gia trên các bộ dữ liệu một lần nữa, yay!)
  • một lần nữa chúng ta có thể chỉ dựa vào những ẩn ý (vì vậy không cần phải đi kryokhắp nơi)
  • gần như hoàn toàn tương thích ngược với import spark.implicits._(với một số đổi tên liên quan)
  • không không chúng ta hãy tham gia vào các kyrocột nhị phân tuần tự, hãy để một mình trên cánh đồng những người có thể có
  • có tác dụng phụ khó chịu khi đổi tên một số cột tuple thành "value" (nếu cần, điều này có thể được hoàn tác bằng cách chuyển đổi .toDF, chỉ định tên cột mới và chuyển đổi lại thành tập dữ liệu - và tên lược đồ dường như được giữ nguyên thông qua các phép nối , nơi họ cần nhất).

Giải pháp một phần cho các lớp học nói chung

Điều này là ít dễ chịu và không có giải pháp tốt. Tuy nhiên, bây giờ chúng ta có giải pháp tuple ở trên, tôi có linh cảm giải pháp chuyển đổi ngầm từ một câu trả lời khác sẽ bớt đau hơn một chút vì bạn có thể chuyển đổi các lớp phức tạp hơn của mình thành tuple. Sau đó, sau khi tạo tập dữ liệu, có thể bạn sẽ đổi tên các cột bằng cách sử dụng phương pháp khung dữ liệu. Nếu mọi việc suôn sẻ, đây thực sự là một sự cải tiến vì bây giờ tôi có thể thực hiện các phép nối trên các lĩnh vực của lớp mình. Nếu tôi chỉ sử dụng một kryoserializer nhị phân phẳng thì điều đó sẽ không thể xảy ra.

Dưới đây là một ví dụ mà không một chút tất cả mọi thứ: Tôi có một lớp học MyObjtrong đó có lĩnh vực loại Int, java.util.UUIDSet[String]. Việc đầu tiên tự chăm sóc bản thân. Thứ hai, mặc dù tôi có thể tuần tự hóa bằng cách sử dụng kryosẽ hữu ích hơn nếu được lưu trữ dưới dạng String(vì UUIDthường là thứ tôi sẽ muốn tham gia). Thứ ba thực sự chỉ thuộc về một cột nhị phân.

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)

Bây giờ, tôi có thể tạo một tập dữ liệu với một lược đồ đẹp bằng cách sử dụng máy móc này:

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]

Và lược đồ hiển thị cho tôi các cột tôi với tên đúng và với cả hai điều đầu tiên tôi có thể tham gia.

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

Có thể tạo một lớp tùy chỉnh ExpressionEncoderbằng cách sử dụng tuần tự hóa JSON không? Trong trường hợp của tôi, tôi không thể thoát khỏi các bộ dữ liệu và kryo cho tôi một cột nhị phân ..
Alexey Svyatkovskiy

1
@AlexeyS Tôi không nghĩ vậy. Nhưng tại sao bạn lại muốn điều đó? Tại sao bạn không thể thoát khỏi giải pháp cuối cùng tôi đề xuất? Nếu bạn có thể đặt dữ liệu của mình vào JSON, bạn sẽ có thể trích xuất các trường và đặt chúng vào một lớp tình huống ...
Alec

1
Thật không may, điểm mấu chốt của câu trả lời này là không có giải pháp nào hiệu quả.
baol

@baol Sắp xếp của. Nhưng hãy nhớ rằng những gì Spark đang làm là khó khăn. Hệ thống kiểu của Scala đơn giản là không đủ mạnh để "rút ra" các bộ mã hóa đi qua đệ quy qua các trường. Thành thật mà nói, tôi chỉ ngạc nhiên không ai tạo ra một macro chú thích cho việc này. Có vẻ như giải pháp tự nhiên (nhưng khó khăn).
Alec

1
@combinatorist Hiểu biết của tôi là Datasets và Dataframes (nhưng không phải RDD, vì chúng không cần bộ mã hóa!) tương đương từ góc độ hiệu năng. Đừng đánh giá thấp mức độ an toàn của Bộ dữ liệu! Chỉ vì Spark bên trong sử dụng rất nhiều phản xạ, phôi, v.v. không có nghĩa là bạn không nên quan tâm đến sự an toàn về kiểu của giao diện được phơi bày. Nhưng nó làm tôi cảm thấy tốt hơn khi tạo các hàm an toàn loại dựa trên Dataset của riêng tôi, sử dụng Dataframes dưới mui xe.
Alec

32
  1. Sử dụng bộ mã hóa chung.

    Hiện tại có hai bộ mã hóa chung kryovà hiện tại bộ mã hóa javaSerializationsau được mô tả rõ ràng là:

    cực kỳ không hiệu quả và chỉ nên được sử dụng như là phương sách cuối cùng.

    Giả sử theo lớp

    class Bar(i: Int) {
      override def toString = s"bar $i"
      def bar = i
    }

    bạn có thể sử dụng các bộ mã hóa này bằng cách thêm bộ mã hóa ẩn:

    object BarEncoders {
      implicit def barEncoder: org.apache.spark.sql.Encoder[Bar] = 
      org.apache.spark.sql.Encoders.kryo[Bar]
    }

    có thể được sử dụng cùng nhau như sau:

    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()
      }
    }

    Nó lưu trữ các đối tượng dưới dạng binarycột để khi chuyển đổi thành DataFramelược đồ sau:

    root
     |-- value: binary (nullable = true)

    Cũng có thể mã hóa các bộ dữ liệu bằng cách sử dụng kryobộ mã hóa cho trường cụ thể:

    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]

    Xin lưu ý rằng chúng tôi không phụ thuộc vào các bộ mã hóa ẩn ở đây mà vượt qua bộ mã hóa một cách rõ ràng để điều này rất có thể sẽ không hoạt động với toDSphương thức.

  2. Sử dụng chuyển đổi ngầm định:

    Cung cấp chuyển đổi ngầm giữa biểu diễn có thể được mã hóa và lớp tùy chỉnh, ví dụ:

    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()
      }
    }

Câu hỏi liên quan:


Giải pháp 1 dường như không hoạt động đối với các bộ sưu tập đã nhập (ít nhất Set) tôi nhận được Exception in thread "main" java.lang.UnsupportedOperationException: No Encoder found for Set[Bar].
Victor P.

@VictorP. Tôi dự kiến ​​tôi sợ Trong trường hợp như thế này, bạn sẽ cần một bộ mã hóa cho loại cụ thể ( kryo[Set[Bar]]. Cùng một cách nếu lớp chứa một trường Barbạn cần bộ mã hóa cho toàn bộ đối tượng. Đây là các phương thức rất thô sơ.
zero323

@ zero323 Tôi đang đối mặt với cùng một vấn đề. Bạn có thể đặt một ví dụ mã về cách mã hóa toàn bộ dự án? Cảm ơn nhiều!
Rock

@Rock Tôi không chắc ý của bạn là "toàn bộ dự án"
zero323

@ zero323 mỗi bình luận của bạn, "nếu lớp chứa một trường Barbạn cần bộ mã hóa cho toàn bộ đối tượng". Câu hỏi của tôi là làm thế nào để mã hóa "toàn bộ dự án" này?
Rock

9

Bạn có thể sử dụng UDTRegistration và sau đó là Case Class, Tuples, v.v ... tất cả đều hoạt động chính xác với Loại do người dùng xác định!

Giả sử bạn muốn sử dụng Enum tùy chỉnh:

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
}

Đăng ký nó như thế này:

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

Sau đó, SỬ DỤNG CNTT!

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

Giả sử bạn muốn sử dụng Bản ghi đa hình:

trait CustomPoly
case class FooPoly(id:Int) extends CustomPoly
case class BarPoly(value:String, secondValue:Long) extends CustomPoly

... và việc sử dụng nó như thế này:

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

Bạn có thể viết một UDT tùy chỉnh mã hóa mọi thứ thành byte (Tôi đang sử dụng tuần tự hóa java ở đây nhưng có lẽ tốt hơn là sử dụng bối cảnh Kryo của Spark).

Đầu tiên xác định lớp 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]
}

Sau đó đăng ký nó:

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

Sau đó, bạn có thể sử dụng nó!

// 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
Tôi không thấy nơi kryo bạn được sử dụng (trong CustomPolyUDT)
Mathieu

Tôi đang cố gắng xác định UDT trong dự án của mình và tôi gặp lỗi này "Biểu tượng UserDefinedType không thể truy cập được từ nơi này". Có ai giúp đỡ không?
Rijo Joseph

Xin chào @RijoJoseph. Bạn cần tạo một gói org.apache.spark trong dự án của bạn và đặt mã UDT của bạn vào đó.
ChoppyTheLumberjack

6

Bộ mã hóa làm việc ít nhiều giống nhau trong Spark2.0. Và Kryovẫn là serializationsự lựa chọn được đề nghị .

Bạn có thể xem ví dụ sau với shell-spark

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.

Cho đến bây giờ] không có appropriate encoderstrong phạm vi hiện tại vì vậy người của chúng tôi không được mã hóa dưới dạng binarygiá trị. Nhưng điều đó sẽ thay đổi khi chúng tôi cung cấp một số implicitbộ mã hóa bằng cách sử dụng Kryotuần tự hóa.

// 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

Trong trường hợp lớp Java Bean, điều này có thể hữu ích

import spark.sqlContext.implicits._
import org.apache.spark.sql.Encoders
implicit val encoder = Encoders.bean[MyClasss](classOf[MyClass])

Bây giờ bạn có thể chỉ cần đọc DataFrame dưới dạng DataFrame tùy chỉnh

dataFrame.as[MyClass]

Điều này sẽ tạo ra một bộ mã hóa lớp tùy chỉnh và không phải là một bộ nhị phân.


1

Các ví dụ của tôi sẽ có trong Java, nhưng tôi không tưởng tượng nó sẽ khó thích nghi với Scala.

Tôi đã khá thành công khi chuyển đổi RDD<Fruit>sang Dataset<Fruit>sử dụng spark.createDatasetEncoders.bean miễn Fruitlà một Java Bean đơn giản .

Bước 1: Tạo Java Bean đơn giản.

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
}

Tôi sẽ gắn bó với các lớp với các kiểu nguyên thủy và Chuỗi dưới dạng các trường trước khi các DataBricks tăng cường Bộ mã hóa của chúng. Nếu bạn có một lớp với đối tượng lồng nhau, hãy tạo một Java Bean đơn giản khác với tất cả các trường của nó được làm phẳng, vì vậy bạn có thể sử dụng các phép biến đổi RDD để ánh xạ kiểu phức tạp thành kiểu đơn giản hơn. Chắc chắn đó là một công việc nhỏ hơn, nhưng tôi tưởng tượng nó sẽ giúp ích rất nhiều cho hiệu suất làm việc với một lược đồ phẳng.

Bước 2: Nhận Dataset của bạn từ 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);

Và Voila! Lót, rửa sạch, lặp lại.


Tôi khuyên bạn nên chỉ ra rằng đối với các cấu trúc đơn giản, bạn sẽ được phục vụ tốt hơn bằng cách lưu trữ chúng trong các loại Spark gốc, thay vì nối tiếp chúng thành một đốm màu. Chúng hoạt động tốt hơn trên cổng Python, trong suốt hơn trong Parquet và thậm chí có thể được chuyển sang các cấu trúc có cùng hình dạng.
metasim

1

Đối với những người có thể trong tình huống của tôi, tôi cũng đặt câu trả lời của mình ở đây.

Cụ thể,

  1. Tôi đã đọc 'Đặt dữ liệu đã nhập' từ SQLContext. Vì vậy, định dạng dữ liệu gốc là DataFrame.

    val sample = spark.sqlContext.sql("select 1 as a, collect_set(1) as b limit 1") sample.show()

    +---+---+ | a| b| +---+---+ | 1|[1]| +---+---+

  2. Sau đó chuyển đổi nó thành RDD bằng cách sử dụng rdd.map () với loại mutable.WrappingArray.

    sample .rdd.map(r => (r.getInt(0), r.getAs[mutable.WrappedArray[Int]](1).toSet)) .collect() .foreach(println)

    Kết quả:

    (1,Set(1))


0

Ngoài các đề xuất đã được đưa ra, một tùy chọn khác mà tôi mới phát hiện ra là bạn có thể khai báo lớp tùy chỉnh của mình bao gồm cả đặc điểm org.apache.spark.sql.catalyst.DefinedByConstructorParams.

Điều này hoạt động nếu lớp có một hàm tạo sử dụng các kiểu mà ExpressionEncoder có thể hiểu, tức là các giá trị nguyên thủy và các bộ sưu tập tiêu chuẩn. Nó có thể hữu ích khi bạn không thể khai báo lớp là lớp trường hợp, nhưng không muốn sử dụng Kryo để mã hóa nó mỗi khi nó được bao gồm trong Bộ dữ liệu.

Ví dụ, tôi muốn khai báo một lớp trường hợp bao gồm một vectơ Breeze. Bộ mã hóa duy nhất có thể xử lý thông thường sẽ là Kryo. Nhưng nếu tôi đã khai báo một lớp con mở rộng Breeze DenseVector và DefinedByConstructorParams, ExpressionEncoder hiểu rằng nó có thể được tuần tự hóa thành một mảng Nhân đôi.

Đây là cách tôi tuyên bố:

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]

Bây giờ tôi có thể sử dụng SerializableDenseVectortrong Bộ dữ liệu (trực tiếp hoặc là một phần của Sản phẩm) bằng ExpressionEncoder đơn giản và không có Kryo. Nó hoạt động giống như Breeze DenseVector nhưng được tuần tự hóa dưới dạng Mảng [Double].

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.