La ricerca full-text di PostgreSQL può coprire molte applicazioni. Usa una regola decisionale semplice, una query di partenza e una checklist di indicizzazione per sapere quando aggiungere un motore di ricerca.

La maggior parte delle persone non chiede “full-text search”. Chiedono una casella di ricerca che sia veloce e trovi ciò che intendevano nella prima pagina. Se i risultati sono lenti, rumorosi o ordinati in modo strano, agli utenti non importa se usi PostgreSQL full-text search o un motore separato. Smettono semplicemente di fidarsi della ricerca.
La decisione è una: mantenere la ricerca dentro Postgres o aggiungere un motore dedicato. L'obiettivo non è una rilevanza perfetta, ma una base solida che si lanci rapidamente, sia facile da gestire e sia adeguata all'uso reale della tua app.
Per molte app, PostgreSQL full-text search è sufficiente per molto tempo. Se hai pochi campi di testo (titolo, descrizione, note), un ranking di base e uno o due filtri (stato, categoria, tenant), Postgres può gestirlo senza infrastrutture aggiuntive. Avrai meno parti in movimento, backup più semplici e meno incidenti del tipo “perché la ricerca è giù ma l'app funziona?”.
“Abbastanza” solitamente significa poter centrare tre obiettivi contemporaneamente:
Un esempio concreto: una dashboard SaaS dove gli utenti cercano progetti per nome e note. Se una query come “onboarding checklist” restituisce il progetto giusto nella top 5, in meno di un secondo, e non stai costantemente tarando analyzer o reindicizzando, quello è “abbastanza”. Quando non riesci a raggiungere questi obiettivi senza aggiungere complessità, allora la scelta “ricerca integrata vs motore di ricerca” diventa reale.
I team spesso descrivono la ricerca in termini di funzionalità, non di risultati. È utile tradurre ogni funzionalità in quanto costa costruirla, tararla e mantenerla affidabile.
Le richieste iniziali solitamente suonano così: tolleranza agli errori, facet e filtri, evidenziazioni, ranking “intelligente” e autocomplete. Per una prima versione, separa i must-have dai nice-to-have. Una casella di ricerca di base di solito ha bisogno solo di trovare elementi rilevanti, gestire forme di parola comuni (plurale, tempi verbali), rispettare filtri semplici e restare veloce con la crescita della tabella. È esattamente qui che PostgreSQL full-text search tende a essere adatto.
Postgres brilla quando i tuoi contenuti vivono in normali campi di testo e vuoi la ricerca vicino ai dati: articoli di help, post del blog, ticket di supporto, documentazione interna, titoli e descrizioni di prodotto o note sui record cliente. Sono per lo più problemi del tipo “trova il record giusto”, non “costruisci un prodotto di ricerca”.
I nice-to-have sono dove si insinua la complessità. Tolleranza agli errori e autocomplete ricco di solito ti spingono verso tool aggiuntivi. I facet sono possibili in Postgres, ma se vuoi molti facet, analytics profondi e conteggi istantanei su dataset enormi, un motore dedicato diventa più attraente.
Il costo nascosto raramente è la licenza. È il secondo sistema. Una volta aggiunto un motore di ricerca, devi anche gestire sincronizzazione e backfill dei dati (e i bug che ne derivano), monitoraggio e aggiornamenti, il lavoro di supporto su “perché la ricerca mostra dati vecchi?” e due insiemi di controlli per la rilevanza.
Se non sei sicuro, inizia con Postgres, rilascia qualcosa di semplice e aggiungi un altro motore solo quando un requisito chiaro non può essere soddisfatto.
Usa una regola a tre controlli. Se passi tutti e tre, resta con PostgreSQL full-text search. Se ne falli uno in modo marcato, considera un motore dedicato.
Requisiti di rilevanza: sono accettabili risultati “abbastanza buoni” o ti serve un ordinamento quasi perfetto su molti edge case (errori di battitura, sinonimi, “altri utenti hanno cercato”, risultati personalizzati)? Se puoi tollerare un ordinamento occasionalmente imperfetto, Postgres di solito funziona.
Volume di query e latenza: quante ricerche al secondo ti aspetti in picco e qual è il budget di latenza reale? Se la ricerca è una porzione piccola del traffico e puoi mantenere le query rapide con indici adeguati, Postgres va bene. Se la ricerca diventa un workload principale e compete con letture e scritture core, è un campanello d'allarme.
Complessità: stai cercando uno o due campi di testo, o stai combinando molti segnali (tag, filtri, decadimento temporale, popolarità, permessi) e più lingue? Più la logica è complessa, più sentirai attrito dentro SQL.
Un punto di partenza sicuro è semplice: rilascia una baseline in Postgres, registra le query lente e le ricerche “nessun risultato”, e solo dopo decidi. Molte app non lo superano mai e eviti di gestire e sincronizzare un secondo sistema troppo presto.
Segnali d'allarme che di solito puntano a un motore dedicato:
Segnali verdi per restare in Postgres:
PostgreSQL full-text search è un modo integrato per trasformare il testo in qualcosa che il database può cercare velocemente, senza scansionare ogni riga. Funziona meglio quando i tuoi contenuti sono già in Postgres e vuoi una ricerca rapida e decente con operazioni prevedibili.
Ci sono tre pezzi che vale la pena conoscere:
ts_rank (o ts_rank_cd) per mettere prima le righe più rilevanti.La configurazione della lingua conta perché cambia come Postgres tratta le parole. Con la configurazione giusta, “running” e “run” possono corrispondere (stemming) e le parole poco informative vengono ignorate (stop words). Con la configurazione sbagliata, la ricerca può sembrare rotta perché il linguaggio dell'utente non corrisponde a ciò che è stato indicizzato.
Il prefix matching è la funzionalità che molti cercano per un comportamento “typeahead”, come far corrispondere “dev” a “developer”. In Postgres FTS questo si fa tipicamente con l'operatore di prefisso (ad esempio term:*). Migliora la qualità percepita, ma spesso aumenta il lavoro per query, quindi trattalo come upgrade opzionale, non come default.
Cosa Postgres non vuole essere: una piattaforma di ricerca completa con ogni funzione. Se ti servono correzione fuzzy della grafia, autocomplete avanzato, learning-to-rank, analyzer complessi per campo o indicizzazione distribuita su molti nodi, sei fuori dalla zona di comfort integrata. Per molte app, però, PostgreSQL full-text search offre la maggior parte di ciò che gli utenti si aspettano con molte meno parti mobili.
Ecco una forma piccola e realistica per contenuti che vuoi cercare:
-- Minimal example table
CREATE TABLE articles (
id bigserial PRIMARY KEY,
title text NOT NULL,
body text NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now()
);
Una buona baseline per PostgreSQL full-text search è: costruire una query dall'input utente, filtrare le righe prima (quando puoi), poi fare il ranking delle corrispondenze rimaste.
-- $1 = user search text, $2 = limit, $3 = offset
WITH q AS (
SELECT websearch_to_tsquery('english', $1) AS query
)
SELECT
a.id,
a.title,
a.updated_at,
ts_rank_cd(
setweight(to_tsvector('english', coalesce(a.title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(a.body, '')), 'B'),
q.query
) AS rank
FROM articles a
CROSS JOIN q
WHERE
a.updated_at >= now() - interval '2 years' -- example safe filter
AND (
setweight(to_tsvector('english', coalesce(a.title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(a.body, '')), 'B')
) @@ q.query
ORDER BY rank DESC, a.updated_at DESC, a.id DESC
LIMIT $2 OFFSET $3;
Alcuni dettagli che fanno risparmiare tempo più avanti:
WHERE prima del ranking (status, tenant_id, range di date). Classifichi meno righe, quindi resta veloce.ORDER BY (come updated_at, poi id). Questo mantiene la paginazione stabile quando molti risultati hanno lo stesso rank.websearch_to_tsquery per l'input utente. Gestisce virgolette e operatori semplici come gli utenti si aspettano.Una volta che questa baseline funziona, sposta l'espressione to_tsvector(...) in una colonna memorizzata. Questo evita di ricalcolarla a ogni query e rende l'indicizzazione più semplice.
La maggior parte delle storie “PostgreSQL full-text search è lenta” si riduce a una cosa: il DB costruisce il documento di ricerca a ogni query. Correggi prima questo memorizzando un tsvector precompilato e indicizzandolo.
tsvector: colonna generata o trigger?Una colonna generata è l'opzione più semplice quando il documento di ricerca è costruito da colonne nella stessa riga. Rimane corretta automaticamente ed è difficile da dimenticare durante gli aggiornamenti.
Usa un tsvector mantenuto da trigger quando il documento dipende da tabelle correlate (ad esempio combinando una riga prodotto con il nome della sua categoria), o quando vuoi logica personalizzata non facile da esprimere come una singola espressione generata. I trigger aggiungono parti in movimento, quindi tienili piccoli e testali.
Crea un indice GIN sulla colonna tsvector. È la baseline che rende PostgreSQL full-text search istantanea per la ricerca tipica nelle app.
Un setup che funziona per molte app:
tsvector nella stessa tabella delle righe che cerchi più spesso.tsvector.@@ contro il tsvector memorizzato, non to_tsvector(...) calcolato al volo.VACUUM (ANALYZE) dopo grandi backfill così il planner capisce il nuovo indice.Mantenere il vettore nella stessa tabella è di solito più veloce e semplice. Una tabella separata può avere senso se la tabella base è molto scritta, o se stai indicizzando un documento combinato che copre molte tabelle e vuoi aggiornamenti a tuo ritmo.
Gli indici parziali aiutano quando cerchi solo un sottoinsieme di righe, come status = 'active', un singolo tenant in un'app multi-tenant o una lingua specifica. Ridurranno la dimensione dell'indice e possono velocizzare le ricerche, ma solo se le tue query includono sempre lo stesso filtro.
Puoi ottenere risultati sorprendentemente buoni con PostgreSQL full-text search se mantieni le regole di rilevanza semplici e prevedibili.
La vittoria più semplice è il weighting per campo: le corrispondenze nel titolo devono pesare più di quelle nel corpo. Costruisci un tsvector combinato dove il titolo è pesato più della descrizione e poi ordina con ts_rank o ts_rank_cd.
Se vuoi che elementi “freschi” o “popolari” galleggino verso l'alto, falla con attenzione. Un piccolo bonus va bene, ma non lasciare che sovrasti la rilevanza testuale. Un pattern pratico è: ordina prima per testo, poi rompi i pareggi con la recenza, o aggiungi un bonus capato così un elemento nuovo ma irrilevante non batte una corrispondenza perfetta più vecchia.
Sinonimi e matching per frase sono dove le aspettative divergono spesso. I sinonimi non sono automatici; li ottieni solo aggiungendo un thesaurus o un dizionario personalizzato, o espandendo le query manualmente (ad esempio trattando “auth” come “authentication”). Il matching per frasi non è predefinito: le query semplici matchano parole ovunque, non una “esatta frase”. Se gli utenti digitano frasi citate o domande lunghe, considera phraseto_tsquery o websearch_to_tsquery per meglio riflettere come cercano.
Contenuti in lingue miste richiedono una decisione. Se conosci la lingua per documento, memorizzala e genera il tsvector con la configurazione giusta (English, Russian, ecc.). Se non la conosci, una fallback sicura è indicizzare con la configurazione simple (nessuno stemming), o mantenere due vettori: uno specifico per lingua quando noto e uno simple per tutto.
Per validare la rilevanza, mantieni il processo piccolo e concreto:
Questo di solito è abbastanza per la ricerca nelle caselle di app come “template”, “docs” o “projects”.
La maggior parte delle storie “PostgreSQL full-text search è lento o irrilevante” deriva da pochi errori evitabili. Correggerli è spesso più semplice che aggiungere un nuovo sistema di ricerca.
Una trappola comune è trattare tsvector come un valore calcolato che resta corretto da solo. Se memorizzi tsvector in una colonna ma non lo aggiorni a ogni insert/update, i risultati sembreranno casuali perché l'indice non corrisponde più al testo. Se calcoli to_tsvector(...) al volo nella query, i risultati possono essere corretti ma lenti, e potresti perdere il vantaggio di un indice dedicato.
Un altro modo semplice per peggiorare le prestazioni è fare il ranking prima di restringere il set di candidati. ts_rank è utile, ma dovrebbe generalmente eseguire dopo che Postgres ha usato l'indice per trovare le righe corrispondenti. Se calcoli il rank su una grande porzione della tabella (o fai join ad altre tabelle prima), puoi trasformare una ricerca veloce in una scansione completa.
La gente si aspetta anche che “contains” si comporti come LIKE '%term%'. Le wildcard iniziali non si adattano bene all'FTS perché l'FTS è basato su parole (lemmi), non su sottostringhe arbitrarie. Se ti serve substring search per codici prodotto o ID parziali, usa uno strumento diverso per quel caso (ad esempio indicizzazione trigram) invece di incolpare l'FTS.
I problemi di performance spesso derivano dalla gestione dei risultati, non dal matching. Due pattern da tenere d'occhio:
OFFSET, che costringe Postgres a saltare sempre più righe man mano che vai avanti.Le questioni operative contano anche. L'bloat degli indici può accumularsi dopo molti aggiornamenti e il reindexing può essere costoso se aspetti fino a che la situazione è dolorosa. Misura i tempi reali delle query (e guarda EXPLAIN ANALYZE) prima e dopo le modifiche. Senza numeri, è facile “fixare” PostgreSQL full-text search peggiorando qualcos'altro.
Prima di incolpare PostgreSQL full-text search, esegui questi controlli. La maggior parte dei bug “Postgres search è lento o irrilevante” nasce da basi mancanti, non dalla funzionalità stessa.
Costruisci un vero tsvector: memorizzalo in una colonna generata o mantenuta, usa la config lingua giusta (english, simple, ecc.) e applica pesi se mescoli campi (title > subtitle > body).
Normalizza ciò che indicizzi: tieni fuori dal tsvector i campi rumorosi (ID, boilerplate, testo di navigazione) e tronca blob enormi se gli utenti non li cercano.
Crea l'indice giusto: aggiungi un indice GIN sulla colonna tsvector e conferma che venga usato in EXPLAIN. Se solo un sottoinsieme è ricercabile (es. status = 'published'), un indice parziale può ridurre dimensione e velocizzare le letture.
Mantieni le tabelle sane: tuple morte possono rallentare gli index scan. Il vacuuming regolare è importante, specialmente sui contenuti aggiornati frequentemente.
Pianifica il reindex: migrazioni grandi o indici gonfi a volte richiedono una finestra di reindex controllata.
Una volta che dati e indice sono corretti, concentrati sulla forma della query. PostgreSQL full-text search è veloce quando può restringere presto il set di candidati.
Filtra prima, poi classifica: applica filtri stretti (tenant, lingua, pubblicato, categoria) prima del ranking. Classificare migliaia di righe che poi scarterai è lavoro sprecato.
Usa ordinamento stabile: ordina per rank e poi un tie-breaker come updated_at o id così i risultati non saltano tra aggiornamenti.
Evita “la query fa tutto”: se ti serve fuzzy matching o tolleranza agli errori, fallo intenzionalmente (e misura). Non forzare scansioni sequenziali per sbaglio.
Testa query reali: raccogli le top 20 ricerche, controlla la rilevanza a mano e tieni una piccola lista di risultati attesi per catturare regressioni.
Controlla i percorsi lenti: registra query lente, analizza EXPLAIN (ANALYZE, BUFFERS) e monitora dimensione dell'indice e hit rate della cache così puoi vedere quando la crescita cambia il comportamento.
Un help center SaaS è un buon punto di partenza perché l'obiettivo è semplice: aiutare le persone a trovare l'articolo che risponde alla loro domanda. Hai poche migliaia di articoli, ciascuno con titolo, sommario e corpo. La maggior parte dei visitatori digita 2–5 parole come “reset password” o “fattura di pagamento”.
Con PostgreSQL full-text search questo si può risolvere sorprendentemente in fretta. Memorizzi un tsvector per i campi combinati, aggiungi un indice GIN e ordini per rilevanza. Il successo è: risultati in meno di 100 ms, le prime 3 posizioni di solito corrette e niente babysitting continuo.
Poi il prodotto cresce. Il supporto vuole filtri per area prodotto, piattaforma (web, iOS, Android) e piano (free, pro, business). I redattori vogliono sinonimi, “volevi dire” e migliore gestione degli errori di battitura. Il marketing vuole analytics come “principali ricerche senza risultati”. Il traffico cresce e la ricerca diventa uno degli endpoint più usati.
Questi sono segnali che un motore dedicato potrebbe valere il costo:
Un percorso pratico di migrazione è mantenere Postgres come source of truth, anche dopo aver aggiunto un motore. Inizia registrando query di ricerca e casi “nessun risultato”, poi esegui un job di sync asincrono che copia solo i campi ricercabili nel nuovo indice. Esegui entrambi in parallelo per un po' e passa gradualmente, invece di scommettere tutto il primo giorno.
Se la tua ricerca è per lo più “trova documenti che contengono queste parole” e il dataset non è massiccio, PostgreSQL full-text search di solito è sufficiente. Parti da lì, falla funzionare e aggiungi un motore dedicato solo quando puoi nominare la funzionalità mancante o il problema di scalabilità.
Un riepilogo utile:
tsvector, aggiungere un indice GIN e i tuoi bisogni di ranking sono di base.Un passo pratico: implementa la query starter e l'indice mostrati prima, poi registra poche metriche semplici per una settimana. Monitora p95 della query, query lente e un segnale di successo come “search -> click -> nessun bounce immediato” (anche un semplice contatore di eventi aiuta). Vedrai rapidamente se serve migliorare il ranking o solo l'UX (filtri, evidenziazioni, snippet migliori).
Inizia a pianificare un motore dedicato quando una di queste diventa un requisito reale (non un nice-to-have): autocomplete forte o ricerca istantanea ad ogni battuta a scala, forte tolleranza typo e correzione ortografica, facet e aggregazioni su molti campi con conteggi rapidi, tooling di rilevanza avanzato (set di sinonimi, learning-to-rank, boost per query) o carico sostenuto e indici grandi difficili da mantenere veloci.
Se vuoi muoverti in fretta sul lato app, Koder.ai (koder.ai) può essere un modo utile per prototipare l'interfaccia e l'API di ricerca via chat, poi iterare in sicurezza usando snapshot e rollback mentre misuri il comportamento reale degli utenti.
PostgreSQL full-text search è “sufficiente” quando riesci a colpire tre obiettivi contemporaneamente:
Se puoi ottenere tutto questo con un tsvector memorizzato e un indice GIN, di solito sei in una buona posizione.
Di norma parti con PostgreSQL full-text search. Si rilascia più in fretta, mantiene dati e ricerca nello stesso posto ed evita di costruire e mantenere una pipeline di indicizzazione separata.
Passa a un motore dedicato quando hai un requisito chiaro che Postgres non può soddisfare bene (tolleranza avanzata agli errori di battitura, autocomplete ricco, facet pesanti o traffico di ricerca che compete con il DB principale).
Una regola semplice: resta in Postgres se superi questi tre controlli:
Se falli gravemente uno di questi (soprattutto requisiti come typo/autocomplete o traffico elevato), considera un motore dedicato.
Usa Postgres FTS quando la ricerca è principalmente “trova il record giusto” su pochi campi come titolo/testo/note, con filtri semplici (tenant, stato, categoria).
È adatto a help center, documentazione interna, ticket, ricerca di articoli/blog e dashboard SaaS dove gli utenti cercano per nomi di progetto o note.
Una buona query di base di solito:
websearch_to_tsquery.Memorizza un tsvector precompilato e aggiungi un indice GIN. Così eviti di ricalcolare to_tsvector(...) a ogni richiesta.
Setup pratico:
Usa una colonna generata quando il documento di ricerca è costruito da colonne della stessa riga (semplice e difficile da rompere).
Usa una colonna mantenuta da trigger quando il testo di ricerca dipende da tabelle correlate o logica personalizzata.
Scelta predefinita: colonna generata prima, trigger solo quando serve composizione cross-table.
Inizia con regole di rilevanza prevedibili:
Valida con una piccola lista di query reali e risultati attesi.
FTS è basato sulle parole, non sulle sottostringhe. Non si comporta come LIKE '%term%' per ricerche con wildcard iniziale.
Se ti servono ricerche per sottostringhe (codici prodotto, frammenti), gestiscile separatamente (ad esempio con indice trigram) invece di forzare l'FTS.
Segnali comuni che hai superato Postgres FTS:
Un percorso pratico è mantenere Postgres come source of truth e aggiungere l'indicizzazione asincrona quando il requisito è chiaro.
@@ contro un tsvector memorizzato.ts_rank/ts_rank_cd con un tie-breaker stabile come updated_at, id.Questo mantiene i risultati rilevanti, veloci e stabili per la paginazione.
tsvectortsvector_column @@ tsquery.Questa è la soluzione più comune quando la ricerca sembra lenta.