Todos os produtos
Search
Central de documentação

:Operadores de feature personalizados

Última atualização: Jun 28, 2026

O framework Feature Generation (FG) suporta operadores de feature personalizados como plugins de biblioteca compartilhada. Utilize-os quando os operadores nativos não atenderem à sua lógica de transformação específica de domínio — por exemplo, distância de edição entre dois campos de texto, distância esférica a partir de coordenadas GPS ou qualquer fórmula numérica aplicada elemento a elemento em uma sequência.

Como funciona

  1. Implemente a interface C++ IFeatureOP e registre o operador com REGISTER_PLUGIN.

  2. Compile a implementação em uma biblioteca compartilhada (.so).

  3. Referencie o operador no arquivo fg.json definindo feature_type como custom_feature e operator_name com o nome registrado.

  4. Implante o arquivo .so: os serviços online o descobrem automaticamente no diretório custom_fg_lib, enquanto tarefas offline exigem o upload do arquivo como um resource do MaxCompute.

Pré-requisitos

Antes de começar, certifique-se de ter:

  • Um ambiente de build compatível com C++17 usando a imagem oficial do compilador (consulte Compilar um operador personalizado)

  • O pacote de dependências da API: fg-api.tar.gz — contém todos os arquivos de cabeçalho necessários

  • Familiaridade com o framework FG e o funcionamento da configuração fg.json

Implementar um operador

Exemplo mínimo

Todo operador personalizado segue o mesmo padrão: herdar de IFeatureOP, implementar Initialize e pelo menos um método ProcessWith*, além de registrar com REGISTER_PLUGIN.

Abaixo está um operador mínimo que gera um número inteiro constante:

#include "api/base_op.h"

namespace fg {

class ConstantOp : public IFeatureOP {
 public:
  int Initialize(const string& feature_config) override {
    // Parse JSON config if needed; return 0 on success.
    return 0;
  }

  int ProcessWithInt32Outputs(const vector<FieldPtr>& inputs,
                              vector<int32>& outputs) override {
    outputs.push_back(42);
    return 0;  // 0 = success
  }
};

}  // namespace fg

REGISTER_PLUGIN("ConstantOp", ConstantOp);

Requisitos principais:

  • Inclua um construtor sem parâmetros (o construtor padrão funciona neste caso).

  • O método Initialize recebe toda a entrada fg.json como uma string JSON. Faça o parse dos itens de configuração necessários a partir dela.

  • Retorne 0 para indicar sucesso em todos os métodos; valores diferentes de zero indicam falha.

  • Chame REGISTER_PLUGIN no arquivo de implementação, não no cabeçalho.

Escolher entre ProcessWith* e BatchProcess

Duas interfaces de processamento estão disponíveis. Escolha com base nos seus requisitos de throughput e complexidade:

**ProcessWith***

**BatchProcess**

Granularidade

Um registro por vez

Um lote de registros

Tipos de saída

Apenas tipos escalares básicos (string, int32, int64, float, double)

Qualquer tipo válido de VariantVector, incluindo Map

Quando usar

Transformações simples, baixo custo de implementação

Necessário alto throughput; features do lado do usuário com uma amostra por requisição (use o mecanismo de broadcast para evitar parsing redundante)

Obrigatório

Pelo menos um método correspondente ao value_type

Sobrescrever HasBatchProcessImpl() para retornar true

Quando BatchProcess é implementado e HasBatchProcessImpl() retorna true, o framework ignora todos os métodos ProcessWith*.

Interface C++ completa

#pragma once
#ifndef FEATURE_GENERATOR_PLUGIN_BASE_H
#define FEATURE_GENERATOR_PLUGIN_BASE_H

#include <absl/container/flat_hash_map.h>
#include <absl/strings/string_view.h>
#include <absl/types/optional.h>

#include <stdexcept>
#include <utility>
#include <vector>

#include "fsmap.h"
#include "integral_types.h"

namespace fg {

using absl::optional;
using std::string;
using std::vector;

template <typename T>
using List = std::vector<T>;
template <typename K, typename V>
using Map = absl::flat_hash_map<K, V>;
template <typename K, typename V>
using MapArray = std::vector<std::pair<K, V>>;
using Matrix = std::vector<std::vector<float>>;
using MatrixL = std::vector<std::vector<int64>>;
using MatrixS = std::vector<std::vector<string>>;
template <typename K, typename V>
using FSMap = featurestore::type::fs_map<K, V>;

using FieldPtr = absl::variant<
    const optional<string>*, const optional<int32>*, const optional<int64>*,
    const optional<float>*, const optional<double>*,
    const optional<absl::string_view>*,

