pgvector

pgvector e' un'estensione di PostgreSQL molto interessante perche' consente di memorizzare gli embedding ed effettuare efficienti ricerche di similarita'.
Tecnicamente pgvector consente di memorizzare, interrogare ed indicizzare vettori. Sembra un'astrusa tipologia di ricerca riservata ai soli matematici... ma gli Embedding creati dai modelli di Machine Learning sono proprio rappresentazioni vettoriali. Percio' le ricerche effettuate con pgvector sono la base per lavorare con le piu' attuali intelligenze artificiali generative. Con pgvector e' possibile fornire le funzionalita' richieste dalle AI generative per le applicazioni utilizzando normali query SQL in PostgreSQL.

L'uso dell'estensione pgvector e' utile a chi si occupa di AI (Artificial Intelligence) e di RAG (Retrieval-Augmented Generation), ma vi e' un fortissimo e crescente interesse per queste funzionalita' e sempre piu' applicazioni ne faranno uso.

Ho fretta!

pgvector e' l'estensione PostgreSQL per le AI generative che consente la memorizzazione ed il confronto tra Embeddings con normali query SQL.

Su un qualsiasi Linux [NdA ed anche su MacOS] con un PostgreSQL recente possiamo installare l'estensione pgvector con:

cd /tmp git clone --branch v0.7.4 https://github.com/pgvector/pgvector.git cd pgvector make make install

Quindi collegati sul DB possiamo lanciare i comandi :

CREATE EXTENSION vector; CREATE TABLE doc ( id bigserial PRIMARY KEY, title text NOT NULL, author text, pub_date date NOT NULL, content TEXT NOT NULL ); CREATE TABLE doc_embeddings ( id bigint PRIMARY KEY, embedding vector(1536) NOT NULL ); CREATE INDEX doc_embeddings_01_idx ON doc_embeddings USING hnsw (embedding vector_cosine_ops);

Gli Embeddings vanno generati con un modello, ad esempio, con le OpenAI API [NdE openai.Embedding.create(input=content, model="text-embedding-3-small") ] ed i dati ottenuti possono essere inseriti nelle tabelle con normali INSERT.

Ora che le strutture sono state create ed i dati sono stati caricati e' possibile interrogare la base dati con query SQL:

WITH pgv AS (
    SELECT embedding
      FROM doc_embeddings JOIN doc USING (id)
     WHERE title like 'PostgreSQL%'
)
SELECT title, author, content
  FROM doc_embeddings
  JOIN doc USING (id)
 WHERE embedding <=> (SELECT embedding FROM pgv) > 0.5;

Gia' fatto!
Ma se c'e' qualcosa che non e' chiaro... continuate a leggere!

Introduzione

Grazie alle recenti grandi innovazioni gli strumenti l'intelligenza artificiale (AI, Artificial Intelligence) sono in grado di creare risposte efficaci basate su modelli linguistici di grandi dimensioni (LLM, large language model). Per ottenere questo risultato l'AI viene addestrata, con un processo abbastanza lungo e costoso, attraverso un numero enorme di dati che sono la sua base di conoscenza.

Per fornire dati piu' aggiornati e, sopratutto, per includere informazioni relative ad un particolare dominio (eg. i prodotti di un'azienda, i manuali di un'apparecchiatura complessa, ...) e' possibile arricchire il modello con informazioni con la Retrieval-Augmented Generation (RAG).
L'idea di base della RAG e' quella di incrementare la domanda presentata al modello, che resta lo stesso, con un contesto ricavato dai dati aggiuntivi interni all'azienda.
I dati non strutturati possono essere di qualsiasi tipo e vengono rappresentati come Embeddings. Gli Embeddings tecnicamente sono dei vettori con centinaia di dimensioni che rappresentano il significato dell'informazione contenuta. Quanto piu' due Embeddings sono vicini quanto piu' hanno un significato simile.
Per un progetto RAG e' necessario creare un database che contenga le informazioni di contesto da passare al modello scegliendole tra quelle piu' attinenti alla richiesta.

