Кастомный алгоритм сопоставления

Система Юниверс предоставляет возможность создавать свои алгоритмы для поиска и объединения дубликатов записей.

В системе представлено сопоставление на основе базы Postgres. Также возможно добавить свой алгоритм или написать свою реализацию хранилища.

Реализация пользовательского алгоритма для хранилища на базе Postgres

Примеры алгоритмов можно посмотреть в пакетах org.unidata.mdm.matching.storage.postgres.service.impl.algorithm и com.universe.mdm.sdk.spring.service.impl.algorithm.

Зависимости

  • Модуль с алгоритмом нуждается в зависимостях:

'org.unidata.mdm:org.unidata.mdm.matching.core:6.9.0-RELEASE'
'org.unidata.mdm:org.unidata.mdm.matching.storage.postgres:6.9.0-RELEASE'
  • Также необходимо указать зависимости в имплементации модуля dependencies:

@Override
public Collection<Dependency> getDependencies() {
    return Arrays.asList(
        // system, core, etc
        new Dependency("org.unidata.mdm.matching.core", "6.0"),
        new Dependency("org.unidata.mdm.matching.storage.postgres", "6.0")
    );
}

Интерфейсы

  • Пользовательский алгоритм на базе PostgreSQL должен быть наследником org.unidata.mdm.matching.core.service.impl.algorithm.AbstractSystemMatchingAlgorithm.

public static final String ALGORITHM_NAME = "sdkMatchingAlgorithm";
private static final String ALGORITHM_DISPLAY_NAME = MODULE_ID + ".algorithm.display.name";
private static final String ALGORITHM_DESCRIPTION = MODULE_ID + ".algorithm.description";

public ExampleMatchingAlgorithm() {
    super(ALGORITHM_NAME,
            () -> TextUtils.getText(ALGORITHM_DISPLAY_NAME),
            () -> TextUtils.getText(ALGORITHM_DESCRIPTION),
            PostgresMatchingStorageConfigurationConstants.MATCHING_STORAGE_NAME);
}

Примечание

Обязательно необходимо задать локализацию для имени и описания алгоритма в resources/sdk_spring_messages_{en|ru}.properties

com.universe.mdm.sdk.spring.algorithm.display.name - Пример алгоритма сопоставления com.universe.mdm.sdk.spring.description - Алгоритм ищет дубликаты записей по совпадению двум первым символам строкового параметра

  • Реализуйте интерфейс org.unidata.mdm.matching.core.type.algorithm.MatchingAlgorithm::configure(), даже если не требуются дополнительные настройки:

@Override
public MatchingAlgorithmConfiguration configure() {
    return MatchingAlgorithmConfiguration.configuration()
            .parameter(Collections.emptyList())
            .build();
}
  • Если необходимы параметры для работы алгоритма, определите их и добавьте в MatchingAlgorithmConfiguration

AlgorithmParamConfiguration<Boolean> caseInsensitive = AlgorithmParamConfiguration.bool()
           .name(CASE_INSENSITIVE_PARAM)
           .displayName(() -> TextUtils.getText(CASE_INSENSITIVE_PARAM_DISPLAY_NAME))
           .description(() -> TextUtils.getText(CASE_INSENSITIVE_PARAM_DESCRIPTION))
           .required(false)
           .defaultValue(false)
           .build();

   return MatchingAlgorithmConfiguration.configuration()
           .parameter(Collections.singletonList(caseInsensitive))
           .build();
  • Реализуйте интерфейс org.unidata.mdm.matching.storage.postgres.type.algorithm.PostgresMatchingAlgorithm с двумя методами:

/**
 * Обработка столбца MT. Создает коллекцию физических колонок БД и вычисляет правила предварительной обработки.
 * Вызывается при выполнении операций DDL и хранения данных (вставка, удаление и т.д.).
 *
 * @param column   matching column definition
 * @param settings matching algorithm settings
 * @return columns
 */
Collection<Column> processColumn(MatchingColumnElement column, AlgorithmSettingsElement settings);

/**
 * Обработка столбца MT. Создает коллекцию физических колонок БД и вычисляет правила предварительной обработки.
 * Вызывается операция MATCH.
 *
 * @param column   matching column definition
 * @param settings matching algorithm settings
 * @return columns
 */
Collection<Column> matchColumn(MatchingColumnElement column, AlgorithmSettingsElement settings);
  • Возвращаемый объект Column описывает столбец в БД с возможностью задать дополнительные обработчики или правила сопоставления. Если алгоритм работает только со строками:

