Todos los productos
Search
Centro de documentos

:Custom feature operators

Última actualización: Sep 05, 2026

El framework de generación de funciones (FG) admite operadores de funciones personalizados como plugins de biblioteca compartida. Utilícelos cuando los operadores integrados no cubran su lógica de transformación específica del dominio; por ejemplo, la distancia de edición entre dos campos de texto, la distancia esférica a partir de coordenadas GPS o cualquier fórmula numérica aplicada elemento a elemento en una secuencia.

Funcionamiento

  1. Implemente la interfaz C++ IFeatureOP y registre el operador con REGISTER_PLUGIN.

  2. Compile la implementación en una biblioteca compartida (.so).

  3. Haga referencia al operador en fg.json; para ello, establezca feature_type como custom_feature y operator_name con el nombre registrado.

  4. Despliegue el archivo .so: los servicios en línea lo detectan automáticamente desde el directorio custom_fg_lib, mientras que las tareas sin conexión requieren cargarlo como recurso de MaxCompute.

Requisitos previos

Antes de empezar, asegúrese de contar con lo siguiente:

  • Un entorno de compilación compatible con C++17 que utilice la imagen oficial del compilador (consulte Compilar un operador personalizado)

  • El paquete de dependencias de la API: fg-api.tar.gz, que contiene todos los archivos de encabezado obligatorios

  • Conocimiento del framework FG y del funcionamiento de la configuración de fg.json

Implementar un operador

Ejemplo mínimo

Todo operador personalizado sigue el mismo patrón: heredar de IFeatureOP, implementar Initialize y al menos un método ProcessWith*, además de registrarse con REGISTER_PLUGIN.

A continuación se muestra un operador mínimo que genera un entero 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 clave:

  • Incluya un constructor sin parámetros (el constructor predeterminado funciona en este caso).

  • El método Initialize recibe la entrada completa de fg.json como una cadena JSON. Analice sintácticamente los elementos de configuración que necesite.

  • Devuelva 0 en todos los métodos para indicar éxito; un valor distinto de cero indica error.

  • Llame a REGISTER_PLUGIN en el archivo de implementación, no en el encabezado.

Elegir entre ProcessWith* y BatchProcess

Hay dos interfaces de procesamiento disponibles. Elija según sus requisitos de throughput y complejidad:

**ProcessWith***

**BatchProcess**

Granularidad

Un registro a la vez

Un lote de registros

Tipos de salida

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

Cualquier tipo válido de VariantVector, incluido Map

Uso recomendado

Transformaciones sencillas con bajo costo de implementación

Se requiere alto throughput; funciones del lado del usuario con una muestra por solicitud (utilice el mecanismo de difusión para evitar análisis sintácticos redundantes)

Obligatorio

Al menos un método que coincida con value_type

Sobrescribir HasBatchProcessImpl() para que devuelva true

Cuando se implementa BatchProcess y HasBatchProcessImpl() devuelve true, el framework omite todos los métodos ProcessWith*.

Interfaz 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 implementación

  • Tipos de entrada: Implemente los métodos ProcessWith* para todos los tipos de entrada que desee admitir. Para los tipos no compatibles, lance una excepción directamente. VariantRecord define todos los tipos de campos de función que el framework puede procesar. Los tipos FSMap son necesarios para la integración con Feature Store, ya que mejoran significativamente el rendimiento del procesador en línea.

  • Discretización: Implemente únicamente la lógica de transformación previa a la discretización. Si se configura hash_bucket_size, vocab_list, boundaries u otra operación de discretización, el framework la gestiona automáticamente.

  • Seguridad de hilos: De forma predeterminada (is_op_thread_safe=true), el operador debe carecer de estado o usar exclusivamente variables thread_local. Establezca is_op_thread_safe=false para que el framework cree una réplica del objeto por hilo; esto simplifica la implementación, pero consume más memoria.

  • Entradas string_view: Los servicios en línea (EasyRecProcessor, TorchEasyRec Processor) pasan las funciones de cadena del lado del ítem como absl::string_view para mayor eficiencia. Si su operador no es compatible con string_view, establezca disable_string_view=true en la configuración para que el framework las convierta a string antes de llamar a su operador. Tenga en cuenta que esto reduce el rendimiento.

  • Elementos de configuración personalizados: Puede usar cualquier nombre de clave en la entrada JSON, ya que el framework pasa la cadena JSON completa a Initialize. No reutilice nombres de clave reservados por el framework (como feature_type, operator_name, value_type). Las claves que hagan referencia a archivos externos deben terminar con _file para que el framework pueda sincronizarlas en tareas sin conexión.

  • Directorio de operadores: Utilice la variable de entorno FEATURE_OPERATOR_DIR para especificar el directorio donde se encuentran los archivos de biblioteca de vínculo dinámico. Cada biblioteca de vínculo dinámico puede contener implementaciones de varios operadores de funciones.

  • Tipo de retorno de BatchProcess: El tipo de VariantVector que devuelve BatchProcess depende de los valores de is_sequence, value_dimension y value_type. Para obtener más información, consulte el esquema de la tabla de salida en Referencia de configuración. Cuando se configura stub_type=true y no se establece ninguna operación de agrupamiento, BatchProcess puede devolver cualquier tipo válido, como Map.

