Come usare Laravel Scout

Laravel Scout è un pacchetto ufficiale del framework Laravel che fornisce un'implementazione semplice e basata su driver per aggiungere la ricerca full-text ai modelli Eloquent. Grazie a Scout, è possibile mantenere automaticamente sincronizzati gli indici di ricerca con i record del database, rendendo la ricerca full-text accessibile e facilmente configurabile anche in applicazioni complesse.

In questo articolo vedremo come installare e configurare Laravel Scout, come integrarlo con i principali driver disponibili, come indicizzare i modelli Eloquent e come eseguire ricerche avanzate all'interno di un'applicazione Laravel.

Installazione

Laravel Scout viene installato tramite Composer. Per aggiungere il pacchetto al progetto è sufficiente eseguire il seguente comando dalla radice del progetto:

composer require laravel/scout

Dopo l'installazione, è necessario pubblicare il file di configurazione di Scout con il comando Artisan dedicato:

php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"

Questo comando creerà il file config/scout.php nella directory di configurazione del progetto, dove è possibile definire il driver da utilizzare e le relative opzioni.

Configurazione del file scout.php

Il file di configurazione generato contiene le impostazioni principali per il funzionamento di Scout. Di seguito è riportata la struttura di base con le opzioni più rilevanti:

<?php

// Configurazione principale di Laravel Scout
return [

    // Driver di ricerca da utilizzare (algolia, meilisearch, typesense, database, collection)
    'driver' => env('SCOUT_DRIVER', 'algolia'),

    // Prefisso applicato agli indici di ricerca
    'prefix' => env('SCOUT_PREFIX', ''),

    // Indica se la sincronizzazione con l'indice è attiva
    'queue' => env('SCOUT_QUEUE', false),

    // Numero di modelli processati per ogni operazione di importazione in batch
    'chunk' => [
        'searchable' => 500,
        'unsearchable' => 500,
    ],

    // Configurazione Algolia
    'algolia' => [
        'id' => env('ALGOLIA_APP_ID', ''),
        'secret' => env('ALGOLIA_SECRET', ''),
    ],

    // Configurazione Meilisearch
    'meilisearch' => [
        'host' => env('MEILISEARCH_HOST', 'http://localhost:7700'),
        'key' => env('MEILISEARCH_KEY', null),
        'index-settings' => [],
    ],

    // Configurazione Typesense
    'typesense' => [
        'client-settings' => [
            'api_key' => env('TYPESENSE_API_KEY', 'xyz'),
            'nodes' => [
                [
                    'host' => env('TYPESENSE_HOST', 'localhost'),
                    'port' => env('TYPESENSE_PORT', '8108'),
                    'protocol' => env('TYPESENSE_PROTOCOL', 'http'),
                ],
            ],
        ],
        'model-settings' => [],
    ],

];

I driver disponibili

Laravel Scout supporta diversi driver di ricerca, ognuno con caratteristiche e requisiti specifici. La scelta del driver dipende dalle esigenze del progetto, dal budget e dall'infrastruttura disponibile.

Driver Algolia

Algolia è un servizio di ricerca cloud hosted, noto per le sue prestazioni elevate e per la facilità di integrazione. Per utilizzarlo è necessario installare il pacchetto client ufficiale:

composer require algolia/algoliasearch-client-php

Le credenziali di accesso vanno inserite nel file .env:

# Configurazione Algolia nel file .env
SCOUT_DRIVER=algolia
ALGOLIA_APP_ID=il_tuo_app_id
ALGOLIA_SECRET=la_tua_secret_key

Driver Meilisearch

Meilisearch è un motore di ricerca open source, self-hosted e altamente performante. Può essere eseguito localmente o tramite Docker. Per installare il client PHP necessario:

composer require meilisearch/meilisearch-php http-interop/http-factory-guzzle

La configurazione nel file .env prevede l'indirizzo del server e, se configurata, la chiave di autenticazione:

# Configurazione Meilisearch nel file .env
SCOUT_DRIVER=meilisearch
MEILISEARCH_HOST=http://localhost:7700
MEILISEARCH_KEY=la_tua_master_key

