Partilhar via


Mapeie dados usando fluxos de dados

Importante

Esta página inclui instruções para gerenciar componentes do Azure IoT Operations usando manifestos de implantação do Kubernetes, que está em visualização. Esse recurso é fornecido com várias limitações e não deve ser usado para cargas de trabalho de produção.

Veja Termos de Utilização Complementares da Pré-visualizações do Microsoft Azure para obter os termos legais que se aplicam às funcionalidades do Azure que estão na versão beta, na pré-visualização ou que ainda não foram lançadas para disponibilidade geral.

Use a linguagem de mapeamento de fluxo de dados para transformar dados nas Operações IoT do Azure. A sintaxe é uma maneira simples, mas poderosa, de definir mapeamentos que transformam dados de um formato para outro. Este artigo fornece uma visão geral da linguagem de mapeamento de fluxo de dados e dos principais conceitos.

O mapeamento permite transformar dados de um formato para outro. Considere o seguinte registro de entrada:

{
  "Name": "Grace Owens",
  "Place of birth": "London, TX",
  "Birth Date": "19840202",
  "Start Date": "20180812",
  "Position": "Analyst",
  "Office": "Kent, WA"
}

Compare-o com o registro de saída:

{
  "Employee": {
    "Name": "Grace Owens",
    "Date of Birth": "19840202"
  },
  "Employment": {
    "Start Date": "20180812",
    "Position": "Analyst, Kent, WA",
    "Base Salary": 78000
  }
}

No registro de saída, as seguintes alterações são feitas nos dados do registro de entrada:

  • Campos renomeados: O Birth Date campo agora Date of Birthé .
  • Campos reestruturados: Ambos Name e Date of Birth estão agrupados na nova Employee categoria.
  • Campo excluído: o Place of birth campo é removido porque não está presente na saída.
  • Campo adicionado: O Base Salary campo é um novo campo na Employment categoria.
  • Valores de campo alterados ou mesclados: O Position campo na saída combina os Position campos e Office da entrada.

As transformações são alcançadas através de mapeamento, que normalmente envolve:

  • Definição de entrada: Identificando os campos nos registros de entrada que são usados.
  • Definição de saída: Especificando onde e como os campos de entrada são organizados nos registros de saída.
  • Conversão (opcional): Modificar os campos de entrada para caber nos campos de saída. expression é necessário quando vários campos de entrada são combinados em um único campo de saída.

O mapeamento a seguir é um exemplo:

{
  inputs: [
    'BirthDate'
  ]
  output: 'Employee.DateOfBirth'
}
{
  inputs: [
    'Position'  // - - - - $1
    'Office'    // - - - - $2
  ]
  output: 'Employment.Position'
  expression: '$1 + ", " + $2'
}
{
  inputs: [
    '$context(position).BaseSalary'
  ]
  output: 'Employment.BaseSalary'
}

Os mapas de exemplo:

  • Mapeamento um-para-um: BirthDate é mapeado diretamente sem Employee.DateOfBirth conversão.
  • Mapeamento muitos-para-um: combina Position e Office em um único Employment.Position campo. A fórmula de conversão ($1 + ", " + $2) mescla esses campos em uma cadeia de caracteres formatada.
  • Dados contextuais: BaseSalary são adicionados a partir de um conjunto de dados contextual chamado position.

Referências de campo

As referências de campo mostram como especificar caminhos na entrada e saída usando notação de pontos como Employee.DateOfBirth ou acessando dados de um conjunto de dados contextual via $context(position).

Propriedades de metadados MQTT e Kafka

Quando você usa MQTT ou Kafka como origem ou destino, você pode acessar várias propriedades de metadados na linguagem de mapeamento. Essas propriedades podem ser mapeadas na entrada ou saída.

