Реализация автоматического маппинга характеристик товаров с фильтрами сайта
Поставщики называют одни и те же характеристики по-разному: «цвет», «Цвет изделия», «color», «Colour», «RAL». На сайте — один фильтр «Цвет». Ручное приведение характеристик от десятков поставщиков к фильтрам сайта — это сотни часов работы, которые нужно автоматизировать.
Архитектура системы маппинга
Три уровня абстракции:
Характеристика поставщика → Атрибут сайта → Значение фильтра
"Цвет изделия: Красный" → color → "Красный"
"color: Red" → color → "Красный" (с нормализацией значения)
"RAL 3020" → color → "Красный" (через справочник RAL)
Таблица маппинга имён атрибутов:
CREATE TABLE attribute_name_mapping (
id serial PRIMARY KEY,
supplier_id int, -- NULL = универсальный маппинг
supplier_name varchar(200), -- как называет поставщик
site_attribute varchar(100), -- внутренний ключ атрибута
created_at timestamptz DEFAULT now(),
UNIQUE (supplier_id, supplier_name)
);
Таблица маппинга значений атрибутов:
CREATE TABLE attribute_value_mapping (
id serial PRIMARY KEY,
site_attribute varchar(100),
supplier_value varchar(500),
site_value varchar(500), -- нормализованное значение
supplier_id int, -- NULL = для всех
UNIQUE (site_attribute, supplier_value, supplier_id)
);
Алгоритм маппинга
class AttributeMapper
{
public function mapAttribute(
string $supplierName,
string $supplierValue,
?int $supplierId = null
): ?MappedAttribute {
// Шаг 1: найти маппинг имени атрибута
$siteAttribute = $this->resolveAttributeName($supplierName, $supplierId);
if (!$siteAttribute) {
$this->logUnknownAttribute($supplierName, $supplierId);
return null;
}
// Шаг 2: найти маппинг значения
$siteValue = $this->resolveAttributeValue($siteAttribute, $supplierValue, $supplierId);
if (!$siteValue) {
// Пробуем нормализовать без маппинга
$siteValue = $this->autoNormalizeValue($siteAttribute, $supplierValue);
}
return new MappedAttribute($siteAttribute, $siteValue);
}
private function resolveAttributeName(string $name, ?int $supplierId): ?string
{
$normalized = mb_strtolower(trim($name));
// Сначала специфичный маппинг для поставщика
if ($supplierId) {
$mapping = AttributeNameMapping::where([
'supplier_id' => $supplierId,
'supplier_name' => $normalized,
])->value('site_attribute');
if ($mapping) return $mapping;
}
// Затем универсальный
return AttributeNameMapping::whereNull('supplier_id')
->where('supplier_name', $normalized)
->value('site_attribute');
}
}
Автонормализация значений
Часть значений не требует словарного маппинга — их можно привести к стандарту автоматически:
private function autoNormalizeValue(string $attribute, string $raw): string
{
return match ($attribute) {
'spec_weight_kg' => $this->parseWeight($raw),
'spec_dimensions' => $this->parseDimensions($raw),
'spec_voltage' => $this->parseVoltage($raw),
default => $this->capitalizeFirst($raw),
};
}
private function parseWeight(string $raw): string
{
// "2,5 кг" | "2500 г" | "2.5kg" → "2.5"
if (preg_match('/(\d+[.,]\d+|\d+)\s*г(?:рамм)?/iu', $raw, $m)) {
return (string) (((float) str_replace(',', '.', $m[1])) / 1000);
}
if (preg_match('/(\d+[.,]\d+|\d+)\s*кг/iu', $raw, $m)) {
return str_replace(',', '.', $m[1]);
}
return $raw;
}
Fuzzy-маппинг новых значений
Для новых значений, которых ещё нет в маппинге, используем pg_trgm схожесть:
public function suggestValueMapping(string $attribute, string $supplierValue): array
{
$normalized = mb_strtolower(trim($supplierValue));
return DB::select(
"SELECT site_value,
similarity(lower(supplier_value), ?) AS score
FROM attribute_value_mapping
WHERE site_attribute = ?
AND similarity(lower(supplier_value), ?) > 0.6
ORDER BY score DESC
LIMIT 5",
[$normalized, $attribute, $normalized]
);
}
Предлагать оператору: «Похоже, "Красн." = "Красный" (score 0.82). Создать маппинг?»
Автообучение из подтверждённых маппингов
Когда оператор принимает предложенный маппинг — он попадает в attribute_value_mapping. При следующем импорте от этого поставщика значение маппится автоматически.
public function confirmMapping(
string $attribute,
string $supplierValue,
string $siteValue,
?int $supplierId
): void {
AttributeValueMapping::updateOrCreate(
[
'site_attribute' => $attribute,
'supplier_value' => mb_strtolower(trim($supplierValue)),
'supplier_id' => $supplierId,
],
['site_value' => $siteValue]
);
}
Управление фильтрами: свзяь атрибутов и фасетов
Каждый сопоставленный атрибут привязан к фильтру сайта:
CREATE TABLE filter_attributes (
filter_id int REFERENCES filters(id),
attribute varchar(100),
display_name varchar(200),
sort smallint,
PRIMARY KEY (filter_id, attribute)
);
После маппинга значения пишутся в денормализованную таблицу product_filter_values — именно по ней работает поиск с фасетной фильтрацией:
CREATE TABLE product_filter_values (
product_id int,
filter_id int,
value varchar(500),
value_slug varchar(500),
PRIMARY KEY (product_id, filter_id, value)
);
CREATE INDEX pfv_filter_value_idx ON product_filter_values (filter_id, value_slug);
Нераспознанные атрибуты: очередь обработки
Все атрибуты, для которых не нашлось маппинга, копятся в очереди:
CREATE TABLE unmapped_attributes (
id serial PRIMARY KEY,
supplier_id int,
attribute_name varchar(200),
sample_values text[], -- до 10 примеров значений
occurrences int DEFAULT 1,
first_seen_at timestamptz DEFAULT now()
);
В админке — таблица с частотой встречаемости. Атрибуты, встречающиеся чаще всего, — первые кандидаты для создания маппинга.
Сроки реализации
- Базовый маппинг имён атрибутов (словарь + запрос) + запись в
product_filter_values— 3 дня - Автонормализация числовых значений (вес, размеры, напряжение) — +1 день
- Fuzzy-предложения + очередь нераспознанных + admin UI — +2–3 дня
- Полная интеграция с фасетным поиском и пересчёт при изменении маппинга — +1–2 дня







