Connect(); 2016

Volumen 31, número 12

Connect(); - Aplicaciones inteligentes: extensibilidad en aplicaciones de macrodatos de U-SQL

Por Michael Rys; 2016

El enfoque tradicional para resolver las grandes V de los microdatos (volumen, velocidad y variedad) durante el procesamiento de macrodatos se ha centrado principalmente en proporcionar una plataforma escalable para procesar el volumen de datos, en agregar funcionalidades de procesamiento casi en tiempo real y en ofrecer la capacidad de procesar una variedad de formatos de datos de entrada, de CSV a formatos binarios personalizados, pasando por JSON. Una variedad que a veces ha sido un tipo de añadido es la variedad asociada con el procesamiento de datos personalizados, no solo en términos de formato, sino también respecto a la capacidad de facilitar la extensión del análisis con algoritmos personalizados a la vez que se conserva la naturaleza declarativa de la experiencia del lenguaje de consulta.

Algunos lenguajes modernos de consulta y procesamiento de macrodatos ya están empezando a tenerlo en cuenta. En concreto, U-SQL se diseñó desde cero para combinar la eficacia declarativa de un lenguaje basado en SQL con la flexibilidad de usar las bibliotecas de código existentes y de desarrollar nuevos algoritmos personalizados.

En un artículo anterior (bit.ly/1OtXM2K), presenté U-SQL y mostré que el uso del sistema de tipos de Microsoft .NET Framework junto con el lenguaje de expresión basado en C# de U-SQL facilita la extensión del análisis con expresiones de código personalizado. Expliqué cómo usar ensamblados C# para crear funciones definidas por el usuario (UDF) y usarlas en scripts de consulta U-SQL.

U-SQL no solo le permite agregar sus propias funciones C#, sino que también proporciona un marco que le permite agregar sus propios operadores definidos por el usuario (UDO) como sus propios extractores, generadores de salida y operadores de conjuntos de filas (por ejemplo, procesadores, aplicadores, reductores y combinadores personalizados). El marco consta de dos partes:

  1. Interfaces de .NET que proporcionan el contrato para que compile estos operadores de forma que pueda concentrarse en el código y dejar la ejecución escalada horizontalmente a U-SQL. Tenga en cuenta que el código de lógica empresarial real no se tiene que implementar en .NET, como mostraré más adelante.
  2. Expresiones U-SQL como EXTRACT y REDUCE que invocan a los operadores personalizados y los ejecutan a escala en sus datos.

En este artículo, me basaré en el anterior y mostraré que puede usar los mecanismos de extensibilidad de U-SQL para procesar una variedad de datos, desde JSON a datos de imagen. También le mostraré cómo agregar sus propios operadores.

Administrar código personalizado en U-SQL

Antes de empezar con algunos ejemplos, es necesario comprender mejor cómo puede usar U-SQL el código personalizado.

Como ya mencionamos, SQL sigue a C# con su lenguaje de expresión escalar, que se usa en lugares como predicados U-SQL y en las expresiones de una cláusula select. Para que el código personalizado sea visible para el compilador U-SQL, el código debe estar empaquetado en un ensamblado .NET al que debe hacer referencia el script U-SQL. Para poder hacer referencia al ensamblado, se debe haber registrado previamente en el servicio de metadatos de U-SQL mediante una instrucción CREATE ASSEMBLY.

Registrar ensamblados U-SQL y hacer referencia a ellos Sugiero usar Azure Data Lake Tools para Visual Studio (aka.ms/adltoolsvs), que facilita la compilación y el registro de ensamblados que funcionan con U-SQL. Si escribe código personalizado en un proyecto de biblioteca de clases para aplicación U-SQL (consulte la Figura 1), puede escribir el código, compilar el proyecto y registrar el archivo DLL de ensamblado generado directamente con un clic con el botón derecho (consulte la Figura 2).

Proyecto de biblioteca de clases (para aplicación U-SQL)
Figura 1 Proyecto de biblioteca de clases (para aplicación U-SQL)

Registrar un ensamblado U-SQL
Figura 2 Registrar un ensamblado U-SQL

Después, lo único que necesitará en el script U-SQL es la instrucción REFERENCE ASSEMBLY para que los métodos y las clases públicas se puedan usar en el script U-SQL, como se muestra en la Figura 3.

Figura 3 Referencia a una función definida por el usuario desde un ensamblado personalizado

REFERENCE ASSEMBLY master.TweetAnalysis;
USING tweet_fns = TweetAnalysis.Udfs;
@t =
  EXTRACT date string,
          time string,
          author string,
          tweet string
  FROM "/Samples/Data/Tweets/Tweets.csv"
  USING Extractors.Csv();
// Get the mentions from the tweet string
@m =
  SELECT origin
       , tweet_fns.get_mentions(tweet) AS mentions
       , author AS mentioned_by
FROM @t;
...

