DanLevy.net

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.

Mappa degli strumenti di ricerca PostgresUn confronto tra pg_trgm, ricerca full-text, pgvector e ricerca ibrida per forma di input e intento della query.Scegli la primitiva di ricerca per forma dell’inputLa stessa tabella Postgres può supportarli tutti e quattro. Il trucco è abbinare la query al testo.Le parole esatte contanoIl significato contaTesto breve / strutturatoProsa lunga / blocchifuzzypg_trgmNomi, indirizzi, titoli, refusi,autocompletamento, stringhe parziali.Similarità ortografica: distanza ortografica.similepgvectorElementi correlati, ticket duplicati,raccomandazioni da brevi descrizioni.Similarità degli embedding: distanza di significato.lessicaleRicerca full-textArticoli, documentazione, log, contenuti di supportodove le parole della query devono apparire.Lessemi, stemming, ranking, filtri booleani.ibridaFTS + pgvectorDocumentazione tecnica e RAG dove gli utenti pongonodomande concettuali più simboli esatti.Esegui entrambi, fondi i rank con RRF.Parti dall’intento della query, poi verifica la forma del testo
Le quattro primitive di ricerca Postgres mappate per intento della query (esatto vs. semantico) e forma del testo (strutturato vs. prosa). La stessa tabella può ospitare tutti e quattro gli indici — la scelta è per query, non per tabella.

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 posts
WHERE 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 automaticamente
ALTER 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);
-- Query
SELECT title, ts_rank(search_vector, query) AS rank
FROM posts, to_tsquery('english', 'postgres & performance') query
WHERE search_vector @@ query
ORDER BY rank DESC
LIMIT 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


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 è:

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 score
FROM users
WHERE name % $1 -- operatore % = soglia di similarità (default 0.3)
ORDER BY score DESC
LIMIT 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 users
WHERE name ILIKE $1 || '%'
ORDER BY name
LIMIT 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 score
FROM users
WHERE $1 <% name
ORDER BY score DESC
LIMIT 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

ScenarioUsa
Ricerca nomi persone/aziende con refusipg_trgm
Autocompletamento / ricerca per prefissopg_trgm (o FTS con query per prefisso)
Stringhe brevi, identificatori, codicipg_trgm
Articoli in prosa, documentazione, ticketFTS
Messaggi di log per parole chiaveFTS
Ricerca nomi multilinguepg_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 ambiguo
SELECT 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 corpo
SELECT id, title,
ts_rank(search_vector, to_tsquery('simple', $1)) AS fts_rank,
similarity(title, $1) AS trgm_score
FROM posts
WHERE search_vector @@ to_tsquery('simple', $1)
OR title % $1
ORDER BY (ts_rank(search_vector, to_tsquery('simple', $1)) + similarity(title, $1)) DESC
LIMIT 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_refresh
BEFORE INSERT OR UPDATE OF title, body ON posts
FOR 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_refresh
BEFORE INSERT OR UPDATE OF name ON users
FOR 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, name
FROM users
WHERE name_search % unaccent($1)
ORDER BY similarity(name_search, unaccent($1)) DESC
LIMIT 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 fuzzy
CREATE 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 rank
FROM posts
WHERE posts @@@ paradedb.fuzzy_phrase(field => 'title', value => 'postgres performnce')
ORDER BY rank DESC
LIMIT 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 cercandoConsigliato
Articoli in prosa, documentazione, ticketFTS
Nomi persone/aziende con refusipg_trgm
Autocompletamento, ricerca per prefissopg_trgm
Codici brevi, identificatoripg_trgm
Messaggi di log per parole chiaveFTS
Nomi internazionalipg_trgm + unaccent
Contenuti grandi, ranking migliorepg_search (ParadeDB BM25)
Chiavi primarie, email esatte, IDIndice B-tree
Date, intervalli, liste ordinateIndice B-tree
Permessi, categorie, filtriClausola WHERE normale
Domande, parafrasi, concettipgvector (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 con Reciprocal Rank FusionUna query si dirama verso la ricerca full-text e la ricerca vettoriale, ognuna produce rank, e Reciprocal Rank Fusion li combina in un’unica lista di risultati.La ricerca ibrida sono due segnali onesti, poi un rank unitoNon fare la media dei punteggi grezzi. Il rank FTS e la distanza coseno sono valute diverse.Query utente”withRetrytimeout errors”FTS / BM25Simboli e parole esatti1. Riferimento API2. Guida RetrypgvectorVicini concettuali1. Guasti di rete2. Guida RetryUnione RRFDai a ogni risultato credito perdove si è classificato in ogni lista.1 / (60 + rank)Risultati finaliIl risultato migliore è dove i termini esattie il significato semantico concordano.
Una query si dirama verso FTS e pgvector in parallelo. Ognuno produce la propria lista classificata. RRF assegna un punteggio a ogni documento in base alla sua posizione in ogni lista e somma i punteggi — il risultato mette in superficie i documenti su cui entrambi i segnali concordano.
-- Ricerca ibrida: FTS + pgvector uniti con RRF
WITH 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_score
FROM fts FULL JOIN vec ON fts.id = vec.id
ORDER BY rrf_score DESC
LIMIT 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.