@Override
public Collection<Column> processColumn(MatchingColumnElement column, AlgorithmSettingsElement settings) {
    return List.of(
        new Column(column.getName(), ColumnType.STRING).withAlias(column.getName())
    );
}
  • Параметр Column.preprocessingRule обрабатывает входное значение, результат будет сохранен в таблицу сопоставления.

Например, для реализации "поиск дубликатов по первым двум символам" можно обработать входной параметр, сократив его:

@Override
public Collection<Column> processColumn(MatchingColumnElement column, AlgorithmSettingsElement settings) {
    Column def = new Column(column.getName(), ColumnType.STRING)
            .withAlias(column.getName())
            .withPreprocessingRule(f -> f.getValue() == null || f.getValue().toString().length() < 3
                    ? null
                    : f.getValue().toString().substring(0, 2));
    return List.of(def);
}

@Override
public Collection<Column> matchColumn(MatchingColumnElement column, AlgorithmSettingsElement settings) {
    return processColumn(column, settings);
}
  • Параметр Column.postCreateRule обеспечивает возможность задачи дополнительных настроек столбца в базе данных, например, создание дополнительного индекса. Код из org.unidata.mdm.matching.storage.postgres.service.impl.algorithm.InexactAlgorithm:

Column original = new Column(originalColumnName, ColumnType.STRING)
            .withAlias(column.getName())
            .withInexact(true)
            .withPostCreationRule(t -> new StringBuilder()
                    .append("create index ix_")
                    .append(t.getTableName().toLowerCase())
                    .append("_")
                    .append(originalColumnName)
                    .append(" on ")
                    .append(t.getTableName().toLowerCase())
                    .append(" using gin (")
                    .append(originalColumnName)
                    .append(" gin_trgm_ops)")
                    .toString());
  • Параметр Column.conditionAppendingRule задается в PostgresMatchingAlgorithm::matchColumn и обеспечивает возможность переопределения поиска дубликатов (по умолчанию equals). Пример из org.unidata.mdm.matching.storage.postgres.service.impl.algorithm.SetAlgorithm:

@Override
public Collection<Column> matchColumn(MatchingColumnElement column, AlgorithmSettingsElement settings) {

    final String columnName = column.getName();
    final String setMatchingType = settings.getParameter(SET_MATCHING_TYPE_PARAM).getValue();
    final boolean intersection = StringUtils.equals(setMatchingType, INTERSECTION_ENUM_VALUE);

    Column value = new Column(columnName, ColumnType.HSTORE)
            .withAlias(column.getName())
            .withConditionAppendingRule(b -> b
                    .append(" and ((t1.")
                    .append(columnName)
                    .append(" is null and t2.")
                    .append(columnName)
                    .append(" is null) or t1.")
                    .append(columnName)
                    .append(intersection ? " ??| akeys(t2." : " ??& akeys(t2.")
                    .append(columnName)
                    .append("))"));

    return List.of(value);
}

Регистрация алгоритма в системе

Реализуйте в компоненте интерфейс AfterPlatformStartup и обновите модель. Пример com.universe.mdm.sdk.spring.service.impl.algorithm.RegisterMatchingAlgorithmsComponent:

@Override
  public void afterPlatformStartup() {
          MatchingStorage storage =
                  matchingStorageService.getMatchingStorage(PostgresMatchingStorageDescriptors.MATCHING_STORAGE.getStorageId());

          boolean notExists = storage != null
                  && !storage.descriptor().getMatchingAlgorithms().contains(ExampleMatchingAlgorithm.class.getName());
          // Если алгоритм уже зарегистрирован в системе, повторно регистрировать его не нужно
          if (notExists) {
              MatchingAlgorithmSource source = new MatchingAlgorithmSource()
                      .withCreateDate(OffsetDateTime.now())
                      .withDisplayName(TextUtils.getText(ExampleMatchingAlgorithm.ALGORITHM_DISPLAY_NAME))
                      .withDescription(TextUtils.getText(ExampleMatchingAlgorithm.ALGORITHM_DESCRIPTION))
                      .withLibraryName(PostgresMatchingStorageConfigurationConstants.MATCHING_STORAGE_LIBRARY_NAME)
                      .withLibraryVersion(MatchingAlgorithmJavaLibraryType.DEFAULT_LIBRARY_VERSION)
                      .withMatchingStorageId(PostgresMatchingStorageConfigurationConstants.MATCHING_STORAGE_NAME)
                      .withJavaClass(ExampleMatchingAlgorithm.class.getName())
                      .withName(ExampleMatchingAlgorithm.ALGORITHM_NAME);

              MatchingModelUpsertContext uCtx = MatchingModelUpsertContext.builder()
                      .algorithmsUpdate(List.of(source))
                      .force(true)
                      .build();

              metaModelService.upsert(uCtx);
          }
  }