Confronto tra la ricerca full-text di PostgreSQL e Elasticsearch

Un database o una vera stack di ricerca

Indice

Il vero argomento non è se PostgreSQL può cercare testo o se Elasticsearch può archiviare documenti. Entrambi possono farlo. La domanda interessante è dove dovrebbe risiedere la complessità della ricerca.

La ricerca full-text di PostgreSQL risiede all’interno di un database relazionale transazionale con tsvector, tsquery, dizionari, ranking e indici GIN. Elasticsearch è un motore di ricerca e analisi distribuito basato su Lucene, con analizzatori, scoring BM25, scalabilità basata su shard, aggregazioni e indicizzazione near-real-time.

postgresql-vs-elasticsearch

Queste sono filosofie operative diverse prima ancora che liste di funzionalità diverse.

Se stai mappando questa scelta su storage, pipeline e operazioni, questa panoramica sull’infrastruttura dati fornisce il contesto del sistema più ampio.

Di cosa si tratta realmente questo confronto

A un livello basso, entrambi i sistemi si basano su idee di indice invertito, ma li imballano in modo molto diverso. PostgreSQL raccomanda GIN come tipo di indice di ricerca testuale preferito e lo descrive come un indice invertito sui lemmi nei valori tsvector. Elasticsearch analizza i campi text e li indicizza per la ricerca full-text, per poi distribuire quegli indici tra shard e nodi per la scalabilità. Nella pratica, PostgreSQL sembra una ricerca incorporata nel database dell’applicazione, mentre Elasticsearch sembra una piattaforma di ricerca dedicata con il proprio runtime, ciclo di vita e modello di scalabilità.

Questo confronto riguarda principalmente la ricerca full-text nativa di PostgreSQL più l’aiuto molto comune pg_trgm per una corrispondenza approssimativa. Quel ambito è importante perché l’ecosistema più ampio di PostgreSQL sta diventando sempre più orientato alla ricerca nel tempo. Estensioni come RUM aggiungono un comportamento di indice più ricco per la ricerca per frase e scansioni orientate al ranking, mentre PGroonga estende PostgreSQL con un’altra via di indicizzazione full-text. Questo non rende il PostgreSQL nativo uguale a Elasticsearch, ma significa che il confine è meno statico di quanto molti vecchi confronti presuppongano.

La mia cornice di opinione è semplice. La ricerca è solitamente una funzionalità finché non diventa una superficie del prodotto. PostgreSQL tende a vincere mentre la ricerca è ancora una funzionalità. Elasticsearch tende a vincere quando la ricerca diventa la cosa che gli utenti giudicano per prima. Questo è meno legato ai nomi dei marchi e più a dove la logica di rilevanza, la politica di indicizzazione e il dolore operativo sono autorizzati a risiedere.

Come funziona la ricerca full-text di PostgreSQL

La ricerca full-text di PostgreSQL inizia trasformando il testo grezzo in lemmi. to_tsvector tokenizza il testo, lo normalizza attraverso i dizionari configurati, scarta le stop word e archivia i lemmi sopravvissuti con le posizioni. setweight ti permette di etichettare i lemmi da diverse parti del documento, come titolo, abstract e corpo, in modo che quelle parti possano influenzare il ranking in modo diverso. PostgreSQL supporta anche più configurazioni linguistiche predefinite e ti permette di costruire configurazioni personalizzate con parser e dizionari. Se vuoi un riferimento SQL compatto mentre implementi questi pattern, questa Cheat Sheet di PostgreSQL e questa Cheat Sheet SQL con i comandi SQL più utili sono compagni pratici.

Un pattern di produzione tipico è una colonna tsvector generata memorizzata più un indice GIN. La documentazione di PostgreSQL è chiara: la ricerca testuale pratica richiede solitamente un indice, e mostra esplicitamente una colonna generata memorizzata che alimenta un indice GIN. Quel pattern evita di ricalcolare to_tsvector durante la verifica e mantiene la superficie di interrogazione pulita.

alter table posts
  add column search_vector tsvector
  generated always as (
    setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
    setweight(to_tsvector('english', coalesce(summary, '')), 'B') ||
    setweight(to_tsvector('english', coalesce(body, '')), 'D')
  ) stored;

create index posts_search_idx
  on posts using gin (search_vector);

select id,
       title,
       ts_rank_cd(
         search_vector,
         websearch_to_tsquery('english', '"query planner" -mysql')
       ) as rank
