Middle: питання на співбесіді з теми «Пошук»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
2 питання
Scout - це загальний інтерфейс до пошукових рушіїв: модель позначається трейтом, а рушій (Meilisearch, Algolia, Typesense, база) підмінюється конфігурацією.
class Vacancy extends Model
{
use Searchable;
/**
* @return array<string, mixed>
*/
public function toSearchableArray(): array
{
return [
'title' => $this->title,
'description' => $this->description,
'company' => $this->company?->name,
];
}
}
Vacancy::search('laravel middle')->paginate(15);
Індексація відбувається автоматично на збереженні моделі, а масово - через php artisan scout:import.
Коли вистачить LIKE:
- Пошук по одній-двох колонках, записів - тисячі.
- Достатньо точного входження, без урахування словоформ.
- Не хочеться ще одного сервісу в інфраструктурі.
Vacancy::where('title', 'like', "%{$term}%")->get();
Де LIKE перестає працювати:
- Не використовує індекс із
%на початку - на великій таблиці це повний перебір. - Не знає словоформ. «розробник» не знайдеться за запитом «розробники», і жодних синонімів.
- Не ранжує. Збіг у заголовку та згадка в кінці опису однакові за вагою.
- Не прощає помилок - один зайвий символ, і результат порожній.
- Не шукає по кількох сутностях одразу.
Проміжний варіант - повнотекстовий індекс самої бази: whereFullText() у MySQL, tsvector у PostgreSQL. Він знімає перші три пункти без окремого сервісу, хоча за якістю ранжування поступається спеціалізованим рушіям.
Практичне правило: починайте з LIKE, переходьте на повнотекстовий індекс, коли обсяг виріс, і на Scout - коли потрібні релевантність, помилки в запиті й фасети.
Трейт Searchable підписується на події моделі: після saved модель надсилається в індекс, після deleted - видаляється з нього. Окремо нічого викликати не потрібно.
Що потрапляє в індекс - визначає toSearchableArray:
public function toSearchableArray(): array
{
return [
'id' => (int) $this->id,
'title' => $this->title,
'body' => strip_tags($this->body),
'author' => $this->author->name,
'tags' => $this->tags->pluck('name')->all(),
'published_at' => $this->published_at?->timestamp,
];
}
Правила:
- лише поля для пошуку й фільтрів, а не вся модель - менший індекс, швидший пошук, менше витоків даних;
- дати - числом (timestamp), щоб по них можна було фільтрувати й сортувати;
- дані зі зв'язків - денормалізуються в документ. Але тоді зміна імені автора не оновить його статті в індексі - про це треба подбати окремо.
Що індексувати взагалі:
public function shouldBeSearchable(): bool
{
return $this->isPublished();
}
Чернетки не потраплять у пошук, а при знятті з публікації запис видалиться з індексу.
Коли оновлювати:
public function searchIndexShouldBeUpdated(): bool
{
return $this->wasRecentlyCreated || $this->wasChanged(['title', 'body']);
}
Без цього кожне оновлення лічильника переглядів відправляло б документ в індекс.
Черга - обов'язкова для зовнішніх рушіїв:
// config/scout.php
'queue' => ['connection' => 'redis', 'queue' => 'scout'],
Інакше кожне збереження моделі чекає HTTP-запиту до Meilisearch, а недоступний пошуковий сервер ламає збереження. Завдання Scout унікальні - дублікати для тієї самої моделі не ставляться, поки попереднє в черзі.
Масові операції:
Post::where(...)->searchable()/->unsearchable()- додати чи прибрати набір;Post::withoutSyncingToSearch(fn () => ...)- імпорт чи міграція без тисяч завдань індексування, з однимscout:importпотім;- масовий
update()через query builder не викликає подій моделі - індекс не оновиться, його треба синхронізувати вручну.
Початкове наповнення: php artisan scout:import "App\Models\Post" або scout:queue-import --chunk=500 для великих таблиць, а makeAllSearchableUsing - для жадібного завантаження зв'язків під час імпорту.
Докладніше в документації: Scout: налаштування даних для пошуку