> $map */ 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); } }; } it('ранжирует кандидатов по близости к профилю клиента (косинус)', function () { // профиль клиента — «автоломбард / займ под залог авто» $embedder = fakeEmbedder([ 'залог авто' => [1.0, 0.0, 0.0], // клиентский профиль и близкие к нему 'автозайм' => [0.9, 0.1, 0.0], 'пластиковые окна' => [0.0, 1.0, 0.0], // совсем другое ]); $rel = new EmbeddingRelevance($embedder); $ranked = $rel->rank( clientExamples: ['Автоломбард Клиента залог авто'], candidates: [ ['name' => 'Окна Комфорт', 'description' => 'пластиковые окна и балконы'], ['name' => 'АвтоДеньги', 'description' => 'автозайм под ПТС'], ['name' => 'ЗалогЦентр', 'description' => 'займ под залог авто'], ], ); // ближайший — «займ под залог авто» (точное совпадение профиля), затем «автозайм», окна — последними expect(array_column($ranked, 'name'))->toBe(['ЗалогЦентр', 'АвтоДеньги', 'Окна Комфорт']); expect($ranked[0]['relevance_pct'])->toBe(100); expect($ranked[2]['relevance_pct'])->toBe(0); foreach ($ranked as $c) { expect($c['relevance_pct'])->toBeGreaterThanOrEqual(0)->toBeLessThanOrEqual(100); } }); it('без профиля клиента похожесть 0, порядок сохраняется', function () { $rel = new EmbeddingRelevance(fakeEmbedder([])); $ranked = $rel->rank(clientExamples: [], candidates: [ ['name' => 'А', 'description' => 'x'], ['name' => 'Б', 'description' => 'y'], ]); expect($ranked)->toHaveCount(2); expect($ranked[0]['relevance_pct'])->toBe(0); expect($ranked[1]['relevance_pct'])->toBe(0); }); it('пустой список кандидатов → пустой результат', function () { $rel = new EmbeddingRelevance(fakeEmbedder([])); expect($rel->rank(['что-то'], []))->toBe([]); });