Per avviare Meilisearch tramite Docker Compose, è possibile usare la seguente configurazione:

# Servizio Meilisearch per Docker Compose
services:
  meilisearch:
    image: getmeili/meilisearch:latest
    ports:
      - "7700:7700"
    volumes:
      - meilisearch_data:/meili_data
    environment:
      MEILI_MASTER_KEY: la_tua_master_key

volumes:
  meilisearch_data:

Driver Typesense

Typesense è un altro motore di ricerca open source, veloce e tollerante agli errori tipografici. L'installazione del client richiede:

composer require typesense/typesense-php

Driver database e collection

Per applicazioni più semplici o in fase di sviluppo, Scout mette a disposizione due driver nativi che non richiedono servizi esterni. Il driver database utilizza le funzionalità LIKE di SQL per eseguire le ricerche direttamente sul database, mentre il driver collection filtra i modelli in memoria usando le collection di Laravel. Questi driver sono adatti per set di dati ridotti e non per ambienti di produzione con grandi volumi.

# Driver database per ambienti di sviluppo
SCOUT_DRIVER=database

Rendere un modello ricercabile

Per abilitare la ricerca full-text su un modello Eloquent, è sufficiente aggiungere il trait Searchable alla classe del modello. Scout si occuperà automaticamente di mantenere l'indice aggiornato ogni volta che il record viene creato, modificato o eliminato.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;

class Article extends Model
{
    // Abilitazione della ricerca full-text sul modello
    use Searchable;

    protected $fillable = [
        'title',
        'content',
        'author',
        'published_at',
    ];
}

Personalizzare i dati indicizzati

Per impostazione predefinita Scout serializza tutti gli attributi del modello e li invia all'indice. È possibile personalizzare i dati indicizzati sovrascrivendo il metodo toSearchableArray nel modello:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;

class Article extends Model
{
    use Searchable;

    protected $fillable = [
        'title',
        'content',
        'author',
        'category_id',
        'published_at',
        'is_published',
    ];

    /**
     * Definisce i campi da includere nell'indice di ricerca.
     */
    public function toSearchableArray(): array
    {
        return [
            // Solo i campi rilevanti per la ricerca
            'id'           => $this->id,
            'title'        => $this->title,
            'content'      => $this->content,
            'author'       => $this->author,
            'published_at' => $this->published_at?->toDateString(),
        ];
    }
}

Personalizzare il nome dell'indice

Per impostazione predefinita Scout utilizza il nome della tabella del modello come nome dell'indice. È possibile sovrascrivere questo comportamento tramite il metodo searchableAs:

/**
 * Restituisce il nome dell'indice di ricerca per il modello.
 */
public function searchableAs(): string
{
    // Prefisso personalizzato per separare gli indici per ambiente
    return 'articles_index';
}

Importare i dati esistenti

Quando si aggiunge Scout a un progetto già esistente con dati nel database, è necessario importare manualmente i record nell'indice tramite il comando Artisan dedicato:

# Importazione di tutti i record del modello Article nell'indice
php artisan scout:import "App\Models\Article"

Per rimuovere tutti i record di un modello dall'indice, si utilizza invece:

# Rimozione di tutti i record del modello Article dall'indice
php artisan scout:flush "App\Models\Article"

È anche possibile eliminare e ricreare completamente un indice:

# Eliminazione dell'indice
php artisan scout:delete-index articles_index

# Ricreazione dell'indice e reimportazione dei dati
php artisan scout:import "App\Models\Article"

Eseguire ricerche

Una volta configurato Scout e indicizzati i dati, è possibile eseguire ricerche full-text sui modelli tramite il metodo statico search. Scout restituisce un'istanza di Builder compatibile con Eloquent, che supporta la paginazione e altri metodi di query.

<?php

namespace App\Http\Controllers;

use App\Models\Article;
use Illuminate\Http\Request;

class SearchController extends Controller
{
    /**
     * Esegue la ricerca full-text sugli articoli.
     */
    public function search(Request $request)
    {
        // Ricerca semplice per termine
        $query = $request->input('query', '');

        $articles = Article::search($query)->get();

        return response()->json($articles);
    }
}