Usar código existente con ensamblados U-SQL A menudo querrá usar bibliotecas de código existentes o incluso código que no sea de .NET. Si quiere usar código que no es de .NET (por ejemplo, una biblioteca nativa o incluso un entorno en tiempo de ejecución de lenguaje diferente como Python o JavaScript, debe encapsular el código que no es de .NET con una capa de interoperabilidad de C# que se invocará desde U-SQL y que, después, llama al código que no es de .NET, serializa los datos entre los componentes e implementa un contrato de interfaz de UDO. En ese caso, los artefactos de código que no son de NET, como los archivos .dll nativos o los archivos del entorno en tiempo de ejecución distinto se deben agregar como archivos adicionales. Se puede hacer desde la opción de archivo adicional del registro de ensamblado. Estos archivos se implementan automáticamente en todos los nodos cuando se hace referencia al ensamblado .NET en un script y se ponen a disposición del directorio de trabajo del ensamblado .NET localmente en el nodo.

Para usar bibliotecas .NET existentes, debe registrar las bibliotecas de código existentes como dependencias administradas en su propio ensamblado o, si vuelve a usar una biblioteca que puede usar directamente en U-SQL, registrarla directamente en la base de datos U-SQL. En cualquier caso, el script debe hacer referencia a todos los ensamblados .NET que necesite.

Mostraré algunos ejemplos de estas opciones de registro en el resto del artículo, mientras describo algunos escenarios de código personalizados en los que tiene sentido usar el modelo de extensibilidad. Estos escenarios incluyen: combinar rangos que se solapan con un reductor personalizado, procesar documentos JSON, procesar datos de imagen y procesar datos espaciales. Los describiré de uno en uno.

Combinar rangos que se superponen con un reductor personalizado

Supongamos que tiene un archivo de registro que hace un seguimiento cuando un usuario interactúa con el servicio. Supongamos también que un usuario puede interactuar con su servicio de varias formas; (por ejemplo, realizando búsquedas en Bing desde distintos dispositivos o ventanas del explorador). Como parte del trabajo de U-SQL que prepara el archivo de registro para un análisis posterior, es recomendable combinar los rangos que se solapan.

Por ejemplo, si el archivo de registro de entrada tiene el aspecto de la Figura 4, es recomendable combinar los rangos que se solapan para cada usuario como en la Figura 5.

Figura 4 Archivo de registro con intervalos de tiempo que se solapan

Hora de inicio  Hora de finalización  Nombre de usuario
5:00  6:00  ABC
5:00  6:00  XYZ
8:00  9:00  ABC
8:00  10:00  ABC
10:00  14:00  ABC
7:00  11:00  ABC
9:00  11:00  ABC
11:00  11:30  ABC
23:40  23:59  FOO
23:50  00:40  FOO

Figura 5 Archivo de registro después de combinar los intervalos de tiempo que se solapan

Hora de inicio  Hora de finalización  Nombre de usuario
5:00  6:00  ABC
5:00  6:00  XYZ
7:00  14:00  ABC
23:40  00:40  FOO

Si observa el problema, enseguida se dará cuenta de que es recomendable definir algo como una agregación definida por el usuario para combinar los intervalos de tiempo que se solapan. Sin embargo, si observa los datos de entrada, se dará cuenta de que debido a que los datos no están ordenados, tendrá que conservar el estado para todos los intervalos posibles y, después, combinar los intervalos no contiguos a medida que aparezcan intervalos puente, o tendrá que ordenar previamente los intervalos para cada nombre de usuario a fin de facilitar la combinación de intervalos.

La agregación ordenada es más fácil de escalar horizontalmente, pero U-SQL no proporciona agregadores definidos por el usuario ordenados (UDAGG). Además, los UDAGG suelen producir una fila por grupo, mientras que, en este caso, puedo tener varias filas por grupo si los rangos no son contiguos.

Por suerte, U-SQL proporciona un UDO escalable llamado reductor (bit.ly/2evGsDA) que puede agregar un conjunto de filas basado en una clave de agrupación, mediante un código personalizado.

En primer lugar, escribamos la lógica de U-SQL, donde ReduceSample.Range­Reducer es nuestro reductor definido por el usuario (UDO de reductor) del ensamblado RangeReducer y los datos del registro se encuentran en el archivo /Samples/Blogs/MRys/Ranges/ranges.txt (bit.ly/2eseZyw) y usan "-" como delimitador de columna. Este es el código:

REFERENCE ASSEMBLY RangeReducer;
@in = EXTRACT start DateTime, end DateTime, user string
FROM "/Samples/Blogs/MRys/Ranges/ranges.txt"
USING Extractors.Text(delimiter:'-');
@r =  REDUCE @in PRESORT start ON user
      PRODUCE start DateTime, end DateTime, user string
      READONLY user
      USING new ReduceSample.RangeReducer();
OUTPUT @r
TO "/temp/result.csv"
USING Outputters.Csv();

La expresión REDUCE usa el conjunto de filas @in como entrada, lo divide según la columna de usuario, preordena las particiones según los valores de la columna de inicio y aplica RangeReducer. Como resultado, produce el mismo esquema de conjunto de filas. Dado que el reductor solo ajusta el rango de principio a fin, no toca la columna de usuario, de modo que la marca como de SOLO LECTURA. Esto da permiso al marco del reductor para pasar los datos automáticamente a través de esa columna y, a cambio, permite que el procesador de consultas U-SQL aplique optimizaciones agresivamente alrededor de las columnas de solo lectura; por ejemplo, para insertar predicados de una columna de solo lectura antes que el reductor.

La forma de escribir un reductor es implementar una instancia de Microsoft.Analytics.Interfaces.IReducer. En este caso, dado que no necesita proporcionar ningún parámetro, solo tendrá que sobrescribir el método abstracto Reduce. Puede copiar el código en una biblioteca C# para U-SQL y registrarlo como ensamblado RangeReducer, como se describe más arriba. La Figura 6 muestra la implementación de RangeReducer. (Tenga en cuenta que las prácticas de sangría de código se han alterado en algunos ejemplos de código debido a restricciones de espacio).

Figura 6 Implementación de RangeReducer

using Microsoft.Analytics.Interfaces;
using Microsoft.Analytics.Types.Sql;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ReduceSample
{
  public class RangeReducer : IReducer
  {
    public override IEnumerable<IRow> Reduce(
      IRowset input, IUpdatableRow output)
    {
      // Init aggregation values
      bool first_row_processed = false;
      var begin = DateTime.MaxValue;
      var end = DateTime.MinValue;
      // Requires that the reducer is PRESORTED on begin and
      // READONLY on the reduce key.
      foreach (var row in input.Rows)
      {
        // Initialize the first interval with the first row if i is 0
       if (!first_row_processed)
        {
         first_row_processed = true; // Mark that the first row was handled
          begin = row.Get<DateTime>("start");
          end = row.Get<DateTime>("end");
          // If the end is just a time and not a date, it can be earlier
          // than the begin, indicating it is on the next day;
          // this let's you fix up the end to the next day in that case
          if (end < begin) { end = end.AddDays(1); }
        }
        else // Handle the remaining rows
        {
          var b = row.Get<DateTime>("start");
          var e = row.Get<DateTime>("end");
          // Fix up the date if end is earlier than begin
          if (e < b) { e = e.AddDays(1); }
          // If begin time is still inside the interval,
          // increase the interval if it is longer
          if (b <= end)
          {
            // If the new end time is later than the current,
            // extend the interval
            if (e > end) { end = e; }
          }
          else // Output the previous interval and start a new one
          {
            output.Set<DateTime>("start", begin);
            output.Set<DateTime>("end", end);
            yield return output.AsReadOnly();
            begin = b; end = e;
          } // if
        } // if
      } // foreach
      // Now output the last interval
      output.Set<DateTime>("start", begin);
      output.Set<DateTime>("end", end);
      yield return output.AsReadOnly();
    } // Reduce
  } // RangeReducer
} // ReduceSample

La expresión U-SQL REDUCE aplicará el método Reduce una vez para cada clave de partición distinta en paralelo. Así pues, el parámetro de entrada solo contendrá las filas de un grupo determinado y la implementación puede devolver entre cero y muchas filas como salida.

Dado que la cláusula PRESORT garantiza que las filas estén ordenadas, la lógica interna puede asumir que los datos están ordenados. Por otra parte, dado que la columna user está marcada como READONLY, se pasará automáticamente y podrá escribir el código de UDO de un modo más genérico, centrándose solo en las columnas que quiere transformar.

Si ahora aplica el reductor en un gran conjunto de datos, y si puede que algunos usuarios usen el sistema con más frecuencia que otros, se encontrará con algo llamado asimetría de datos, una situación en que algunos usuarios tienen grandes particiones y, otros, solo pequeñas. Dado que, con el contrato del reductor, se garantiza que se verán todos los datos de la partición, todos los datos se deben ordenar aleatoriamente en este nodo y se deben leer en una llamada. Debido a este requisito, la asimetría de datos, en el mejor de los casos, puede dar lugar a que algunas particiones tarden más que el resto en procesarse. En el peor de los casos, puede provocar que algunos reductores se queden sin recursos de tiempo y memoria (el tiempo de espera de un vértice U-SQL se agota después de ejecutarse durante unas cinco horas).

Si la semántica del reductor es asociativa y conmutativa y su esquema de salida es el mismo que el de entrada, un reductor se puede marcar como recursivo, lo que permite al motor de consulta dividir grandes grupos en subgrupos más pequeños y aplicar recursivamente el reductor en estos subgrupos para calcular el resultado final. Esta aplicación recursiva permite al reductor equilibrar y paralelizar mejor en caso de asimetría de datos. Para marcar un reductor como recursivo, se usa la anotación de propiedad SqlUserDefinedReducer(IsRecursive = true):

namespace ReduceSample
{
  [SqlUserDefinedReducer(IsRecursive = true)]
  public class RangeReducer : IReducer
  {
    public override IEnumerable<IRow> Reduce(
      IRowset input, IUpdatableRow output)
    {
      // Insert the code from Figure 6 here
    } // Reduce
  } // RangeReducer
} // ReduceSample

En nuestro caso, el reductor se puede marcar como recursivo para mejorar la escalabilidad y el rendimiento, suponiendo que el procesamiento conservará el orden en las filas en cada invocación recursiva.

Para buscar un proyecto de Visual Studio para el ejemplo en nuestro repositorio de GitHub, consulte bit.ly/2ecLe5B.

Procesar documentos JSON

Uno de los formatos de datos más frecuentes, después de los archivos de texto separados por comas, es JSON. A diferencia de los formatos de archivo CSV, U-SQL no proporciona un extractor de JSON integrado. Sin embargo, la comunidad U-SQL ha proporcionado un ensamblado de ejemplo en bit.ly/2d9O4va compatible con la extracción y el procesamiento de documentos JSON y XML.

Esta solución usa la biblioteca Json.NET de Newtonsoft (bit.ly/2evWJbz) para el trabajo pesado con JSON y System.XML para el procesamiento XML. El ensamblado puede extraer datos de un documento JSON mediante JsonExtractor (bit.ly/2dPARsM), dividir un documento JSON para generar SqlMap a fin de permitir la navegación y la descomposición de documentos JSON con la función JsonTuple (bit.ly/2e8tSuX) y, finalmente, transformar un conjunto de filas en un archivo con formato JSON con JSONOutputter (bit.ly/2e4uv3W).

Tenga en cuenta que el ensamblado se ha diseñado para ser un procesador JSON genérico, lo que significa que no realiza ninguna hipótesis sobre la estructura del documento JSON y debe ser resistente a la naturaleza semiestructurada de JSON, incluidos los elementos de tipo heterogéneo (escalar o estructurado, distintos tipos de datos para el mismo elemento, elementos que faltan, etc.). Si sabe que sus documentos JSON siguen un esquema concreto, puede crear un extractor de JSON más eficiente.

A diferencia del ejemplo del reductor anterior, en que escribía su propio ensamblado y, a continuación, lo implementaba, en este caso, la solución está lista para usarse. Puede cargar la solución desde nuestro repositorio de GitHub a Visual Studio y compilarla e implementarla por sí mismo, o puede buscar los archivos DLL en el directorio bin\Debug de la solución.

Como ya hemos mencionado, la dependencia que no es del sistema requiere que los ensamblados Samples.Format y Json.NET se registren en el almacén de metadatos de U-SQL (puede seleccionar el ensamblado Newtonsoft como dependencia administrada al registrar el ensamblado Format con la herramienta de Visual Studio) y se debe hacer referencia a ambos si quiere procesar documentos JSON. Si ha instalado los ensamblados JSON en el catálogo de U-SQL con el nombre [Microsoft.Analytics.Samples.Formats] y [NewtonSoft.Json] en la base de datos U-SQL JSONBlog (consulte la Figura 7), para usar los ensamblados, haga referencia a ellos al principio de los scripts con:

REFERENCE ASSEMBLY JSONBlog.[NewtonSoft.Json];
REFERENCE ASSEMBLY JSONBlog.[Microsoft.Analytics.Samples.Formats];

Registrar el ensamblado Formats en Visual Studio
Figura 7 Registrar el ensamblado Formats en Visual Studio

El extractor JSON implementa la interfaz IExtractor U-SQL. Dado que los documentos JSON se deben analizar completamente para asegurarse de que su formato es correcto, un archivo que contenga un único documento JSON se debe procesar en un único vértice de extractor. Para indicar que el extractor debe ver todo el contenido del archivo, defina la propiedad AtomicFileProcessing como true (consulte la Figura 8). Se puede llamar al extractor con un parámetro opcional denominado rowpath que nos permite identificar los objetos JSON que se asignarán a una fila con la expresión JSONPath (bit.ly/1EmvgKO).

Figura 8 El extractor JSON

[SqlUserDefinedExtractor(AtomicFileProcessing = true)]
public class JsonExtractor : IExtractor
{
  private string rowpath;            
  public JsonExtractor(string rowpath = null)
  {
    this.rowpath = rowpath;
  }
  public override IEnumerable<IRow> Extract(
    IUnstructuredReader input, IUpdatableRow output)
  {
    // Json.NET
    using (var reader = new JsonTextReader(
      new StreamReader(input.BaseStream)))
    {
      // Parse Json
      var root = JToken.ReadFrom(reader);
      // Rows
      // All objects are represented as rows
      foreach (JObject o in SelectChildren(root, this.rowpath))
      {
        // All fields are represented as columns
        this.JObjectToRow(o, output);
        yield return output.AsReadOnly();
      }
    }
  }
}

La implementación del extractor pasará la secuencia de entrada que el marco del extractor U-SQL proporciona al extractor a JsonTextReader de Json.NET. A continuación, usará rowpath para obtener los subárboles que se están asignando a una fila con SelectChildren. Dado que los objetos JSON pueden ser heterogéneos, el código devuelve el valor genérico de JObject, en lugar de valores escalares o JArray de posición.

Tenga en cuenta que este extractor carga el documento JSON en la memoria. Si el documento es demasiado grande, puede provocar una condición de memoria insuficiente. En ese caso, tendría que escribir su propio extractor para que transmita el documento sin tener que cargarlo completamente en la memoria.

Ahora usemos el extractor JSON y la función de tupla de JSON para analizar el complejo documento JSON de /Samples/Blogs/MRys/JSON/complex.json (bit.ly/2ekwOEQ), como se muestra en la Figura 9.

Figura 9 Documento de ejemplo de JSON

[{
  "person": {
    "personid": 123456,
    "name": "Person 1",
    "addresses": {
      "address": [{
        "addressid": "2",
        "street": "Street 2",
        "postcode": "1234 AB",
        "city": "City 1"
      }, {
        "addressid": "2",
        "street": "Street 2",
        "postcode": "5678 CD",
        "city": "City 2"
      }]
    }
  }
}, {
     "person": {
     "personid": 798,
     "name": "Person 2",
     "addresses": {
       "address": [{
         "addressid": "1",
         "street": "Street 1",
         "postcode": "1234 AB",
         "city": "City 1"
     }, {
         "addressid": "4",
         "street": "Street 7",
         "postcode": "98799",
         "city": "City 3"
     }]
   }
  }
}]

El formato es una matriz de "objetos" de persona (técnicamente, objetos que contienen una clave de persona única cada uno) que a su vez contienen propiedades de persona y objetos de dirección. El script U-SQL de la Figura 10 extrae una fila por combinación de persona y dirección.

Figura 10 Script U-SQL que procesa el documento JSON de ejemplo de la Figura 9

DECLARE @input string = "/Samples/Blogs/MRys/JSON/complex.json";
REFERENCE ASSEMBLY JSONBlog.[Newtonsoft.Json];
REFERENCE ASSEMBLY JSONBlog.[Microsoft.Analytics.Samples.Formats];
USING Microsoft.Analytics.Samples.Formats.Json;
@json =
  EXTRACT personid int,
          name string,
          addresses string
  FROM @input
  USING new JsonExtractor("[*].person");
@person =
  SELECT personid,
         name,
         JsonFunctions.JsonTuple(
           addresses, "address")["address"] AS address_array
  FROM @json;
@addresses =
  SELECT personid,
         name,
         JsonFunctions.JsonTuple(address) AS address
  FROM @person
       CROSS APPLY
         EXPLODE (JsonFunctions.JsonTuple(address_array).Values)
           AS A(address);
@result =
  SELECT personid,
         name,
         address["addressid"]AS addressid,
         address["street"]AS street,
         address["postcode"]AS postcode,
         address["city"]AS city
  FROM @addresses;
OUTPUT @result
TO "/output/json/persons.csv"
USING Outputters.Csv();

Tenga en cuenta que el script pasa la expresión JSONPath [*].person al extractor y, de este modo, se genera una fila para cada campo de persona en la matriz de nivel superior. El extractor usa el esquema EXTRACT para obtener las propiedades de objeto resultantes en columnas. Dado que el campo de direcciones en sí es un documento JSON anidado, la primera invocación de la función JsonTuple crea un mapa que contiene objetos de direcciones que, a continuación, se asignan a una fila por dirección con la expresión CROSS APPLY EXPLODE. Finalmente, todas las propiedades de dirección se proyectan desde el tipo de datos de asignación para ofrecerle el conjunto de filas, como se muestra en la Figura 11.

Figura 11 Conjunto de filas generado al procesar el documento JSON de la Figura 9

123456 Person 1 2 Street 2 1234 AB City 1
123456 Person 1 2 Street 2 5678 CD City 2
798 Person 2 1 Street 1 1234 AB City 1
798 Person 2 4 Street 7 98799 City 3

Puede encontrar un proyecto de Visual Studio de ejemplo y otros escenarios de procesamiento de JSON, incluidos varios documentos JSON en un archivo, en nuestro repositorio de GitHub: bit.ly/2dzceLv.

Procesar datos de imagen

En este ejemplo, estoy procesando algunos datos sin estructurar de mayor tamaño: imágenes. En concreto, quiero procesar imágenes JPEG y extraer algunas propiedades EXIF JPEG, así como crear una miniatura de la imagen. Afortunadamente, .NET proporciona una variedad de funcionalidades de procesamiento de imágenes en la clase System.Drawing. De modo que solo tengo que compilar los operadores y funciones de extensión U-SQL y delegar el procesamiento de JPEG a estas clases.

Existen muchas maneras de hacerlo. Para empezar, podría cargar todas las imágenes como matrices de bytes en un conjunto de filas y, después, aplicar funciones individuales definidas por el usuario para extraer cada una de las propiedades y crear la miniatura, como se muestra en la Figura 12.

Figura 12 Cargar imágenes en filas para procesar imágenes en U-SQL

REFERENCE ASSEMBLY Images;
USING Images;
@image_data =
  EXTRACT image_data byte[]  // Max size of row is 4MB!
        , name string
        , format string
  FROM @"/Samples/Data/Images/{name}.{format}"
  USING new ImageExtractor();
// Use UDFs
@image_properties =
  SELECT ImageOps.getImageProperty(image_data, ImageProperties.copyright)
         AS image_copyright,
         ImageOps.getImageProperty(image_data, ImageProperties.equipment_make)
         AS image_equipment_make,
         ImageOps.getImageProperty(image_data, ImageProperties.equipment_model)
         AS image_equipment_model,
         ImageOps.getImageProperty(image_data, ImageProperties.description)
         AS image_description
  FROM @image_data
  WHERE format IN ("JPEG", "jpeg", "jpg", "JPG");

Sin embargo, este enfoque presenta algunos inconvenientes:

  • Las filas U-SQL pueden tener un tamaño de hasta 4 MB, lo que limita la solución a imágenes de un tamaño de 4 MB (menos el tamaño de las otras columnas).
  • Cada invocación de función puede sumar presión a la memoria y requiere la incorporación de la matriz de bytes al procesamiento de U-SQL.

Por lo tanto, es recomendable realizar la extracción de propiedades y la creación de miniaturas directamente dentro del extractor personalizado. La Figura 13 muestra un script U-SQL revisado.

Figura 13 Extraer las características con un extractor para procesar imágenes en U-SQL

REFERENCE ASSEMBLY Images;
@image_features =
  EXTRACT copyright string,
          equipment_make string,
          equipment_model string,
          description string,
          thumbnail byte[],
          name string,
          format string
  FROM @"/Samples/Data/Images/{name}.{format}"
  USING new Images.ImageFeatureExtractor(scaleWidth:500, scaleHeight:300);
@image_features =
  SELECT *
  FROM @image_features
  WHERE format IN ("JPEG", "jpeg", "jpg", "JPG");
OUTPUT @image_features
TO @"/output/images/image_features.csv"
USING Outputters.Csv();
@scaled_image =
  SELECT thumbnail
  FROM @image_features
  WHERE name == "GT4";
OUTPUT @scaled_image
TO "/output/images/GT4_thumbnail_2.jpg"
USING new Images.ImageOutputter();

Este script extrae las propiedades y la miniatura de las imágenes que especifica el patrón de conjunto de archivos (bit.ly/2ektTY6): /Samples/Data/Images/{name}.{format}. A continuación, la instrucción SELECT restringe la extracción a los archivos JPEG. Para hacerlo, usa un predicado solo en la columna de formato que eliminará todos los archivos que no sean JPEG de la extracción (el optimizador solo aplicará el extractor a los archivos que cumplan el predicado en la columna de formato). El extractor ofrece la opción de especificar las dimensiones de la miniatura. A continuación, el script exporta las características a un archivo CSV y usa un sencillo generador de salida de nivel de secuencia be bytes para crear un archivo de miniatura para una de las imágenes reducidas verticalmente.

En la Figura 14 se muestra la implementación del extractor.

Figura 14 Extractor de características de imagen

using Microsoft.Analytics.Interfaces;
using Microsoft.Analytics.Types.Sql;
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;
namespace Images
{
  public static class UpdatableRowExtensions
  {
    public static void SetColumnIfExists<T>(this IUpdatableRow source
                                           , string colName, T value)
    {
      var colIdx = source.Schema.IndexOf(colName);
      if (colIdx != -1)
      { source.Set<T>(colIdx, value); }
    }
  }
  [SqlUserDefinedExtractor(AtomicFileProcessing = true)]
  public class ImageFeatureExtractor : IExtractor
  {
    private int _scaleWidth, _scaleHeight;
    public ImageFeatureExtractor(int scaleWidth = 150, int scaleHeight = 150)
    { _scaleWidth = scaleWidth; _scaleHeight = scaleHeight; }
    public override IEnumerable<IRow> Extract(IUnstructuredReader input
                                             , IUpdatableRow output)
    {
      byte[] img = ImageOps.GetByteArrayforImage(input.BaseStream);
      using (StreamImage inImage = new StreamImage(img))
      {
        output.SetColumnIfExists("image", img);
        output.SetColumnIfExists("equipment_make",
          inImage.getStreamImageProperty(ImageProperties.equipment_make));
        output.SetColumnIfExists("equipment_model",
          inImage.getStreamImageProperty(ImageProperties.equipment_model));
        output.SetColumnIfExists("description",
          inImage.getStreamImageProperty(ImageProperties.description));
        output.SetColumnIfExists("copyright",
          inImage.getStreamImageProperty(ImageProperties.copyright));
        output.SetColumnIfExists("thumbnail",
          inImage.scaleStreamImageTo(this._scaleWidth, this._scaleHeight));
      }
      yield return output.AsReadOnly();
    }
  }
}

De nuevo, el extractor necesita ver todo el archivo y trabaja con input.BaseStream, pero ahora crea solo una imagen en memoria, a diferencia del script de la Figura 12. El extractor también comprueba cada una de las columnas solicitadas y solo procesa los datos de los nombres de columna solicitados con el método de extensión SetColumnIfExists.

Para obtener más información, consulte el proyecto de Visual Studio de nuestro sitio de GitHub en bit.ly/2dngXCE.

Procesar datos espaciales

En este ejemplo, voy a mostrar cómo usar el ensamblado de tipo espacial de SQL Server Microsoft.SqlServer.Types.dll en U-SQL. En concreto, quiero usar las funciones de biblioteca espacial de los scripts U-SQL como funciones definidas por el usuario. Como en el caso del extractor JSON comentado antes, esto significa que puede registrar un ensamblado existente en U-SQL sin tener que escribir su propio ensamblado.

En primer lugar, descargue e instale el ensamblado del Feature Pack de SQL Server 2016 (bit.ly/2dZTw1k). Seleccione la versión de 64 bits del instalador (ENU\x64\SQLSysClrTypes.msi) para asegurarse de tener la versión de 64 bits de las bibliotecas.

El instalador instala el ensamblado administrado Microsoft.Sql­­Server.Types.dll en C:\Archivos de programa (x86)\Microsoft SQL Server\130\SDK\Assemblies y el ensamblado nativo SqlServerSpatial130.dll en \Windows\System32\. A continuación, cargue los ensamblados en Azure Data Lake Store (por ejemplo, en una carpeta denominada /upload/asm/spatial). Dado que el instalador ha instalado la biblioteca nativa en la carpeta del sistema c:\Windows\System32, debe asegurarse de copiar SqlServerSpatial130.dll fuera de esa carpeta antes de cargarla o de que la herramienta que usa no redirija el sistema de archivos (bit.ly/1TYm9YZ) en carpetas del sistema. Por ejemplo, si quiera cargarla con el Explorador de archivos de ADL de Visual Studio actual, antes deberá copiar el archivo en otro directorio. De lo contrario, en el momento en que se escribe este artículo, se carga la versión de 32 bits (porque Visual Studio es una aplicación de 32 bits que aplica la redirección del sistema de archivos en su ventana de selección de archivos de carga de ADL) y, cuando ejecute un script U-SQL que llame al ensamblado nativo, recibirá el siguiente error (interno) en tiempo de ejecución: "Inner exception from user expression: An attempt was made to load a program with an incorrect format. (Exception from HRESULT: 0x8007000B)" (Excepción interna de expresión de usuario: se ha intentado cargar un programa con un formato incorrecto. [Excepción de HRESULT: 0x8007000B]).

Después de cargar los dos archivos de ensamblado, regístrelos en una base de datos denominada SQLSpatial con este script:

DECLARE @ASSEMBLY_PATH string = "/upload/asm/spatial/";
DECLARE @SPATIAL_ASM string = @ASSEMBLY_PATH+"Microsoft.SqlServer.Types.dll";
DECLARE @SPATIAL_NATIVEDLL string = @ASSEMBLY_PATH+"SqlServerSpatial130.dll";
CREATE DATABASE IF NOT EXISTS SQLSpatial;
USE DATABASE SQLSpatial;
DROP ASSEMBLY IF EXISTS SqlSpatial;
CREATE ASSEMBLY SqlSpatial
FROM @SPATIAL_ASM
WITH ADDITIONAL_FILES =
  (
    @SPATIAL_NATIVEDLL
  );

Tenga en cuenta que, en este caso, solo registra un ensamblado U-SQL e incluye el ensamblado nativo como dependencia fuerte del ensamblado U-SQL. Para usar ensamblados espaciales, solo tiene que hacer referencia al ensamblado U-SQL y el archivo adicional estará disponible automáticamente para el ensamblado. La Figura 15 muestra un script de ejemplo sencillo con el ensamblado espacial.

Figura 15 Usar las funcionalidades espaciales en U-SQL

REFERENCE SYSTEM ASSEMBLY [System.Xml];
REFERENCE ASSEMBLY SQLSpatial.SqlSpatial;
USING Geometry = Microsoft.SqlServer.Types.SqlGeometry;
USING Geography = Microsoft.SqlServer.Types.SqlGeography;
USING SqlChars = System.Data.SqlTypes.SqlChars;
@spatial =
    SELECT * FROM (VALUES
                   // The following expression is not using the native DDL
                   ( Geometry.Point(1.0,1.0,0).ToString()),   
                   // The following expression is using the native DDL
                   ( Geometry.STGeomFromText(
                     new SqlChars("LINESTRING (100 100, 20 180, 180 180)"),
                     0).ToString())
                  ) AS T(geom);
OUTPUT @spatial
TO "/output/spatial.csv"
USING Outputters.Csv();

La biblioteca de tipos SQL tiene una dependencia del ensamblado System.Xml, por lo que le debe hacer referencia. Además, algunos métodos usan los tipos System.Data.SqlTypes, en lugar de los tipos de C# integrados. Como System.Data ya se incluye por defecto, puede hacer referencia, simplemente, al tipo SQL necesario. El código de la Figura 15 está disponible en nuestro sitio de GitHub en bit.ly/2dMSBm9.

Resumen: Algunas sugerencias y procedimientos recomendados para UDO

En este artículo, aunque no se profundiza en las eficaces funcionalidades de extensibilidad de U-SQL, se muestra que el mecanismo de extensibilidad de U-SQL permite reutilizar el código específico de dominio existente y usar el marco de extensión de U-SQL para escalar horizontalmente el procesamiento con el volumen típico de macrodatos.

Sin embargo, usar mal esta herramienta tan eficaz es muy fácil, así que aquí tiene algunas sugerencias y procedimientos recomendados.

Aunque los formatos de datos personalizados suelen necesitar un extractor personalizado y pueden necesitar un generador de salida, es necesario considerar atentamente si el formato de datos se puede extraer en paralelo (por ejemplo, formatos de tipo CSV) o si el procesamiento necesita ver todos los datos en una única instancia de operador. Además, hacer los operadores lo suficientemente genéricos para que solo se realice el procesamiento si se solicita una columna específica puede mejorar el rendimiento, potencialmente.

Al considerar los UDO como procesadores, reductores, combinadores y aplicadores, es muy recomendable considerar en primer lugar una solución U-SQL pura que use operadores integrados. Por ejemplo, el script reductor de rango descrito antes se podría escribir con un uso inteligente del sistema de ventanas y las funciones de categoría. Estos son algunos motivos por los que se puede considerar el uso de UDO:

  • La lógica necesita acceder dinámicamente al esquema de entrada o salida del conjunto de filas que se está procesando. Por ejemplo, crear un documento JSON para los datos de la fila en que las columnas no se conocen de antemano.
  • Una solución que usa varias funciones definidas por el usuario en la expresión SELECT crea demasiada presión de memoria y puede escribir su propio código para que su consumo de memoria sea más eficiente en un UDO de procesador.
  • Necesita un agregador ordenado o un agregador que produzca más de una fila por grupo y no se pueden crear con funciones de ventana.

Cuando use UDO, no olvide las sugerencias siguientes:

  • Use la cláusula READONLY para permitir la inserción de predicados mediante UDO.
  • Use la cláusula REQUIRED para permitir la eliminación de columnas mediante UDO.
  • Use la cardinalidad en la expresión de consulta que usa un UDO si el optimizador de consulta elige un plan equivocado.

Michael Rys es administrador de programas principal en Microsoft. Ha trabajado en procesamiento de datos y lenguajes de consulta desde la década de los 80. Ha representado a Microsoft en los comités de diseño de XQuery y SQL, y ha llevado a SQL Server más allá de lo relacional con XML, la tecnología geoespacial y la búsqueda semántica. Actualmente, trabaja en lenguajes de consulta de macrodatos como SCOPE y U-SQL cuando no está pasando tiempo en familia bajo el agua o haciendo autocross. Sígale en Twitter: @MikeDoesBigData.

Gracias a los siguientes expertos técnicos de Microsoft por revisar este artículo: Clemens Szyperski, Ed Triou, Saveen Reddy y Michael Kadaner
Clemens Szyperski es administrador de ingeniería de grupo principal en Microsoft. Durante décadas, su pasión han sido los lenguajes especializados, las herramientas y los enfoques que facilitan la construcción de sistemas de software complejos. Actualmente, dirige los equipos de Scope y U-SQL de Azure Data Lake, excepto cuando está navegando con su familia. Sígale en Twitter: @ClemensSzy.

Ed Triou es jefe de desarrollo principal en Microsoft. Durante los últimos 20 años, se ha centrado en la programación de datos (ODBC, OLEDB, ADO.NET, JDBC, PHP y EDM), con una especialización en compiladores y lenguajes de consulta (IDL, TSQl, LINQ a SQL/entidades, eSQL, SCOPE y U-SQL).  Actualmente, dirige los equipos de lenguaje y compilador U-SQL e intenta ir un paso por delante de nuestras empresas externas e internas que dependen a diario de ADL y Cosmos a escala de exabytes.

Saveen Reddy is administrador de programas principal en Microsoft y se centra en el diseño y la compilación de la plataforma Azure Data Lake: los componentes y experiencias subyacentes a los servicios en la nube de macrodatos de Microsoft. Saveen tiene un porcentaje de finalización del 100 por cien en Metal Gear Solid V: The Phantom Pain. Sígale en Twitter: @saveenr.

Michael Kadaner es ingeniero de software principal en Microsoft. A pesar de décadas de experiencia en varias áreas de informática y desarrollo de software, afirma que escribir programas es un arte exacto y que el software puede estar libre de errores. Su verdadera pasión es resolver problemas algorítmicos y de ingeniería, así como implementar las soluciones en un código conciso y elegante correctamente diseñado. Divide su tiempo libre entre la lectura y los proyectos de autoproducción.