it-swarm.com.ru

Выполните типизированное объединение в Scala с наборами данных Spark

Мне нравятся наборы данных Spark, так как они дают мне ошибки анализа и синтаксические ошибки во время компиляции, а также позволяют мне работать с получателями вместо жестко заданных имен/чисел. Большинство вычислений может быть выполнено с помощью высокоуровневых API-интерфейсов Dataset. Например, гораздо проще выполнять операции agg, select, sum, avg, map, filter или groupBy , получая доступ к типизированным объектам набора данных, чем используя поля данных строк RDD.

Однако операция соединения отсутствует, я прочитал, что я могу сделать соединение, как это

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

Но это не то, что я хочу, так как я бы предпочел сделать это через интерфейс класса case, так что-то вроде этого

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

На данный момент лучшая альтернатива, кажется, заключается в создании объекта рядом с классом case и предоставлении этой функции для предоставления мне правильного имени столбца в виде String. Так что я бы использовал первую строку кода, но поставил бы функцию вместо жестко закодированного имени столбца. Но это не выглядит достаточно элегантно ..

Может кто-нибудь посоветовать мне другие варианты здесь? Цель состоит в том, чтобы получить абстракцию от фактических имен столбцов и работать предпочтительно через геттеры класса case.

Я использую Spark 1.6.1 и Scala 2.10

25
Sparky

Наблюдение

Spark SQL может оптимизировать объединение, только если условие объединения основано на операторе равенства. Это означает, что мы можем рассматривать эквиджоины и неэквиноины отдельно.

Эквисоединения

Equijoin может быть реализован безопасным для типов способом путем сопоставления обоих кортежей Datasets (key, value), выполнения объединения на основе ключей и изменения формы результата:

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-эквисоединения

Может быть выражен с использованием операторов реляционной алгебры как R Sθ S = σθ (R × S) и преобразован непосредственно в код.

Spark 2.0

Включите crossJoin и используйте joinWith с тривиально равным предикатом:

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

Используйте метод 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)
}

Примеры

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)

Заметки

  • Следует отметить, что эти методы качественно отличаются от прямого приложения joinWith и требуют дорогостоящих преобразований DeserializeToObject/SerializeFromObject (по сравнению с этим прямым joinWith могут использовать логические операции с данными). 

    Это похоже на поведение, описанное в Набор данных Spark 2.0 против DataFrame .

  • Если вы не ограничены Spark SQL API frameless предоставляет интересные безопасные расширения типов для Datasets (на сегодняшний день он поддерживает только Spark 2.0):

    import frameless.TypedDataset
    
    val typedPoints1 = TypedDataset.create(points1)
    val typedPoints2 = TypedDataset.create(points2)
    
    typedPoints1.join(typedPoints2, typedPoints1('x), typedPoints2('x))
    
  • API Dataset нестабилен в 1.6, поэтому я не думаю, что имеет смысл использовать его там.

  • Конечно, этот дизайн и описательные названия не нужны. Вы можете легко использовать класс типов, чтобы неявно добавить эти методы к Dataset, и нет конфликта со встроенными сигнатурами, так что оба могут быть вызваны joinWith.

25
user6910411

Кроме того, еще одна большая проблема для не типизированного API Spark заключается в том, что при соединении двух Datasets он дает DataFrame. И тогда вы теряете типы из ваших исходных двух наборов данных. 

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