Guida alla Ricerca Testuale su Postgres 2026
Gli strumenti di ricerca già nel tuo database e quando ognuno si ripaga.
La maggior parte dei team usa un solo strumento di ricerca su Postgres. I team che li conoscono tutti e tre distribuiscono ricerche migliori con meno complessità — ed evitano la costosa deviazione verso un servizio di ricerca dedicato di cui non avevano ancora bisogno.
Questa guida copre l’intero set di opzioni native di Postgres: cosa fa ognuna, quando è la scelta giusta e come combinarle.
I Tre Strumenti
La ricerca full-text (tsvector / indice GIN) è lessicale. Tokenizza il testo in lessemi, li riduce alla radice (stemming) e confronta le query con l’indice. “Running” e “runs” collassano nello stesso lessema. Lo stesso vale per “dog” e “dogs.” La funzione di ranking (ts_rank) premia i documenti in cui i termini della query compaiono spesso o in posizioni prominenti.
I trigrammi (pg_trgm) spezzano le stringhe in fette sovrapposte di 3 caratteri e misurano quante fette due stringhe hanno in comune. “Dan” → " da", "dan", "an ". “Micheal” e “Michael” condividono la maggior parte dei loro trigrammi, quindi la similarità è alta. Questo rende pg_trgm eccellente per la ricerca fuzzy di nomi, la tolleranza agli errori di battitura e l’autocompletamento — lo spazio in cui FTS performa male.
Gli indici exact-match (B-tree, hash) gestiscono chiavi primarie, indirizzi email, ID, SKU e tutto ciò dove la risposta è binaria: corrisponde o non corrisponde. Questi non sembrano “ricerca,” ma appartengono a questa conversazione perché il pattern peggiore è usare ricerca fuzzy o semantica per problemi che hanno risposte corrette.
La scelta non riguarda la sofisticazione. Riguarda l’abbinamento dello strumento alla forma della query.
Quando la Ricerca Full-Text Vince
Ricerca di parole chiave nella prosa. Post di blog, documentazione, descrizioni di prodotti, ticket di supporto, documenti legali. FTS è progettato per questa forma di contenuto: recupero indicizzato e classificato su testo in linguaggio naturale.
Query utente basate su parole chiave. Gli utenti digitano un termine di ricerca, filtrano per tag o navigano per parola chiave. FTS gestisce questo intento in modo nativo senza alcuna infrastruttura di embedding.
Risultati classificati senza dipendenze esterne. Gli indici FTS sono veloci, deterministici e non richiedono chiamate API. Il segnale di rilevanza deriva dalla frequenza dei termini pesata per posizione del campo.
Filtraggio booleano insieme alla ricerca. FTS si compone naturalmente con la tua logica di query esistente:
SELECT * FROM postsWHERE search_vector @@ to_tsquery('english', 'postgres & performance') AND category = 'tutorial' AND published_at > NOW() - INTERVAL '6 months';Configurazione di FTS
-- La colonna generata mantiene l'indice aggiornato automaticamenteALTER TABLE posts ADD COLUMN search_vector tsvector GENERATED ALWAYS AS ( setweight(to_tsvector('english', coalesce(title, '')), 'A') || setweight(to_tsvector('english', coalesce(body, '')), 'B') ) STORED;
CREATE INDEX posts_search_idx ON posts USING GIN (search_vector);
-- QuerySELECT title, ts_rank(search_vector, query) AS rankFROM posts, to_tsquery('english', 'postgres & performance') queryWHERE search_vector @@ queryORDER BY rank DESCLIMIT 10;setweight assegna importanza: A (titolo) batte B (corpo). Questo è l’intero modello di rilevanza per la maggior parte dei casi d’uso di ricerca di contenuti.
Cosa FTS Non Gestisce Bene
- Refusi nelle query — “javascipt” non corrisponderà a “javascript”
- Nomi di persone, indirizzi, nomi propri che non si riducono alla radice in modo prevedibile
- Prefissi / autocompletamento senza configurazione speciale
- Query in cui l’utente descrive un concetto invece di nominarlo
Quando i Trigrammi Vincono (pg_trgm)
pg_trgm copre l’intermedio scomodo che FTS sbaglia costantemente.
FTS tokenizza il testo in lessemi e lo riduce alla radice. Per la prosa è corretto. Per nomi e identificatori brevi spesso non lo è:
- Nomi di persone (“Dan Levy” → ridotto alla radice in modo diverso a seconda del dizionario e della configurazione della lingua)
- Nomi di aziende, indirizzi, titoli di prodotti dove l’ortografia esatta conta
- Query con refusi — “Micheal Jordan”, “Amaon”, “javascipt”
- Autocompletamento / ricerca per prefisso
- Corrispondenza di stringhe parziali (“son” che corrisponde a “Johnson”, “Anderson”)
pg_trgm è anche indipendente dalla lingua, il che conta per nomi da background linguistici diversi. FTS richiede la configurazione del dizionario per ogni lingua.
Ricerca Fuzzy di Nomi
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX users_name_trgm_idx ON users USING GIN (name gin_trgm_ops);
-- Trova "Micheal Jordan" cercando "Michael Jordan"SELECT id, name, similarity(name, $1) AS scoreFROM usersWHERE name % $1 -- operatore % = soglia di similarità (default 0.3)ORDER BY score DESCLIMIT 10;L’operatore % usa pg_trgm.similarity_threshold (default 0.3, intervallo 0–1). Per la ricerca di nomi, 0.3–0.4 cattura i refusi mantenendo basso il rumore.
Autocompletamento, Ricerca per Prefisso e per Contenuto
-- Corrispondenza per prefisso per l'autocompletamento. Un indice GIN trigramma può aiutare,-- ma un indice B-tree per pattern può essere migliore per prefissi puri ancorati a sinistra.SELECT name FROM usersWHERE name ILIKE $1 || '%'ORDER BY nameLIMIT 10;
-- word_similarity per corrispondenze parziali all'interno di stringhe più lunghe-- ("Johnson" all'interno di "Andrew Johnson III")SELECT id, name, word_similarity($1, name) AS scoreFROM usersWHERE $1 <% nameORDER BY score DESCLIMIT 10;L’indice GIN trigramma è particolarmente utile per le query di contenuto ILIKE '%pattern%' e la corrispondenza tollerante ai refusi — pattern che di solito sono scansioni complete della tabella senza un indice trigramma.
Quando Preferire pg_trgm rispetto a FTS
| Scenario | Usa |
|---|---|
| Ricerca nomi persone/aziende con refusi | pg_trgm |
| Autocompletamento / ricerca per prefisso | pg_trgm (o FTS con query per prefisso) |
| Stringhe brevi, identificatori, codici | pg_trgm |
| Articoli in prosa, documentazione, ticket | FTS |
| Messaggi di log per parole chiave | FTS |
| Ricerca nomi multilingue | pg_trgm (indipendente dalla lingua) |
Quando la Corrispondenza Esatta SQL Vince
Alcuni problemi di “ricerca” non sono affatto ricerca.
“Trovare l’utente con email dan@example.com” è un controllo di uguaglianza. “Trovare l’ordine ORD-12345” è una lookup per chiave primaria. “Elencare i post nella categoria tutorial ordinati per data” è una query filtrata. Questi appartengono a indici B-tree o hash.
Usare FTS o trigrammi qui aggiunge complessità senza migliorare la correttezza — e per identificatori esatti, una quasi-corrispondenza è peggio di nessuna corrispondenza.
CREATE INDEX users_email_idx ON users (email);
-- Lookup esatto: veloce e non ambiguoSELECT id, name FROM users WHERE email = $1;La lezione più ampia: la ricerca approssimativa per problemi con risposte corrette è un errore di categoria. Restituisce qualcosa — che potrebbe essere tranquillamente sbagliato.
Combinare Questi Strumenti
Questi strumenti si compongono in modo pulito. Non ne scegli esattamente uno.
FTS + pg_trgm per una casella di ricerca che tollera refusi nelle parole chiave:
-- La similarità trigramma sul titolo cattura i refusi; ts_rank gestisce la rilevanza del corpoSELECT id, title, ts_rank(search_vector, to_tsquery('simple', $1)) AS fts_rank, similarity(title, $1) AS trgm_scoreFROM postsWHERE search_vector @@ to_tsquery('simple', $1) OR title % $1ORDER BY (ts_rank(search_vector, to_tsquery('simple', $1)) + similarity(title, $1)) DESCLIMIT 10;FTS + unaccent per contenuti internazionali:
-- Rimuovi i segni diacritici così "José" corrisponde a "Jose"CREATE EXTENSION IF NOT EXISTS unaccent;
CREATE TEXT SEARCH CONFIGURATION public.simple_unaccent (COPY = pg_catalog.simple);
ALTER TEXT SEARCH CONFIGURATION public.simple_unaccent ALTER MAPPING FOR hword, hword_part, word WITH unaccent, simple;
ALTER TABLE posts ADD COLUMN search_vector tsvector;
CREATE TRIGGER posts_search_vector_refreshBEFORE INSERT OR UPDATE OF title, body ON postsFOR EACH ROW EXECUTE FUNCTION tsvector_update_trigger(search_vector, 'public.simple_unaccent', title, body);unaccent + pg_trgm per la ricerca internazionale di nomi:
ALTER TABLE users ADD COLUMN name_search text;
CREATE FUNCTION users_name_search_refresh()RETURNS trigger LANGUAGE plpgsql AS $$BEGIN NEW.name_search := unaccent(coalesce(NEW.name, '')); RETURN NEW;END;$$;
CREATE TRIGGER users_name_search_refreshBEFORE INSERT OR UPDATE OF name ON usersFOR EACH ROW EXECUTE FUNCTION users_name_search_refresh();
CREATE INDEX users_name_search_trgm_idx ON users USING GIN (name_search gin_trgm_ops);
SELECT id, nameFROM usersWHERE name_search % unaccent($1)ORDER BY similarity(name_search, unaccent($1)) DESCLIMIT 10;Gli esempi di trigger evitano di usare unaccent() all’interno di espressioni di colonne generate o indici, dove le regole di immutabilità di PostgreSQL contano. Se avvolgi unaccent() nella tua funzione immutabile, documenta che stai accettando il rischio di aggiornamento/configurazione.
Estensioni Degne di Nota
pg_trgm è incluso nella maggior parte delle distribuzioni di Postgres ma richiede un’abilitazione esplicita. La base per la corrispondenza fuzzy di stringhe in Postgres.
unaccent rimuove i segni diacritici prima dell’indicizzazione e della query. Si abbina bene sia con pg_trgm che con FTS per contenuti in lingue europee. Incluso con Postgres.
pg_bigm estende l’approccio trigramma ai bigrammi (fette di 2 caratteri), il che migliora significativamente i risultati per le lingue CJK (cinese, giapponese, coreano) dove pg_trgm performa male. Deve essere installato separatamente; non è incluso.
pg_search (da ParadeDB) sostituisce lo stack standard GIN / tsvector con un indice BM25 basato su Tantivy. Questo ti dà punteggio BM25 (spesso migliore di ts_rank), corrispondenza fuzzy all’interno delle query FTS, ricerca sfaccettata e indicizzazione drasticamente più veloce su tabelle grandi. È un percorso di aggiornamento drop-in quando FTS standard inizia a mostrare limiti di ranking o prestazioni.
-- pg_search: ricerca full-text BM25 con corrispondenza fuzzyCREATE INDEX posts_bm25_idx ON posts USING bm25 (id, title, body) WITH (key_field = 'id', text_fields = '{"title": {}, "body": {}}');
-- Query con punteggio BM25 + corrispondenza fuzzy (cattura "javascipt")SELECT id, title, paradedb.score(id) AS rankFROM postsWHERE posts @@@ paradedb.fuzzy_phrase(field => 'title', value => 'postgres performnce')ORDER BY rank DESCLIMIT 10;pgvector aggiunge archiviazione di vettori densi e ricerca per similarità. È lo strumento giusto quando gli utenti descrivono cosa vogliono invece di nominarlo — ricerca semantica, RAG, raccomandazioni di contenuti correlati, query multilingue. Trattato in profondità in Ricerca Vettoriale Semantica e Strategie Ibride.
Tabella Decisionale
| Cosa stai cercando | Consigliato |
|---|---|
| Articoli in prosa, documentazione, ticket | FTS |
| Nomi persone/aziende con refusi | pg_trgm |
| Autocompletamento, ricerca per prefisso | pg_trgm |
| Codici brevi, identificatori | pg_trgm |
| Messaggi di log per parole chiave | FTS |
| Nomi internazionali | pg_trgm + unaccent |
| Contenuti grandi, ranking migliore | pg_search (ParadeDB BM25) |
| Chiavi primarie, email esatte, ID | Indice B-tree |
| Date, intervalli, liste ordinate | Indice B-tree |
| Permessi, categorie, filtri | Clausola WHERE normale |
| Domande, parafrasi, concetti | pgvector (vedi prossimo articolo) |
In caso di dubbio: stringhe brevi con variazione ortografica → trigrammi. Prosa lunga per query per parole chiave → FTS. Identificatori strutturati → indici regolari. Query concettuali o in linguaggio naturale → pgvector.
Ricerca Ibrida: Due Segnali, Un Rank
Quando una query come "withRetry timeout errors" arriva in una casella di ricerca, porta due tipi di intento: nomi di simboli esatti che l’utente conosce (withRetry) e una descrizione concettuale (timeout errors). Nessuna singola primitiva copre entrambi. Eseguire FTS e ricerca vettoriale in parallelo — per poi unire le loro liste classificate con Reciprocal Rank Fusion — sì.
RRF assegna a ogni risultato un punteggio di 1 / (60 + rank) in ogni lista e somma tra le liste. La costante 60 attenua il vantaggio dei rank superiori, così un risultato che si piazza secondo in entrambe le liste può battere un risultato che vince una lista e manca completamente l’altra. Fondamentalmente, RRF non fa mai la media dei punteggi grezzi tra i metodi — il rank FTS e la distanza coseno sono valute diverse e non possono essere combinati aritmeticamente.
-- Ricerca ibrida: FTS + pgvector uniti con RRFWITH fts AS ( SELECT id, ts_rank(search_vector, query) AS score, ROW_NUMBER() OVER (ORDER BY ts_rank(search_vector, query) DESC) AS rank FROM docs, to_tsquery('english', 'withRetry & timeout') query WHERE search_vector @@ query LIMIT 60),vec AS ( SELECT id, ROW_NUMBER() OVER (ORDER BY embedding <=> $embedding) AS rank FROM docs ORDER BY embedding <=> $embedding LIMIT 60)SELECT COALESCE(fts.id, vec.id) AS id, (COALESCE(1.0 / (60 + fts.rank), 0) + COALESCE(1.0 / (60 + vec.rank), 0)) AS rrf_scoreFROM fts FULL JOIN vec ON fts.id = vec.idORDER BY rrf_score DESCLIMIT 10;Il pool di candidati da 60 documenti per ramo (LIMIT 60) è un punto di partenza comune. Allargalo se il recall è basso; riducilo per la velocità.
Prossimi Passi
La ricerca testuale su Postgres copre molto terreno, ma ha un soffitto. Quando gli utenti descrivono cosa vogliono invece di nominarlo — “qualcosa per aiutarmi a dormire in aereo,” “articoli sul debugging con fiducia per un nuovo ingegnere” — sia la ricerca lessicale che quella trigramma falliscono.
Quello è il territorio degli embedding vettoriali, della ricerca semantica e delle architetture ibride. Trattato in Ricerca Vettoriale Semantica e Strategie Ibride.