web-dev-qa-db-ja.com

スキーマを文字列として(つまり、JSONでエンコードされたスキーマ)from_jsonを使用するにはどうすればよいですか?

Kafkaからストリームを読み取っていて、値をKafka(JSON)からStructureに変換します。

from_jsonにはタイプStringのスキーマをとるバリアントがありますが、サンプルが見つかりませんでした。以下のコードで何が問題なのかを教えてください。

エラー

Exception in thread "main" org.Apache.spark.sql.catalyst.parser.ParseException: 
extraneous input '(' expecting {'SELECT', 'FROM', 'ADD', 'AS', 'ALL', 'DISTINCT',

== SQL ==
STRUCT ( `firstName`: STRING, `lastName`: STRING, `email`: STRING, `addresses`: ARRAY ( STRUCT ( `city`: STRING, `state`: STRING, `Zip`: STRING )  )  ) 
-------^^^

at org.Apache.spark.sql.catalyst.parser.ParseException.withCommand(ParseDriver.scala:217)

プログラム

public static void main(String[] args) throws AnalysisException {
    String master = "local[*]";
    String brokers = "quickstart:9092";
    String topics = "simple_topic_6";

    SparkSession sparkSession = SparkSession
            .builder().appName(EmployeeSchemaLoader.class.getName())
            .master(master).getOrCreate();

   String employeeSchema = "STRUCT ( firstName: STRING, lastName: STRING, email: STRING, " +
            "addresses: ARRAY ( STRUCT ( city: STRING, state: STRING, Zip: STRING )  )  ) ";

    SparkContext context = sparkSession.sparkContext();
    context.setLogLevel("ERROR");
    SQLContext sqlCtx = sparkSession.sqlContext();

    Dataset<Row> employeeDataset = sparkSession.readStream().
            format("kafka").
            option("kafka.bootstrap.servers", brokers)
            .option("subscribe", topics).load();
    employeeDataset.printSchema();
    employeeDataset = employeeDataset.withColumn("strValue", employeeDataset.col("value").cast("string"));
    employeeDataset = employeeDataset.withColumn("employeeRecord",
            functions.from_json(employeeDataset.col("strValue"),employeeSchema, new HashMap<>()));

    employeeDataset.printSchema();
    employeeDataset.createOrReplaceTempView("employeeView");

    sparkSession.catalog().listTables().show();

    sqlCtx.sql("select * from employeeView").show();
}
6
Manjesh

あなたの質問は、Stringベースのスキーマを持つfrom_jsonのバリアントがJavaでのみ利用可能であり、 最近 が_に追加されていることを見つけるのに役立ちましたSpark AP​​I for Scala次の2.3.0。私は長い間、Spark AP​​I for Scalaが常に最も機能が豊富であり、変更前はそうではなかったはずだということを学ぶのに役立ったという強い信念を持って生きてきました。 2.3.0で(!)

質問に戻ると、実際には文字列ベースのスキーマをJSONまたはDDL形式で定義できます。

JSONを手動で作成するのは少し面倒なので、別のアプローチを取ります(私がScala開発者であることを考えると、かなり簡単です)。

まず、ScalaのSparkAPIを使用してスキーマを定義しましょう。

import org.Apache.spark.sql.types._
val addressesSchema = new StructType()
  .add($"city".string)
  .add($"state".string)
  .add($"Zip".string)
val schema = new StructType()
  .add($"firstName".string)
  .add($"lastName".string)
  .add($"email".string)
  .add($"addresses".array(addressesSchema))
scala> schema.printTreeString
root
 |-- firstName: string (nullable = true)
 |-- lastName: string (nullable = true)
 |-- email: string (nullable = true)
 |-- addresses: array (nullable = true)
 |    |-- element: struct (containsNull = true)
 |    |    |-- city: string (nullable = true)
 |    |    |-- state: string (nullable = true)
 |    |    |-- Zip: string (nullable = true)

それはあなたのスキーマと一致しているようですよね?

これにより、スキーマをJSONでエンコードされた文字列に変換するのは、jsonメソッドで簡単に行えました。

val schemaAsJson = schema.json

schemaAsJsonはまさにあなたのJSON文字列で、かなり...うーん...複雑に見えます。表示の目的で、私はむしろprettyJsonメソッドを使用したいと思います。

scala> println(schema.prettyJson)
{
  "type" : "struct",
  "fields" : [ {
    "name" : "firstName",
    "type" : "string",
    "nullable" : true,
    "metadata" : { }
  }, {
    "name" : "lastName",
    "type" : "string",
    "nullable" : true,
    "metadata" : { }
  }, {
    "name" : "email",
    "type" : "string",
    "nullable" : true,
    "metadata" : { }
  }, {
    "name" : "addresses",
    "type" : {
      "type" : "array",
      "elementType" : {
        "type" : "struct",
        "fields" : [ {
          "name" : "city",
          "type" : "string",
          "nullable" : true,
          "metadata" : { }
        }, {
          "name" : "state",
          "type" : "string",
          "nullable" : true,
          "metadata" : { }
        }, {
          "name" : "Zip",
          "type" : "string",
          "nullable" : true,
          "metadata" : { }
        } ]
      },
      "containsNull" : true
    },
    "nullable" : true,
    "metadata" : { }
  } ]
}

これがJSONのスキーマです。

DataTypeを使用して、JSON文字列を「検証」できます(Sparkがfrom_jsonの内部で使用する DataType.fromJson を使用)。

import org.Apache.spark.sql.types.DataType
val dt = DataType.fromJson(schemaAsJson)
scala> println(dt.sql)
STRUCT<`firstName`: STRING, `lastName`: STRING, `email`: STRING, `addresses`: ARRAY<STRUCT<`city`: STRING, `state`: STRING, `Zip`: STRING>>>

すべて問題ないようです。サンプルデータセットでこれをチェックしているのでしょうか?

val rawJsons = Seq("""
  {
    "firstName" : "Jacek",
    "lastName" : "Laskowski",
    "email" : "[email protected]",
    "addresses" : [
      {
        "city" : "Warsaw",
        "state" : "N/A",
        "Zip" : "02-791"
      }
    ]
  }
""").toDF("rawjson")
val people = rawJsons
  .select(from_json($"rawjson", schemaAsJson, Map.empty[String, String]) as "json")
  .select("json.*") // <-- flatten the struct field
  .withColumn("address", explode($"addresses")) // <-- explode the array field
  .drop("addresses")  // <-- no longer needed
  .select("firstName", "lastName", "email", "address.*") // <-- flatten the struct field
scala> people.show
+---------+---------+---------------+------+-----+------+
|firstName| lastName|          email|  city|state|   Zip|
+---------+---------+---------------+------+-----+------+
|    Jacek|Laskowski|[email protected]|Warsaw|  N/A|02-791|
+---------+---------+---------------+------+-----+------+
8
Jacek Laskowski