110 lines
5.1 KiB
PHP
110 lines
5.1 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
use App\Services\Autopodbor\Agent\Aggregator\AggregatorClassifier;
|
||
|
|
use App\Services\Autopodbor\Agent\Aggregator\AggregatorFilter;
|
||
|
|
use App\Services\Autopodbor\Agent\FindCompetitorsAssembler;
|
||
|
|
use App\Services\Autopodbor\Agent\Similarity\Embedder;
|
||
|
|
use App\Services\Autopodbor\Agent\Similarity\EmbeddingRelevance;
|
||
|
|
use App\Services\Autopodbor\AutopodborDedup;
|
||
|
|
use App\Services\Autopodbor\AutopodborNormalizer;
|
||
|
|
|
||
|
|
function assembler(array $aggByName, array $vecByNeedle): FindCompetitorsAssembler
|
||
|
|
{
|
||
|
|
$classifier = new class($aggByName) implements AggregatorClassifier
|
||
|
|
{
|
||
|
|
public function __construct(private array $map) {}
|
||
|
|
|
||
|
|
public function isAggregator(string $name, ?string $siteUrl, ?string $description): ?bool
|
||
|
|
{
|
||
|
|
return $this->map[$name] ?? null;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
$embedder = new class($vecByNeedle) implements Embedder
|
||
|
|
{
|
||
|
|
public function __construct(private array $map) {}
|
||
|
|
|
||
|
|
public function embed(array $texts): array
|
||
|
|
{
|
||
|
|
return array_map(function (string $t): array {
|
||
|
|
foreach ($this->map as $needle => $vec) {
|
||
|
|
if (str_contains($t, $needle)) {
|
||
|
|
return $vec;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return [0.0, 0.0, 0.0];
|
||
|
|
}, $texts);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return new FindCompetitorsAssembler(
|
||
|
|
new AggregatorFilter($classifier),
|
||
|
|
new AutopodborDedup(new AutopodborNormalizer),
|
||
|
|
new EmbeddingRelevance($embedder),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function sampleCandidates(): array
|
||
|
|
{
|
||
|
|
return [
|
||
|
|
['name' => 'Авито', 'site_url' => 'avito.ru', 'description' => 'объявления'], // агрегатор
|
||
|
|
['name' => 'АвтоДеньги', 'site_url' => 'avtodengi.ru', 'description' => 'займ под залог авто', 'directory_urls' => ['https://2gis.ru/firm/1'], 'is_federal' => false],
|
||
|
|
['name' => 'АвтоДеньги', 'site_url' => 'https://avtodengi.ru/', 'description' => 'автозайм', 'directory_urls' => ['https://yandex.ru/maps/org/x/2'], 'is_federal' => false], // дубль по домену
|
||
|
|
['name' => 'Окна Комфорт', 'site_url' => 'okna.ru', 'description' => 'пластиковые окна', 'is_federal' => false], // нерелевантный
|
||
|
|
['name' => 'Мой Бизнес', 'site_url' => 'moy-biznes.ru', 'description' => 'это клиент'], // сам клиент
|
||
|
|
['name' => 'CarMoney', 'site_url' => 'carmoney.ru', 'description' => 'залог авто онлайн', 'is_federal' => true], // федерал
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
it('собирает ядро: отсев агрегаторов → слияние/вычет клиента → похожесть → DTO §7.2', function () {
|
||
|
|
$a = assembler(
|
||
|
|
aggByName: ['Авито' => true],
|
||
|
|
vecByNeedle: ['залог авто' => [1.0, 0.0, 0.0], 'пластиковые окна' => [0.0, 1.0, 0.0]],
|
||
|
|
);
|
||
|
|
|
||
|
|
$res = $a->assemble(
|
||
|
|
candidates: sampleCandidates(),
|
||
|
|
clientExamples: ['Автоломбард Клиента залог авто'],
|
||
|
|
clientKeys: ['moy-biznes.ru'],
|
||
|
|
includeFederal: false,
|
||
|
|
maxCompetitors: 10,
|
||
|
|
);
|
||
|
|
|
||
|
|
$names = array_column($res->competitors, 'name');
|
||
|
|
expect($names)->not->toContain('Авито') // агрегатор отсеян
|
||
|
|
->not->toContain('Мой Бизнес') // клиент вычтен
|
||
|
|
->not->toContain('CarMoney'); // федерал исключён (includeFederal=false)
|
||
|
|
expect($names)->toBe(['АвтоДеньги', 'Окна Комфорт']); // отсортированы по похожести
|
||
|
|
|
||
|
|
$top = $res->competitors[0];
|
||
|
|
expect($top['name'])->toBe('АвтоДеньги');
|
||
|
|
expect($top['directory_urls'])->toContain('https://2gis.ru/firm/1')->toContain('https://yandex.ru/maps/org/x/2'); // дубль слит
|
||
|
|
expect($top['relevance_pct'])->toBe(100);
|
||
|
|
expect($top['relevance_pct'])->toBeGreaterThan($res->competitors[1]['relevance_pct']);
|
||
|
|
// форма §7.2
|
||
|
|
expect($top)->toHaveKeys(['name', 'description', 'is_federal', 'relevance_pct', 'site_url', 'directory_urls', 'provenance']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('includeFederal=true оставляет федерала; maxCompetitors режет хвост', function () {
|
||
|
|
$a = assembler(
|
||
|
|
aggByName: ['Авито' => true],
|
||
|
|
vecByNeedle: ['залог авто' => [1.0, 0.0, 0.0]],
|
||
|
|
);
|
||
|
|
|
||
|
|
$res = $a->assemble(
|
||
|
|
candidates: sampleCandidates(),
|
||
|
|
clientExamples: ['залог авто'],
|
||
|
|
clientKeys: ['moy-biznes.ru'],
|
||
|
|
includeFederal: true,
|
||
|
|
maxCompetitors: 1,
|
||
|
|
);
|
||
|
|
|
||
|
|
expect($res->competitors)->toHaveCount(1); // срез до 1
|
||
|
|
// CarMoney (залог авто, федерал) теперь допустим как кандидат верхнего уровня
|
||
|
|
$a2 = assembler(['Авито' => true], ['залог авто' => [1.0, 0.0, 0.0]]);
|
||
|
|
$full = $a2->assemble(sampleCandidates(), ['залог авто'], ['moy-biznes.ru'], true, 10);
|
||
|
|
expect(array_column($full->competitors, 'name'))->toContain('CarMoney');
|
||
|
|
});
|