RAG con PostgreSQL e pgvector per applicazioni Laravel: guida pratica

RAG con PostgreSQL e pgvector per applicazioni Laravel: guida pratica

La ricerca testuale tradizionale - WHERE nome LIKE '%valvola%' - funziona quando l'utente conosce esattamente il nome del prodotto nel catalogo. Ma nel mondo reale, l'utente del portale B2B di un cliente del settore distribuzione industriale cerca "raccordo per tubo da 2 pollici in acciaio inox" e il catalogo ha il prodotto con nome "FITTING AISI 316 DN50" - due descrizioni dello stesso prodotto che condividono zero parole ma significano la stessa cosa. La ricerca testuale restituisce zero risultati. La ricerca semantica con embedding vettoriali restituisce quel prodotto in prima posizione, perché confronta il significato della query con il significato della descrizione, non le parole letterali.

A settembre 2025 ho implementato un sistema di ricerca semantica RAG (Retrieval-Augmented Generation) per il catalogo prodotti di quel cliente - 50.000 articoli con descrizioni tecniche, specifiche e nomi commerciali in un mix di italiano, inglese e abbreviazioni di settore. Lo stack: PostgreSQL con pgvector come database vettoriale (senza aggiungere un servizio separato come Pinecone o Weaviate), l'API di embedding di OpenAI per generare i vettori dalle descrizioni dei prodotti, e Laravel come applicazione che orchestra il flusso. Il risultato: gli utenti del portale trovano i prodotti cercando in linguaggio naturale, con terminologia diversa dal catalogo, in italiano o in inglese, e la precisione della ricerca è passata dal 45% (ricerca testuale LIKE) all'89% (ricerca semantica con pgvector) misurata su un set di 200 query di test validate manualmente dal team commerciale del cliente.

Come funziona RAG e perché pgvector è sufficiente per la maggior parte dei casi d'uso?

RAG è un pattern architetturale che combina la ricerca (Retrieval) con la generazione (Generation): quando l'utente fa una domanda o una ricerca, il sistema cerca i documenti più rilevanti nel database vettoriale e li passa come contesto all'LLM, che genera una risposta basata su quei documenti specifici - non sulla sua conoscenza generale. Per il catalogo prodotti, il flusso è: l'utente cerca "raccordo inox 2 pollici", il sistema converte la query in un vettore di embedding, cerca i vettori più simili nel database (i prodotti con descrizione semanticamente più vicina alla query), e restituisce i prodotti ordinati per similarità. L'LLM in questo caso non è necessario per la ricerca - serve solo se vuoi generare una risposta in linguaggio naturale ("Ecco i raccordi in acciaio inox disponibili nel diametro DN50...") invece di mostrare una lista di risultati.

pgvector è un'estensione di PostgreSQL che aggiunge il tipo di dato vector e gli operatori di similarità (coseno, distanza euclidea, prodotto interno) con supporto per indici HNSW e IVFFlat per ricerche efficienti su milioni di vettori. Il vantaggio rispetto a database vettoriali dedicati (Pinecone, Weaviate, Milvus, Qdrant) è che non serve un servizio aggiuntivo: pgvector vive dentro PostgreSQL, usa lo stesso backup, lo stesso monitoring, la stessa connessione del database applicativo. Per un catalogo da 50.000 prodotti con embedding a 1536 dimensioni (il formato di OpenAI text-embedding-3-small), il consumo aggiuntivo di storage è di circa 300 MB - trascurabile. La latenza di ricerca con indice HNSW è di 5-15 ms per una query su 50.000 vettori - più che sufficiente per una ricerca interattiva.

Nel mio profilo professionale trovi il dettaglio dell'esperienza che porto nell'integrazione AI con database relazionali - pgvector è uno dei casi dove la scelta dello strumento giusto elimina un'intera categoria di complessità operativa (il servizio vettoriale separato) senza sacrificare le prestazioni.

Implementazione: dalla generazione degli embedding alla ricerca in Laravel

L'implementazione si articola in tre passaggi: installazione di pgvector, generazione degli embedding per i prodotti del catalogo, e integrazione della ricerca semantica nell'applicazione Laravel.

L'installazione di pgvector su PostgreSQL 16+ è un singolo comando:

-- Abilita l'estensione pgvector
CREATE EXTENSION IF NOT EXISTS vector;

-- Aggiungi la colonna embedding alla tabella prodotti
ALTER TABLE prodotti ADD COLUMN embedding vector(1536);