from posts
where search_vector @@ websearch_to_tsquery('english', '"query planner" -mysql')
order by rank desc
limit 20;

Dal lato delle interrogazioni, PostgreSQL ti offre diversi parser perché l’input dell’utente è più disordinato di quanto ammettano i blog ingegneristici. to_tsquery è esplicito e potente. phraseto_tsquery preserva l’ordine delle parole con l’operatore <->. websearch_to_tsquery accetta input simile a quello di un motore di ricerca, comprende frasi tra virgolette, OR e negazione -, e non genera mai errori di sintassi su input grezzo dell’utente. PostgreSQL supporta anche la corrispondenza per prefisso attaccando * a un lemma in to_tsquery.

Il ranking è dove il PostgreSQL nativo mostra sia la sua forza che il suo limite. ts_rank e ts_rank_cd possono usare frequenza, prossimità e pesi strutturali, e il modello di pesatura è sorprendentemente buono per molti compiti di ricerca applicativa. Allo stesso tempo, i documenti stessi di PostgreSQL notano che il ranking può essere costoso e che le funzioni di ranking integrate non usano informazioni globali. Questo è il limite silenzioso ma importante della ricerca full-text nativa di PostgreSQL. Può fare il ranking, ma la rilevanza non è il centro di gravità del motore.

Quando PostgreSQL è sufficiente per la ricerca full-text

PostgreSQL è sufficiente più spesso di quanto i vendor di ricerca dedicati vorrebbero. È particolarmente convincente quando la ricerca rimane molto vicina alle righe transazionali, alle join, ai permessi e alle scritture fresche. Il modello MVCC di PostgreSQL fornisce consistenza transazionale e letture basate su snapshot, quindi lo stesso database che accetta la scrittura può rispondere alla ricerca senza una finestra di refresh stile Elasticsearch. Quando una casella di ricerca è davvero “trova record all’interno dell’app che ho appena modificato”, quella proprietà è più importante delle dimostrazioni di rilevanza lucide.

È anche sufficiente quando il filtro SQL è metà della funzionalità. Filtri di stato, isolamento dei tenant, stati di pubblicazione, timestamp e join relazionali spesso contano tanto quanto la rilevanza delle parole chiave nei sistemi lineari di business. In quei casi, la ricerca full-text di PostgreSQL si comporta come un altro predicato indicizzato in un piano di interrogazione relazionale, non come una piattaforma separata che deve essere alimentata e mantenuta calda. Questa è un’architettura noiosa, e la noia è spesso la giusta specie di velocità.

Come funziona Elasticsearch come motore di ricerca

Elasticsearch si presenta in modo molto diverso. I suoi stessi documenti lo definiscono come un motore di ricerca e analisi distribuito, archivio dati scalabile e database vettoriale basato su Apache Lucene, ottimizzato per velocità e rilevanza a scala di produzione e operante in near real time. Elasticsearch divide ogni indice in shard, replica quegli shard e li distribuisce tra nodi per aumentare la capacità di indicizzazione e interrogazione. Questo è il motivo per cui Elasticsearch raramente è “solo un indice”. È un’architettura di cluster.

Sotto il cofano, gli analizzatori fanno la maggior parte del lavoro pesante. Un analizzatore Elasticsearch è una composizione di filtri caratteri, tokenizer e filtri token. Ci sono analizzatori integrati, analizzatori linguistici e analizzatori personalizzati, e la gestione dei sinonimi è una parte di primo piano dell’analisi. Questo significa che il comportamento della ricerca non riguarda solo l’interrogazione. Riguarda anche come sia i documenti che le interrogazioni sono normalizzati prima che lo scoring inizi.

Per un riferimento API pratico mentre si implementano questi pattern, questa Cheat Sheet di Elasticsearch raccoglie comandi essenziali e scorciatoie operative.

PUT posts
{
  "mappings": {
    "properties": {
      "title":   { "type": "text" },
      "summary": { "type": "text" },
      "body":    { "type": "text" },
      "tags":    { "type": "keyword" }
    }
  }
}

GET posts/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "multi_match": {
            "query": "query planner",
            "fields": ["title^3", "summary^2", "body"],
            "type": "best_fields"
          }
        }
      ],
      "must_not": [
        { "match": { "body": "mysql" } }
      ]
    }
  },
  "aggs": {
    "by_tag": {
      "terms": {
        "field": "tags"
      }
    }
  },
  "highlight": {
    "fields": {
      "body": {}
    }
  }
}