    const List<string>*, const List<int32>*, const List<int64>*,
    const List<float>*, const List<double>*, const List<absl::string_view>*,

    const Map<string, string>*, const Map<string, int32>*,
    const Map<string, int64>*, const Map<string, float>*,
    const Map<string, double>*, const Map<string, absl::string_view>*,

    const Map<absl::string_view, absl::string_view>*,
    const Map<absl::string_view, int32>*, const Map<absl::string_view, int64>*,
    const Map<absl::string_view, float>*, const Map<absl::string_view, double>*,
    const Map<absl::string_view, string>*,

    const Map<int32, string>*, const Map<int32, int32>*,
    const Map<int32, int64>*, const Map<int32, float>*,
    const Map<int32, double>*, const Map<int32, absl::string_view>*,

    const Map<int64, string>*, const Map<int64, float>*,
    const Map<int64, double>*, const Map<int64, int32>*,
    const Map<int64, int64>*, const Map<int64, absl::string_view>*,

    const FSMap<absl::string_view, absl::string_view>*,
    const FSMap<absl::string_view, int32>*,
    const FSMap<absl::string_view, int64>*,
    const FSMap<absl::string_view, float>*,
    const FSMap<absl::string_view, double>*,

    const FSMap<int32, int32>*, const FSMap<int32, int64>*,
    const FSMap<int32, float>*, const FSMap<int32, double>*,
    const FSMap<int32, absl::string_view>*,

    const FSMap<int64, float>*, const FSMap<int64, double>*,
    const FSMap<int64, int32>*, const FSMap<int64, int64>*,
    const FSMap<int64, absl::string_view>*,

    const MapArray<string, string>*, const MapArray<string, int32>*,
    const MapArray<string, int64>*, const MapArray<string, float>*,
    const MapArray<string, double>*,

    const MapArray<int32, string>*, const MapArray<int32, float>*,
    const MapArray<int32, double>*, const MapArray<int32, int32>*,
    const MapArray<int32, int64>*,

    const MapArray<int64, string>*, const MapArray<int64, float>*,
    const MapArray<int64, double>*, const MapArray<int64, int32>*,
    const MapArray<int64, int64>*, const Matrix*, const MatrixL*,
    const MatrixS*>;

// Represents a COLUMN of the feature table.
using VariantVector = absl::variant<
    vector<optional<string>>, vector<optional<int32>>, vector<optional<int64>>,
    vector<optional<float>>, vector<optional<double>>,
    vector<optional<absl::string_view>>,

    vector<List<string>>, vector<List<int32>>, vector<List<int64>>,
    vector<List<float>>, vector<List<double>>, vector<List<absl::string_view>>,

    vector<Map<string, string>>, vector<Map<string, int32>>,
    vector<Map<string, int64>>, vector<Map<string, float>>,
    vector<Map<string, double>>, vector<Map<string, absl::string_view>>,

    vector<Map<absl::string_view, absl::string_view>>,
    vector<Map<absl::string_view, int32>>,
    vector<Map<absl::string_view, int64>>,
    vector<Map<absl::string_view, float>>,
    vector<Map<absl::string_view, double>>,

    vector<Map<int32, string>>, vector<Map<int32, int32>>,
    vector<Map<int32, int64>>, vector<Map<int32, float>>,
    vector<Map<int32, double>>, vector<Map<int32, absl::string_view>>,

    vector<Map<int64, string>>, vector<Map<int64, float>>,
    vector<Map<int64, double>>, vector<Map<int64, int32>>,
    vector<Map<int64, int64>>, vector<Map<int64, absl::string_view>>,

    vector<FSMap<absl::string_view, absl::string_view>>,
    vector<FSMap<absl::string_view, int32>>,
    vector<FSMap<absl::string_view, int64>>,
    vector<FSMap<absl::string_view, float>>,
    vector<FSMap<absl::string_view, double>>,

    vector<FSMap<int32, int32>>, vector<FSMap<int32, int64>>,
    vector<FSMap<int32, float>>, vector<FSMap<int32, double>>,
    vector<FSMap<int32, absl::string_view>>,

    vector<FSMap<int64, float>>, vector<FSMap<int64, double>>,
    vector<FSMap<int64, int32>>, vector<FSMap<int64, int64>>,
    vector<FSMap<int64, absl::string_view>>,

    vector<MapArray<string, string>>, vector<MapArray<string, int32>>,
    vector<MapArray<string, int64>>, vector<MapArray<string, float>>,
    vector<MapArray<string, double>>,

