У матеріалі розбирається підхід автора статті з Laravel News до пакету mozex/laravel-searchable від Mozex. Пакет додає trait для Eloquent-моделей, який дозволяє єдиним скоупом шукати по локальних полях, звичайних відношеннях і поліморфних зв'язках, а також інтегрувати цей пошук з Filament.
1. Коли має сенс саме mozex/laravel-searchable
Автор порівнює цей пакет зі Spatie's Laravel Searchable та іншими рішеннями й фокусується на сценаріях, де потрібно фільтрувати вже існуючий Eloquent-запит, а не збирати спільний список результатів з різних моделей.
- Spatie's Laravel Searchable створює окремий search-запит, який комбінує результати різних моделей і джерел. Це зручно для сторінок, де треба показувати разом, наприклад, пости й відео.
- mozex/laravel-searchable працює як Eloquent-scope
search(), який фільтрує поточний query builder. Тобто післяModel::search()ви можете продовжувати ланцюжок: додаватиwhere,with,paginateтощо.
Саме тому авторка статті (через приклади) відносить mozex/laravel-searchable до задач типу:
- модераційні екрани (списки коментарів, замовлень, заявок), де вже є фільтри й сортування;
- адмін-панелі на Filament, де один і той самий Eloquent-запит живе і в ресурсі, і в таблиці, і в панельному глобальному пошуку;
- складні реляційні схеми з поліморфією й різними підключеннями до БД, де не хочеться дублювати
whereHasіwhereHasMorphу кожному місці.
2. Пошук по полях моделі: як працює скоуп search ()
Щоб модель можна було шукати через пакет, потрібно підключити trait і описати стовпці, які беруть участь у пошуку.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Mozex\Searchable\Searchable;
class Post extends Model
{
use Searchable;
public function searchableColumns(): array
{
return ['title', 'body'];
}
}
Після цього можна викликати скоуп search() на Eloquent-запиті.
use App\Models\Post;
$posts = Post::search('laravel')->paginate(20);
Ключові деталі, на які звертає увагу автор:
- Пріоритет полів задається порядком у масиві
searchableColumns(). Наприклад,['title', 'body']означає, що збіг уtitleважить більше, ніж збіг лише вbody(це впливає на релевантність, особливо у зв'язці з Filament). - Розбиття рядка пошуку: багатослівний запит розбивається на терми, і кожен з них має зустрічатися принаймні десь у налаштованих стовпцях. Тобто це логіка «AND по термах».
- Фразовий пошук: подвійні лапки фіксують фразу. Наприклад,
"laravel scout" queueшукає одну фразу й додаткові окремі терми. - Обмеження кількості термів: за замовчуванням враховуються перші 10 термів, решта відкидається. Це захищає від надто довгих або зловмисних рядків.
- Символи % і _ із введення користувача сприймаються буквально, а не як шаблони для
LIKE. Це зменшує «магичність» поведінки й робить запит безпечнішим.
У цій частині автор фактично показує, як Searchable замінює ручне конструювання набору where/orWhere для пошуку по кількох полях, але з уже вбудованою логікою розбору рядка й сортування за релевантністю.
3. Пошук по поліморфних відношеннях з типізованим синтаксисом
Окремий практичний блок присвячений модераційному екрану коментарів, які можуть належати різним сутностям, наприклад постам і відео. Потрібно шукати одночасно:
- по самому коментарю (поле
body); - по заголовку поста, якщо коментар до поста;
- по назві відео, якщо коментар до відео.
Спочатку задається morphMap для типів:
use App\Models\Post;
use App\Models\Video;
use Illuminate\Database\Eloquent\Relations\Relation;
Relation::morphMap([
'post' => Post::class,
'video' => Video::class,
]);
Далі в моделі коментаря задаються «шляхи» пошуку, включно з поліморфними цільовими моделями.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Mozex\Searchable\Searchable;
class Comment extends Model
{
use Searchable;
public function searchableColumns(): array
{
return [
'body',
'commentable:post.title',
'commentable:video.name',
];
}
public function commentable(): MorphTo
{
return $this->morphTo();
}
}
Ключі на кшталт commentable:post.title мають структуру:
- ліворуч від двокрапки — ім'я поліморфного відношення (
commentable); - праворуч — конкретний morph-тип (
postабоvideo) та шлях до колонки (title,name); - можливі вкладені шляхи, наприклад
commentable:post.author.name, якщо в моделіPostє відношенняauthor.
Звичайні (неполіморфні) відношення задаються крапковою нотацією, наприклад author.company.name.
Після налаштування колонок пошук виглядає як звичний Eloquent-запит:
Автор робить кілька важливих уточнень:
- Пакет групує свої умови пошуку в підзапит, тому попередні обмеження (
where('approved', false)) зберігаються й не «перекриваються» пошуком. - Аналогічний запит можна було б написати вручну через
whereHasMorph(), але тоді всі поля типуcommentable:post.titleдовелося б дублювати в кожному місці використання. Тут же оголошення централізоване вsearchableColumns().
Такий підхід добре масштабується для поліморфних схем, де одна «дитяча» модель (коментар, файл, медіа-тег) може належати різним «батьківським» моделям, кожна зі своїми полями для пошуку.
4. Відношення на іншому підключенні до БД і обмеження external lookup
Окремий сценарій, на якому зупиняється автор, — моделі, пов'язані через BelongsTo, але зберігаються в іншій базі. Наприклад, локальна таблиця постів і загальна таблиця користувачів на іншому підключенні.
Якщо в searchableColumns() зазначити щось на кшталт author.name, пакет:
- визначає, що цільова модель у відношенні
authorживе на іншомуconnection; - виконує окремий запит до пов'язаної моделі й отримує список її первинних ключів (ID);
- фільтрує головну модель по цих ID.
Те ж саме може застосовуватися до підтримуваних morph-цілей.
Щоб запобігти вибуху кількості ID, пакет вводить ліміт: за замовчуванням не більше 50 збігів для однієї колонки й одного терма. Це налаштовується прямо в пошуковому виклику:
Автор підкреслює наслідки такого ліміту:
- якщо користувачів з ім'ям «Jane» більше за ліміт, частина потенційно релевантних постів буде відфільтрована ще на етапі зовнішнього пошуку;
- збільшення ліміту дасть повніший результат, але призведе до більшої кількості ID і довшого
IN (...)у головному запиті; - оптимальне значення залежить від ваших даних і профілю навантаження.
У версії 1.3.0, за словами автора, є ще два важливі обмеження для таких external lookup:
- фільтрація йде тільки по первинному ключу цільової моделі;
- додаткові
where-умови, визначені всередині методу відношення (наприклад, фільтр по активних користувачах), не враховуються.
Звідси випливають практичні наслідки:
- якщо
BelongsToналаштований на нестандартний owner key, або логіка сильно покладається на додатковіwhereу відношенні, автор рекомендує для таких випадків окремий, вручну написаний запит; - пакет не намагається проходити довгі ланцюжки відношень через різні підключення — «шорткат» працює для прямого
BelongsToі підтриманих morph-мішеней, але не для довільних глибоких шляхів.
5. Інтеграція з Filament: relevance-сортування і global search
Для моделей, де використовується trait, пакет додає Filament-макрос advancedSearchable() для колонок таблиць. Автор показує це на прикладі текстової колонки:
Цей підхід дає одразу кілька ефектів:
- Перевикористання однієї схеми пошуку: колонка не описує сама, як саме й по яких полях шукати — вона покладається на
searchableColumns()в моделі, включно з typed morph-шляхами. - Автоматичне ранжування по релевантності: під час пошуку в таблиці Filament спочатку сортує по оціненій релевантності збігів, а вже потім застосовує дефолтне сортування таблиці для розв'язання «нічиєї».
- Пріоритет користувацького сортування: якщо користувач клікає на заголовок колонки й задає явний
order by, це сортування має вищий пріоритет, ніж релевантність.
Щодо самої моделі релевантності автор фіксує такі моменти:
- порядок у
searchableColumns()задає пріоритет полів (перші — важливіші за наступні); - для «прямих» колонок (які належать самій моделі) градація така:
- повний збіг — найвищий рейтинг;
- префікс (початок рядка збігається) — нижчий;
- підрядок (деінде в середині значення) — ще нижчий.
- для однострибкових відношень (simple relation, morph) також є градації;
- для глибших шляхів і external lookup використовується спрощена схема «збіг/нема збігу» без тонкого ранжування.
Якщо автоматичне relevance-сортування в таблицях не підходить, його можна вимкнути через конфіг-ключ searchable.filament.relevance_sort.
Окремо згадується SearchableGlobalSearchProvider для панельного глобального пошуку Filament. Його можна зареєструвати в панелі, а для кожного ресурсу використати стандартний метод getGloballySearchableAttributes(), щоб задати, які атрибути ресурс експонує в глобальному пошуку. При цьому діють звичайні Filament-правила доступу й формування URL, а результати сортуються всередині категорії ресурсу за релевантністю.
6. Вибір між mozex/laravel-searchable та іншими пакетами
У фінальному блоці автор розкладає по полицях відмінності між різними підходами до пошуку в Laravel-проєктах.
Cross Eloquent Search від Protone Media автор згадує як альтернативу, яка:
- уміє шукати по вкладених відношеннях;
- може використовувати нативний full-text-пошук MySQL/PostgreSQL;
- вміє відразу пагінувати об'єднаний список результатів з кількох моделей.
Його зручно застосовувати там, де потрібен спільний результат на кшталт «пости + відео в одному списку результатів». При цьому в документації Cross Eloquent Search зазначено, що його relevance-сортування не підтримує ранжування результатів, отриманих через відношення.
На противагу цьому, mozex/laravel-searchable:
- заточений під фільтрацію однієї моделі (наприклад,
Comment), яка шукає через свої відношення до інших таблиць; - має оцінку релевантності для однострибкових відношень (у тому числі morph) і бінарну для глибших шляхів;
- не потребує окремого search-індексу — запити будуються на
LIKE '%term%'та підзапитах.
Автор окремо згадує, що альтернативою «без індексу» можуть бути й можливості Scout Database Engine, який підтримує різні стратегії пошуку (префіксну, full-text залежно від БД). Але якщо потрібні:
- толерантність до опечаток;
- зовнішній пошуковий індекс;
автор рекомендує дивитися в бік інтеграції Laravel Scout з Meilisearch (посилання на відповідний гайд є в оригінальному тексті).
Важливе технічне попередження від автора стосується продуктивності mozex/laravel-searchable:
- substring-пошук реалізований через
LIKE '%term%', що, як правило, «ламає» можливість B-tree-індексів прискорювати збіг; - додавання реляційних пошуків і ранжування означає наявність додаткових підзапитів;
- для завантажених endpoint-ів такий підхід потрібно обов'язково тестувати саме на своїх даних і expected-навантаженні.
Тому, за підсумком автора, основний аргумент на користь mozex/laravel-searchable — це адмінський пошук, насичений відношеннями (включно з поліморфними й міжконекційними), який інакше вимагав би складного й дубльованого коду запитів. Особливо якщо ті самі моделі використовуються в Filament-таблицях і глобальному пошуку панелі.
7. Установка та налаштування пакета
Автор зазначає, що на момент написання статті (версія 1.3.0) пакет вимагає:
- PHP 8.2+;
- Laravel версій
^12.69або^13.30; - для опціональної інтеграції з Filament — підтримуються Filament 4 і 5.
Установка виглядає стандартно через Composer:
Пакет не містить власних міграцій. Щоб опублікувати конфіг config/searchable.php і налаштувати дефолтні значення (ліміт термів, externalLimit та ін.), автор пропонує використати команду php artisan searchable:install.
Подальші приклади використання й вихідний код доступні в репозиторії Mozex Laravel Searchable на GitHub, який автор і радить як основне джерело деталей реалізації.
Джерело: Yannick Lyn Fatt — Laravel News