Il Query DSL è dove Elasticsearch inizia a sentirsi nativo per la ricerca piuttosto che simile a un database. bool combina clausole con must, should, filter e must_not. multi_match può cercare attraverso molti campi con boost dei campi e diversi modi di esecuzione come best_fields, most_fields, cross_fields, phrase e bool_prefix. Aggregazioni, evidenziazioni e filtri possono tutti stare accanto all’interrogazione principale nella stessa richiesta. BM25 è il modello di similarità predefinito.

Il modello di freschezza è anche esplicito. Elasticsearch è near real time, non immediatamente coerente per la ricerca. Le operazioni recenti diventano visibili alla ricerca quando un refresh apre un nuovo segmento, e di default quel refresh avviene ogni secondo sugli indici che sono stati interrogati recentemente. I documenti di Elastic avvertono anche che i refresh sono intensivi per le risorse e raccomandano di aspettare i refresh periodici o usare refresh=wait_for quando un flusso di lavoro ha bisogno di visibilità della ricerca read-after-write. Questo è un contratto molto diverso da quello di PostgreSQL.

Perché Elasticsearch solitamente classifica meglio ricerche complesse

Questo è il motivo tecnico più profondo per cui molti team alla fine si spostano dalla ricerca full-text di PostgreSQL a Elasticsearch. Le funzioni di ranking integrate di PostgreSQL non usano informazioni globali, mentre Elasticsearch usa BM25 di default e espone impostazioni di similarità specifiche per campo, analizzatori, forme di interrogazione multi-campo e un DSL di ricerca progettato intorno al tuning della rilevanza. Una volta che la ricerca diventa meno “ha corrisposto” e più “perché questi dieci risultati hanno vinto”, Elasticsearch solitamente ha più spazio espressivo.

Elasticsearch ha anche un chiaro bias verso documenti denormalizzati. La documentazione del suo campo join avverte esplicitamente contro la modellazione di più livelli di relazioni per replicare uno schema relazionale e raccomanda la denormalizzazione per una migliore prestazione di ricerca. Questa scelta di design spiega molti dei punti di forza e delle frustrazioni di Elasticsearch. Non sta cercando di essere PostgreSQL con un LIKE più veloce. Sta cercando di essere un motore di ricerca che può classificare e recuperare rapidamente grandi collezioni di documenti.

Ricerca full-text PostgreSQL vs Elasticsearch su funzionalità reali

La tolleranza ai refusi è dove i due sistemi divergono nettamente. Elasticsearch fornisce interrogazioni fuzzy basate sulla distanza di modifica Levenshtein e offre anche tipi di campo dedicati per suggerimenti e scrittura a mano libera. La ricerca full-text nativa di PostgreSQL non è tollerante ai refusi di per sé. La risposta usuale di PostgreSQL è pg_trgm, che aggiunge operatori di similarità e supporto di indice per similarità trigramma, LIKE e ILIKE. Questo funziona bene, ma è una strategia di composizione piuttosto che un set di funzionalità di motore di ricerca integrato.

L’evidenziazione esiste in entrambi gli stack, ma i dettagli di implementazione raccontano una storia. PostgreSQL usa ts_headline, che può restituire frammenti utili, tuttavia i documenti notano che usa il documento originale, può essere lento e non è garantito sicuro per l’inserimento diretto nelle pagine web. L’evidenziazione di Elasticsearch può usare offset di postings o vettori di termini, che è particolarmente prezioso su campi grandi perché evita di ri-analizzare il testo completo per ogni richiesta di evidenziazione. In breve, PostgreSQL può evidenziare, mentre Elasticsearch è costruito per evidenziare a scala.

Facets e analisi della ricerca sono un’altra linea di faglia. Elasticsearch tratta le aggregazioni come una parte di primo piano del modello di ricerca, con aggregazioni metriche, a secchi e a pipeline disponibili direttamente nella risposta di ricerca. PostgreSQL può ovviamente aggregare perché è SQL, ma una volta che i secchi contati, gli istogrammi e le analisi di ricerca componibili diventano parte del prodotto di ricerca stesso, Elasticsearch si sente molto più nativo. La differenza non è la capacità in principio. È quanto l’ergonomia delle interrogazioni e la politica delle prestazioni il motore dedichi a quel carico di lavoro.