    vector<MapArray<int32, string>>, vector<MapArray<int32, float>>,
    vector<MapArray<int32, double>>, vector<MapArray<int32, int32>>,
    vector<MapArray<int32, int64>>,

    vector<MapArray<int64, string>>, vector<MapArray<int64, float>>,
    vector<MapArray<int64, double>>, vector<MapArray<int64, int32>>,
    vector<MapArray<int64, int64>>, vector<Matrix>, vector<MatrixL>,
    vector<MatrixS>>;

/**
 * @brief The public base class for custom feature operators.
 *
 * The framework checks if a subclass overrides the `BatchProcess` method. If it is overridden,
 * the framework calls this method to perform the feature transformation.
 * Otherwise, the framework selects one of the `ProcessWith*` methods based on the `value_type` configuration.
 * Implement the method that corresponds to the required output type.
 */
class IFeatureOP {
 public:
  class NotOverriddenException : public std::exception {
   public:
    explicit NotOverriddenException(std::string msg) : msg_(std::move(msg)) {}
    const char* what() const noexcept override {
      if (msg_.empty()) {
        return "unimplemented method called";
      }
      // Cache the message to a member variable to ensure that the returned pointer remains valid.
      cached_ = "unimplemented method called: " + msg_;
      return cached_.c_str();
    }

   private:
    std::string msg_;
    mutable std::string cached_;
  };

  virtual ~IFeatureOP() = default;

  /**
   * @brief Initialization method.
   * @param feature_config The full fg.json entry for this feature, as a JSON string.
   * @return 0 on success; non-zero indicates failure.
   */
  virtual int Initialize(const string& feature_config) = 0;

  /**
   * @brief Processes one record and outputs string values.
   * @param inputs A record that can contain multiple fields.
   * @param outputs The outputs of the feature transformation.
   * @return 0 on success.
   */
  virtual int ProcessWithStrOutputs(const vector<FieldPtr>& inputs,
                                    vector<string>& outputs) {
    throw NotOverriddenException("ProcessWithStrOutputs(FieldPtr)");
  }

  /**
   * @brief Processes one record and outputs int32 values.
   */
  virtual int ProcessWithInt32Outputs(const vector<FieldPtr>& inputs,
                                      vector<int32>& outputs) {
    throw NotOverriddenException("ProcessWithInt32Outputs(FieldPtr)");
  }

  /**
   * @brief Processes one record and outputs int64 values.
   */
  virtual int ProcessWithInt64Outputs(const vector<FieldPtr>& inputs,
                                      vector<int64>& outputs) {
    throw NotOverriddenException("ProcessWithInt64Outputs(FieldPtr)");
  }

  /**
   * @brief Processes one record and outputs float values.
   */
  virtual int ProcessWithFloatOutputs(const vector<FieldPtr>& inputs,
                                      vector<float>& outputs) {
    throw NotOverriddenException("ProcessWithFloatOutputs(FieldPtr)");
  }

  /**
   * @brief Processes one record and outputs double values.
   */
  virtual int ProcessWithDoubleOutputs(const vector<FieldPtr>& inputs,
                                       vector<double>& outputs) {
    throw NotOverriddenException("ProcessWithDoubleOutputs(FieldPtr)");
  }

  /**
   * @brief Optional batch interface that processes one batch of records.
   *
   * @param inputs A vector of input columns; each `VariantVector` represents one feature column.
   * @param outputs The transformed features. Supports complex output types usable as inputs for downstream operators.
   * @return 0 on success.
   */
  virtual int BatchProcess(const vector<VariantVector>& inputs,
                           VariantVector& outputs) {
    throw NotOverriddenException("BatchProcess");
  }

  /**
   * @brief Declares whether the subclass implements BatchProcess.
   *
   * Override this to return `true` if you implement `BatchProcess`.
   * This avoids exception propagation issues across dynamic library boundaries
   * when the .so and the main program use different C++ ABIs.
   *
   * @return true if BatchProcess is implemented; false otherwise (default).
   */
  virtual bool HasBatchProcessImpl() const { return false; }
};

using CreateOperatorFunc = IFeatureOP* (*)();

inline FieldPtr GetFieldPtr(const VariantVector& input, size_t i) {
  return absl::visit(
      [&](const auto& vec) -> FieldPtr {
        if (i >= vec.size()) {
          throw std::out_of_range("GetFieldPtr: index " + std::to_string(i) +
                                  " out of range [0, " +
                                  std::to_string(vec.size()) + ")");
        }
        return &vec.at(i);
      },
      input);
}
}  // namespace fg