Propriedades de metadados

  • Tópico: Funciona para MQTT e Kafka. Ele contém a cadeia de caracteres onde a mensagem foi publicada. Exemplo: $metadata.topic.
  • Propriedade do usuário: No MQTT, isso se refere aos pares chave/valor de forma livre que uma mensagem MQTT pode carregar. Por exemplo, se a mensagem MQTT foi publicada com uma propriedade de usuário com chave "prioridade" e valor "alto", então a $metadata.user_property.priority referência mantém o valor "alto". As chaves de propriedade do usuário podem ser cadeias de caracteres arbitrárias e podem exigir fuga: $metadata.user_property."weird key" usa a chave "chave estranha" (com um espaço).
  • Propriedade do sistema: este termo é usado para cada propriedade que não é uma propriedade do usuário. Atualmente, apenas uma única propriedade do sistema é suportada: $metadata.system_property.content_type, que lê a propriedade de tipo de conteúdo da mensagem MQTT (se definida).
  • Cabeçalho: Este é o equivalente Kafka da propriedade de usuário MQTT. Kafka pode usar qualquer valor binário para uma chave, mas o fluxo de dados suporta apenas chaves de cadeia de caracteres UTF-8. Exemplo: $metadata.header.priority. Essa funcionalidade é semelhante às propriedades do usuário.

Mapeando propriedades de metadados

Mapeamento de entrada

No exemplo a seguir, a propriedade MQTT topic é mapeada para o origin_topic campo na saída:

inputs: [
  '$metadata.topic'
]
output: 'origin_topic'

Se a propriedade priority user estiver presente na mensagem MQTT, o exemplo a seguir demonstra como mapeá-la para um campo de saída:

inputs: [
  '$metadata.user_property.priority'
]
output: 'priority'
Mapeamento de saída

Você também pode mapear propriedades de metadados para um cabeçalho de saída ou propriedade de usuário. No exemplo a seguir, o MQTT topic é mapeado para o origin_topic campo na propriedade user da saída:

inputs: [
  '$metadata.topic'
]
output: '$metadata.user_property.origin_topic'

Se a carga de entrada contiver um priority campo, o exemplo a seguir demonstra como mapeá-lo para uma propriedade de usuário MQTT:

inputs: [
  'priority'
]
output: '$metadata.user_property.priority'

O mesmo exemplo para Kafka:

inputs: [
  'priority'
]
output: '$metadata.header.priority'

Seletores de conjunto de dados de contextualização

Esses seletores permitem que mapeamentos integrem dados extras de bancos de dados externos, que são chamados de conjuntos de dados de contextualização.

Filtragem de registos

A filtragem de registros envolve a definição de condições para selecionar quais registros devem ser processados ou descartados.

Notação de pontos

A notação de pontos é amplamente utilizada em ciência da computação para campos de referência, mesmo recursivamente. Na programação, os nomes de campo normalmente consistem em letras e números. Um exemplo de notação de pontos padrão pode ser semelhante a este exemplo:

inputs: [
  'Person.Address.Street.Number'
]

Em um fluxo de dados, um caminho descrito pela notação de pontos pode incluir cadeias de caracteres e alguns caracteres especiais sem precisar escapar:

inputs: [
  'Person.Date of Birth'
]

Em outros casos, a fuga é necessária:

inputs: [
  'Person."Tag.10".Value'
]

O exemplo anterior, entre outros caracteres especiais, contém pontos dentro do nome do campo. Sem escapar, o nome do campo serviria como um separador na própria notação de pontos.

Enquanto um fluxo de dados analisa um caminho, ele trata apenas dois caracteres como especiais:

  • Os pontos (.) atuam como separadores de campo.
  • Aspas simples, quando colocadas no início ou no final de um segmento, iniciam uma seção com escape onde os pontos não são tratados como separadores de campo.

Quaisquer outros caracteres são tratados como parte do nome do campo. Essa flexibilidade é útil em formatos como JSON, onde os nomes de campo podem ser cadeias de caracteres arbitrárias.

