如何在spark-avro 2.4模式中设置逻辑类型?

时间:2019-02-06 18:14:57

标签: scala apache-spark avro spark-avro

我们从应用程序中的avro文件中读取时间戳信息。我正在测试从Spark 2.3.1到Spark 2.4的升级,其中包括新内置的spark-avro集成。但是,我无法弄清楚如何告诉avro模式我希望时间戳具有“ timestamp-millis”的逻辑类型,而不是默认的“ timestamp-micros”。

仅使用Databricks spark-avro 4.0.0软件包在Spark 2.3.1下查看测试avro文件,我们就有以下字段/架构:

{"name":"id","type":["string","null"]},
{"name":"searchQuery","type":["string","null"]},
{"name":"searchTime","type":["long","null"]},
{"name":"score","type":"double"},
{"name":"searchType","type":["string","null"]}

自纪元存储很长时间以来,其中的searchTime以毫秒为单位。一切都很好。

当我将其升级到Spark 2.4和内置的spark-avro 2.4.0软件包时,我有了以下较新的字段/模式:

{"name":"id","type":["string","null"]},
{"name":"searchQuery","type":["string","null"]},
{"name":"searchTime","type":[{"type":"long","logicalType":"timestamp-micros"},"null"]},
{"name":"score","type":"double"},
{"name":"searchType","type":["string","null"]}

可以看到,底层类型仍然很长,但是现在使用“ timestamp-micros”的逻辑类型进行了扩充。这完全符合the release notes所说的那样,但是,我找不到一种方法来指定使用'timestamp-millis'选项的模式。

这成为一个问题,当我将一个Timestamp对象写入到avro文件时,该对象初始化为在新纪元后10,000秒时,它将被读回10,000,000秒。在2.3.1 / databricks-avro下,它很长,没有任何关联的信息,所以它随它进入就出来了。

我们当前通过以下方式思考感兴趣的对象来构建模式:

val searchSchema: StructType = ScalaReflection.schemaFor[searchEntry].dataType.asInstanceOf[StructType]

我尝试通过创建一个修改过的架构来尝试增强此功能,该架构试图如下替换与searchTime条目对应的StructField:

    val modSearchSchema = StructType(searchSchema.fields.map {
      case StructField(name, _, nullable, metadata) if name == "searchTime" =>
        StructField(name, org.apache.spark.sql.types.DataTypes.TimestampType, nullable, metadata)
      case f => f
    })

但是,spark.sql.types中定义的StructField对象没有可以增加其中的dataType的逻辑类型的概念。

case class StructField(
    name: String,
    dataType: DataType,
    nullable: Boolean = true,
    metadata: Metadata = Metadata.empty) 

我还试图通过两种方式从JSON表示形式创建模式:

val schemaJSONrepr = """{
          |          "name" : "id",
          |          "type" : "string",
          |          "nullable" : true,
          |          "metadata" : { }
          |        }, {
          |          "name" : "searchQuery",
          |          "type" : "string",
          |          "nullable" : true,
          |          "metadata" : { }
          |        }, {
          |          "name" : "searchTime",
          |          "type" : "long",
          |          "logicalType" : "timestamp-millis",
          |          "nullable" : false,
          |          "metadata" : { }
          |        }, {
          |          "name" : "score",
          |          "type" : "double",
          |          "nullable" : false,
          |          "metadata" : { }
          |        }, {
          |          "name" : "searchType",
          |          "type" : "string",
          |          "nullable" : true,
          |          "metadata" : { }
          |        }""".stripMargin

第一次尝试只是从中创建一个数据类型

// here spark is a SparkSession instance from a higher scope.
val schema = DataType.fromJSON(schemaJSONrepr).asInstanceOf[StructType]
spark.read
     .schema(schema)
     .format("avro")
     .option("basePath", baseUri)
     .load(uris: _*)

这失败了,因为无法在其中为searchTime节点创建StructType,因为它具有“ logicalType”。第二次尝试是通过传入原始JSON字符串来简单地创建模式。

spark.read
     .schema(schemaJSONrepr)
     .format("avro")
     .option("basePath", baseUri)
     .load(uris: _*)

这不能说:

mismatched input '{' expecting {'SELECT', 'FROM', ...

== SQL ==

{
^^^

我发现在spark-avro API中,有一种方法可以从模式中获取逻辑类型,但无法弄清楚如何设置它。

您可以在上面看到我的失败尝试,我尝试使用Schema.Parser创建一个Avro模式对象,但是spark.read.schema中唯一接受的类型是String和StructType。

如果任何人都可以提供有关如何更改/指定此logicalType的见解,我将非常感谢。谢谢

1 个答案:

答案 0 :(得分:0)

好的,我想我回答了我自己的问题。当我修改以编程方式构建的架构以使用显式的时间戳类型

val modSearchSchema = StructType(searchSchema.fields.map {
      case StructField(name, _, nullable, metadata) if name == "searchTime" =>
        StructField(name, org.apache.spark.sql.types.DataTypes.TimestampType, nullable, metadata)
      case f => f
    })

当我们有要读取的Row对象时,在进行读取时我没有改变逻辑。最初,我们将读取Long并将其转换为Timestamp,这是事情发生的地方,因为它以毫秒为单位回读Long,这使它比我们预期的大1000倍。通过将读取更改为读取Timestamp对象,可以直接让底层逻辑解决此问题,从而将其从我们(我的手)手中夺走。所以:

// searchTime = new Timestamp(row.getAs[Long]("searchTime")) BROKEN

searchTime = row.getAs[Timestamp]("searchTime") // SUCCESS