#if defined(__GNUC__)
#define PLUGIN_API_HIDDEN \
  __attribute__((visibility("hidden"))) __attribute__((used))
#define PLUGIN_API_EXPORT \
  __attribute__((visibility("default"))) __attribute__((used))
#else
#define PLUGIN_API_HIDDEN
#define PLUGIN_API_EXPORT
#endif

std::vector<std::string>& getLocalNames();
std::vector<std::pair<std::string, void*>>& getLocalRegs();

#define REGISTER_PLUGIN(OpName, OpClass)                            \
  extern "C" PLUGIN_API_EXPORT fg::IFeatureOP* create##OpClass() {  \
    return new fg::OpClass();                                       \
  }                                                                 \
  namespace {                                                       \
  struct _Reg_##OpClass {                                           \
    _Reg_##OpClass() {                                              \
      getLocalNames().push_back(OpName);                            \
      getLocalRegs().emplace_back(OpName, (void*)&create##OpClass); \
    }                                                               \
  };                                                                \
  static _Reg_##OpClass _dummy_##OpClass __attribute__((used));     \
  }

#endif  // FEATURE_GENERATOR_PLUGIN_BASE_H

Notas de implementação

  • Tipos de entrada: Implemente métodos ProcessWith* para todos os tipos de entrada que pretende suportar. Para tipos não suportados, lance uma exceção diretamente. VariantRecord define todos os tipos de campos de feature que o framework pode processar. Os tipos FSMap são necessários na integração com o Feature Store — eles melhoram significativamente o desempenho do processador online.

  • Discretização: Implemente apenas a lógica de transformação anterior à discretização. Se hash_bucket_size, vocab_list, boundaries ou outra operação de discretização estiver configurada, o framework cuidará disso automaticamente.

  • Segurança de threads: Por padrão (is_op_thread_safe=true), o operador deve ser stateless ou usar apenas variáveis thread_local. Defina is_op_thread_safe=false para que o framework crie uma réplica de objeto por thread — isso simplifica a implementação, mas consome mais memória.

  • Entradas string_view: Serviços online (EasyRecProcessor, TorchEasyRec Processor) passam features de string do lado do item como absl::string_view para maior eficiência. Caso seu operador não consiga lidar com string_view, defina disable_string_view=true na configuração para que o framework as converta em string antes de chamar seu operador. Isso reduz o desempenho.

  • Itens de configuração personalizados: Use quaisquer nomes de chave na entrada JSON — o framework passa a string JSON completa para Initialize. Não reutilize nomes de chave reservados pelo framework (como feature_type, operator_name, value_type). Chaves que referenciam arquivos externos devem terminar com _file para que o framework possa sincronizá-las em tarefas offline.

  • Diretório de operadores: Utilize a variável de ambiente FEATURE_OPERATOR_DIR para especificar o diretório onde os arquivos de biblioteca de vínculo dinâmico estão localizados. Cada biblioteca de vínculo dinâmico pode conter implementações de múltiplos operadores de feature.

  • Tipo de retorno de BatchProcess: O tipo do VariantVector retornado por BatchProcess depende dos valores de is_sequence, value_dimension e value_type. Para mais informações, consulte o schema da tabela de saída em Referência de configuração. Quando stub_type=true está configurado e nenhuma operação de binning está definida, BatchProcess pode retornar qualquer tipo válido, como Map.

Referência de configuração

Estrutura da entrada fg.json

{
    "feature_name": "my_custom_fg_op",
    "feature_type": "custom_feature",
    "operator_name": "EditDistance",
    "operator_lib_file": "libedit_distance.so",
    "expression": [
        "user:query",
        "item:title"
    ],
    "value_type": "string",
    "separator": ",",
    "default_value": "-1",
    "value_dimension": 1,
    "normalizer": "method=expression,expr=x>16?16:x",
    "num_buckets": 10000,
    "stub_type": false,
    "is_sequence": false,
    "is_op_thread_safe": true
}

Adicione quaisquer itens de configuração adicionais que seu operador necessite. Toda a entrada JSON é passada para Initialize.

Parâmetros de configuração

Parâmetro

Obrigatório

Descrição

feature_type

Sim

Defina como custom_feature.

operator_name

Sim

Nome usado em REGISTER_PLUGIN. Deve corresponder ao nome da classe registrada. O mesmo operador pode ser reutilizado em várias features.

operator_lib_file

Offline: obrigatório; Online: opcional