pgvector e' un estensione di PostgreSQL che consente di memorizzare, ricercare in modo efficiente e confrontare tra loro gli Embeddings.
Poiche' pgvector si basa su un potente database relazionale ne eredita tutte le caratteristiche come le proprieta' ACID, un completo linguaggio SQL, gli strumenti di amministrazione e tuning, la scalabilita', ... oltre alla possibilita' di mantenere, oltre agli Embeddings, tutti gli altri dati strutturati organizzati come piu' opportuno alle applicazioni.
Per default pgvector effettua una ricerca esatta, tuttavia e' possibile creare indici specifici [NdA al momento sono disponibili due differenti tipologie di indici per i dati vettoriali: IVFFlat, HNSW] per effettare ricerche approssimate molto piu' efficienti.

Installazione e configurazione

pgvector non fa parte delle core extension PostgreSQL e quindi tipicamente richiede un'installazione che tuttavia e' molto semplice. Su una macchina linux i comandi sono:

cd /tmp git clone --branch v0.7.0 https://github.com/pgvector/pgvector.git cd pgvector make make install

In caso di problemi o per maggiori dettagli basta fare riferimento alla documentazione ufficiale.
Se si opera su un database in Cloud non e' possibile effettuare l'installazione di una estension aggiuntiva ma... la maggioranza dei Cloud provider ha gia' inserito pgvector tra le estensioni disponibili quindi non e' necessario installarla!

Ora che l'estensione e' stata installata puo' essere utilizzata su qualsiasi database presente con:

CREATE EXTENSION vector;

Creata l'estensione sono disponibili nuovi datatype, indici e funzioni per operare con i vettori.

pgvector

Una volta attivata l'estensione pgvector sono disponibili nuovi datatype, operatori, funzioni ed indici sulla base dati PostgreSQL come vedremo in questo capitolo.

Datatype

Puo' essere utile ricordare alcuni datatype standard supportati in PostgreSQL: INTEGER 4 byte da -2G a 2G, REAL 4 byte con precisione 6 cifre decimali, DOUBLE PRECISION 8 byte con precisione 15 cifre decimali.

L'estensione pgvector introduce in PostgreSQL nuovi datatype:

Datatype Max dimensions Size Note
vector 16.000 4*dim +8 Il datatype principale supportato dalla prima vesione di pgvector. Consente la memorizzazione di vettori di REAL.
halfvec 16.000 2*dim +8 Consente un numero maggiore di dimensioni nell'indicizzazione per i modelli che lo richiedono.
bit 64.000 dim/8 +8 Un numero molto elevato di dimensioni per valori con 0 o 1.
sparsevec 1.000 8*non-zero items +16 Memorizza solo gli elementi diversi da 0.

I datatype di pgvector sono adatti alla memorizzazione degli Embeddings.

Operatori e Funzioni

L'estensione pgvector introduce in PostgreSQL alcune funzioni di confronto che, per semplicita' di utilizzo, vengono spesso richiamate come operatori e quindi iniziamo con questo elenco:
Operatore Funzione Note
<-> L2 distance Distanza euclidea. Si calcola con il noto teorema di pitagora applicato ai vettori.
<=> cosine distance Calcolo del coseno tra i due vettori. Normalizzato tra 1 e -1. Si calcola in modo efficiente.
<#> negative inner product Prodotto tra i due vettori, da moltiplicare per -1.
<+> L1 distance Manhattan distance. Di semplice calcolo perche' opera per differenza su ogni dimensione.
<%> Jaccard distance Utilizzabile tra vettori binari. E' calcolato come rapporto tra la cardinalita' dell'intersezione e dell'unione e va tra 0 ed 1.
<~> Hamming distance Utilizzabile tra vettori binari. Conta il numero di elementi diversi tra due vettori.

Vediamo ora le principali funzioni con l'eventuale indicazione della versione da cui disponibili:

Funzione Versione Note
l2_distance(vector, vector) → double precision
cosine_distance(vector, vector) → double precision
inner_product(vector, vector) → double precision
vector_dims(vector) → integer
vector_norm(vector) → double precision
avg(vector) → vector
l1_distance(vector, vector) → double precision0.5.0
sum(vector) → vector0.5.0
l2_normalize(vector) → vector0.7.0
binary_quantize(vector) → bit0.7.0
subvector(vector, integer, integer) → vector0.7.0
hamming_distance(bit, bit) → double precision0.7.0
jaccard_distance(bit, bit) → double precision0.7.0

Le funzioni principali sono quelle di distanza o di similitudine. Questo consente di trovare gli embeddings piu' pertinenti alla ricerca effettuata. Le funzioni di distanza classiche sono la L1/Manhattan distance e la L2/Euclidean distance; pgvector le fornisce entrambe e ne aggiunge di ulteriori.
Le funzioni e gli operatori possono essere utilizzati sia nelle clausole di WHERE che nell'ORDER BY.