Per ottenere risultati paginati, si utilizza il metodo paginate, identico a quello di Eloquent:

// Ricerca con paginazione: 15 risultati per pagina
$articles = Article::search($query)->paginate(15);

return view('articles.index', compact('articles'));

Ricerca con vincoli aggiuntivi via Eloquent

È possibile combinare la ricerca Scout con i normali vincoli Eloquent tramite il metodo query. Questo permette di filtrare i risultati restituiti dall'indice applicando ulteriori condizioni a livello di database:

// Ricerca full-text combinata con filtri Eloquent aggiuntivi
$articles = Article::search($query)
    ->query(function ($builder) {
        // Filtra solo gli articoli pubblicati
        $builder->where('is_published', true)
                ->orderBy('published_at', 'desc');
    })
    ->paginate(10);

Filtraggio nativo dell'indice (where)

Oltre ai vincoli Eloquent, Scout mette a disposizione il metodo where per applicare filtri direttamente sull'indice di ricerca, senza passare dal database. Questa modalità è supportata dai driver Algolia, Meilisearch e Typesense:

// Filtro diretto sull'indice di ricerca
$articles = Article::search($query)
    ->where('author', 'Mario Rossi')
    ->get();

È possibile anche filtrare per valori multipli tramite il metodo whereIn:

// Filtro per più autori sull'indice
$articles = Article::search($query)
    ->whereIn('author', ['Mario Rossi', 'Luca Bianchi'])
    ->get();

Ordinamento dei risultati

Scout supporta l'ordinamento dei risultati tramite il metodo orderBy, che viene applicato direttamente sull'indice quando il driver lo supporta:

// Ordinamento per data di pubblicazione decrescente
$articles = Article::search($query)
    ->orderBy('published_at', 'desc')
    ->get();

Soft delete e ricerca

Se il modello utilizza il trait SoftDeletes, Scout è in grado di gestire automaticamente l'esclusione dei record eliminati dall'indice. Per abilitare questa funzionalità occorre impostare l'opzione soft_delete nella configurazione:

// In config/scout.php
'soft_delete' => true,

Con questa impostazione attiva, Scout mantiene i record eliminati nell'indice ma li filtra nelle ricerche normali. Per includere i record eliminati nei risultati si usa il metodo withTrashed:

// Ricerca che include i record con soft delete
$articles = Article::search($query)->withTrashed()->get();

// Ricerca che restituisce solo i record eliminati
$deletedArticles = Article::search($query)->onlyTrashed()->get();

Sospendere e riprendere l'indicizzazione

In alcuni scenari è necessario eseguire operazioni di massa sui modelli senza aggiornare l'indice di ricerca ad ogni modifica, per evitare un numero eccessivo di chiamate al servizio. Scout fornisce i metodi withoutSyncingToSearch e disableSearchSyncing per gestire questi casi:

// Esecuzione di operazioni in massa senza aggiornare l'indice
Article::withoutSyncingToSearch(function () {
    // Queste modifiche non verranno sincronizzate con Scout
    Article::where('is_published', false)->update([
        'content' => 'Contenuto aggiornato in blocco',
    ]);
});

In alternativa, è possibile usare i metodi di istanza sul singolo modello:

$article = Article::find(1);

// Disabilitazione della sincronizzazione per questa istanza
$article->disableSearchSyncing();
$article->update(['title' => 'Nuovo titolo']);

// Riabilitazione della sincronizzazione
$article->enableSearchSyncing();

Indicizzazione manuale

È possibile aggiungere o rimuovere manualmente un modello dall'indice tramite i metodi searchable e unsearchable:

$article = Article::find(1);

// Aggiunta manuale all'indice
$article->searchable();

// Rimozione manuale dall'indice
$article->unsearchable();

Questi metodi funzionano anche su collection di modelli:

// Aggiunta in blocco di più articoli all'indice
Article::where('is_published', true)->searchable();

// Rimozione in blocco dall'indice
Article::where('is_published', false)->unsearchable();

Utilizzo della coda per l'indicizzazione