Referencia de configuración

Estructura de entrada de 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
}

Agregue cualquier elemento de configuración adicional que requiera su operador. La entrada JSON completa se pasa a Initialize.

Parámetros de configuración

Parámetro

Obligatorio

Descripción

feature_type

Sí

Establézcalo como custom_feature.

operator_name

Sí

Nombre utilizado en REGISTER_PLUGIN. Debe coincidir con el nombre de la clase registrada. El mismo operador puede reutilizarse en varias funciones.

operator_lib_file

Sin conexión: obligatorio; En línea: opcional

Nombre del archivo .so. Los servicios en línea examinan el subdirectorio custom_fg_lib del directorio del modelo fg.json y cargan automáticamente todos los archivos .so. Para operadores de extensión oficiales, establézcalo como pyfg/lib/libxxx.so. Para tareas sin conexión, cargue el archivo .so como recurso de MaxCompute con el mismo nombre.

expression

Sí

Campos de entrada. Admite múltiples entradas.

value_type

Sí

Tipo de salida. Uno de los siguientes: string, int32, int64, float, double.

default_value

Sí

Valor predeterminado como cadena. El framework lo convierte al tipo definido en value_type.

separator

Cuando la salida es multidimensional

Divide default_value en varios valores para funciones multidimensionales.

stub_type

No

Si es true, el operador solo puede producir resultados intermedios y no puede ser un nodo hoja en el grafo de ejecución acíclico dirigido (DAG). Valor predeterminado: false.

is_sequence

No

Indica si la salida es una función de secuencia. Valor predeterminado: false.

sequence_length

Cuando is_sequence=true

Longitud máxima de la secuencia. Las secuencias más largas se truncan.

sequence_delim

Cuando la entrada es una secuencia de tipo cadena

Separador entre los elementos de la secuencia.

split_sequence

No

Para secuencias de entrada de tipo cadena, indica si el framework divide la cadena antes de pasarla al operador. Valor predeterminado: true. Tras la división, el tipo de campo se convierte en std::vector<std::string>, incluso si originalmente era un escalar. La división utiliza el conjunto de instrucciones CPU AVX-512 para mejorar el rendimiento. Si algunas entradas son secuencias y otras son escalares, evalúe si la división a nivel de framework es adecuada.

value_dimension

No

Dimensión de la función de salida. Valor predeterminado: 0. Se usa para truncar la salida en tareas sin conexión y afecta el esquema de la tabla de salida. Omítalo si la dimensión de salida es variable.

normalizer

No

Normalización posterior a la transformación para funciones numéricas. Consulte Frameworks de normalización.

placeholder

Cuando is_sequence=true y value_dimension != 1

Rellena las posiciones vacías en un elemento de secuencia con múltiples valores. Valor predeterminado para flotantes: NaN; valor predeterminado para enteros: valor mínimo del tipo. Las funciones dispersas con discretización generan un valor irregular; las funciones densas sin discretización utilizan default_value en su lugar.

disable_string_view

No

Convierte las entradas de tipo string_view a string antes de llamar al operador. Valor predeterminado: false. Habilite esta opción si su operador no admite string_view. Nota: habilitar esta opción reduce el rendimiento. Las claves y valores de mapa de tipo string_view no se convierten; debe gestionarlos en su operador.

is_op_thread_safe

No

Indica si el operador es seguro para hilos. Valor predeterminado: true (el operador debe carecer de estado o usar solo variables thread_local). Establézcalo en false para que el framework cree un objeto por hilo (más sencillo, pero consume más memoria).

Esquema de la tabla de salida