-- Crea l'indice HNSW per ricerche efficienti
CREATE INDEX idx_prodotti_embedding ON prodotti
    USING hnsw (embedding vector_cosine_ops)
    WITH (m = 16, ef_construction = 200);

La generazione degli embedding è un job Laravel batch che processa tutti i prodotti del catalogo, inviando il testo descrittivo all'API di embedding e salvando il vettore risultante nel database:

// Job di generazione embedding per i prodotti del catalogo
class GenerateProductEmbeddings implements ShouldQueue
{
    public function handle(): void
    {
        Prodotto::whereNull('embedding')
            ->chunk(100, function ($prodotti) {
                foreach ($prodotti as $prodotto) {
                    // Componi il testo da vettorializzare
                    $testo = implode(' ', [
                        $prodotto->nome,
                        $prodotto->descrizione,
                        $prodotto->specifiche_tecniche,
                        $prodotto->categoria->nome,
                    ]);

                    // Genera l'embedding via API OpenAI
                    $response = Http::withToken(config('openai.api_key'))
                        ->post('https://api.openai.com/v1/embeddings', [
                            'model' => 'text-embedding-3-small',
                            'input' => $testo,
                        ]);

                    $embedding = $response->json('data.0.embedding');

                    // Salva il vettore in PostgreSQL con pgvector
                    $prodotto->update([
                        'embedding' => json_encode($embedding),
                    ]);
                }
            });
    }
}

La ricerca semantica nell'applicazione Laravel è una query PostgreSQL con l'operatore di similarità coseno (<=>) che confronta il vettore della query con i vettori dei prodotti:

// Service di ricerca semantica con pgvector
class SemanticSearchService
{
    public function search(string $query, int $limite = 20): Collection
    {
        // Genera l'embedding della query dell'utente
        $queryEmbedding = $this->generateEmbedding($query);

        // Ricerca per similarità coseno in PostgreSQL
        return Prodotto::select('prodotti.*')
            ->selectRaw('1 - (embedding <=> ?) AS similarita', [
                json_encode($queryEmbedding),
            ])
            ->whereNotNull('embedding')
            ->orderByRaw('embedding <=> ?', [json_encode($queryEmbedding)])
            ->limit($limite)
            ->get();
    }
}

L'operatore <=> calcola la distanza coseno tra due vettori - un valore tra 0 (identici) e 2 (opposti). L'indice HNSW rende questa operazione efficiente anche su grandi dataset: la ricerca su 50.000 vettori a 1536 dimensioni impiega 8-12 ms con l'indice HNSW, contro 200+ ms senza indice (scan lineare). Per cataloghi fino a 500.000 prodotti, un singolo server PostgreSQL con pgvector è sufficiente; oltre quella soglia, servono partitioning o un database vettoriale dedicato.

Misurare la qualità della ricerca semantica: dal "sembra funzionare" ai numeri

Un aspetto che distingue un'implementazione RAG professionale da un prototipo è la misurazione oggettiva della qualità dei risultati. "La ricerca sembra funzionare meglio" non è una metrica - è una sensazione. Per validare l'implementazione del catalogo del cliente, ho costruito un test set di 200 query con i risultati attesi, validati dal team commerciale che conosce il catalogo. Ogni query ha una lista di prodotti "corretti" che dovrebbero apparire nei primi 5 risultati. La metrica che uso è il Precision@5 - la percentuale di query dove il prodotto corretto appare nei primi 5 risultati - e il Mean Reciprocal Rank - la posizione media del primo risultato corretto.

I risultati confrontati tra ricerca testuale e ricerca semantica sul test set di 200 query sono stati: Precision@5 del 45% per la ricerca testuale LIKE, 72% per la full-text search PostgreSQL con tsvector, e 89% per la ricerca semantica con pgvector. Il MRR è passato da 0,38 (testuale) a 0,61 (full-text) a 0,84 (semantica) - il che significa che nella ricerca semantica il prodotto corretto appare in media come primo o secondo risultato, mentre nella ricerca testuale appare in media come terzo o quarto (quando appare).

Un pattern che ho scoperto durante la validazione è che la ricerca semantica eccelle particolarmente nelle query con vocabulary mismatch - quando l'utente usa termini diversi dal catalogo. "Guarnizione per tenuta idraulica" trova "O-Ring NBR 70 Shore A" perché l'embedding cattura la relazione semantica tra "guarnizione per tenuta" e "O-Ring" che la ricerca testuale non può vedere. Ma la ricerca semantica fallisce su query con codici prodotto esatti: cercare "ABC-001-25" con la ricerca semantica può restituire prodotti con codici simili ma diversi, perché l'embedding non è progettato per il match esatto di stringhe alfanumeriche. La soluzione è un approccio ibrido: prima cerca con match esatto sul codice prodotto, e se non trova risultati, fallback sulla ricerca semantica. Questo approccio ibrido porta il Precision@5 al 94% - il meglio di entrambi i mondi.