No Bicep, todas as cadeias de caracteres são colocadas entre aspas simples ('). Os exemplos sobre cotação adequada no YAML para uso do Kubernetes não se aplicam.

Fuga

A função principal de escapar em um caminho com anotação de pontos é acomodar o uso de pontos que fazem parte de nomes de campos em vez de separadores:

inputs: [
  'Payload."Tag.10".Value'
]

Neste exemplo, o caminho consiste em três segmentos: Payload, Tag.10e Value.

Escapando de regras na notação de pontos

  • Escape de cada segmento separadamente: se vários segmentos contiverem pontos, esses segmentos devem ser colocados entre aspas duplas. Outros segmentos também podem ser citados, mas isso não afeta a interpretação do caminho:

    inputs: [
      'Payload."Tag.10".Measurements."Vibration.$12".Value'
    ]
    

  • Uso adequado de aspas duplas: aspas duplas devem abrir e fechar um segmento com escape. Todas as aspas no meio do segmento são consideradas parte do nome do campo:

    inputs: [
      'Payload.He said: "Hello", and waved'
    ]
    

Este exemplo define dois campos: Payload e He said: "Hello", and waved. Quando um ponto aparece nessas circunstâncias, ele continua a servir como separador:

inputs: [
  'Payload.He said: "No. It is done"'
]

Neste caso, o caminho é dividido nos segmentos Payload, He said: "Noe It is done" (começando com um espaço).

Algoritmo de segmentação

  • Se o primeiro caractere de um segmento for uma aspas, o analisador procurará as aspas seguintes. A cadeia de caracteres entre essas aspas é considerada um único segmento.
  • Se o segmento não começar com aspas, o analisador identificará segmentos procurando o próximo ponto ou o final do caminho.

Caráter universal

Em muitos cenários, o registro de saída se assemelha muito ao registro de entrada, com apenas pequenas modificações necessárias. Quando você lida com registros que contêm vários campos, especificar manualmente mapeamentos para cada campo pode se tornar tedioso. Os curingas simplificam esse processo, permitindo mapeamentos generalizados que podem ser aplicados automaticamente a vários campos.

Vamos considerar um cenário básico para entender o uso de asteriscos em mapeamentos:

inputs: [
  '*'
]
output: '*'

Esta configuração mostra um mapeamento básico onde cada campo na entrada é diretamente mapeado para o mesmo campo na saída sem alterações. O asterisco (*) serve como um curinga que corresponde a qualquer campo no registro de entrada.

Veja como o asterisco (*) opera nesse contexto:

  • Correspondência de padrões: o asterisco pode corresponder a um único segmento ou a vários segmentos de um caminho. Ele serve como um espaço reservado para quaisquer segmentos no caminho.
  • Correspondência de campo: Durante o processo de mapeamento, o algoritmo avalia cada campo no registro de entrada em relação ao padrão especificado no inputs. O asterisco no exemplo anterior corresponde a todos os caminhos possíveis, ajustando efetivamente cada campo individual na entrada.
  • Segmento capturado: a parte do caminho à qual o asterisco corresponde é chamada de captured segment.
  • Mapeamento de saída: Na configuração de saída, o captured segment é colocado onde o asterisco aparece. Isso significa que a estrutura da entrada é preservada na saída, com o captured segment preenchimento do espaço reservado fornecido pelo asterisco.

Outro exemplo ilustra como curingas podem ser usados para corresponder subseções e movê-las juntas. Este exemplo efetivamente nivela estruturas aninhadas dentro de um objeto JSON.

JSON original:

{
  "ColorProperties": {
    "Hue": "blue",
    "Saturation": "90%",
    "Brightness": "50%",
    "Opacity": "0.8"
  },
  "TextureProperties": {
    "type": "fabric",
    "SurfaceFeel": "soft",
    "SurfaceAppearance": "matte",
    "Pattern": "knitted"
  }
}

Configuração de mapeamento que usa curingas:

{
  inputs: [
    'ColorProperties.*'
  ]
  output: '*'
}
{
  inputs: [
    'TextureProperties.*'
  ]
  output: '*'
}

JSON resultante:

{
  "Hue": "blue",
  "Saturation": "90%",
  "Brightness": "50%",
  "Opacity": "0.8",
  "type": "fabric",
  "SurfaceFeel": "soft",
  "SurfaceAppearance": "matte",
  "Pattern": "knitted"
}

Colocação de curinga

Ao colocar um curinga, você deve seguir estas regras:

  • Asterisco único por referência de dados: Apenas um asterisco (*) é permitido dentro de uma única referência de dados.
  • Correspondência completa de segmentos: O asterisco deve sempre corresponder a um segmento inteiro do caminho. Ele não pode ser usado para corresponder apenas a uma parte de um segmento, como path1.partial*.path3.
  • Posicionamento: O asterisco pode ser posicionado em várias partes de uma referência de dados:
    • No início: *.path2.path3 - Aqui, o asterisco corresponde a qualquer segmento que conduza a path2.path3.
    • No meio: path1.*.path3 - Nesta configuração, o asterisco corresponde a qualquer segmento entre path1 e path3.
    • No final: path1.path2.* - O asterisco no final corresponde a qualquer segmento que se siga depois de path1.path2.
  • O caminho que contém o asterisco deve ser colocado entre aspas simples (').

Curingas de entrada múltipla

JSON original:

{
  "Saturation": {
    "Max": 0.42,
    "Min": 0.67,
  },
  "Brightness": {
    "Max": 0.78,
    "Min": 0.93,
  },
  "Opacity": {
    "Max": 0.88,
    "Min": 0.91,
  }
}

Configuração de mapeamento que usa curingas:

inputs: [
  '*.Max'  // - $1
  '*.Min'  // - $2
]
output: 'ColorProperties.*'
expression: '($1 + $2) / 2'

JSON resultante:

{
  "ColorProperties" : {
    "Saturation": 0.54,
    "Brightness": 0.85,
    "Opacity": 0.89 
  }    
}

Se você usar curingas de várias entradas, o asterisco (*) deverá representar consistentemente o mesmo Captured Segment em todas as entradas. Por exemplo, quando * capturas Saturation no padrão *.Max, o algoritmo de mapeamento espera que o correspondente Saturation.Min corresponda ao padrão *.Min. Aqui, * é substituído pelo Captured Segment da primeira entrada, orientando a correspondência para as entradas subsequentes.

Considere este exemplo detalhado:

JSON original:

{
  "Saturation": {
    "Max": 0.42,
    "Min": 0.67,
    "Mid": {
      "Avg": 0.51,
      "Mean": 0.56
    }
  },
  "Brightness": {
    "Max": 0.78,
    "Min": 0.93,
    "Mid": {
      "Avg": 0.81,
      "Mean": 0.82
    }
  },
  "Opacity": {
    "Max": 0.88,
    "Min": 0.91,
    "Mid": {
      "Avg": 0.89,
      "Mean": 0.89
    }
  }
}

Configuração de mapeamento inicial que usa curingas:

inputs: [
  '*.Max'    // - $1
  '*.Min'    // - $2
  '*.Avg'    // - $3
  '*.Mean'   // - $4
]

Esse mapeamento inicial tenta criar uma matriz (por exemplo, para Opacity: [0.88, 0.91, 0.89, 0.89]). Esta configuração falha porque:

  • A primeira entrada *.Max captura um segmento como Saturation.
  • O mapeamento espera que as entradas subsequentes estejam presentes no mesmo nível:
    • Saturation.Max
    • Saturation.Min
    • Saturation.Avg
    • Saturation.Mean

Como Avg e Mean estão aninhados no Mid, o asterisco no mapeamento inicial não captura corretamente esses caminhos.

Configuração de mapeamento corrigida:

inputs: [
  '*.Max'        // - $1
  '*.Min'        // - $2
  '*.Mid.Avg'    // - $3
  '*.Mid.Mean'   // - $4
]

Este mapeamento revisado captura com precisão os campos necessários. Ele especifica corretamente os caminhos para incluir o objeto aninhado Mid , o que garante que os asteriscos funcionem efetivamente em diferentes níveis da estrutura JSON.

Segunda regra vs. especialização

Ao usar o exemplo anterior de curingas de várias entradas, considere os seguintes mapeamentos que geram dois valores derivados para cada propriedade:

{
  inputs: [
    '*.Max'   // - $1
    '*.Min'   // - $2
  ]
  output: 'ColorProperties.*.Avg'
  expression: '($1 + $2) / 2'
}
{
  inputs: [
    '*.Max'   // - $1
    '*.Min'   // - $2
  ]
  output: 'ColorProperties.*.Diff'
  expression: '$1 - $2'
}

Esse mapeamento destina-se a criar dois cálculos separados (Avg e Diff) para cada propriedade em ColorProperties. Este exemplo mostra o resultado:

{
  "ColorProperties": {
    "Saturation": {
      "Avg": 0.54,
      "Diff": 0.25
    },
    "Brightness": {
      "Avg": 0.85,
      "Diff": 0.15
    },
    "Opacity": {
      "Avg": 0.89,
      "Diff": 0.03
    }
  }
}

Aqui, a segunda definição de mapeamento nas mesmas entradas atua como uma segunda regra para mapeamento.

Agora, considere um cenário em que um campo específico precisa de um cálculo diferente:

{
  inputs: [
    '*.Max'   // - $1
    '*.Min'   // - $2
  ]
  output: 'ColorProperties.*'
  expression: '($1 + $2) / 2'
}
{
  inputs: [
    'Opacity.Max'   // - $1
    'Opacity.Min'   // - $2
  ]
  output: 'ColorProperties.OpacityAdjusted'
  expression: '($1 + $2 + 1.32) / 2'
}

Neste caso, o Opacity campo tem um cálculo único. Duas opções para lidar com esse cenário de sobreposição são:

  • Inclua ambos os mapeamentos para Opacity. Como os campos de saída são diferentes neste exemplo, eles não substituiriam uns aos outros.
  • Use a regra mais específica e Opacity remova a mais genérica.

Considere um caso especial para os mesmos campos para ajudar a decidir a ação certa:

{
  inputs: [
    '*.Max'   // - $1
    '*.Min'   // - $2
  ]
  output: 'ColorProperties.*'
  expression: '($1 + $2) / 2'
}
{
  inputs: [
    'Opacity.Max'   // - $1
    'Opacity.Min'   // - $2
  ]
  output: ''
}

Um campo vazio output na segunda definição implica não escrever os campos no registro de saída (removendo Opacityefetivamente ). Esta configuração é mais do Specialization que um Second Rulearquivo .

Resolução de mapeamentos sobrepostos por fluxos de dados:

  • A avaliação progride a partir da regra superior na definição do mapeamento.
  • Se um novo mapeamento for resolvido para os mesmos campos de uma regra anterior, as seguintes condições se aplicam:
    • A Rank é calculado para cada entrada resolvida com base no número de segmentos capturados pelo curinga. Por exemplo, se o Captured Segments são Properties.Opacity, o Rank é 2. Se for apenas Opacity, o Rank é 1. Um mapeamento sem curingas tem um Rank de 0.
    • Se a Rank última regra for igual ou superior à regra anterior, um fluxo de dados a tratará como um Second Rule.
    • Caso contrário, o fluxo de dados trata a configuração como um Specializationarquivo .

Por exemplo, o mapeamento que direciona Opacity.Max e Opacity.Min para uma saída vazia tem um Rank de 0. Como a segunda regra tem um valor menor Rank do que a anterior, ela é considerada uma especialização e substitui a regra anterior, que calcularia um valor para Opacity.

Curingas em conjuntos de dados de contextualização

Agora, vamos ver como os conjuntos de dados de contextualização podem ser usados com curingas por meio de um exemplo. Considere um conjunto de dados chamado position que contenha o seguinte registro:

{
  "Position": "Analyst",
  "BaseSalary": 70000,
  "WorkingHours": "Regular"
}

Em um exemplo anterior, usamos um campo específico desse conjunto de dados:

inputs: [
  '$context(position).BaseSalary'
]
output: 'Employment.BaseSalary'

Esse mapeamento copia BaseSalary do conjunto de dados de contexto diretamente para a Employment seção do registro de saída. Se quiser automatizar o processo e incluir todos os campos do conjunto de position dados na seção, você pode usar curingas Employment :

inputs: [
  '$context(position).*'
]
output: 'Employment.*'

Essa configuração permite um mapeamento dinâmico onde cada campo dentro do position conjunto de dados é copiado para a Employment seção do registro de saída:

{
    "Employment": {      
      "Position": "Analyst",
      "BaseSalary": 70000,
      "WorkingHours": "Regular"
    }
}

Último valor conhecido

Você pode acompanhar o último valor conhecido de uma propriedade. Sufixe o campo de entrada com ? $last para capturar o último valor conhecido do campo. Quando uma propriedade está faltando um valor em uma carga de entrada subsequente, o último valor conhecido é mapeado para a carga útil de saída.

Por exemplo, considere o seguinte mapeamento:

inputs: [
  'Temperature ? $last'
]
output: 'Thermostat.Temperature'

Neste exemplo, o último valor conhecido de Temperature é rastreado. Se uma carga útil de entrada subsequente não contiver um Temperature valor, o último valor conhecido será usado na saída.