Le differenti funzioni di calcolo delle distanze o delle similitudini sono adatte a casi diversi e dipendono dal modello LLM, spesso si utilizzano le formule piu' efficienti.
La Manhattan distance o distanza nella Taxicab geometry e' di semplice comprensione per me che abito a Torino: le strade sono tutte ortogonali tra loro e la distanza e' quella effettivamente richiesta per raggiungere la destinazione.

Indici

Per default pgvector esegue la nearest neighbor search in modo esatto, aggiungendo un indice e' possibile eseguire la ricerca in modo molto piu' veloce ma approssimato [NdA il termine esatto per il tipo di ricerca e' ANN: Approximate Nearest Neighbor search]. Le ricerche esatte di solito non servono... va comunque sottolineato che con la presenza o meno degli indici i risultati delle query possono cambiare.

L'estensione pgvector introduce in PostgreSQL nuovi tipi di indici:

Index Note
IVFFlat Indice a liste invertite
HNSW Indice a grafo multilayer

L'indice va creato sulla funzione di distanza utilizzata nelle query (eg. vector_l2_ops, vector_cosine_ops). Il numero di dimensioni supportate dipende dal tipo di indice e dal datatype come riassunto nella seguente tabella:

Index vector halfvec bit sparsevec
IVFFlat 2.000 4.000 64.000 N.A.
HNSW 2.000 4.000 64.000 1.000

Gli indici disponibili con pgvector hanno caratteristiche diverse tra loro.

Gli indici IVFFlat (Inverted File with Flat Compression) vanno creati solo dopo aver inserito i dati sulla tabella perche' necessitano dei dati per la scelta delle liste invertite da creare. Nella creazione dell'indice e' opportuno indicare il numero di liste. Ad esempio: WITH (lists = 100) dove il valore puo' essere rows/1000 fino a 1M e sqrt(rows) oltre.
Se il numero di record nella tabella non e' sufficiente e' possibile che le query non ottengano i risultati previsti; in questo caso semplicemente basta cancellare l'indice.

Gli indici HNSW (Hierarchical Navigable Small Worlds) richiedono piu' memoria e sono piu' lenti da costruire ma piu' efficaci nelle ricerche. Gli indici HNSW possono essere creati anche con tabelle vuote. Nella creazione dell'indice e' opportuno indicare i parametri m (numero di connessioni per layer, default 16) ed ef_construction (dimensione dinamica per la costruzione del grafo, default 64). Ad esempio: WITH (m = 16, ef_construction = 128) che richiede tempi maggiori per la costruzione dell'indice ma migliora le ricerche rispetto al default.

Utilizzo

Per utilizzare pgvector basta conoscere l'SQL. Sono sfruttabili tutte le funzionalita' dell'SQL anche quelle piu' avanzate o specifiche di PostgreSQL.
Tipicamente vengono utilizzati gli operatori di similitudine come condizioni di ricerca o per l'ordinamento.

-- OpenAI uilizza 1536 dimensioni per i modelli text-embedding-3-small e text-embedding-ada-002 CREATE TABLE doc_embeddings ( id bigint PRIMARY KEY, embedding vector(1536) NOT NULL ); CREATE INDEX doc_embeddings_01_idx ON doc_embeddings USING hnsw (embedding vector_cosine_ops); -- Ricerca dei cinque documenti piu' simili ad uno su una specifica riga SELECT * FROM doc_embeddings WHERE id != 69 ORDER BY embedding <=> (SELECT embedding FROM doc_embeddings WHERE id = 69) LIMIT 5; -- Ricerca dei tre documenti piu' simili ad uno dato usando differenti funzioni/operatori SELECT id, 1-(embedding <=> '[custom_vector]') as cosine_similarity FROM doc_embeddings ORDER BY cosine_similarity DESC LIMIT 3; SELECT id, embedding <-> '[custom_vector]' as L2_distance FROM doc_embeddings ORDER BY L2_distance LIMIT 3; SELECT id, embedding <#> '[custom_vector]' as inner_product FROM doc_embeddings ORDER BY as inner_product DESC LIMIT 3; -- Ricerca dei documenti simili ad altri ricercati per titolo WITH pgv AS ( SELECT embedding FROM doc_embeddings JOIN doc USING (id) WHERE title like 'PostgreSQL%' ) SELECT title, author, content FROM doc_embeddings JOIN doc USING (id) WHERE embedding <=> (SELECT embedding FROM pgv) > 0.5;

Dal punto di vista di gestione le query sui vettori sono identiche a tutte le altre: le query che utilizzano i vettori possono esssere analizzate con l'EXPLAIN, le query correnti sono riportate nella vista pg_stat_activity, gli statement vengono registrati da pg_stat_statements [NdA se configurato, come sempre suggerito su un database di produzione], puo' essere sfruttato il parallelismo dei thread nelle select, possono essere utilizzate le repliche, ...

Tuning

In generale l'estensione pgvector non richiede un tuning particolare per essere utilizzata, tuttavia vi sono una serie di suggerimenti che possono essere utili per un uso efficiente.

Tutte le indicazioni relative al tuning di PostgreSQL ed all'ottimizzazione dell'SQL sono ovviamente valide anche per pgvector.
In generale le impostazioni di shared_buffers, max_connections, effective_cache_size, work_mem, max_wal_size sono i principali parametri di tuning di una base dati PostgreSQL ed hanno un significativo impatto anche per l'estensione pgvector.
In particolare... gli indici IVFFlat e HNSW possono essere creati senza lock in scrittura utilizzando la clausola CONCURRENTLY, i tempi di creazione possono essere ridotti eseguendo il tuning del parametro maintenance_work_mem e le fasi di creazione possono essere controllate nella vista pg_stat_progress_create_index. E' anche possibile sfruttare il parallelismo nella creazione degli indici HNSW impostando il parametro max_parallel_maintenance_workers.

In fase di creazione di un indice IVFFlat e' possibile indicare il numero di liste da utilizzare con la clausola: WITH (lists = 1000).
In fase di ricerca e' invece possibile indicare il numero di liste da esplorare con l'impostazione del parametro SET ivfflat.probes = 32
Un'impostazione consigliata per le lists dell'indice e' 1000 fino ad un milione di record e poi la radice quadrata del numero di record per valori superiori. L'impostazione consigliata dei probes e' la radice quadrata del numero di liste, un valore superiore migliora la ricerca, un valore inferiore migliora la velocita'.

In fase di creazione di un indice HNSW e' possibile indicare due parametri: il numero massimo di connessioni per livello ed il numero di numero di liste candidate per creare il grafo con la clausola WITH (m = 16, ef_construction = 64).
In fase di ricerca e' invece possibile indicare il numero di liste candidate con l'impostazione del parametro: SET hnsw.ef_search = 40;

La scelta della funzione della distanza dipende da diversi fattori. Nel caso di OpenAI gli embedding sono normalizzati ad 1 come lunghezza quindi la distanza euclidea (L2) e la distanza per coseno ottengono gli stessi risultati.
Quindi come funzione di distanza si preferisce il calcolo del coseno perche' piu' efficiente come calcolo [NdA oppure prodotto dei vettori che e' praticamente equivalente].

Un consiglio suggerito dagli antichi romani: divide ed impera!
Quando le dimensioni iniziano a diventare significative diventa importante separare i componenti ed ottimizzare ogni singola parte. Quindi i dati vettoriali e' opportuno siano posti su tabelle dedicate e, eventualmente le tabelle con dati vettoriali su un'istanza dedicata. In questo modo la ricostruzione degli indici o il tuning del database diventano piu' semplici da gestire.
Anche il partizionamento, l'utilizzo di indici parziali e la riduzione del numero di dimensioni dei vettori possono essere utilizzati efficacemente con pgvector quando le cardinalita' sono molto elevate.

Come ultimo il consiglio piu' semplice e valido anche per altri tipi di indice: e' sempre meglio creare gli indici dopo aver caricato tutti i dati.

Cosa manca?

LLM Token limit
AWS Titan8.000
Llama38.000
GPT-48.192
GPT 3.5 Turbo16.385
Mistral32.000
GPT-4 Turbo128.000
GPT Vision128.000
Cohere Command R128.000
Claude3200.000
Gemini 1.51.000.000
In questa pagina mancano moltissime informazioni necessarie per realizzare un progetto RAG con pgvector.

Per interagire con OpenAI, o con un qualsiasi altro modello LLM, bisogna utilizzare le API fornite. Spesso si utilizza il Python come linguaggio che e' di semplice comprensione e potente. Ma e' troppo lungo per parlarne...

Anche se in PostgreSQL si possono memorizzare testi di grandi dimensioni in realta' i modelli hanno limiti molto piu' bassi definiti in token. I token sono le unita' minime in cui vengono separate le parole e vengono utilizzati perche' sono molto piu' efficienti dei caratteri per la rappresentazione dei testi. In un testo in inglese indicativamente un token corrisponde a 4 caratteri o ai 3/4 di una parola. Differenti modelli hanno limiti diversi, il limite sul numero di token non indica la qualita' del modello ma e' una scelta di implementazione.
E' quindi spesso necessario trattare i testi da inserire sulla base dati e da cui ricavare gli Embeddings.
Ma e' troppo lungo per parlarne...

La scelta degli embeddings con cui arricchire il contesto e' molto importante: quanti, con quale funzione di calcolo della distanza, con quali ulteriori condizioni. Anche considerare le sole informazioni testuali puo' essere una limitazione: a volte un'immagine vale piu' di mille parole! A tutti gli effetti gli Embeddings non sono limitati ai testi ma possono essere utilizzati per le immagini, per i suoni, in pratica in tutti i contesti in cui possono essere applicati i modelli di ML (Machine Learning). Ma e' troppo lungo per parlarne...

Troppo lungo da trattare qui, quindi semplicemente riporto alcuni link interessanti di cui suggerisco la lettura se interessati all'uso di pgvector in applicazioni AI: PostgreSQL as a Vector Database: A pgvector Tutorial, What is pgvector and How Can It Help You?, How to use PostgreSQL to store and query vector embeddings, ...

Storia

L'estensione pgvector e' disponibile dal 2021 ed ha avuto una significativa evoluzione in termini di funzionalita' incrementando il numero di dimensioni e le funzioni di confronto tra vettori. Questa paginetta e' aggiornata alla versione 0.7 disponibile dal 2024-04 perche' contiene parecchie nuove funzionalita'.

(Sources: Changelog )

Version
Features
Last release
Date (from)
Notes
0.7 New datatypes: halfvec, sparsevec; new functions: binary_quantize, hamming_distance, jaccard_distance, l2_normalize, subvector; CPU dispatching on Linux x86-64, comparison for vectors with different dimensions, ... 0.7.42024-04
0.6 Improved performance of HNSW, use external storage for vectors, ... Desupport: PG11 0.6.22024-01
0.5 HNSW index type, l1_distance fuction, sum() aggregate function for vectors, ... 0.5.12023-08
0.4 Use extended storage for vectors (was plain), max dimensions for vector from 1024 to 16000, max dimensions for index from 1024 to 2000, avg() aggregate function for vectors, ... Desupport: PG10 0.4.42023-01
0.3 PG15, many fixes, ... Desupport: PG9.6 0.3.22022-10
0.2 PG14, (0.2.3 2022-01): indexing progress, ... 0.2.72021-10
0.1 First release! 0.1.82021-04

Per le versioni di PostgreSQL e' possibile fare riferimento a questa paginetta.

Varie ed eventuali

Anche se pgvector non e' una core extension moltissimi Cloud Provider hanno deciso di inserirla tra le estensioni preinstallate e quindi e' sufficiente il comando di CREATE EXTENSION per utilizzarla immediatamente.

PostgreSQL e' stato uno dei primi database relazionali a consentire la memorizzazione e la ricerca efficiente degli embeddings nella base dati in anticipo di quasi due anni rispetto alla maggioranza degli altri DB relazionali. Altri esempi di database Open Source che hanno inserito la fuzionalita' della vector search sono: ClickHouse, OpenSearch, Cassandra, ... E' importante riportare anche la nascita di diversi vector database, generalmente molto efficienti nella gestione degli Embedding ma a cui mancano spesso funzionalita' quali la gestione dei dati tradizionali, le transazioni, la replica, ... Tra questi riportiamo: Milvus, Vespa, Weaviate, Qdrant, ...

La documentazione in linea di pgvector e' molto completa e come la documentazione ufficiale riporta tutti i dettagli su PostgreSQL...


Titolo: pgvector
Livello: Intermedio (2/5)
Data: 1 Aprile 2023
Versione: 1.0.3 - 15 Agosto 2024
Autore: mail [AT] meo.bogliolo.name