Версія v1.6.5 Попередні версії: документація на GitHub
Глобальний пошук
На цій сторінці
Глобальний пошук шукає записи одразу в кількох Entity з будь-якої сторінки панелі. Він відкривається кнопкою «Пошук…» у шапці або гарячою клавішею Ctrl+K (на macOS ⌘K) і показує результати, згруповані за Entity, у порядку бічного меню.
Участь Entity в пошуку вмикається явно (opt-in). Записи шукаються звичайним SQL по колонках $search
або, якщо модель використовує Laravel Scout, через пошуковий рушій. В обох випадках записи беруться
лише з indexQuery() і перевіряються політиками. canSee діє на SQL-шляху (колонки пошуку) і на назву
й підзаголовок результату. На Scout-шляху збіг визначає індекс рушія (toSearchableArray()), тож
поле, приховане canSee, усе ще може дати збіг: не кладіть такі поля в індекс
(див. Вимкнення Scout для Entity).
Додаткових залежностей Composer чи npm не потрібно. Scout необов'язковий.
Швидкий старт
class Post extends Entity
{
public static string $model = \App\Models\Post::class;
public static string $title = 'title';
public static array $search = ['id', 'title', 'category.name'];
public static bool $globallySearchable = true;
public static ?string $subtitle = 'category.name';
}
Цього достатньо: у шапці з'являється кнопка пошуку, а Ctrl/⌘+K відкриває палітру. Результат показує
назву запису ($title) і підзаголовок ($subtitle) та веде на сторінку запису.
Кнопка й гаряча клавіша з'являються лише тоді, коли поточний користувач може переглядати список хоча б
однієї Entity з $globallySearchable = true.
Налаштування Entity
| Член | Тип, значення за замовчуванням | Опис |
|---|---|---|
$globallySearchable |
bool, false |
Чи бере Entity участь у глобальному пошуку |
$globalSearchLimit |
?int, null |
Скільки записів показувати для Entity. null бере larsadmin.search.limit (5). Значення обмежується діапазоном 1..20 |
$subtitle |
?string, null |
Атрибут другого рядка результату: колонка або relation.attribute одного рівня (див. видимість). null вимикає підзаголовок |
subtitle() |
?string |
Другий рядок результату. Перевизначте, щоб зібрати його самостійно. Порожній рядок вважається відсутнім підзаголовком |
usesScout() |
bool |
Чи шукати через Laravel Scout замість колонок $search (див. Laravel Scout) |
scoutQuery($request, $search) |
\Laravel\Scout\Builder |
Хук для налаштування запиту до рушія Scout |
Назва результату будується з $title (Назва запису). Порожня назва
показується як #<id>.
Видимість назви й підзаголовка
Підзаголовок з $subtitle дорівнює null, якщо:
- поле приховане
canSee(перевіряється перший сегмент шляху, тожcategory.nameприховує полеcategory); - це
relation.attribute, а для пов'язаної моделі немає зареєстрованої Entity; - користувач не має
viewAnyна пов'язаній Entity; attributeприхованеcanSeeна пов'язаній Entity;- пов'язаний запис поза
detailQuery()пов'язаної Entity; - зв'язок «до багатьох»;
- шлях глибший за один зв'язок (
a.b.c); - значення є масивом або об'єктом.
Enum показується як ->value (backed) або ->name.
$title виду relation.attribute підлягає тим самим правилам, але замість null стає '', і палітра
показує #<id>. Назва з простої колонки працює як раніше. Зв'язок одного рівня з $title або
$subtitle завантажується автоматично (eager load) і ніколи не довантажується ліниво; $with для
нього не потрібен.
Перевизначені title() і subtitle() використовуються як є: за видимість того, що вони повертають,
відповідає ваш код.
Власний підзаголовок:
public function subtitle(): ?string
{
return $this->record->email.' · '.$this->record->created_at?->format('d.m.Y');
}
Entity, прихована з меню ($displayInNavigation = false), теж шукається, якщо має
$globallySearchable = true.
Що шукається
Без Scout пошук використовує ту саму логіку, що й поле пошуку на сторінці списку (Пошук):
- колонки з
$search:LIKEза входженням, символи%,_і!екрануються, на PostgreSQLilike(без урахування регістру); - ключ моделі: рівність; для цілого ключа шукається лише цифровий термін;
relation.column: лише серед записів, які пропускаєindexQuery()пов'язаної Entity;- колонки полів, прихованих від користувача через
canSee, не шукаються.
Записи беруться лише з indexQuery() Entity. Фільтри Entity (filters()) і $sort не
застосовуються.
Порядок результатів: спершу записи, чия назва починається з терміна, далі новіші за ключем (desc).
Порядок за назвою застосовується лише тоді, коли $title є простою колонкою з $search, не ключем і не
прихованою canSee. Інакше записи впорядковуються лише за ключем.
Entity не повертає нічого, якщо жодна колонка $search не застосовна до терміна: усі колонки приховані
canSee або єдиний елемент $search це цілий ключ, а термін не цифровий. Так пошук не повертає
всі рядки таблиці.
Entity з $globallySearchable = true, але з порожнім $search і без Scout, мовчки пропускається.
Термін
- Термін обрізається по пробілах і не може бути довшим за 100 символів.
- Шукаються терміни від
larsadmin.search.min_lengthсимволів (2). Термін лише з цифр шукається з одного символу, щоб можна було знайти запис за ID. - Цифровий термін, коротший за
min_length, на SQL-шляху порівнюється лише з ключем, і лише якщо ключ є в$searchі не прихованийcanSee. Scout-Entity передає такий термін рушію як звичайний. - Коротший термін не надсилає жодного запиту до бази: відповідь містить порожні групи.
«Показати всі»
Під результатами кожної Entity з непорожнім $search є рядок «Показати всі — Назва». Він відкриває
список Entity з тим самим терміном у полі пошуку (?search=). Entity без $search (лише Scout)
такого рядка не має. Пошук у списку завжди SQL, тож для Scout-Entity список може відрізнятися від
результатів палітри.
Конфігурація
config/larsadmin.php:
'search' => [
'limit' => 5,
'min_length' => 2,
'throttle' => 60,
'scout_candidates' => 50,
'time_budget_ms' => 3000,
],
| Ключ | За замовчуванням | Опис |
|---|---|---|
search.limit |
5 |
Результатів на Entity; $globalSearchLimit має пріоритет. Обмежується 1..20 |
search.min_length |
2 |
Найкоротший термін, що шукається (щонайменше 1). Цифровий термін шукається завжди |
search.throttle |
60 |
Запитів на хвилину на користувача (лімітер larsadmin-search); null вимикає обмеження |
search.scout_candidates |
50 |
Скільки ключів брати з рушія Scout до обмеження рядків. Обмежується 1..500, але не менше за ліміт + 1 |
search.time_budget_ms |
3000 |
Бюджет часу на один пошук у мілісекундах; null вимикає бюджет |
Опублікований раніше конфіг без секції search працює зі значеннями за замовчуванням.
Laravel Scout
Якщо модель Entity використовує трейт Laravel\Scout\Searchable і пакет laravel/scout встановлено,
глобальний пошук для цієї Entity автоматично йде через рушій Scout. Пакет larsadmin не залежить від
Scout: він лише пропонує його в suggest (laravel/scout ^11.0).
use Laravel\Scout\Searchable;
class Post extends Model
{
use Searchable;
public function toSearchableArray(): array
{
return [
'id' => $this->id,
'title' => $this->title,
'team_id' => $this->team_id,
];
}
}
Як працює Scout-шлях:
- Рушій повертає до
search.scout_candidatesключів-кандидатів у порядку своєї релевантності. - Рядки завжди завантажуються з бази через
indexQuery()Entity за цими ключами. Ключ, якого немає в області користувача або вже немає в базі, відкидається. - Порядок рушія зберігається. Далі кожен запис перевіряється політикою
view, як і в SQL-шляху.
$search і canSee на збіг у Scout-шляху не впливають: що шукається, визначає toSearchableArray()
моделі (ADR-021). Назва й підзаголовок результату підлягають правилам
видимості, як і на SQL-шляху. Тримайте поля, приховані canSee,
поза toSearchableArray().
Вимкнення Scout для Entity
Якщо індекс містить дані, які частина користувачів не повинна бачити (наприклад, поле, приховане
canSee), пошук через рушій дозволяє перевіряти, чи існує запис із певним значенням, навіть якщо саме
значення не показується. Для таких Entity поверніться до SQL-шляху:
public static function usesScout(): bool
{
return false;
}
Або не додавайте чутливі поля в toSearchableArray().
Попередній фільтр у рушії: scoutQuery()
Рушій не знає про indexQuery(). Якщо indexQuery() відсікає більшість кандидатів (типово для
multi-tenant), результатів може бути менше за ліміт, хоча підходящі записи існують. Відфільтруйте
кандидатів ще в рушії:
use Illuminate\Http\Request;
/**
* @param \Laravel\Scout\Builder $search
* @return \Laravel\Scout\Builder
*/
public static function scoutQuery(Request $request, $search)
{
return $search->where('team_id', $request->user()->team_id);
}
Обмеження рядків через indexQuery() застосовується після цього в будь-якому разі, тож scoutQuery()
лише покращує вибір кандидатів і не замінює indexQuery(). Параметри без нативних типів навмисно: так
код працює і в проєктах без Scout. Інший спосіб пом'якшити проблему це збільшити
search.scout_candidates.
Помилки рушія
Якщо рушій недоступний або кинув виняток, Entity потрапляє в список «Не вдалося пошукати в: …», а
виняток передається в report(). Резервного SQL-пошуку немає. Решта Entity відповідає як зазвичай.
Авторизація
Глобальний пошук не показує нічого, чого користувач не побачив би в списку Entity:
- Ендпоінт працює в групі
auth_middleware: гість отримує 401, користувач без доступу до панелі (viewLarsadmin) отримує 403. - Entity без
$globallySearchableабо без дозволуviewAnyмовчки пропускається й не згадується у відповіді. Політика без методуviewAnyзабороняє (fail closed); модель без політики підкоряєтьсяallow_without_policy. - Рядки беруться лише з
indexQuery(), і в SQL-, і в Scout-шляху. - Колонки полів, прихованих
canSee, не шукаються й не впливають на порядок (SQL-шлях). На Scout-шляху збіг визначає індекс рушія. - Кожен результат перевіряється політикою
view; записи без дозволу не показуються. - Назва й підзаголовок з поля, прихованого
canSee, або з пов'язаного запису, який користувач не може бачити, не показуються (див. Видимість назви й підзаголовка). - Записи, ключ яких не можна безпечно вставити в шлях URL, не показуються.
- Перехід за посиланням іде на звичайну сторінку запису, яка ще раз перевіряє
detailQuery()іview.
Помилка в одній Entity не ламає відповідь: Entity потрапляє в failed, а виняток у report().
Виняток авторизації або abort(403) / abort(404) в indexQuery() означає «Entity недоступна»: вона
мовчки пропускається без запису в failed. Інші HTTP-помилки (наприклад, abort(500)) потрапляють у
failed. Виняток під час перевірки viewAny теж пропускає Entity без згадки у відповіді.
Результати не кешуються, а відповідь має заголовок Cache-Control: no-store, private.
Див. також Авторизація.
Інтерфейс і клавіатура
- Відкрити: кнопка «Пошук…» у шапці (на вузьких екранах іконка) або Ctrl+K / ⌘K. Клавіша працює і на українській розкладці. Поверх іншого модального вікна палітра не відкривається. Повторне Ctrl/⌘+K у відкритій палітрі виділяє текст запиту.
- Вибір: ↑/↓ циклічно по всіх результатах і рядках «Показати всі», миша теж робить рядок активним.
- Відкрити результат: Enter або клік. Ctrl+Enter (⌘+Enter), клік середньою кнопкою або Ctrl/⌘+клік відкривають у новій вкладці. Підказка клавіш на macOS показує ⌘ замість Ctrl.
- Закрити: Esc або клік поза палітрою; фокус повертається туди, де був.
Запит надсилається через 250 мс після останнього натискання; попередній незавершений запит скасовується. Збіг у назві підсвічується. Палітра показує лише посилання на той самий origin (відносні шляхи панелі), інші URL відкидає. Історії запитів і нещодавніх записів немає: порожня палітра показує лише підказку.
Якщо пошук не вклався в бюджет часу, палітра повідомляє, що показано не всі розділи. Якщо окремі Entity не відповіли, палітра називає їх. Помилку запиту, зокрема перевищення ліміту (429), можна повторити кнопкою «Спробувати ще раз».
Палітра це modal dialog з combobox і listbox (WCAG 2.1 AA): кількість результатів оголошується скрінрідерам після кожної відповіді.
HTTP-ендпоінт
Палітра звертається до JSON-ендпоінта, яким можна користуватися й напряму:
GET /admin/api/search?search=київ
Маршрут larsadmin.api.search, лімітер larsadmin-search.
{
"search": "київ",
"groups": [
{
"entity": "posts",
"label": "Дописи",
"results": [
{ "id": 12, "title": "Київ восени", "subtitle": "Подорожі", "url": "/admin/entities/posts/12" }
],
"more": true,
"indexUrl": "/admin/entities/posts?search=%D0%BA%D0%B8%D1%97%D0%B2"
}
],
"failed": [{ "entity": "users", "label": "Користувачі" }],
"partial": false
}
groups: лише Entity з результатами, у порядку бічного меню.more: знайдено більше записів, ніж ліміт (рахується до перевіркиview).indexUrl: посилання «Показати всі» абоnull, якщо Entity не має$search.failed: Entity, пошук у яких завершився помилкою.partial: вичерпано бюджет часу, частину Entity не опитано.
| Ситуація | Код |
|---|---|
| Гість | 401 |
| Немає доступу до панелі | 403 |
search відсутній, порожній, масив або довший за 100 символів |
422 |
Перевищено search.throttle |
429 |
| Жодної доступної Entity або термін закороткий | 200 з порожніми groups |
Фронтенд дізнається, чи показувати пошук, зі спільного Inertia-пропа larsadmin.search: null, якщо
користувачеві недоступна жодна Entity з пошуком, інакше { minLength, maxLength }.
Продуктивність
Кожен пошук робить окремий запит для кожної доступної Entity з $globallySearchable (плюс підзапити
для relation.column, eager loads $with і перевірки view). Entity опитуються по черзі.
- Додайте індекси на колонки
$search.LIKE '%термін%'не використовує звичайний B-tree індекс, тож для великих таблиць краще Scout. - Бюджет
search.time_budget_msперевіряється перед кожною Entity: коли його вичерпано, решта Entity пропускається, а відповідь маєpartial: true. Один повільний запит бюджет не перериває: обмежте час виконання запитів у самій базі (statement timeout). - Вмикайте
$globallySearchableлише там, де пошук справді потрібен.
Обмеження
- Для Scout-Entity поле пошуку на сторінці списку лишається SQL по
$search. - Немає повнотекстового пошуку й ранжування в SQL-шляху: термін шукається як підрядок цілком.
- Немає пошуку по дашбордах, метриках і діях, команд у палітрі, історії запитів, іконок результатів і пагінації в палітрі.
- Гаряча клавіша лише Ctrl/⌘+K, змінити її не можна.
- На SQLite
LIKEігнорує регістр лише для латиниці: «огляд» не знаходить «Огляд» (так само в пошуку списку й у рушії Scoutdatabase). На MySQL і PostgreSQL регістр кирилиці ігнорується. - Рушій Scout
databaseне екранує%і_у терміні, тож вони працюють як шаблониLIKE(SECS-3). SQL-шлях їх екранує.
Далі: Журнал аудиту.