Per evitare che le operazioni di indicizzazione rallentino le richieste HTTP, Scout supporta l'esecuzione asincrona tramite le code di Laravel. Per abilitare questa modalità è sufficiente impostare l'opzione queue nel file .env:

# Abilitazione della coda per Scout
SCOUT_QUEUE=true

Con la coda abilitata, tutte le operazioni di sincronizzazione con l'indice verranno inviate alla coda di Laravel e processate in background dal worker. È necessario assicurarsi che il worker sia in esecuzione:

# Avvio del worker per processare i job in coda
php artisan queue:work

Configurare le impostazioni degli indici Meilisearch

Meilisearch consente di configurare attributi filtrabili e ordinabili direttamente dal file di configurazione di Scout tramite la chiave index-settings. Questo è utile per ottimizzare le prestazioni dell'indice in base ai campi effettivamente utilizzati nelle ricerche:

// In config/scout.php, sezione meilisearch
'meilisearch' => [
    'host' => env('MEILISEARCH_HOST', 'http://localhost:7700'),
    'key'  => env('MEILISEARCH_KEY', null),
    'index-settings' => [
        // Impostazioni specifiche per l'indice degli articoli
        App\Models\Article::class => [
            'filterableAttributes' => ['author', 'is_published', 'published_at'],
            'sortableAttributes'   => ['published_at', 'title'],
        ],
    ],
],

Dopo aver modificato queste impostazioni, è necessario sincronizzarle con Meilisearch tramite il comando Artisan:

# Sincronizzazione delle impostazioni degli indici con Meilisearch
php artisan scout:sync-index-settings

Esempio completo: controller di ricerca

Di seguito è riportato un esempio completo di controller che integra la ricerca Scout con validazione dell'input e risposta JSON strutturata, adatto a essere usato come endpoint API:

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Article;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class ArticleSearchController extends Controller
{
    /**
     * Esegue la ricerca full-text sugli articoli pubblicati.
     */
    public function __invoke(Request $request): JsonResponse
    {
        // Validazione dei parametri di ricerca
        $validated = $request->validate([
            'query'    => ['required', 'string', 'min:2', 'max:100'],
            'per_page' => ['integer', 'min:5', 'max:50'],
            'author'   => ['nullable', 'string'],
        ]);

        $query   = $validated['query'];
        $perPage = $validated['per_page'] ?? 15;

        // Costruzione della ricerca con filtri opzionali
        $searchBuilder = Article::search($query)
            ->query(function ($builder) {
                // Limita la ricerca solo agli articoli pubblicati
                $builder->where('is_published', true)
                        ->orderBy('published_at', 'desc');
            });

        // Aggiunta del filtro per autore se specificato
        if (isset($validated['author'])) {
            $searchBuilder->where('author', $validated['author']);
        }

        $results = $searchBuilder->paginate($perPage);

        return response()->json([
            'data'  => $results->items(),
            'meta'  => [
                'current_page' => $results->currentPage(),
                'last_page'    => $results->lastPage(),
                'per_page'     => $results->perPage(),
                'total'        => $results->total(),
            ],
        ]);
    }
}

La route corrispondente da registrare in routes/api.php:

// Registrazione della route di ricerca API
use App\Http\Controllers\Api\ArticleSearchController;

Route::get('/articles/search', ArticleSearchController::class);

Conclusioni

Laravel Scout rappresenta una soluzione elegante e flessibile per integrare la ricerca full-text nelle applicazioni Laravel, senza dover scrivere da zero la logica di indicizzazione e sincronizzazione. Grazie all'astrazione dei driver, è possibile scegliere il motore di ricerca più adatto alle proprie esigenze, passando da soluzioni cloud come Algolia a soluzioni self-hosted come Meilisearch, fino ai driver nativi per lo sviluppo locale.

La profonda integrazione con Eloquent rende Scout particolarmente intuitivo per chi è già familiare con l'ORM di Laravel: la logica di ricerca si affianca naturalmente alle query esistenti, riducendo al minimo la quantità di codice necessaria per offrire un'esperienza di ricerca efficace agli utenti.