Laravel Scout
Com implementar cerca full-text amb Laravel Scout: Meilisearch, Algolia, driver de base de dades, índexs, filtres i paginació.
Que es Laravel Scout?#
Laravel Scout es un paquet oficial que afegeix cerca full-text als models Eloquent d'una manera senzilla i basada en drivers. Scout sincronitza automaticament els indexs de cerca quan es creen, actualitzen o eliminen registres, i proporciona una interficie unificada per cercar independentment del motor de cerca que utilitzis.
Per que la cerca dedicada es millor que SQL LIKE?#
Quan una aplicacio creix, les consultes WHERE column LIKE '%terme%' es tornen insuficients per diverses raons:
- Rendiment: Les consultes LIKE amb comodin al principi (
%terme) no poden aprofitar indexs de base de dades, provocant escaneigs complets de taula que es tornen molt lents amb milions de registres. - Rellevancia: SQL no pot ordenar resultats per rellevancia. Un motor de cerca dedicat sap que un article titulat "Laravel" es mes rellevant per a la cerca "laravel" que un article que simplement menciona la paraula una vegada al cos del text.
- Tolerancia a errors: Els motors de cerca gestionen errors tipografics (typos), sinonims i formes derivades de paraules. SQL LIKE no pot trobar "configuracio" si l'usuari escriu "configuracio".
- Velocitat: Motors com Meilisearch o Algolia estan dissenyats especificament per a cerca, amb temps de resposta de mil·lisegons fins i tot amb milions de documents.
Scout abstrau la complexitat d'integrar-se amb aquests motors i et permet canviar de motor sense modificar el codi de l'aplicacio.
Installacio i configuracio de drivers#
Instal·lar Scout#
composer require laravel/scoutDespres, publica el fitxer de configuracio:
php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"Aixo crea el fitxer config/scout.php on pots configurar el driver, la connexio de cues i altres opcions.
Drivers disponibles#
Scout suporta quatre drivers:
- Meilisearch - El driver recomanat. Motor de cerca de codi obert, rapid i facil de configurar. Ideal per a la majoria d'aplicacions.
- Algolia - Motor de cerca SaaS (al nuvol). Excel·lent rendiment i funcionalitats avancades, pero es un servei de pagament.
- Database - Utilitza la base de dades existent (MySQL o PostgreSQL) per a cerca full-text. No requereix cap servei extern, pero te limitacions.
- Collection - Driver en memoria per a tests. Filtra els resultats d'Eloquent amb PHP pur, sense interactuar amb cap servei de cerca.
Configurar Meilisearch#
Meilisearch es el motor de cerca recomanat per a aplicacions Laravel. Es de codi obert, extremadament rapid (respostes en menys de 50ms), facil de configurar i te una excel·lent integracio amb Scout.
Instal·lar Meilisearch#
Hi ha diverses maneres d'instal·lar Meilisearch:
Amb Docker (recomanat per a desenvolupament):
docker run -d --name meilisearch \
-p 7700:7700 \
-v $(pwd)/meili_data:/meili_data \
-e MEILI_MASTER_KEY='clauMestraMoltSegura' \
getmeili/meilisearch:latestAmb Laravel Sail:
Si utilitzes Sail, pots afegir Meilisearch als serveis:
php artisan sail:install
# Selecciona meilisearch de la llistaInstal·lacio directa (macOS):
brew install meilisearch
meilisearch --master-key='clauMestraMoltSegura'Instal·lar el client PHP i configurar#
composer require meilisearch/meilisearch-php http-interop/http-factory-guzzleConfigura les variables d'entorn al fitxer .env:
SCOUT_DRIVER=meilisearch
MEILISEARCH_HOST=http://localhost:7700
MEILISEARCH_KEY=clauMestraMoltSeguraUn cop Meilisearch estigui en marxa, pots accedir al dashboard web a http://localhost:7700. El dashboard et permet explorar els indexs, fer cerques de prova i veure les configuracions.
Meilisearch proporciona un dashboard web integrat al port 7700. Es una eina excel·lent per depurar cerques, inspeccionar documents indexats i provar configuracions de filtres i ordenacio.
Fer models cercables#
El trait Searchable#
Per fer que un model sigui cercable, afegeix el trait Searchable. Aixo indica a Scout que ha de sincronitzar automaticament el model amb l'index de cerca cada vegada que es crei, actualitzi o elimini un registre:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;
class Article extends Model
{
use Searchable;
}Per defecte, Scout indexa totes les columnes retornades per toArray(). Aixo pot incloure dades innecessaries (timestamps, claus foranes, etc.) que incrementen la mida de l'index sense millorar la cerca.
Personalitzar les dades indexades#
El metode toSearchableArray() et permet controlar exactament quines dades s'envien al motor de cerca. Indexa nomes els camps que els usuaris podrien voler cercar:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Laravel\Scout\Searchable;
class Article extends Model
{
use Searchable;
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'author_id');
}
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class);
}
/**
* Dades que s'indexaran al motor de cerca.
*/
public function toSearchableArray(): array
{
return [
'id' => $this->id,
'title' => $this->title,
'body' => $this->body,
'excerpt' => $this->excerpt,
'author_name' => $this->author->name,
'tags' => $this->tags->pluck('name')->toArray(),
'category' => $this->category,
'published' => $this->is_published,
'published_at' => $this->published_at?->timestamp,
'views_count' => $this->views_count,
];
}
}Fixeu-vos que incloem dades de relacions (nom de l'autor, tags). Aixo permet cercar articles pel nom de l'autor o per etiquetes sense necessitat de joins. Tambe incloem camps com published i category que s'utilitzaran com a filtres, i published_at com a timestamp per a l'ordenacio.
Configurar el nom de l'index#
Per defecte, Scout utilitza el nom de la taula del model com a nom d'index. Pots personalitzar-ho amb el metode searchableAs():
class Article extends Model
{
use Searchable;
/**
* Nom de l'index al motor de cerca.
*/
public function searchableAs(): string
{
return 'articles_index';
}
}Aixo es util quan vols tenir indexs separats per entorn (per exemple, articles_production i articles_staging):
public function searchableAs(): string
{
return 'articles_' . config('app.env');
}Indexacio#
Indexacio automatica#
Amb el trait Searchable, Scout sincronitza automaticament l'index de cerca cada vegada que un model es crea, actualitza o elimina. No cal fer res addicional:
// Crear un article -> s'indexa automaticament
$article = Article::create([
'title' => 'Introduccio a Laravel Scout',
'body' => 'Scout permet cercar models Eloquent...',
]);
// Actualitzar -> l'index s'actualitza automaticament
$article->update(['title' => 'Guia completa de Laravel Scout']);
// Eliminar -> s'elimina de l'index automaticament
$article->delete();Importacio massiva#
Quan afegeixes Scout a un projecte existent que ja te registres a la base de dades, necessites importar-los a l'index. La comanda scout:import ho fa de manera eficient, processant els registres en lots:
# Importar tots els articles
php artisan scout:import "App\Models\Article"
# Tambe funciona amb la barra invertida
php artisan scout:import "App\\Models\\Article"Esborrar l'index#
Per eliminar tots els registres de l'index:
php artisan scout:flush "App\Models\Article"Aixo es util quan vols reconstruir l'index des de zero (flush + import).
Pausar la indexacio#
De vegades necessites realitzar operacions massives sense sincronitzar cada canvi individual amb l'index. El metode withoutSyncingToSearch() pausa temporalment la indexacio:
use App\Models\Article;
// Importar 10.000 articles sense sincronitzar un per un
Article::withoutSyncingToSearch(function () {
// Totes les operacions dins d'aquest bloc no activen indexacio
Article::factory()->count(10000)->create();
});
// Despres, importar tots de cop
// php artisan scout:import "App\Models\Article"Tambe pots desactivar la indexacio per a un model individual:
$article = Article::find(1);
// Actualitzar sense indexar
$article->unsearchable();
$article->update(['title' => 'Nou titol']);
$article->searchable(); // Tornar a indexar manualmentIndexacio amb cues#
Per defecte, Scout sincronitza els indexs de manera sincrona. Aixo significa que cada vegada que crees o actualitzes un model, Scout fa una crida HTTP al motor de cerca dins de la mateixa peticio. Per a aplicacions amb molt de transit, aixo pot afectar el rendiment.
Pots configurar Scout per processar la indexacio a traves de cues:
// config/scout.php
'queue' => true,
// O amb una connexio i cua especifica
'queue' => [
'connection' => 'redis',
'queue' => 'scout',
],SCOUT_QUEUE=trueQuan utilitzeu cues per a la indexacio, els canvis no es reflectiran immediatament a la cerca. Hi haura un petit retard entre guardar el model i que aparegui als resultats de cerca. Considereu aixo per a funcionalitats on la immediatesa es critica.
Cercar#
Cerca basica#
La cerca amb Scout es fa a traves del metode estatic search() del model. Retorna una col·leccio de models Eloquent:
use App\Models\Article;
// Cerca basica
$articles = Article::search('Laravel Scout')->get();
// El metode get() retorna una col·leccio Eloquent
foreach ($articles as $article) {
echo $article->title; // Acces normal al model
echo $article->author->name; // Les relacions funcionen
}Cerca amb filtres (where)#
Pots afegir filtres a la cerca amb where(). Aquests filtres es passen al motor de cerca (no son filtres SQL):
// Articles publicats que contenen "Laravel"
$articles = Article::search('Laravel')
->where('published', true)
->get();
// Articles d'una categoria especifica
$articles = Article::search('tutorial')
->where('category', 'backend')
->get();
// Combinacio de filtres
$articles = Article::search('API REST')
->where('published', true)
->where('category', 'backend')
->get();Per a que els filtres where() funcionin amb Meilisearch, cal que els atributs estiguin configurats com a filtrables. Veure la seccio "Configuracio d'indexs" mes avall.
Paginacio#
Scout s'integra amb el sistema de paginacio de Laravel:
// Paginar resultats (15 per pagina per defecte)
$articles = Article::search('Laravel')->paginate();
// Paginar amb nombre personalitzat
$articles = Article::search('Laravel')->paginate(20);
// A la vista Blade
@foreach ($articles as $article)
<h2>{{ $article->title }}</h2>
@endforeach
{{ $articles->links() }}La paginacio funciona exactament com la paginacio d'Eloquent, incloent la generacio de links de paginacio a les vistes.
Opcions de cerca personalitzades per driver#
Cada driver te opcions especifiques que pots passar amb el metode options():
// Opcions especifiques de Meilisearch
$articles = Article::search('Laravel')
->options([
'attributesToHighlight' => ['title', 'body'],
'highlightPreTag' => '<mark>',
'highlightPostTag' => '</mark>',
'limit' => 50,
'sort' => ['published_at:desc'],
])
->get();
// Opcions d'Algolia
$articles = Article::search('Laravel')
->options([
'filters' => 'published = true AND views_count > 100',
'hitsPerPage' => 25,
])
->get();Resultats en brut#
Si necessites accedir als resultats sense processar del motor de cerca (per exemple, per obtenir metadades com el highlighting o les facetes), utilitza raw():
$rawResults = Article::search('Laravel')->raw();
// Amb Meilisearch, retorna:
// [
// 'hits' => [...],
// 'query' => 'Laravel',
// 'processingTimeMs' => 2,
// 'estimatedTotalHits' => 42,
// 'facetDistribution' => [...],
// ]Soft deletes#
Per defecte, els models amb soft delete s'eliminen de l'index quan es "deleted". Si vols que els models amb soft delete segueixin apareixent als resultats de cerca, configura-ho al model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Laravel\Scout\Searchable;
class Article extends Model
{
use Searchable, SoftDeletes;
/**
* Incloure models amb soft delete als resultats de cerca.
*/
protected $softDeleteSearchable = true;
public function toSearchableArray(): array
{
$array = [
'id' => $this->id,
'title' => $this->title,
'body' => $this->body,
];
// Afegir indicador de soft delete per poder filtrar
$array['__soft_deleted'] = $this->trashed() ? 1 : 0;
return $array;
}
}Despres pots filtrar per models eliminats o no:
// Incloure models eliminats
$articles = Article::search('Laravel')->withTrashed()->get();
// Nomes models eliminats
$articles = Article::search('Laravel')->onlyTrashed()->get();Configuracio d'indexs a Meilisearch#
Meilisearch necessita saber quins atributs son filtrables i quins son ordenables. Sense aquesta configuracio, els metodes where() i orderBy() no funcionaran.
Definir configuracio d'index#
Afegeix la configuracio dels indexs al fitxer config/scout.php:
// config/scout.php
'meilisearch' => [
'host' => env('MEILISEARCH_HOST', 'http://localhost:7700'),
'key' => env('MEILISEARCH_KEY'),
'index-settings' => [
Article::class => [
'filterableAttributes' => [
'published',
'category',
'author_name',
'tags',
'__soft_deleted',
],
'sortableAttributes' => [
'published_at',
'views_count',
'title',
],
'searchableAttributes' => [
'title',
'body',
'excerpt',
'author_name',
'tags',
],
'displayedAttributes' => ['*'],
'typoTolerance' => [
'minWordSizeForTypos' => [
'oneTypo' => 4,
'twoTypos' => 8,
],
],
],
Product::class => [
'filterableAttributes' => ['category', 'brand', 'price', 'in_stock'],
'sortableAttributes' => ['price', 'name', 'created_at'],
'searchableAttributes' => ['name', 'description', 'brand', 'category'],
],
],
],Sincronitzar la configuracio#
Despres de definir la configuracio, sincronitza-la amb Meilisearch:
php artisan scout:sync-index-settingsAquesta comanda envia la configuracio dels indexs a Meilisearch. Cal executar-la cada vegada que modifiques la configuracio d'indexs i tambe com a part del proces de desplegament.
Si no executeu scout:sync-index-settings, els filtres where() i les ordenacions no funcionaran amb Meilisearch. Recordeu afegir aquesta comanda al vostre script de desplegament.
El driver de base de dades#
El driver database utilitza les capacitats de cerca full-text de la base de dades (MySQL o PostgreSQL) sense necessitat d'un servei extern. Es ideal per a aplicacions petites o quan no vols gestionar un servei addicional.
Quan utilitzar el driver de base de dades#
Utilitza el driver de base de dades quan:
- L'aplicacio es petita o mitjana (menys de 100.000 registres cercables)
- No vols afegir la complexitat d'un servei extern
- La cerca no es una funcionalitat critica
- Estes desplegant en un entorn amb recursos limitats
Configuracio#
SCOUT_DRIVER=databaseAmb el driver de base de dades, Scout busca als camps definits a toSearchableArray(). MySQL utilitza consultes LIKE amb optimitzacions, i PostgreSQL pot aprofitar indexs tsvector per a cerca full-text nativa.
Limitacions#
El driver de base de dades te limitacions importants respecte a Meilisearch o Algolia:
- No gestiona typos: La cerca "Laravl" no trobara "Laravel"
- Sense ordenacio per rellevancia: Els resultats no s'ordenen per rellevancia
- Rendiment limitat: Amb centenars de milers de registres, les consultes es ralentiran
- Sense facetes ni highlighting: No suporta funcionalitats avancades de cerca
- Where limitat: Els filtres
where()es tradueixen a clausules SQL
El driver collection#
El driver collection es un driver en memoria dissenyat per a tests. No interactua amb cap servei extern: simplement carrega tots els models Eloquent i filtra els resultats amb PHP:
# Al fitxer .env.testing
SCOUT_DRIVER=collection// Als tests, la cerca funciona sense cap servei extern
test('pot cercar articles', function () {
Article::factory()->create(['title' => 'Introduccio a Laravel Scout']);
Article::factory()->create(['title' => 'Vue.js amb Laravel']);
Article::factory()->create(['title' => 'Django i Python']);
$results = Article::search('Laravel')->get();
expect($results)->toHaveCount(2);
});El driver collection es util per als tests perque no requereix Meilisearch ni Algolia en execucio, simplificant l'entorn de CI/CD. No obstant, no repliqueja exactament el comportament dels motors de cerca reals (com la tolerancia a typos o l'ordenacio per rellevancia).
Cercabilitat condicional#
De vegades no vols indexar tots els registres d'un model. Per exemple, potser nomes vols indexar articles publicats. El metode shouldBeSearchable() et permet controlar si un model individual s'ha d'indexar:
class Article extends Model
{
use Searchable;
/**
* Determina si el model s'ha d'indexar.
*/
public function shouldBeSearchable(): bool
{
return $this->is_published;
}
}Amb aquesta configuracio, quan un article passa d'esborrany a publicat, Scout l'afegeix automaticament a l'index. Quan es despublica, Scout l'elimina. Aixo funciona tant amb la indexacio automatica com amb la importacio massiva.
Pots fer logica mes complexa:
public function shouldBeSearchable(): bool
{
return $this->is_published
&& $this->approved_at !== null
&& ! $this->is_draft;
}Personalitzar la consulta de cerca#
Pots modificar la consulta Eloquent que Scout utilitza per recuperar els models despres d'obtenir els IDs del motor de cerca. Aixo es util per carregar relacions (eager loading) o aplicar filtres addicionals:
$articles = Article::search('Laravel')
->query(function ($query) {
// Eager load de relacions
$query->with(['author', 'tags', 'comments']);
// Filtres SQL addicionals (despres de la cerca)
$query->where('approved', true);
})
->get();El callback query() s'executa despres que Scout obte els IDs dels resultats del motor de cerca. Es a dir, Scout primer cerca "Laravel" a Meilisearch, obte els IDs dels articles coincidents, i despres utilitza Eloquent amb el callback per carregar els models complets.
Scout amb Algolia#
Si prefereixes Algolia (un servei SaaS de cerca al nuvol), la configuracio es similar:
composer require algolia/algoliasearch-client-phpSCOUT_DRIVER=algolia
ALGOLIA_APP_ID=el_teu_app_id
ALGOLIA_SECRET=la_teva_api_key_adminAlgolia ofereix algunes funcionalitats addicionals respecte a Meilisearch, com:
- Analytics de cerca integrats
- Cerca geo-localitzada avancada
- Personalitzacio per usuari (AI)
- CDN global per a latencies baixes
No obstant, Algolia es un servei de pagament amb limits basats en el nombre de registres i operacions de cerca. Per a la majoria d'aplicacions, Meilisearch ofereix una alternativa gratuita i de codi obert amb un rendiment comparable.
Rendiment#
Indexacio amb cues#
Com hem vist, activar les cues per a la indexacio es la millora de rendiment mes important:
SCOUT_QUEUE=trueMida dels lots (chunk size)#
Quan importes registres amb scout:import, Scout els processa en lots. Pots configurar la mida del lot al model:
class Article extends Model
{
use Searchable;
/**
* Configura la mida del lot per a la importacio.
*/
public function searchableUsing(): \Laravel\Scout\EngineManager
{
return app(\Laravel\Scout\EngineManager::class);
}
}O ajustar la mida del chunk a la configuracio:
// config/scout.php
'chunk' => [
'searchable' => 500, // Registres per lot en la importacio
'unsearchable' => 500, // Registres per lot en l'eliminacio
],Indexacio selectiva#
Indexa nomes els camps necessaris per a la cerca. Camps com created_at o updated_at rarament son utils per a la cerca i incrementen innecessariament la mida de l'index:
public function toSearchableArray(): array
{
// Nomes els camps necessaris
return [
'id' => $this->id,
'title' => $this->title,
'body' => $this->body,
'category' => $this->category,
];
}Exemple practic: cerca amb filtres, facetes i paginacio#
A continuacio, un exemple complet d'una funcionalitat de cerca per a un blog, amb filtres per categoria, ordenacio i paginacio:
Model#
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Laravel\Scout\Searchable;
class Article extends Model
{
use Searchable;
protected $fillable = [
'title', 'body', 'excerpt', 'category',
'author_id', 'is_published', 'published_at', 'views_count',
];
protected function casts(): array
{
return [
'is_published' => 'boolean',
'published_at' => 'datetime',
];
}
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'author_id');
}
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class);
}
public function toSearchableArray(): array
{
return [
'id' => $this->id,
'title' => $this->title,
'body' => $this->body,
'excerpt' => $this->excerpt,
'category' => $this->category,
'author_name' => $this->author->name,
'tags' => $this->tags->pluck('name')->toArray(),
'is_published' => $this->is_published,
'published_at' => $this->published_at?->timestamp,
'views_count' => $this->views_count,
];
}
public function shouldBeSearchable(): bool
{
return $this->is_published;
}
}Configuracio de l'index#
// config/scout.php (dins de meilisearch.index-settings)
Article::class => [
'filterableAttributes' => [
'category',
'author_name',
'tags',
'is_published',
],
'sortableAttributes' => [
'published_at',
'views_count',
],
'searchableAttributes' => [
'title',
'body',
'excerpt',
'author_name',
'tags',
],
],php artisan scout:sync-index-settings
php artisan scout:import "App\Models\Article"Controlador#
<?php
namespace App\Http\Controllers;
use App\Models\Article;
use Illuminate\Http\Request;
class SearchController extends Controller
{
public function search(Request $request)
{
$request->validate([
'q' => 'nullable|string|max:255',
'category' => 'nullable|string',
'sort' => 'nullable|in:recent,popular',
'per_page' => 'nullable|integer|min:5|max:50',
]);
$query = $request->input('q', '');
$category = $request->input('category');
$sort = $request->input('sort', 'recent');
$perPage = $request->input('per_page', 15);
// Construir la cerca
$search = Article::search($query);
// Aplicar filtre de categoria si s'ha proporcionat
if ($category) {
$search->where('category', $category);
}
// Configurar ordenacio i highlighting
$sortAttribute = $sort === 'popular' ? 'views_count:desc' : 'published_at:desc';
$search->options([
'sort' => [$sortAttribute],
'attributesToHighlight' => ['title', 'body'],
'highlightPreTag' => '<mark>',
'highlightPostTag' => '</mark>',
'facets' => ['category', 'tags'],
]);
// Carregar relacions i paginar
$articles = $search
->query(fn ($q) => $q->with(['author', 'tags']))
->paginate($perPage)
->withQueryString();
// Obtenir les categories disponibles per al filtre
$categories = Article::query()
->where('is_published', true)
->distinct()
->pluck('category');
return view('search.results', [
'articles' => $articles,
'query' => $query,
'currentCategory' => $category,
'currentSort' => $sort,
'categories' => $categories,
]);
}
}Ruta#
Route::get('/search', [SearchController::class, 'search'])->name('search');Vista#
<!-- resources/views/search/results.blade.php -->
<form action="{{ route('search') }}" method="GET" class="mb-8">
<div class="flex gap-4">
<input type="text"
name="q"
value="{{ $query }}"
placeholder="Cerca articles..."
class="flex-1 rounded-lg border px-4 py-2">
<select name="category" class="rounded-lg border px-4 py-2">
<option value="">Totes les categories</option>
@foreach ($categories as $category)
<option value="{{ $category }}"
@selected($category === $currentCategory)>
{{ $category }}
</option>
@endforeach
</select>
<select name="sort" class="rounded-lg border px-4 py-2">
<option value="recent" @selected($currentSort === 'recent')>Mes recents</option>
<option value="popular" @selected($currentSort === 'popular')>Mes populars</option>
</select>
<button type="submit" class="rounded-lg bg-blue-600 px-6 py-2 text-white">
Cercar
</button>
</div>
</form>
@if ($articles->isEmpty())
<p class="text-gray-500">
No s'han trobat resultats per a "{{ $query }}".
</p>
@else
<p class="mb-4 text-sm text-gray-500">
{{ $articles->total() }} resultats trobats
</p>
@foreach ($articles as $article)
<article class="mb-6 rounded-lg border p-6">
<h2 class="text-xl font-bold">
<a href="{{ route('articles.show', $article) }}">
{{ $article->title }}
</a>
</h2>
<p class="mt-1 text-sm text-gray-500">
Per {{ $article->author->name }}
el {{ $article->published_at->format('d/m/Y') }}
· {{ $article->views_count }} visualitzacions
</p>
<p class="mt-2 text-gray-700">{{ $article->excerpt }}</p>
<div class="mt-3 flex gap-2">
@foreach ($article->tags as $tag)
<span class="rounded bg-gray-100 px-2 py-1 text-xs">
{{ $tag->name }}
</span>
@endforeach
</div>
</article>
@endforeach
{{ $articles->links() }}
@endifAquest exemple proporciona una funcionalitat de cerca completa amb filtratge per categoria, ordenacio per data o popularitat, paginacio, i una interficie d'usuari que mostra els resultats amb les dades de l'autor i les etiquetes. El motor de cerca gestiona la rellevancia, la tolerancia a errors tipografics i la velocitat, mentre que Eloquent s'encarrega de carregar les relacions i generar els models complets.