Las configuraciones de value_dimension y is_sequence determinan el tipo de esquema de la tabla de salida en tareas sin conexión:

**value_dimension**

**is_sequence**

Tipo de esquema

Con discretización

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 especiales: array<array<int>> se fuerza a array<array<bigint>>; array<array<double>> se fuerza a array<array<float>>.

Frameworks de normalización

Para funciones numéricas, agregue un normalizer para procesar aún más el resultado de la transformación:

Framework

Configuración de ejemplo

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)

Cualquier expresión; la variable x representa la entrada.

Para consultar las funciones compatibles en expression, consulte Operadores de funciones integrados.

Operaciones de discretización

Hay seis tipos de discretización disponibles sin necesidad de implementación adicional:

Tipo

Descripción

hash_bucket_size

Operación de hash y módulo sobre el resultado de la transformación.

vocab_list

Convierte el resultado en un índice dentro de una lista.

vocab_dict

Convierte el resultado en un valor dentro de un diccionario. El valor debe ser convertible a int64.

vocab_file

Carga una vocab_list o un vocab_dict desde un archivo.

boundaries

Convierte el resultado en un número de cubeta según los límites de intervalo especificados.

num_buckets

Utiliza el resultado directamente como número de cubeta.

Para obtener más información, consulte Feature discretization (binning).

Ejemplos de configuración

Función de diferencia horaria de secuencia

{
    "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
}

Distancia esférica con normalización

{
    "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 ejemplos utilizan el operador SeqExpr. El campo formula es un elemento de configuración específico de SeqExpr que se transmite mediante Initialize.

  • spherical_distance: Calcula la distancia entre dos pares de coordenadas de latitud y longitud. Los parámetros son [lng1_seq, lat1_seq, lng2, lat2]: los dos primeros son secuencias y los dos últimos son escalares.

Estos ejemplos muestran el formato plano para funciones de secuencia personalizadas. Para ver un ejemplo en formato anidado, consulte sequence_feature.

Funciones de secuencia

Cuando is_sequence=true, los requisitos de salida varían según si la función es dispersa o densa:

Secuencias dispersas

  • Un solo valor por elemento: genere cualquier tipo.

  • Múltiples valores por elemento: genere solo string. Establezca value_type como string y utilice chr(29) como separador entre los valores dentro de un mismo elemento.

Secuencias densas

  • Establezca value_dimension con la dimensión de cada elemento.

  • Elementos escalares: value_dimension=1.

  • Elementos vectoriales: value_dimension= longitud del vector.

  • El número total de valores de salida debe ser un múltiplo entero de value_dimension.

Ejemplo para desarrolladores

El siguiente ejemplo implementa un operador de distancia de edición que calcula la distancia de Levenshtein entre dos entradas de texto.

Archivo de encabezado (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

Archivo de implementación (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);

Descargue el código fuente de la tabla en Operadores disponibles y ejecute el script build.sh para compilar el operador.

Compilar un operador personalizado

Utilice el mismo entorno de compilación que el framework FG: C++17 y la imagen oficial del compilador. Los detalles de la imagen se encuentran en el script build.sh incluido con cada ejemplo.

Imagen

SO base

Notas

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

CentOS 7

ABI C++11 predeterminada (no habilitada)

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

Rocky Linux 8 (compatible con CentOS 8)

Utilice esta imagen si necesita la nueva ABI C++11 (_GLIBCXX_USE_CXX11_ABI=1)

Dependencias de terceros: Integre todas las dependencias como código fuente o utilice vínculos estáticos. El vínculo dinámico a bibliotecas de terceros provoca que el operador no se cargue durante la ejecución.

Dependencia obligatoria:

  • abseil-cpp: utilice la misma versión que el framework FG.

Para obtener detalles sobre la configuración de CMake, consulte el archivo CMakeLists.txt en cada ejemplo para desarrolladores.

Operadores disponibles

A continuación se presentan los operadores disponibles como código fuente y binarios precompilados:

Operador

Descripción

Código fuente

Binario

EditDistance

Distancia de edición entre dos entradas de texto

Download

Download

SeqExpr

Evaluación de expresiones de secuencia

Download

Download

BPETokenize

Tokenización mediante codificación por pares de bytes (BPE)

Download

Incluido en tokenize_feature integrado

Configuración de EditDistance

Parámetro

Opciones

Valor predeterminado

encoding

utf-8, latin

latin

Para consultar un ejemplo de BatchProcess, descargue y revise RegexReplace.

Pasos siguientes