Files
portal/app/tests/Unit/Autopodbor/AutopodborMergeTest.php
T
Дмитрий f1ab608444 feat(автоподбор): масштаб и чистота шага 1 — пул EXA, очередь-throttle, батч агрегатора, узкие рубрики, дедуп по словарю
Масштаб (лимиты внешних сервисов — на КЛЮЧ, общий на всех клиентов):
- EXA: параллельный пул findSites (concurrency=5) вместо 40 запросов по одному.
- Агрегатор: батч вместо ~90 запросов по одному + чанкинг по 40 и длинный
  таймаут 90с (большой список gpt-4o-mini не успевал в 30с).
- Очередь autopodbor + WithoutOverlapping на всех 3 джобах — глобальный
  потолок = число воркеров; assertNoInFlight держит один подбор на клиента.

Чистота выдачи (универсально, без зашитых ниш):
- Анализатор: узкие рубрики, запрет зонтичных слов («финансовые услуги» тащила
  юрфирмы/банки).
- Дедуп: ключ имени минус слова из СЛОВАРЯ рубрик прогона («Яричъ Ломбард»→«Яричъ»);
  слово-категория из данных, не из списка. Гард: склейка только если остаётся
  ровно один слово-токен (генерики вроде «займы под залог» не трогаем).

TDD, 233/233 (unit+feature) зелёные. НЕ прод.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:18:09 +03:00

