web-dev-qa-db-fra.com

Effectuer une jointure dactylographiée dans Scala avec des jeux de données Spark

J'aime les jeux de données Spark, car ils me donnent des erreurs d'analyse et de syntaxe lors de la compilation et me permettent également de travailler avec des getters au lieu de noms/nombres codés en dur. La plupart des calculs peuvent être effectués avec les API de haut niveau de Dataset. Par exemple, il est beaucoup plus simple d’effectuer les opérations agg, select, sum, avg, map, filter ou groupBy en accédant à un objet de type Dataset plutôt qu’à utiliser les champs de données des lignes RDD.

Cependant l'opération de jointure est absente de ceci, j'ai lu que je peux faire une jointure comme ceci

ds1.joinWith(ds2, ds1.toDF().col("key") === ds2.toDF().col("key"), "inner")

Mais ce n’est pas ce que je veux, car je préférerais le faire via l’interface de cas, alors quelque chose de plus semblable à celui-ci.

ds1.joinWith(ds2, ds1.key === ds2.key, "inner")

La meilleure alternative pour le moment semble créer un objet à côté de la classe de cas et donner à cette fonction la possibilité de me fournir le bon nom de colonne sous forme de chaîne. Donc, je voudrais utiliser la première ligne de code, mais mettre une fonction au lieu d'un nom de colonne codé en dur. Mais cela ne semble pas assez élégant ..

Quelqu'un peut-il me conseiller sur d'autres options ici? Le but est d’obtenir une abstraction à partir des noms de colonnes et de travailler de préférence via les accesseurs de la classe de cas.

J'utilise Spark 1.6.1 et Scala 2.10

25
Sparky

Observation

Spark SQL ne peut optimiser la jointure que si la condition de jointure est basée sur l'opérateur d'égalité. Cela signifie que nous pouvons considérer les équijointes et les non-équijoins séparément.

Equijoin

Equijoin peut être implémenté de manière sécurisée en mappant les deux Datasets sur les n-uplets (clé, valeur), en effectuant une jointure basée sur des clés et en remodelant le résultat:

import org.Apache.spark.sql.Encoder
import org.Apache.spark.sql.Dataset

def safeEquiJoin[T, U, K](ds1: Dataset[T], ds2: Dataset[U])
    (f: T => K, g: U => K)
    (implicit e1: Encoder[(K, T)], e2: Encoder[(K, U)], e3: Encoder[(T, U)]) = {
  val ds1_ = ds1.map(x => (f(x), x))
  val ds2_ = ds2.map(x => (g(x), x))
  ds1_.joinWith(ds2_, ds1_("_1") === ds2_("_1")).map(x => (x._1._2, x._2._2))
}

Non-équijoin

Peut être exprimé en utilisant des opérateurs d’algèbre relationnelle sous la forme R ⋈θ S = σθ (R × S) et converti directement en code.

Spark 2.0

Activez crossJoin et utilisez joinWith avec un prédicat trivialement égal:

spark.conf.set("spark.sql.crossJoin.enabled", true)

def safeNonEquiJoin[T, U](ds1: Dataset[T], ds2: Dataset[U])
                         (p: (T, U) => Boolean) = {
  ds1.joinWith(ds2, lit(true)).filter(p.tupled)
}

Spark 2.1

Utilisez la méthode crossJoin:

def safeNonEquiJoin[T, U](ds1: Dataset[T], ds2: Dataset[U])
    (p: (T, U) => Boolean)
    (implicit e1: Encoder[Tuple1[T]], e2: Encoder[Tuple1[U]], e3: Encoder[(T, U)]) = {
  ds1.map(Tuple1(_)).crossJoin(ds2.map(Tuple1(_))).as[(T, U)].filter(p.tupled)
}

Exemples

case class LabeledPoint(label: String, x: Double, y: Double)
case class Category(id: Long, name: String)

val points1 = Seq(LabeledPoint("foo", 1.0, 2.0)).toDS
val points2 = Seq(
  LabeledPoint("bar", 3.0, 5.6), LabeledPoint("foo", -1.0, 3.0)
).toDS
val categories = Seq(Category(1, "foo"), Category(2, "bar")).toDS

safeEquiJoin(points1, categories)(_.label, _.name)
safeNonEquiJoin(points1, points2)(_.x > _.x)

Remarques

  • Il convient de noter que ces méthodes diffèrent qualitativement d'une application directe joinWith et nécessitent des transformations coûteuses DeserializeToObject/SerializeFromObject (par rapport à ce que joinWith peut utiliser des opérations logiques sur les données). 

    Ceci est similaire au comportement décrit dans Spark 2.0 Dataset vs DataFrame .

  • Si vous n'êtes pas limité à l'API Spark SQL frameless fournit des extensions sûres de types intéressants pour Datasets (à ce jour, il ne prend en charge que Spark 2.0):

    import frameless.TypedDataset
    
    val typedPoints1 = TypedDataset.create(points1)
    val typedPoints2 = TypedDataset.create(points2)
    
    typedPoints1.join(typedPoints2, typedPoints1('x), typedPoints2('x))
    
  • L'API Dataset n'est pas stable dans la version 1.6, donc je ne pense pas qu'il soit logique de l'utiliser ici.

  • Bien sûr, cette conception et les noms descriptifs ne sont pas nécessaires. Vous pouvez facilement utiliser la classe type pour ajouter implicitement ces méthodes à Dataset. Il n'y a pas de conflit avec les signatures intégrées et les deux peuvent donc s'appeler joinWith.

25
user6910411

En outre, un autre problème plus important pour l'API Spark non sécurisée est que lorsque vous joignez deux Datasets, cela vous donnera une DataFrame. Et ensuite, vous perdez des types de vos deux jeux de données d'origine. 

val a: Dataset[A]
val b: Dataset[B]

val joined: Dataframe = a.join(b)
// what would be great is 
val joined: Dataset[C] = a.join(b)(implicit func: (A, B) => C)
0
linehrr