Nome do arquivo .so. Serviços online verificam o subdiretório custom_fg_lib do diretório do modelo fg.json e carregam todos os arquivos .so automaticamente. Para operadores de extensão oficiais, defina como pyfg/lib/libxxx.so. Para tarefas offline, faça o upload do .so como um resource do MaxCompute com o mesmo nome.

expression

Sim

Campos de entrada. Suporta múltiplas entradas.

value_type

Sim

Tipo de saída. Um dos seguintes: string, int32, int64, float, double.

default_value

Sim

Valor padrão como string. O framework o converte para value_type.

separator

Quando a saída é multidimensional

Divide default_value em múltiplos valores para features multidimensionais.

stub_type

Não

Se true, o operador só pode produzir resultados intermediários e não pode ser um nó folha no grafo de execução DAG (Directed Acyclic Graph). Padrão: false.

is_sequence

Não

Indica se a saída é uma feature de sequência. Padrão: false.

sequence_length

Quando is_sequence=true

Comprimento máximo da sequência. Sequências mais longas são truncadas.

sequence_delim

Quando a entrada é uma sequência do tipo string

Separador entre elementos da sequência.

split_sequence

Não

Para sequências de entrada do tipo string, indica se o framework divide a string antes de passá-la ao operador. Padrão: true. Após a divisão, o tipo do campo torna-se std::vector<std::string>, mesmo que originalmente fosse um escalar. A divisão utiliza o conjunto de instruções CPU AVX-512 para melhor desempenho. Se algumas entradas forem sequências e outras escalares, avalie se a divisão no nível do framework é apropriada.

value_dimension

Não

Dimensão da feature de saída. Padrão: 0. Usado para truncar a saída em tarefas offline e afeta o schema da tabela de saída. Omita se a dimensão de saída for variável.

normalizer

Não

Normalização pós-transformação para features numéricas. Consulte Frameworks de normalização.

placeholder

Quando is_sequence=true e value_dimension != 1

Preenche posições vazias em um elemento de sequência multivalorado. Padrão para floats: NaN; padrão para inteiros: valor mínimo do tipo. Features esparsas com discretização geram um valor irregular; features densas sem discretização usam default_value em seu lugar.

disable_string_view

Não

Converte entradas do tipo string_view para string antes de chamar o operador. Padrão: false. Ative esta opção se seu operador não conseguir lidar com string_view. Nota: ativar esta opção reduz o desempenho. Chaves e valores de mapa do tipo string_view não são convertidos — trate-os em seu operador.

is_op_thread_safe

Não

Indica se o operador é thread-safe. Padrão: true (o operador deve ser stateless ou usar apenas variáveis thread_local). Defina como false para que o framework crie um objeto por thread (mais simples, mas consome mais memória).

Schema da tabela de saída

As configurações value_dimension e is_sequence determinam o tipo de schema da tabela de saída em tarefas offline:

**value_dimension**

**is_sequence**

Tipo de schema

Com discretização

1

false

value_type

bigint

1

true

array<value_type>

array<bigint>

≠1

false

array<value_type>

array<bigint>

≠1

true

array<array<value_type>>

array<array<bigint>>

Casos especiais: array<array<int>> é forçado para array<array<bigint>>; array<array<double>> é forçado para array<array<float>>.

Frameworks de normalização

Para features numéricas, adicione um normalizer para processar ainda mais o resultado da transformação:

Framework

Exemplo de configuração

Fórmula

log10

method=log10,threshold=1e-10,default=-10

x = x > threshold ? log10(x) : default

zscore

method=zscore,mean=0.0,standard_deviation=10.0

x = (x - mean) / standard_deviation

minmax

method=minmax,min=2.1,max=2.2

x = (x - min) / (max - min)

expression

method=expression,expr=sign(x)

Qualquer expressão; a variável x representa a entrada.

Para funções suportadas em expression, consulte Operadores de feature nativos.

Operações de discretização

Seis tipos de discretização estão disponíveis sem necessidade de implementação adicional:

Tipo

Descrição

hash_bucket_size

Operação de hash e módulo no resultado da transformação.

vocab_list

Converte o resultado em um índice dentro de uma lista.

vocab_dict

Converte o resultado em um valor dentro de um dicionário. O valor deve ser conversível para int64.

vocab_file

Carrega uma vocab_list ou vocab_dict a partir de um arquivo.

boundaries

Converte o resultado em um número de bucket com base nos limites de intervalo especificados.

num_buckets

Usa o resultado diretamente como número de bucket.

