Перейти до вмісту
en English

Версія 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 за входженням, символи %, _ і ! екрануються, на PostgreSQL ilike (без урахування регістру);
  • ключ моделі: рівність; для цілого ключа шукається лише цифровий термін;
  • 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-шлях:

  1. Рушій повертає до search.scout_candidates ключів-кандидатів у порядку своєї релевантності.
  2. Рядки завжди завантажуються з бази через indexQuery() Entity за цими ключами. Ключ, якого немає в області користувача або вже немає в базі, відкидається.
  3. Порядок рушія зберігається. Далі кожен запис перевіряється політикою 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:

  1. Ендпоінт працює в групі auth_middleware: гість отримує 401, користувач без доступу до панелі (viewLarsadmin) отримує 403.
  2. Entity без $globallySearchable або без дозволу viewAny мовчки пропускається й не згадується у відповіді. Політика без методу viewAny забороняє (fail closed); модель без політики підкоряється allow_without_policy.
  3. Рядки беруться лише з indexQuery(), і в SQL-, і в Scout-шляху.
  4. Колонки полів, прихованих canSee, не шукаються й не впливають на порядок (SQL-шлях). На Scout-шляху збіг визначає індекс рушія.
  5. Кожен результат перевіряється політикою view; записи без дозволу не показуються.
  6. Назва й підзаголовок з поля, прихованого canSee, або з пов'язаного запису, який користувач не може бачити, не показуються (див. Видимість назви й підзаголовка).
  7. Записи, ключ яких не можна безпечно вставити в шлях URL, не показуються.
  8. Перехід за посиланням іде на звичайну сторінку запису, яка ще раз перевіряє 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 ігнорує регістр лише для латиниці: «огляд» не знаходить «Огляд» (так само в пошуку списку й у рушії Scout database). На MySQL і PostgreSQL регістр кирилиці ігнорується.
  • Рушій Scout database не екранує % і _ у терміні, тож вони працюють як шаблони LIKE (SECS-3). SQL-шлях їх екранує.

Далі: Журнал аудиту.

Введіть запит, щоб шукати в усіх посібниках. ↑ ↓ — вибір, Enter — відкрити.