feat(bot): KnowledgeSearch — FTS russian top-N с ранжированием
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Bot;
|
||||
|
||||
use App\Models\KnowledgeChunk;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Поиск по базе знаний бота: PostgreSQL FTS (russian) по generated-колонке
|
||||
* search_tsv (title+topics+content), ранжирование ts_rank. websearch_to_tsquery
|
||||
* терпим к пользовательскому вводу (спецсимволы не ломают запрос).
|
||||
* Интерфейс намеренно узкий — замена на pgvector позже не тронет вызывающих.
|
||||
*
|
||||
* @return list<KnowledgeChunk>
|
||||
*/
|
||||
class KnowledgeSearch
|
||||
{
|
||||
public function search(string $question, int $limit = 3): array
|
||||
{
|
||||
$question = trim($question);
|
||||
if ($question === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @var Collection<int, KnowledgeChunk> $hits */
|
||||
$hits = KnowledgeChunk::query()
|
||||
->selectRaw(
|
||||
"knowledge_chunks.*, ts_rank(search_tsv, websearch_to_tsquery('russian', ?)) AS rank",
|
||||
[$question]
|
||||
)
|
||||
->whereRaw("search_tsv @@ websearch_to_tsquery('russian', ?)", [$question])
|
||||
->orderByDesc('rank')
|
||||
->limit($limit)
|
||||
->get();
|
||||
|
||||
return $hits->all();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\KnowledgeChunk;
|
||||
use App\Services\Bot\KnowledgeSearch;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
KnowledgeChunk::create([
|
||||
'source_path' => 'help/p.md', 'title' => 'Что такое проект', 'tour' => 'create-project',
|
||||
'topics' => 'создать проект, заявка на лиды', 'chunk_index' => 0,
|
||||
'content' => 'Проект — это заявка на поток клиентов с выбранного источника.',
|
||||
]);
|
||||
KnowledgeChunk::create([
|
||||
'source_path' => 'help/b.md', 'title' => 'Как пополнить баланс', 'tour' => 'top-up-balance',
|
||||
'topics' => 'пополнить, закинуть деньги, оплата', 'chunk_index' => 0,
|
||||
'content' => 'Пополнить баланс: раздел Биллинг, кнопка Пополнить.',
|
||||
]);
|
||||
});
|
||||
|
||||
it('находит релевантный чанк и ранжирует его первым', function () {
|
||||
$hits = app(KnowledgeSearch::class)->search('а что такое проект?', 3);
|
||||
|
||||
expect($hits)->not->toBeEmpty()
|
||||
->and($hits[0]->title)->toBe('Что такое проект');
|
||||
});
|
||||
|
||||
it('находит по синонимам из topics («закинуть деньги»)', function () {
|
||||
$hits = app(KnowledgeSearch::class)->search('как закинуть деньги', 3);
|
||||
|
||||
expect($hits)->not->toBeEmpty()
|
||||
->and($hits[0]->title)->toBe('Как пополнить баланс');
|
||||
});
|
||||
|
||||
it('на вопрос не по теме возвращает пусто', function () {
|
||||
expect(app(KnowledgeSearch::class)->search('какая погода в москве', 3))->toBeEmpty();
|
||||
});
|
||||
|
||||
it('не падает на спецсимволах в вопросе', function () {
|
||||
expect(app(KnowledgeSearch::class)->search('проект & | ! ( )', 3))->toBeArray();
|
||||
});
|
||||
Reference in New Issue
Block a user