148 lines
7.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
use App\Services\Autopodbor\AutopodborDedup;
use App\Services\Autopodbor\AutopodborNormalizer;
function mergeService(): AutopodborDedup
{
return new AutopodborDedup(new AutopodborNormalizer);
}
it('склеивает конкурента, попавшего и по имени, и по домену (одна группа)', function () {
$merged = mergeService()->mergeCompetitors([
['name' => 'Драйв займ'], // только имя (из канала В)
['name' => 'Драйв Займ', 'site_url' => 'https://драйвзайм.рф/'], // имя + домен (из канала А)
]);
// имя «драйвзайм» = корень домена «драйвзайм» → одна карточка
expect($merged)->toHaveCount(1);
expect($merged[0]['site_url'])->toBe('https://драйвзайм.рф/');
});
it('склеивает по общему домену при разном написании имени', function () {
$merged = mergeService()->mergeCompetitors([
['name' => 'Окна Комфорт', 'site_url' => 'https://okna-komfort.ru/contacts'],
['name' => 'ОКНА-КОМФОРТ', 'site_url' => 'okna-komfort.ru'],
]);
expect($merged)->toHaveCount(1);
});
it('склеивает по общему телефону', function () {
$merged = mergeService()->mergeCompetitors([
['name' => 'Фирма А', 'phones' => ['73912920000']],
['name' => 'Фирма Б', 'phones' => ['73912920000', '79991112233']],
]);
expect($merged)->toHaveCount(1);
expect($merged[0]['phones'])->toContain('73912920000')->toContain('79991112233');
});
it('объединяет ссылки справочников и телефоны внутри группы', function () {
$merged = mergeService()->mergeCompetitors([
['name' => 'X', 'site_url' => 'x.ru', 'directory_urls' => ['https://2gis.ru/firm/1'], 'phones' => ['71112223344']],
['name' => 'X', 'site_url' => 'https://x.ru/', 'directory_urls' => ['https://yandex.ru/maps/org/x/2'], 'phones' => ['75556667788']],
]);
expect($merged)->toHaveCount(1);
expect($merged[0]['directory_urls'])->toContain('https://2gis.ru/firm/1')->toContain('https://yandex.ru/maps/org/x/2');
expect($merged[0]['phones'])->toContain('71112223344')->toContain('75556667788');
});
it('склеивает по общему коду справочника (slug Яндекса), даже при разном написании имени', function () {
// Один и тот же «КрасЛомбард», но имя записано по-разному → по имени НЕ сойдётся.
// Спасает опознавательный код фирмы от Яндекса (slug) — это признак источника, не догадка.
$merged = mergeService()->mergeCompetitors([
['name' => 'КрасЛомбард', 'directory_keys' => ['ya:kraslombard'], 'directory_urls' => ['https://yandex.ru/maps/org/kraslombard/333']],
['name' => 'Красломбард 24 Взлётка', 'directory_keys' => ['ya:kraslombard'], 'directory_urls' => ['https://yandex.ru/maps/org/kraslombard/444']],
]);
expect($merged)->toHaveCount(1)
->and($merged[0]['directory_urls'])->toHaveCount(2);
});
it('склеивает «Имя» и «Имя + слово-рубрику» — слово берётся из рубрики фирмы (не из списка)', function () {
$merged = mergeService()->mergeCompetitors([
['name' => 'Яричъ Ломбард', 'description' => 'Ломбард', 'directory_urls' => ['https://yandex.ru/maps/org/yarichl/1']],
['name' => 'Яричъ', 'description' => 'Ломбард', 'directory_urls' => ['https://yandex.ru/maps/org/yarich/2']],
]);
expect($merged)->toHaveCount(1);
});
it('склейка по словарю рубрик: длинное имя БЕЗ своей рубрики тоже цепляется (слово из общего словаря)', function () {
// У «Яричъ Ломбард» своей рубрики нет, но «Ломбард» есть в словаре (его дала рубрика «Яричъ»).
$merged = mergeService()->mergeCompetitors([
['name' => 'Яричъ Ломбард', 'description' => null],
['name' => 'Яричъ', 'description' => 'Ломбард'],
]);
expect($merged)->toHaveCount(1);
});
it('универсально: та же склейка для стоматолога (слово-категория из рубрики, без зашитых ниш)', function () {
$merged = mergeService()->mergeCompetitors([
['name' => 'Улыбка Стоматология', 'description' => 'Стоматология'],
['name' => 'Улыбка', 'description' => 'Стоматология'],
]);
expect($merged)->toHaveCount(1);
});
it('генерик из нескольких слов НЕ склеивается («Ломбард Займы под залог» ≠ «Займы под залог»)', function () {
// После стрижки остаётся описательная фраза «займы под залог» (3 слова) — это генерик, не бренд.
// Третья фирма кладёт «ломбард» в словарь рубрик (как в реальном прогоне).
$merged = mergeService()->mergeCompetitors([
['name' => 'Ломбард Займы под залог', 'description' => null],
['name' => 'Займы под залог', 'description' => 'Микрофинансовая организация'],
['name' => 'Рубин', 'description' => 'Ломбард'],
]);
expect($merged)->toHaveCount(3);
});
it('НЕ склеивает разные имена с одинаковой рубрикой (защита от пере-склейки)', function () {
$merged = mergeService()->mergeCompetitors([
['name' => 'Быстро Ломбард', 'description' => 'Ломбард'],
['name' => 'Дёшево Ломбард', 'description' => 'Ломбард'],
]);
expect($merged)->toHaveCount(2); // «быстро» ≠ «дёшево»
});
it('голое имя-рубрика не плодит пустой ключ (генерики не слипаются по пустоте)', function () {
$merged = mergeService()->mergeCompetitors([
['name' => 'Ломбард', 'description' => 'Ломбард'],
['name' => 'Займ', 'description' => 'Займ'],
]);
expect($merged)->toHaveCount(2);
});
it('не склеивает разных конкурентов', function () {
$merged = mergeService()->mergeCompetitors([
['name' => 'Окна Комфорт', 'site_url' => 'okna-komfort.ru'],
['name' => 'Пластика Окон', 'site_url' => 'plastika.ru'],
]);
expect($merged)->toHaveCount(2);
});
it('вычитает самого клиента (по домену и по имени)', function () {
$merged = mergeService()->mergeCompetitors(
[
['name' => 'Мой Бизнес', 'site_url' => 'moy-biznes.ru'], // это сам клиент по домену
['name' => 'Драйв займ', 'site_url' => 'драйвзайм.рф'],
['name' => 'Мой Бизнес Плюс'], // сам клиент по имени
],
clientKeys: ['https://moy-biznes.ru', 'Мой Бизнес Плюс'],
);
$names = array_column($merged, 'name');
expect($names)->not->toContain('Мой Бизнес');
expect($names)->not->toContain('Мой Бизнес Плюс');
expect($names)->toContain('Драйв займ');
});