Ми реалізуємо систему агрегації товарів від кількох постачальників: єдина картка з вибором найкращої пропозиції. Без дублювання, з актуальними цінами та залишками. Уявіть: у вас 10 постачальників, кожен зі своїм каталогом. Товари перетинаються на 70%, але SKU різні. Ціни змінюються щодня. Покупець бачить одну картку, а за нею — вибір із 5 пропозицій з різними цінами та строками. Ми розповімо, як побудувати це без дублювання та помилок. Опираємось на 10-річний досвід інтеграції e-commerce рішень. Успішно реалізовано більше 50 проектів з агрегації. Гарантуємо стабільну роботу при 100 000+ товарах. Середня економія при впровадженні — від 200 000 ₽ на рік.
Основи агрегації
Різниця між імпортом та агрегацією
Імпорт — збереження «як є». Агрегація — побудова вітрини над сирими даними кількох постачальників. При імпорті ви ризикуєте отримати дублікати та застарілі ціни. Агрегація дає єдину картку товару з автоматичним вибором найкращої пропозиції. Агрегація включає уніфікацію товарів для побудови єдиної картки.
| Критерій | Імпорт | Агрегація |
|---|---|---|
| Дані | Як є від кожного постачальника | Єдина нормалізована картка |
| Ціна | Кілька записів на товар | Одна картка з вибором |
| Оновлення | Затирання/додавання | Автоматичний перерахунок найкращої пропозиції |
Розпізнавання однакових товарів від різних постачальників
Розпізнавання — ключове завдання агрегації. Ми займаємося об'єднанням каталогів постачальників. Використовуємо декілька рівнів матчингу: точний збіг артикулів (supplier_sku), зіставлення за штрих-кодом та, за потреби, нечітке порівняння назв через Levenshtein distance. У мастер-картці (products) зберігається master_sku, до якого прив'язуються всі пропозиції (product_offers) від різних постачальників. Точність матчингу сягає 99% за наявності артикулів.
Налаштування агрегації: покроковий план
- Аналіз даних постачальників — зібрати формати, виявити перетини та унікальні атрибути.
- Проектування схеми — створити таблиці products та product_offers, налаштувати індекси.
- Реалізація матчингу — написати алгоритми зіставлення з підтримкою fuzzy-пошуку.
- Розробка BestOfferResolver — налаштувати скоринг пропозицій з урахуванням ціни, залишку та строку поставки.
- Інтеграція вітрини цін — підготувати API з агрегованими полями.
Як реалізувати агрегацію технічно?
Схема даних для агрегації
Розгорнути код схеми
-- Мастер-карточка (агрегированная)
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
master_sku VARCHAR(255) UNIQUE NOT NULL,
name TEXT NOT NULL, -- из "главного" поставщика
description TEXT,
attributes JSONB DEFAULT '{}',
category_id INT REFERENCES categories(id),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Предложения поставщиков к мастер-карточке
CREATE TABLE product_offers (
id BIGSERIAL PRIMARY KEY,
product_id BIGINT NOT NULL REFERENCES products(id),
supplier_id INT NOT NULL REFERENCES suppliers(id),
supplier_sku VARCHAR(255) NOT NULL,
price NUMERIC(12,2) NOT NULL,
stock INT NOT NULL DEFAULT 0,
lead_time_days SMALLINT, -- срок поставки
is_primary BOOLEAN DEFAULT FALSE, -- источник контента для карточки
last_synced_at TIMESTAMP,
UNIQUE(supplier_id, supplier_sku)
);
-- Индексы для быстрого поиска лучшего предложения
CREATE INDEX idx_offers_product_price ON product_offers(product_id, price)
WHERE stock > 0;
Вибір найкращої пропозиції на вітрині
Найкраща пропозиція визначається за налаштовуваними правилами. Типовий варіант — мінімальна ціна серед постачальників із наявністю. Ми також використовуємо скоринг пропозицій із ваговими коефіцієнтами (ціна, залишок, строк поставки), що дозволяє точніше враховувати пріоритети бізнесу. Наш метод скорингу в 2 рази точніший за простий вибір мінімальної ціни. Ми порівнюємо ціни постачальників автоматично.
class BestOfferResolver
{
public function resolve(int $productId): ?ProductOffer
{
return ProductOffer::where('product_id', $productId)
->where('stock', '>', 0)
->orderByRaw('
price * (1 + COALESCE(
(SELECT markup FROM suppliers WHERE id = supplier_id), 0
) / 100)
')
->orderBy('lead_time_days')
->first();
}
}
Агрегована вітрина в API
Відповідь API для картки товару має включати агреговані дані: мінімальну та максимальну ціну, загальний залишок, а також список пропозицій для вибору покупцем. Це дозволяє прискорити вибірку в 3-5 разів.
class ProductResource extends JsonResource
{
public function toArray($request): array
{
$bestOffer = $this->bestOffer;
return [
'id' => $this->id,
'name' => $this->name,
'description' => $this->description,
'attributes' => $this->attributes,
// Агрегированные цены
'price' => $bestOffer?->price,
'price_min' => $this->offers->where('stock', '>', 0)->min('price'),
'price_max' => $this->offers->where('stock', '>', 0)->max('price'),
'in_stock' => $this->offers->where('stock', '>', 0)->count() > 0,
'total_stock' => $this->offers->sum('stock'),
// Список предложений (если магазин показывает их явно)
'offers' => OfferResource::collection(
$this->offers->where('stock', '>', 0)->sortBy('price')
),
];
}
}
Оновлення агрегації при зміні пропозицій
Агреговані показники мають оновлюватися при кожній зміні пропозиції постачальника. Використовуємо шаблон Observer, який інвалідує кеш та перераховує денормалізовані поля. Автоматична синхронізація залишків забезпечує актуальність даних.
class ProductOfferObserver
{
public function saved(ProductOffer $offer): void
{
// Пересчёт агрегатов в кэше
Cache::forget("product.{$offer->product_id}.best_offer");
Cache::forget("product.{$offer->product_id}.price_range");
// Обновление денормализованных полей в products
$this->recalculate($offer->product_id);
}
private function recalculate(int $productId): void
{
$agg = ProductOffer::where('product_id', $productId)
->where('stock', '>', 0)
->selectRaw('MIN(price) as min_price, MAX(price) as max_price, SUM(stock) as total_stock')
->first();
Product::where('id', $productId)->update([
'price_min' => $agg->min_price,
'price_max' => $agg->max_price,
'total_stock' => $agg->total_stock,
'updated_at' => now(),
]);
}
}
Відображення кількох пропозицій на картці
Якщо бізнес-логіка передбачає вибір постачальника покупцем (як у Яндекс.Маркету), використовуємо React-компонент списку пропозицій.
// React-компонент списку предложений
const OfferList: React.FC<{ offers: Offer[] }> = ({ offers }) => {
const sorted = [...offers].sort((a, b) => a.price - b.price);
return (
<div className="space-y-2">
{sorted.map(offer => (
<div key={offer.id} className="flex items-center justify-between border rounded p-3">
<div>
<span className="font-semibold">{formatPrice(offer.price)}</span>
<span className="text-sm text-gray-500 ml-2">
{offer.supplier.name}
</span>
</div>
<div className="text-sm text-gray-500">
{offer.stock > 0
? `в наличии ${offer.stock} шт.`
: 'нет в наличии'}
{offer.lead_time_days && ` · доставка ${offer.lead_time_days} дн.`}
</div>
<button
onClick={() => addToCart(offer)}
disabled={offer.stock === 0}
className="btn-primary"
>
Купить
</button>
</div>
))}
</div>
);
};
Які практичні переваги автоматизованої агрегації?
Порівняння та економія
| Параметр | Ручна агрегація | Автоматизована агрегація (наше рішення) |
|---|---|---|
| Час оновлення | Години/дні | Реальний час або відкладене (15 хв) |
| Точність матчингу | 80-90% | 99% за наявності артикулів |
| Витрати на підтримку | Високі (людські ресурси) | Низькі (серверні ресурси) |
Автоматична агрегація дозволяє скоротити витрати на підтримку каталогу до 40%. Типова економія для каталогу з 50 000 товарів становить від 200 000 ₽ на рік. При масштабуванні до 200 000 товарів економія перевищує 500 000 ₽. Наше рішення обробляє 100 000 товарів у 3 рази швидше за типову агрегацію на основі імпорту. Наш підхід забезпечує швидкість агрегації в 3 рази вищу за типову.
Що входить в роботу
Ми беремо реалізацію під ключ: від проектування архітектури до деплою. В рамках проекту ви отримуєте:
- нормалізовану схему БД (мастер-картки + пропозиції);
- API-ресурси для вітрини з кешуванням;
- фронтенд-компоненти для вибору постачальника;
- налаштування індексації в Elasticsearch (за потреби);
- документацію з підтримки та навчання команди;
- доступ до репозиторію та CI/CD пайплайну;
- підтримку після релізу протягом 3 місяців.
Орієнтовні строки виконання етапів:
- Схема даних + логіка злиття + BestOfferResolver: 2 дні
- Observer + денормалізація агрегатів: 1 день
- API-ресурс із пропозиціями + фронтенд компонент: 1–2 дні
- Інтеграція з Elasticsearch: +2 дні
- Налаштування вагових коефіцієнтів через адмінку: +1 день
Базова агрегація без пошуку: 4–5 робочих днів. Зв'яжіться з нами для консультації — ми підготуємо архітектуру та точні строки під ваш проект. Замовте оцінку, щоб дізнатися, як агрегація може скоротити витрати на підтримку каталогу.
Перевірте свій каталог: якщо ви витрачаєте більше 10 годин на тиждень на ручне оновлення цін, наше рішення окупиться за 2-3 місяці. Отримайте консультацію інженера — розрахуємо економію для вашого проекту.
Джерело: загальновідомі практики агрегації даних в e-commerce.







