अपडेट करें
इस उत्तर हालांकि चीजें अब बेहतर हैं, वैध और सूचनात्मक अब भी है 2.2 / 2.3, कहते हैं, जिसके लिए निर्मित एनकोडर समर्थन के बाद से Set
, Seq
, Map
, Date
, Timestamp
, और BigDecimal
। यदि आप केवल केस क्लासेस और सामान्य स्कैला प्रकारों के साथ टाइप करने के लिए चिपके रहते हैं, तो आपको केवल निहितार्थ के साथ ठीक होना चाहिए SQLImplicits
।
दुर्भाग्य से, वस्तुतः इसमें मदद करने के लिए कुछ भी नहीं जोड़ा गया है। के लिए खोज @since 2.0.0
में Encoders.scala
या SQLImplicits.scala
पाता बातें ज्यादातर आदिम प्रकार (और मामले वर्ग के कुछ फेरबदल) से कोई लेना देना। तो, पहली बात कहने के लिए: वर्तमान में कस्टम वर्ग एन्कोडर के लिए कोई वास्तविक अच्छा समर्थन नहीं है । उस रास्ते से, जो कुछ चालें है, जो कि एक अच्छा काम है जैसा कि हम कभी भी उम्मीद कर सकते हैं, यह देखते हुए कि वर्तमान में हमारे पास क्या है। एक अग्रिम अस्वीकरण के रूप में: यह पूरी तरह से काम नहीं करेगा और मैं सभी सीमाओं को स्पष्ट और अग्रिम करने के लिए अपनी पूरी कोशिश करूंगा।
वास्तव में समस्या क्या है
जब आप एक डेटासेट बनाना चाहते हैं, तो स्पार्क को "एनकोडर (आंतरिक स्पार्क एसक्यूएल प्रतिनिधित्व से टाइप टी के जेवीएम ऑब्जेक्ट को परिवर्तित करने के लिए) की आवश्यकता होती है जो आम तौर पर ए से प्राप्त होने के माध्यम से स्वचालित रूप से बनाई जाती है 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)))
आपको निम्न अंतर्निहित संबंधित संकलन समय त्रुटि देगा:
डेटासेट में संग्रहीत प्रकार के लिए एनकोडर खोजने में असमर्थ। आदिम प्रकार (इंट, स्ट्रिंग, आदि) और उत्पाद प्रकार (केस कक्षाएं) 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 के लिए कोई एनकोडर नहीं मिला
इसका कारण यह है कि एनकोडर स्पार्क का निर्माण इनसिक्योरिटी के साथ होता है, जो वास्तव में केवल रनटाइम (स्केला रिलेटेन के माध्यम से) में बनाया जाता है। इस मामले में, संकलन समय पर सभी स्पार्क की जांच होती है कि सबसे बाहरी वर्ग का विस्तार होता है 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
स्पार्क के उपयोग से डेटासेट में प्रत्येक पंक्ति को एक फ्लैट बाइनरी ऑब्जेक्ट के रूप में संग्रहीत किया जाता है। के लिए map
, filter
, foreach
वह पर्याप्त है, लेकिन जैसे कार्यों के लिए join
, स्पार्क वास्तव में इन स्तंभों में विभाजित किया जाना चाहिए। के लिए स्कीमा निरीक्षण d2
या d3
, जैसा कि आप देख सिर्फ एक द्विआधारी स्तंभ है:
d2.printSchema
// root
// |-- value: binary (nullable = true)
ट्यूपल्स के लिए आंशिक समाधान
इसलिए, स्काला में ( 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)
तो, संक्षेप में, यह समाधान:
- हमें tuples के लिए अलग कॉलम प्राप्त करने की अनुमति देता है (इसलिए हम tuples पर फिर से जुड़ सकते हैं, yay!)
- हम फिर से बस इंक्विटिस पर भरोसा कर सकते हैं (इसलिए
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]
और स्कीमा मुझे सही नामों के साथ कॉलम दिखाता है और पहले दो चीजों के साथ मैं उनके खिलाफ जुड़ सकता हूं।
d.printSchema
// root
// |-- i: integer (nullable = false)
// |-- u: string (nullable = true)
// |-- s: binary (nullable = true)
ExpressionEncoder
JSON सीरियलाइज़ेशन का उपयोग करके एक कस्टम क्लास बनाना संभव है ? मेरे मामले में मैं टुपल्स के साथ दूर नहीं जा सकता, और kryo मुझे एक बाइनरी कॉलम देता है ..