Para mais informações, consulte Discretização de features (binning).

Exemplos de configuração

Feature de diferença de tempo em sequência

{
    "feature_name": "time_diff_seq",
    "feature_type": "custom_feature",
    "operator_name": "SeqExpr",
    "expression": ["user:cur_time", "user:clk_time_seq"],
    "formula": "cur_time - clk_time_seq",
    "default_value": "0",
    "value_type": "int32",
    "is_sequence": true,
    "num_buckets": 1000,
    "is_op_thread_safe": false
}

Distância esférica com normalização

{
    "feature_name": "spherical_distance",
    "feature_type": "custom_feature",
    "operator_name": "SeqExpr",
    "expression": ["item:click_id_lng", "item:click_id_lat", "user:j_lng", "user:j_lat"],
    "formula": "spherical_distance",
    "default_value": "0",
    "value_type": "double",
    "is_sequence": true,
    "is_op_thread_safe": true,
    "value_dimension": 1,
    "normalizer": "method=expression,expr=sqrt(x)"
}

Ambos os exemplos utilizam o operador SeqExpr. O campo formula é um item de configuração específico do SeqExpr passado através de Initialize.

  • spherical_distance: Calcula a distância entre dois pares de coordenadas de latitude/longitude. Os parâmetros são [lng1_seq, lat1_seq, lng2, lat2] — os dois primeiros são sequências; os dois últimos são escalares.

Estes exemplos demonstram o formato tiled para features de sequência personalizadas. Para um exemplo em formato aninhado, consulte sequence_feature.

Features de sequência

Quando is_sequence=true, os requisitos de saída diferem dependendo se a feature é esparsa ou densa:

Sequências esparsas

  • Valor único por elemento: saída de qualquer tipo.

  • Múltiplos valores por elemento: saída apenas do tipo string. Defina value_type como string e use chr(29) como separador entre valores dentro de um único elemento.

Sequências densas

  • Defina value_dimension como a dimensão de cada elemento.

  • Elementos escalares: value_dimension=1.

  • Elementos vetoriais: value_dimension= comprimento do vetor.

  • O número total de valores de saída deve ser um múltiplo inteiro de value_dimension.

Exemplo para desenvolvedores

O exemplo a seguir implementa um operador de distância de edição que calcula a distância Levenshtein entre duas entradas de texto.

Arquivo de cabeçalho (edit_distance.h):

#pragma once
#include "api/base_op.h"

namespace fg {
namespace functor {
  class EditDistanceFunctor;
}

using std::string;
using std::vector;

/**
 * @brief EditDistance: takes two strings, outputs their edit distance.
 */
class EditDistance : public IFeatureOP {
 public:
  int Initialize(const string& feature_config) override;

  /// @return 0 on success.
  int ProcessWithStrOutputs(const vector<FieldPtr>& inputs,
                            vector<string>& outputs) override;

  /// @return 0 on success.
  int ProcessWithInt32Outputs(const vector<FieldPtr>& inputs,
                              vector<int32>& outputs) override;

  /// @return 0 on success.
  int ProcessWithInt64Outputs(const vector<FieldPtr>& inputs,
                              vector<int64>& outputs) override;

  /// @return 0 on success.
  int ProcessWithFloatOutputs(const vector<FieldPtr>& inputs,
                              vector<float>& outputs) override;

  /// @return 0 on success.
  int ProcessWithDoubleOutputs(const vector<FieldPtr>& inputs,
                               vector<double>& outputs) override;
 private:
  string feature_name_;
  std::unique_ptr<functor::EditDistanceFunctor> functor_p_;
};

}  // end of namespace fg

Arquivo de implementação (edit_distance.cc):

#include "edit_distance.h"

#include <absl/strings/ascii.h>
#include <absl/strings/str_join.h>

#include <nlohmann/json.hpp>
#include <numeric>  // std::iota
#include <stdexcept>

#include "api/log.h"

namespace fg {
using absl::optional;

namespace functor {
template <class T>
int edit_distance(const T& s1, const T& s2) {
  int l1 = s1.size();
  int l2 = s2.size();
  if (l1 * l2 == 0) {
    return l1 + l2;
  }
  vector<int> prev(l2 + 1);
  vector<int> curr(l2 + 1);
  std::iota(prev.begin(), prev.end(), 0);
  for (int i = 0; i <= l1; ++i) {
    curr[0] = i;
    for (int j = 1; j <= l2; ++j) {
      int d = prev[j - 1];
      if (s1[i - 1] == s2[j - 1]) {
        curr[j] = d;
      } else {
        int d2 = std::min(prev[j], curr[j - 1]);
        curr[j] = 1 + std::min(d, d2);
      }
    }
    prev.swap(curr);
  }
  return prev[l2];
}

enum class Encoding : unsigned int { Latin = 0, UTF8 = 1 };

class EditDistanceFunctor {
 public:
  EditDistanceFunctor(const string& encoding) {
    string enc = absl::AsciiStrToLower(encoding);
    if (enc == "utf-8" || enc == "utf8") {
      encoding_ = Encoding::UTF8;
    } else {
      encoding_ = Encoding::Latin;
    }
  }

