Розробник Бен Бьюрстром (Ben Bjurstrom) у статті на своєму блозі розповів про драйвер Pgvector для Laravel Scout. Він спрощує пошук за векторними ембедингами та їх підтримку в актуальному стані. Scout добре поєднується з pgvector, оскільки його model observers автоматично синхронізують ембединги зі змінами в даних. Репозиторій пакета: benbjurstrom/pgvector-scout.
Навіщо потрібен пошук за поняттям
Автор ставить питання: що, якщо шукати дані за концепцією, а не за ключовим словом, наприклад за абстрактною категорією? Для демонстрації він використовує набір даних Amazon Fine Food Reviews: перші 500 відгуків завантажено в таблицю демонстраційного застосунку.
Запит за ключовим словом на кшталт «pancakes» виконати нескладно, можна навіть додати нечіткий пошук, щоб знаходилась і форма однини «pancake». Але як знайти всі відгуки про сніданкову їжу: млинці, вафлі, яйця, вівсянку тощо? Людина розуміє, що всі ці страви належать до сніданків, а от як змусити пошук це «зрозуміти»?
Векторні ембединги
Відповідь - векторні ембединги. Це числові представлення даних, які передають семантичний зміст. За допомогою великої мовної моделі фрагменти тексту відображаються у багатовимірний простір, де схожі за змістом поняття групуються поруч.
У прикладі зі сніданками відгуки про різні сніданкові страви опиняться приблизно в одній ділянці цього простору. Тоді можна обчислити відстань між вектором загального пошукового терміна, наприклад «Breakfast», і векторами відгуків. Відгуки з найменшою відстанню, імовірно, стосуються сніданків (за умови, що такі є в наборі даних).
А як щодо великих документів?
Відгуки - це короткі тексти. Якщо ж йдеться про довші матеріали (дописи блогу, документацію, PDF), не можна просто віддати весь документ в один ембединг і чекати хороших результатів. Спершу його потрібно розбити на семантичні частини. Для цього автор радить свою статтю про PHP Markdown Chunker, де описано розумне розбиття документів зі збереженням структури та змісту.
Раніше для зберігання ембедингів і обчислення відстаней між векторами потрібна була окрема векторна база даних. Проте розширення Pgvector для PostgreSQL дає змогу зберігати ембединги та виконувати пошук за подібністю в тій самій реляційній базі, де вже лежать ваші дані.
Pgvector і Laravel Scout: ідеальна пара
У Laravel-застосунку потрібно вирішити кілька завдань: перетворити текст відгуків на ембединги через API (наприклад, OpenAI), зробити те саме з пошуковим терміном «Breakfast» і підтримувати синхронізацію ембедингів, коли відгуки редагуються, оновлюються чи видаляються.
Laravel Scout - офіційна бібліотека на основі драйверів, що додає пошук до Eloquent-моделей. Вона містить model observers, які автоматично синхронізують дані з пошуковим індексом під час створення, оновлення чи видалення моделі. Драйвер автора використовує ці observers, тож векторні ембединги завжди відповідають актуальному стану даних.
Демонстраційний застосунок
Щоб показати підхід на практиці, автор підготував демо на основі згаданого датасету. Репозиторій демо: benbjurstrom/pgvector-scout-demo.
Налаштування бази даних і моделі
Застосунок містить міграцію для таблиці reviews і сідери з першими 500 відгуками Amazon Fine Food та відповідними ембедингами, отриманими з моделі OpenAI text-embedding-3-small.
Також є Eloquent-модель для таблиці reviews із двома необхідними трейтами: HasEmbeddings і Searchable.
// App\Models\Review
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;
use BenBjurstrom\PgvectorScout\Models\Concerns\HasEmbeddings;
class Review extends Model
{
use HasEmbeddings, Searchable;
Пошук в індексі
На цьому все налаштування завершено. Контролер індексу приймає запит і передає його в метод search моделі через Laravel Scout. Запит автоматично перетворюється на вектор за допомогою OpenAI, після чого в базі виконується пошук за подібністю. 50 відгуків, найближчих до пошукового терміна у векторному просторі, передаються у Blade-представлення.
// App\Http\Controllers\ReviewController
public function index(Request $request)
{
$query = $request->query('query');
$reviews = Review::search($query)
->paginate(50)
->withQueryString();
return view('reviews.index', [
'reviews' => $reviews,
'query' => $query
]);
}
Результати запиту «Breakfast» показано на скриншоті в оригінальній статті. Права колонка тепер відображає відстань між вектором пошукового терміна та ембедингом відгуку.
Пошук пов'язаних відгуків
Демо також показує, як виводити пов'язані відгуки на сторінці деталей відгуку. Для цього в методі show контролера ReviewController у метод search моделі передається вектор поточного відгуку. Після цього вибирається шість найближчих результатів, які передаються у Blade-представлення.
// App\Http\Controllers\ReviewController
public function show(string $id)
{
$review = Review::findOrFail($id);
$relatedReviews = Review::search($review->embedding->vector)
->take(6)
->get();
return view('reviews.show', compact('review', 'relatedReviews'));
}
У статті наведено приклад шести відгуків, пов'язаних із відгуком про корм для собак.