L’autocompletamento segue lo stesso pattern. PostgreSQL può fare la corrispondenza per prefisso in to_tsquery, che è utile e spesso sufficiente per strumenti interni. Elasticsearch va oltre con campi search_as_you_type che costruiscono automaticamente molteplici sotto-campi analizzati per il completamento di prefisso e infisso, più suggeritori di completamento costruiti appositamente per suggerimenti veloci. Quel divario è minore su un pannello di amministrazione e maggiore su una superficie di scoperta orientata all’utente.

Il costo operativo conta più degli screenshot dei benchmark

La domanda tentatrice sul motore di ricerca è “Elasticsearch è più veloce di PostgreSQL per la ricerca?” La risposta onesta è “per quale forma di ricerca?” Elasticsearch è ingegnerizzato intorno a shard, repliche, indicizzazione bulk, politica di refresh e gestione del ciclo di vita. I documenti di produzione di Elastic approfondiscono la strategia degli shard, la dimensione delle richieste bulk, il throughput di indicizzazione, gli intervalli di refresh e ILM. PostgreSQL evita un secondo cluster, ma la manutenzione GIN non è gratuita. I documenti di PostgreSQL avvertono che le inserzioni GIN possono essere lente, che la pulizia della lista in sospeso può causare fluttuazioni dei tempi di risposta e che la strategia autovacuum è importante se l’indice viene aggiornato pesantemente.

Questo rende la storia delle prestazioni più sfumata di quanto la maggior parte dei post di confronto ammetta. Elasticsearch solitamente ha più margine per grandi ricerche lessicali top-N, faceting, autocompletamento e volume di lettura distribuito perché la sua architettura è dedicata a quei compiti. PostgreSQL spesso sembra più veloce per interrogazioni applicative relazionali con requisiti di freschezza stretti perché non c’è un secondo datastore, nessun confine di refresh e nessun percorso di sincronizzazione da debuggare. Il vincitore è solitamente la forma del carico di lavoro, non lo screenshot del benchmark. Questo è in parte un’inferenza, ma segue direttamente dal modello transazionale MVCC di PostgreSQL e dal design basato su shard near-real-time di Elasticsearch.

Dovrebbero i dati transazionali e gli indici di ricerca risiedere nello stesso sistema? Quando la rilevanza della ricerca è modesta ma la freschezza, i permessi e la verità transazionale sono critici, il design dello stesso sistema ha vantaggi ovvi. Quando la qualità della ricerca, il faceting, la politica dei sinonimi, la tolleranza ai refusi e la scala orizzontale della ricerca diventano preoccupazioni del prodotto di primo piano, un secondo sistema inizia a sembrare giustificato. La guida di Elasticsearch sulla dimensione degli shard dice che non c’è una strategia one-size-fits-all e raccomanda di fare benchmark sui dati di produzione su hardware di produzione. Quella frase cattura il compromesso perfettamente. Elasticsearch compra margine chiedendoti di operare un’architettura più specifica per la ricerca.

Il verdetto pratico

La ricerca full-text di PostgreSQL vince sorprendentemente spesso il primo 80 percento. Supporta tokenizzazione, stop word, stemming, interrogazioni per frase, pesi, ranking, evidenziazione, vettori di ricerca generati, indici GIN e helper di similarità basati su trigrammi. Combinata con la semantica transazionale di PostgreSQL, offre a molte applicazioni uno stack di ricerca che è semplice, attuale e vicino ai dati. Per back office SaaS, strumenti interni, siti di contenuto moderati e ricerca nativa dell’app, quella combinazione è difficile da scartare.

Elasticsearch diventa persuasivo quando la ricerca non è meramente un filtro ma una superficie del prodotto. BM25 di default, analizzatori personalizzati, filtri sinonimi, interrogazioni fuzzy, ranking multi-campo, aggregazioni, opzioni dedicate per l’autocompletamento, strategie di evidenziazione per campi grandi e scalabilità distribuita basata su shard non sono funzionalità laterali. Sono il motivo per cui il motore esiste. Questo è il motivo per cui i confronti di Elasticsearch che si concentrano solo sulla latenza grezza solitamente sbagliano il punto. La differenza più grande è quanto logica del prodotto di ricerca il motore è disposto a possedere.

Il modello mentale più pulito è questo. La ricerca full-text di PostgreSQL è eccellente quando la ricerca appartiene al database. Elasticsearch è eccellente quando il database deve alimentare una piattaforma di ricerca. La maggior parte dei team si concentra troppo sulla velocità e troppo poco sui modi di fallimento. Il vero compromesso è dove il tuning della rilevanza, la freschezza dei dati e la complessità operativa sono autorizzati a risiedere.