  int operator()(absl::string_view s1, absl::string_view s2) {
    if (encoding_ == Encoding::Latin) {
      return edit_distance(s1, s2);
    }
    if (encoding_ == Encoding::UTF8) {
      return edit_distance(from_bytes(s1), from_bytes(s2));
    }
    LOG(ERROR) << "EditDistanceFunctor found unsupported text encoding";
    assert(false);
    return 0;
  }

  const Encoding TextEncoding() const { return encoding_; }

 private:
  Encoding encoding_;

  std::wstring from_bytes(absl::string_view str) {
    std::wstring result;
    int i = 0;
    int len = (int)str.length();
    while (i < len) {
      int char_size = 0;
      int unicode = 0;

      if ((str[i] & 0x80) == 0) {
        unicode = str[i];
        char_size = 1;
      } else if ((str[i] & 0xE0) == 0xC0) {
        unicode = str[i] & 0x1F;
        char_size = 2;
      } else if ((str[i] & 0xF0) == 0xE0) {
        unicode = str[i] & 0x0F;
        char_size = 3;
      } else if ((str[i] & 0xF8) == 0xF0) {
        unicode = str[i] & 0x07;
        char_size = 4;
      } else {
        // Invalid UTF-8 sequence
        ++i;
        continue;
      }

      for (int j = 1; j < char_size; ++j) {
        unicode = (unicode << 6) | (str[i + j] & 0x3F);
      }

      if (unicode <= 0xFFFF) {
        result += static_cast<wchar_t>(unicode);
      } else {
        // Handle surrogate pairs for characters outside the BMP
        unicode -= 0x10000;
        result += static_cast<wchar_t>((unicode >> 10) + 0xD800);
        result += static_cast<wchar_t>((unicode & 0x3FF) + 0xDC00);
      }
      i += char_size;
    }
    return result;
  }
};
}  // namespace functor

// Overloaded helper for absl::visit (C++17).
template <class... Ts>
struct overloaded : Ts... {
  using Ts::operator()...;
};
template <class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;

int EditDistance::Initialize(const string& feature_config) {
  nlohmann::json cfg;
  try {
    cfg = nlohmann::json::parse(feature_config);
  } catch (nlohmann::json::parse_error& ex) {
    LOG(ERROR) << "parse error at byte " << ex.byte;
    LOG(ERROR) << "config: " << feature_config;
    throw std::runtime_error("parse EditDistance config failed");
  }

  feature_name_ = cfg.at("feature_name");
  string encoding = cfg.value("encoding", "latin");
  functor_p_ = std::make_unique<functor::EditDistanceFunctor>(encoding);
  functor::Encoding enc = functor_p_->TextEncoding();
  encoding = (enc == functor::Encoding::UTF8) ? "UTF-8" : "Latin";
  LOG(INFO) << "feature <" << feature_name_ << "> with text encoding: " << encoding;
  return 0;
}

int EditDistance::ProcessWithInt32Outputs(const vector<FieldPtr>& inputs,
                                          vector<int32>& outputs) {
  outputs.clear();
  if (inputs.size() < 2) {
    outputs.push_back(0);
    return -1;  // invalid inputs
  }

  int d = absl::visit(
      overloaded{
          [this](const optional<string>* s1, const optional<string>* s2) {
            absl::string_view empty_view;
            return functor_p_->operator()(*s1 ? **s1 : empty_view, *s2 ? **s2 : empty_view);
          },
          [this](const optional<absl::string_view>* s1,
                 const optional<absl::string_view>* s2) {
            absl::string_view empty_view;
            return functor_p_->operator()(*s1 ? **s1 : empty_view, *s2 ? **s2 : empty_view);
          },
          [this](const optional<absl::string_view>* s1,
                 const optional<string>* s2) {
            absl::string_view empty_view;
            return functor_p_->operator()(*s1 ? **s1 : empty_view, *s2 ? **s2 : empty_view);
          },
          [this](const optional<string>* s1,
                 const optional<absl::string_view>* s2) {
            absl::string_view empty_view;
            return functor_p_->operator()(*s1 ? **s1 : empty_view, *s2 ? **s2 : empty_view);
          },
          [this](const List<string>* s1, const List<string>* s2) {
            string str1 = absl::StrJoin(*s1, "");
            string str2 = absl::StrJoin(*s2, "");
            return functor_p_->operator()(str1, str2);
          },
          [this](const List<absl::string_view>* s1,
                 const List<absl::string_view>* s2) {
            string str1 = absl::StrJoin(*s1, "");
            string str2 = absl::StrJoin(*s2, "");
            return functor_p_->operator()(str1, str2);
          },
          [this](const auto* x, const auto* y) {
            ERROR_EXIT(feature_name_,
                       "unsupported input type: ", typeid(*x).name(), " vs ",
                       typeid(*y).name());
            return 0;
          }},
      inputs.at(0), inputs.at(1));
  outputs.push_back(d);
  return 0;
}

// int32 results are reused for int64, float, and double outputs.
int EditDistance::ProcessWithInt64Outputs(const vector<FieldPtr>& inputs,
                                          vector<int64>& outputs) {
  vector<int32> distances;
  int status = ProcessWithInt32Outputs(inputs, distances);
  if (0 != status) return status;
  outputs.assign(distances.begin(), distances.end());
  return 0;
}

int EditDistance::ProcessWithFloatOutputs(const vector<FieldPtr>& inputs,
                                          vector<float>& outputs) {
  vector<int32> distances;
  int status = ProcessWithInt32Outputs(inputs, distances);
  if (0 != status) return status;
  outputs.assign(distances.begin(), distances.end());
  return 0;
}

int EditDistance::ProcessWithDoubleOutputs(const vector<FieldPtr>& inputs,
                                           vector<double>& outputs) {
  vector<int32> distances;
  int status = ProcessWithInt32Outputs(inputs, distances);
  if (0 != status) return status;
  outputs.assign(distances.begin(), distances.end());
  return 0;
}

int EditDistance::ProcessWithStrOutputs(const vector<FieldPtr>& inputs,
                                        vector<string>& outputs) {
  vector<int32> distances;
  int status = ProcessWithInt32Outputs(inputs, distances);
  if (0 != status) return status;
  outputs.reserve(distances.size());
  std::transform(distances.begin(), distances.end(),
                 std::back_inserter(outputs),
                 [](int32& x) { return std::to_string(x); });
  return 0;
}

}  // end of namespace fg

