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'); });