Files
portal/app/tests/Unit/Autopodbor/Similarity/EmbeddingRelevanceTest.php
T
Дмитрий b900874a72 feat(автоподбор): шаг1 F (офлайн) — похожесть эмбеддингами (косинус), не «мнение модели»
EmbeddingRelevance: профиль клиента (примеры имя+описание) → центроид; кандидат (имя+описание)
→ косинус → relevance_pct [0..100]; сортировка по убыванию (§12.5). Векторы — за границей
Embedder (живой AITUNNEL text-embedding-3-small подключается на блоке H/живом прогоне, ключ
по §12.9). Вся логика ранжирования протестирована офлайн на детерминированном эмбеддере.

Тесты: similarity 3/3; модуль Автоподбора unit 84/84; Pint чисто.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:40:10 +03:00

75 lines
3.2 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\Agent\Similarity\Embedder;
use App\Services\Autopodbor\Agent\Similarity\EmbeddingRelevance;
/** Детерминированный эмбеддер для офлайн-теста: текст → заранее заданный вектор (по подстроке). */
function fakeEmbedder(array $byNeedle): Embedder
{
return new class($byNeedle) implements Embedder
{
/** @param array<string,array<int,float>> $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([]);
});