REGISTER_PLUGIN("EditDistance", EditDistance);

Baixe o código-fonte da tabela em Operadores disponíveis e execute o script build.sh para compilar o operador.

Compilar um operador personalizado

Utilize o mesmo ambiente de compilação do framework FG: C++17 e a imagem oficial do compilador. Os detalhes da imagem estão no script build.sh incluído em cada exemplo.

Imagem

SO Base

Observações

mybigpai-public-registry.cn-beijing.cr.aliyuncs.com/easyrec/feature_generator:centos7-0.1.1

CentOS 7

ABI C++11 padrão (não habilitado)

mybigpai-public-registry.cn-beijing.cr.aliyuncs.com/easyrec/feature_generator:0.1.1

Rocky Linux 8 (compatível com CentOS 8)

Use esta imagem se precisar da nova ABI C++11 (_GLIBCXX_USE_CXX11_ABI=1)

Dependências de terceiros: Incorpore todas as dependências como código-fonte ou use vinculação estática. A vinculação dinâmica a bibliotecas de terceiros faz com que o operador falhe ao carregar em tempo de execução.

Dependência obrigatória:

  • abseil-cpp — use a mesma versão do framework FG.

Para detalhes da configuração CMake, consulte o arquivo CMakeLists.txt em cada exemplo para desenvolvedores.

Operadores disponíveis

Os seguintes operadores estão disponíveis como código-fonte e binários pré-compilados:

Operador

Descrição

Código-fonte

Binário

EditDistance

Distância de edição entre duas entradas de texto

Baixe

Baixe

SeqExpr

Avaliação de expressão de sequência

Baixe

Baixe

BPETokenize

Tokenização Byte Pair Encoding (BPE)

Baixe

Incluído na tokenize_feature nativa

Configuração do EditDistance

Parâmetro

Opções

Padrão

encoding

utf-8, latin

latin

Para um exemplo de BatchProcess, baixe e revise o RegexReplace.

Próximos passos