Un altro aspetto della misurazione è la rilevazione dei falsi positivi convincenti: risultati che il sistema restituisce con alta similarità ma che sono semanticamente sbagliati. Per esempio, "valvola di sicurezza per caldaia" può restituire "valvola a sfera DN25" con similarità 0,82 - un valore alto che suggerisce rilevanza, ma il prodotto è completamente diverso. Questi falsi positivi sono più insidiosi di quelli della ricerca testuale perché sembrano corretti (il punteggio di similarità è alto) e l'utente potrebbe ordinare il prodotto sbagliato. La mitigazione è una soglia minima di similarità (nel mio sistema, scarto risultati con similarità sotto 0,70) combinata con il filtraggio per categoria quando l'utente ha già selezionato una categoria dal menu - due accorgimenti che riducono i falsi positivi dal 15% al 3%.

Costi dell'API di embedding e strategie di ottimizzazione

Il costo operativo del sistema RAG dipende quasi interamente dalla generazione degli embedding. L'API text-embedding-3-small di OpenAI costa 0,02 dollari per milione di token - per un catalogo di 50.000 prodotti con descrizioni medie di 200 token, il costo della generazione iniziale è di circa 0,20 dollari. Un costo irrisorio. Il costo ricorrente dipende dalla frequenza di aggiornamento: se rigeneri gli embedding solo quando un prodotto viene modificato (la scelta corretta), il costo mensile per un catalogo con il 5% di aggiornamenti (2.500 prodotti) è di circa 0,01 dollari - praticamente gratuito.

Il costo delle query di ricerca è più rilevante: ogni query dell'utente richiede una chiamata all'API di embedding per vettorializzare il testo di ricerca. Con 10.000 query al giorno e una media di 20 token per query, il costo giornaliero è di 0,004 dollari - ancora trascurabile. Ma se il sistema viene usato anche per il chatbot con RAG (dove ogni conversazione genera 5-10 query di retrieval), il volume cresce e il costo può diventare significativo per traffico alto. La soluzione è il caching degli embedding delle query più frequenti in Redis: le prime 1.000 query più comuni (che coprono il 60-70% del traffico) vengono cachate con TTL di 24 ore, eliminando il 60-70% delle chiamate all'API di embedding.

Un'alternativa all'API di OpenAI è l'uso di modelli di embedding locali con Ollama - modelli come nomic-embed-text o mxbai-embed-large che girano in locale senza costi per chiamata. Il trade-off è la qualità: i modelli locali hanno tipicamente dimensioni di embedding inferiori (768 dimensioni contro 1536) e una qualità di embedding leggermente inferiore per lingue non-inglesi. Per il catalogo del cliente (con terminologia tecnica in italiano e inglese), ho testato entrambi e l'API OpenAI ha prodotto risultati migliori del 12% nella precisione della ricerca - un margine sufficiente per giustificare il costo (praticamente nullo) dell'API. Ho descritto l'uso di Ollama per l'inferenza locale nel contesto della strategia di LLM privati e proprietari che adotto per i clienti con requisiti di privacy dei dati.

Il sistema RAG con pgvector è in produzione da 8 mesi e il feedback del team commerciale del cliente è che "la ricerca del portale finalmente funziona" - una frase che sintetizza il valore della ricerca semantica meglio di qualsiasi metrica tecnica. Gli utenti trovano i prodotti anche quando usano terminologia diversa dal catalogo, anche quando sbagliano il nome commerciale, e anche quando cercano per caratteristiche tecniche invece che per nome. Il tasso di ricerche senza risultati è sceso dal 35% (ricerca testuale) al 4% (ricerca semantica) - un miglioramento che si traduce direttamente in ordini che prima venivano persi perché l'utente non trovava il prodotto e rinunciava. Se hai un catalogo prodotti o una knowledge base aziendale e vuoi implementare la ricerca semantica senza aggiungere infrastruttura, contattami per una sessione di implementazione: in due giornate installiamo pgvector, generiamo gli embedding, e integriamo la ricerca semantica nell'applicazione Laravel con caching, ottimizzazione dei costi e un test set di validazione per misurare oggettivamente la precisione della ricerca prima e dopo l'implementazione.

Ultima modifica: