Înglobări: teorie și practică
Fiecare sistem de căutare semantică, fiecare conductă RAG și fiecare aplicație AI care funcționează cu limbajul natural are o componentă fundamentală în comun: cel înglobări. Eu sunt traducerea a sensului în numere, puntea dintre lumea textului și cea a matematicii. Fără încorporare, o bază de date nu ar putea distinge „câine” de „mașină” - cu încorporare, știe că „câine” și mai aproape de „pisica” decât de „pâine de pâine”.
În primul articol din această serie am configurat pgvector și a învățat să salveze și vectori de interogare în PostgreSQL. Dar de unde vin acei vectori? Cum se generează o încorporare a calitate? Și mai presus de toate, ce model să alegi dintre zecile disponibile? În acest articol răspundem la toate aceste întrebări, de la teoria matematică la practica cu Python și PostgreSQL.
Prezentare generală a seriei
| # | Articol | Concentrează-te |
|---|---|---|
| 1 | pgvector | Instalare, operatori, indexare |
| 2 | Sunteți aici - Embeddings | Modele, distante, generatie |
| 3 | RAG cu PostgreSQL | Conductă RAG de la capăt la capăt |
| 4 | Căutare avansată de similaritate | Căutare hibridă, filtrare |
| 5 | Indexare și performanță | HNSW, IVFFlat, tuning |
| 6 | RAG în producție | Monitorizare, scalare, CI/CD |
Ce vei învăța
- Ce este o încorporare și de ce este fundamentală pentru IA modernă
- Evoluția istorică: de la codificare one-hot la Word2Vec, GloVe, BERT și Sentence Transformers
- Proprietățile matematice ale înglobărilor: analogii vectoriale și clustering semantic
- Cele patru metrici de distanță cu formule și cazuri de utilizare
- Cum se generează înglobări cu Python: local și prin API
- Cum să salvați și să interogați înglobările în PostgreSQL cu pgvector
- Încorporare multimodale: text, imagini, audio și cod
- Cum se evaluează calitatea unui model de încorporare (MTEB)
- Costuri și strategii de scalare pentru milioane de documente
1. Ce sunt înglobările
Un încorporarea și o reprezentare vectorială densă a unui obiect (cuvânt, propoziție, document, imagine) într-un spațiu continuu cu dimensionalitate redusă. În termeni practici, este o matrice de numere în virgulă mobilă care surprinde „sensul” acelui obiect.
# L'embedding della frase "Il gatto dorme sul divano"
# generato con text-embedding-3-small di OpenAI (1536 dimensioni)
embedding = [
0.0231, -0.0456, 0.0891, -0.0123, 0.0567, -0.0234,
0.0789, -0.0345, 0.0123, -0.0678, 0.0456, -0.0891,
# ... altri 1524 valori ...
]
print(f"Tipo: {type(embedding)}") # <class 'list'>
print(f"Dimensioni: {len(embedding)}") # 1536
Perspectiva cheie este aceasta: într-un spațiu vectorial bine antrenat, distanta geometrica între doi vectori reflectă asemănarea semantică dintre conceptele pe care le reprezintă. Expresii cu semnificație similară vor avea vectori apropiați, propozițiile cu semnificații diferite vor fi departe.
Caracteristicile înglobărilor
| Proprietate | Descriere | Exemplu |
|---|---|---|
| Dens | Fiecare dimensiune are o valoare diferită de zero | [0,023, -0,045, 0,089, ...] |
| Continua | Valori reale, nu discrete | Fiecare componentă este un float32/float16 |
| Dimensionalitate fixă | Același model produce întotdeauna vectori de aceeași lungime | dimensiuni 384, 768, 1536 sau 3072 |
| Semantic semnificativ | Distanțele dintre vectori reflectă relații de sens | sim ("pisica", "pisica") > sim ("pisica", "masina") |
Dacă ne gândim la spațiul de încorporare ca pe o hartă, concepte similare formează „cartiere”: animale într-o zonă, vehicule în alta, emoții în alta. Dar frumusețea stă în faptul că aceste relații apar automat din antrenament, nu vin programat manual.
2. De la cuvinte la vectori: evoluție istorică
Istoria înglobărilor este o progresie a ideilor din ce în ce mai sofisticate, fiecare care rezolvă limitările celui precedent. Înțelegerea acestei evoluții ajută la înțelegerea de ce modelele moderne funcționează atât de bine.
2.1 Codificare One-Hot (anii 1990)
Cea mai simplă abordare: fiecare cuvânt este reprezentat de un vector cu un singur 1 și tot celelalte 0. Dacă vocabularul are V cuvinte, fiecare vector are V dimensiuni.
# Vocabolario: ["gatto", "cane", "pesce", "auto", "moto"]
# Dimensione vettore = dimensione vocabolario = 5
gatto = [1, 0, 0, 0, 0]
cane = [0, 1, 0, 0, 0]
pesce = [0, 0, 1, 0, 0]
auto = [0, 0, 0, 1, 0]
moto = [0, 0, 0, 0, 1]
# Problema 1: la distanza tra "gatto" e "cane" e uguale
# alla distanza tra "gatto" e "auto"
import numpy as np
dist_gatto_cane = np.linalg.norm(
np.array(gatto) - np.array(cane)
) # sqrt(2) = 1.414
dist_gatto_auto = np.linalg.norm(
np.array(gatto) - np.array(auto)
) # sqrt(2) = 1.414 -- identica!
# Problema 2: con un vocabolario di 100.000 parole,
# ogni vettore ha 100.000 dimensioni (sparso, inefficiente)
Limitele codificării One-Hot
Dimensionalitate explozivă: pentru un vocabular de 100.000 de cuvinte, fiecare vector are 100.000 dimensiuni, aproape toate la zero. Fără informații semantice: toți vectorii sunt echidistanți între ei. „Pisica” este la fel de îndepărtată de „felină” ca și de „cutremur”. Această abordare nu surprinde nicio relație de sens între cuvinte.
2.2 TF-IDF (frecvența termenului - frecvența inversă a documentului)
Un pas mai departe: în loc de 0/1, componentele vectoriale indică cât de important este un cuvânt într-un document comparativ cu întregul corpus. Dar fiecare document devine un vector împrăștiat în dimensionalitatea vocabularului.
from sklearn.feature_extraction.text import TfidfVectorizer
documenti = [
"il gatto dorme sul divano",
"il cane gioca nel giardino",
"l'automobile corre sulla strada",
"il felino riposa sulla poltrona",
]
vectorizer = TfidfVectorizer()
tfidf_matrix = vectorizer.fit_transform(documenti)
# Risultato: matrice sparsa (4 documenti x N termini)
print(f"Shape: {tfidf_matrix.shape}") # (4, 14)
print(f"Termini: {vectorizer.get_feature_names_out()}")
# Problema: "gatto dorme" e "felino riposa" sono lontani
# perchè usano parole diverse, anche se il significato e simile
from sklearn.metrics.pairwise import cosine_similarity
sim = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[3:4])
print(f"Similarità gatto-felino: {sim[0][0]:.3f}") # ~0.07 (bassa!)
TF-IDF îmbunătățește codificarea one-hot prin ponderarea cuvintelor după importanță, dar suferă de același lucru problema fundamentala: nu intelege ca "pisica" si "pisica" sunt sinonime, pentru ca gandeste numai pe potriviri lexicale exacte.
2.3 Word2Vec: Revoluția (2013)
În 2013, Tomas Mikolov și echipa sa de la Google au lansat Word2Vec, care a schimbat totul. Ideea geniala: un cuvânt este definit de contextul în care apare. Cuvinte care apar în contexte similare vor avea reprezentări similare.
Word2Vec folosește rețele neuronale de mică adâncime pentru a învăța vectori denși (de obicei 100-300 de dimensiuni) din corpuri mari de text. Două arhitecturi:
Arhitecturi Word2Vec
| Arhitectură | Intrare | Ieșiri | Descriere |
|---|---|---|---|
| CBOW | Cuvinte de context | Cuvânt țintă | Prezice cuvântul central dat fiind contextul înconjurător |
| Skip-gram | Cuvânt țintă | Cuvinte de context | Prezice cuvintele din jur având în vedere cuvântul central |
from gensim.models import Word2Vec
# Corpus di esempio (in produzione: milioni di frasi)
frasi = [
["il", "gatto", "dorme", "sul", "divano"],
["il", "cane", "gioca", "nel", "giardino"],
["il", "felino", "riposa", "sulla", "poltrona"],
["il", "cane", "corre", "nel", "parco"],
]
# Addestramento Word2Vec (Skip-gram)
model = Word2Vec(
sentences=frasi,
vector_size=100, # dimensionalità embedding
window=5, # contesto: 5 parole prima e dopo
min_count=1, # includi parole con almeno 1 occorrenza
sg=1, # 1 = Skip-gram, 0 = CBOW
epochs=100
)
# Ora "gatto" e "felino" sono vicini!
print(model.wv.most_similar("gatto", topn=3))
# [('felino', 0.92), ('cane', 0.85), ('dorme', 0.71)]
# Accesso al vettore
vettore_gatto = model.wv["gatto"]
print(f"Dimensioni: {vettore_gatto.shape}") # (100,)
print(f"Primi 5: {vettore_gatto[:5]}")
2.4 GloVe: Global Vectors (2014)
Stanford a dezvoltat GloVe (Global Vectors for Word Representation) cu o abordare diferită: în loc de o rețea neuronală, GloVe factorizează matricea de co-ocurență corpus global. Combină avantajele metodelor statistice globale (cum ar fi LSA) cu acelea a contextului local Word2Vec.
GloVe minimizează o funcție de cost care asigură că produsul punctual dintre doi vectori de cuvinte este proporțională cu logaritmul probabilității lor de co-apariție:
2.5 FastText: încorporare de subcuvinte (2016)
Facebook AI Research (FAIR) a extins Word2Vec cu FastText, pe care o reprezintă fiecare cuvânt ca un set de n-grame de caractere. Aceasta rezolvă două probleme critice:
- Cuvinte rare sau în afara vocabularului (OOV): FastText poate genera înglobări pentru cuvinte nevăzute anterior prin compunerea vectorilor sub-segment
- Morfologie: cuvintele înrudite morfologic (de exemplu, „a alerga”, „a alergat”, „a alergat”) au n-grame în comun și, prin urmare, au vectori similari
Evoluție: de la reprezentări rare la dense
| Metodă | An | Tip | Dimensiuni tipice | Semantică |
|---|---|---|---|---|
| Una fierbinte | - | împrăștiat | V (vocabular) | Nici unul |
| TF-IDF | 1972 | împrăștiat | V (vocabular) | Statistici |
| Word2Old | 2013 | Dens | 100-300 | Contextual local |
| mănușă | 2014 | Dens | 50-300 | Global + local |
| FastText | 2016 | Dens | 100-300 | Subcuvânt + context |
| BERT | 2018 | Dens | 768 | Dinamica contextuală |
| Transformatori de propoziții | 2019 | Dens | 384-1024 | Propoziții întregi |
3. Proprietăţile matematice ale înglobărilor
Una dintre cele mai fascinante descoperiri ale Word2Vec este că spațiul vectorial învață relaţii algebrice între concepte. Operații aritmetice pe vectori produce rezultate coerente din punct de vedere semantic.
3.1 Analogii vectoriale
Celebra analogie: rege - bărbat + femeie = regină. În termeni vectoriali, diferența dintre „rege” și „bărbat” surprinde conceptul de „regalitate” și adăugându-l la „femeie” primești „regina”. Oficial:
import gensim.downloader as api
# Carica embeddings GloVe pre-addestrati
model = api.load("glove-wiki-gigaword-100")
# king - man + woman = ?
result = model.most_similar(
positive=["king", "woman"],
negative=["man"],
topn=3
)
print(result)
# [('queen', 0.7698), ('princess', 0.6450), ('monarch', 0.6345)]
# Altre analogie che funzionano:
# Parigi - Francia + Italia = Roma
result2 = model.most_similar(
positive=["paris", "italy"],
negative=["france"],
topn=1
)
print(result2) # [('rome', 0.8722)]
# buono - cattivo + triste = ?
result3 = model.most_similar(
positive=["good", "sad"],
negative=["bad"],
topn=1
)
print(result3) # [('happy', 0.6891)]
3.2 Clustering semantic
Înglobările formează în mod natural clustere în spațiul vectorial. Dacă proiectăm vectori în 2D (folosind t-SNE sau UMAP), observăm că cuvintele din aceeași categorie se grupează: animale în apropierea animalelor, țări în apropierea țărilor, profesii în apropiere la profesii.
Această proprietate este fundamentală pentru aplicațiile practice: căutarea de similaritate funcționează tocmai pentru că documentele pe subiecte similare au înglobări apropiate în spațiul vectorial.
4. Embeddings moderne: Embeddings contextuale
Word2Vec și GloVe generează a un singur vector pentru cuvânt, independent de context. Dar „școală” are semnificații diferite în „birou de școală” și „școală de pești”. THE înglobări contextuale, introdus cu BERT în 2018, rezolvă această problemă: același cuvânt are vectori diferiți în funcție de context.
4.1 Înglobări BERT
BERT (Bidirectional Encoder Representations from Transformers) procesează întreaga propoziție și ieșirile un vector pentru fiecare jeton. Pentru a încorpora întreaga propoziție, utilizați de obicei:
- Jetoane CLS: primul simbol special [CLS] conține o reprezentare agregată a propoziției
- Adunarea medie: media tuturor vectorilor token - produce în general rezultate mai bune pentru căutarea de similaritate
BERT nu este optim pentru căutarea de similaritate
BERT inițial nu a fost instruit pentru a produce încorporare de sentințe de calitate. Jetonul CLS și optimizat pentru clasificare, nu pentru similaritate semantică. Pentru căutarea de similaritate, sunt necesare modele specializate precum Sentence Transformers.
4.2 Transformatori de propoziții (SBERT)
În 2019, Reimers și Gurevych au introdus Sentence-BERT, perfecționând BERT cu un Siameze pentru a produce încorporari semnificative de propoziții. Acest lucru a revoluționat asemănarea căutare: pentru prima dată, a fost posibil să se compare propoziții cu o distanță cosinus simplă, obtinerea de rezultate de inalta calitate.
4.3 Modele de încorporare: comparație cuprinzătoare
Compararea modelelor de încorporare (2026)
| Model | Furnizorii | Dimensiuni | Scorul MTEB | Cost / 1 M token | Note |
|---|---|---|---|---|---|
| text-incorporare-3-mic | OpenAI | 1536 | 62.3 | 0,02 USD | Raport calitate/pret bun |
| text-incorporare-3-mare | OpenAI | 3072 | 64,6 | 0,13 USD | Calitate maximă OpenAI |
| încorporați-v3 | Coere | 1024 | 64,5 | 0,10 USD | Suporta peste 100 de limbi |
| călătorie-3 | Voyage AI | 1024 | 67.1 | 0,06 USD | Top pentru recuperare |
| toate-MiniLM-L6-v2 | HuggingFace | 384 | 56.3 | Gratuit | Rapid, local, compact |
| all-mpnet-base-v2 | HuggingFace | 768 | 57,8 | Gratuit | Cel mai bun model de bază open-source |
| gte-large-en-v1.5 | Alibaba (HF) | 1024 | 65.4 | Gratuit | Competitiv cu modelele comerciale |
| bge-large-en-v1.5 | BAAI (HF) | 1024 | 64.2 | Gratuit | Excelent pentru RAG |
Cum să alegi modelul
- Prototip/buget limitat: all-MiniLM-L6-v2 (gratuit, rapid, 384 dim)
- Productie, costuri reduse: text-embedding-3-small (OpenAI, token de 0,02 USD/1 milion)
- Recuperare de cea mai înaltă calitate: voyage-3 sau gte-large-en-v1.5
- Multilingv: Cohere embed-v3 (100+ limbi)
- Auto-găzduit / confidențialitate: bge-large-en-v1.5 sau gte-large-en-v1.5
5. Măsurătorile distanței între vectori
Alegerea metricii distanței influențează direct calitatea căutării de similaritate. Să vedem cele patru metrici principale cu formulele lor matematice, punctele lor forte și când să le folosești.
5.1 Similitudinea cosinusului
Cea mai utilizată măsură pentru încorporarea textului. Măsoară unghiul dintre doi vectori, ignorând amploarea (lungimea) acestora. Doi vectori îndreptați în aceeași direcție au asemănarea cosinusului 1, ortogonală 0, opuse -1.
În pgvector, operatorul <=> calculați distanta cosinus
(= 1 - asemănarea cosinusului), unde 0 înseamnă identic și 2 înseamnă contrarii.
5.2 Distanța euclidiană (L2)
Distanța „în aer liber” dintre două puncte din spațiu. Se ia în considerare atât direcție decât a mărimii vectorilor.
În pgvector, operatorul <-> calculați distanța L2.
5.3 Produs punctual (Produs punctual)
Produsul punctual măsoară atât direcția, cât și mărimea. Pentru vectori normalizați (normă = 1), produsul punctual este echivalent cu asemănarea cosinusului.
În pgvector, operatorul <#> calculați produsul intern negativ (pentru compatibilitate
cu COMANDA ASC).
5.4 Distanța până la Manhattan (L1)
Suma diferențelor absolute componentă cu componentă. Mai puțin sensibil la valori aberante faţă de distanţa euclidiană.
import numpy as np
from scipy.spatial.distance import cosine, euclidean, cityblock
# Due vettori di esempio (normalizzati)
a = np.array([0.5, 0.3, 0.8, 0.1, 0.6])
b = np.array([0.4, 0.35, 0.75, 0.15, 0.55])
# Normalizzazione L2
a_norm = a / np.linalg.norm(a)
b_norm = b / np.linalg.norm(b)
# 1. Cosine Similarity (1 - cosine distance)
cos_sim = 1 - cosine(a_norm, b_norm)
print(f"Cosine similarity: {cos_sim:.6f}") # ~0.999
# 2. Distanza Euclidea (L2)
l2_dist = euclidean(a_norm, b_norm)
print(f"Distanza L2: {l2_dist:.6f}") # ~0.042
# 3. Dot Product (per vettori normalizzati = cosine similarity)
dot = np.dot(a_norm, b_norm)
print(f"Dot product: {dot:.6f}") # ~0.999
# 4. Distanza Manhattan (L1)
l1_dist = cityblock(a_norm, b_norm)
print(f"Distanza L1: {l1_dist:.6f}") # ~0.072
# Relazione L2-Cosine per vettori normalizzati:
# d_L2^2 = 2 * (1 - cos_sim)
print(f"\nVerifica: L2^2 = {l2_dist**2:.6f}")
print(f"2*(1-cos) = {2*(1-cos_sim):.6f}") # uguale!
Când să utilizați ce metrică
| Metric | operator pgvector | Utilizați Când | Evitați Când |
|---|---|---|---|
| Lucruri mărunte | <=> |
Încorporarea textului, când magnitudinea nu contează | Date spațiale în care magnitudinea este semnificativă |
| L2 (euclidian) | <-> |
Imagini, date numerice, când magnitudinea contează | Vectori cu scări diferite între componente |
| Produs punct | <#> |
Vectori deja normalizați (performanță puțin mai bună) | Vectori nenormalizați (rezultate distorsionate după mărime) |
| Manhattan (L1) | Non-nativ în pgvector | Date rare, robustețe la valori aberante | Utilizare generală cu înglobări dense |
Regulă practică
Pentru 95% din cazurile cu încorporare de text, utilizați distanta cosinus
(<=> în pgvector). Modelele moderne de încorporare produc vectori
deja normalizat, ceea ce face ca produsele cosinus și punctual să fie practic echivalente. Distanța
Euclidian are sens pentru date spațiale sau atunci când magnitudinea vectorului poartă informații.
6. Generați încorporare în Python
Să vedem acum cum să generăm înglobări cu trei abordări diferite: modele locale cu Sentence Transformers, OpenAI API și HuggingFace Inference API. Fiecare abordare are avantaje specifice și compromisuri.
6.1 Transformatori de propoziții (locale)
Cea mai flexibilă și privată abordare: modelul rulează pe mașina dvs., fără date iese din rețea, fără costuri pentru apelul API.
# pip install sentence-transformers
from sentence_transformers import SentenceTransformer
import numpy as np
# Carica il modello (scaricato automaticamente al primo uso)
model = SentenceTransformer("all-MiniLM-L6-v2")
# Embedding di una singola frase
frase = "PostgreSQL e un database relazionale open-source"
embedding = model.encode(frase)
print(f"Tipo: {type(embedding)}") # numpy.ndarray
print(f"Dimensioni: {embedding.shape}") # (384,)
# Embedding di più frasi (batch - molto più efficiente)
frasi = [
"PostgreSQL e un database relazionale open-source",
"pgvector aggiunge il supporto per vettori a PostgreSQL",
"Il machine learning richiede grandi quantità di dati",
"La pizza margherita e un piatto tipico napoletano",
]
embeddings = model.encode(
frasi,
batch_size=32, # processa 32 frasi alla volta
show_progress_bar=True, # mostra progresso per batch grandi
normalize_embeddings=True # normalizza a norma L2 = 1
)
print(f"Shape: {embeddings.shape}") # (4, 384)
# Calcola similarità tra tutte le coppie
from sentence_transformers.util import cos_sim
similarities = cos_sim(embeddings, embeddings)
print(f"\nMatrice di similarità:\n{similarities}")
# Le prime 2 frasi (su PostgreSQL) avranno alta similarità
# La frase sulla pizza sarà distante dalle altre
6.2 OpenAI Embedding API
API-ul OpenAI oferă modele de înaltă calitate fără management al infrastructurii. Ideal pentru producție de volum moderat.
# pip install openai
from openai import OpenAI
import os
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
def get_embeddings(
texts: list[str],
model: str = "text-embedding-3-small"
) -> list[list[float]]:
"""Genera embeddings per una lista di testi."""
response = client.embeddings.create(
input=texts,
model=model,
)
return [item.embedding for item in response.data]
# Singolo embedding
testo = "PostgreSQL come vector database per AI"
embedding = get_embeddings([testo])[0]
print(f"Dimensioni: {len(embedding)}") # 1536
# Batch di embeddings (fino a 2048 testi per chiamata)
testi = [
"Come installare pgvector su Docker",
"Tutorial per similarity search in PostgreSQL",
"Guida alla cottura della pasta al forno",
]
embeddings = get_embeddings(testi)
print(f"Embeddings generati: {len(embeddings)}") # 3
# Dimensione ridotta con text-embedding-3-small
# Puoi specificare dimensioni inferiori per risparmiare spazio
response = client.embeddings.create(
input=["Testo di esempio"],
model="text-embedding-3-small",
dimensions=512 # ridotto da 1536 a 512
)
emb_ridotto = response.data[0].embedding
print(f"Dimensioni ridotte: {len(emb_ridotto)}") # 512
6.3 API-ul HuggingFace Inference
Un compromis între modelele locale și API-urile comerciale: acces la mii de modele open-source prin API, cu un nivel generos gratuit.
# pip install huggingface_hub
from huggingface_hub import InferenceClient
import os
client = InferenceClient(
token=os.getenv("HF_TOKEN")
)
def get_hf_embeddings(
texts: list[str],
model: str = "BAAI/bge-large-en-v1.5"
) -> list[list[float]]:
"""Genera embeddings usando HuggingFace Inference API."""
result = client.feature_extraction(
text=texts,
model=model,
)
return result
# Genera embeddings
testi = [
"Vector search con PostgreSQL e pgvector",
"Come creare indici HNSW per ricerca veloce",
]
embeddings = get_hf_embeddings(testi)
print(f"Embeddings: {len(embeddings)}") # 2
print(f"Dimensioni: {len(embeddings[0])}") # 1024 (bge-large)
6.4 Procesare eficientă în loturi
Atunci când trebuie să generați încorporare pentru mii sau milioane de documente, eficiență procesarea loturilor devine critică.
import time
from typing import Generator
from sentence_transformers import SentenceTransformer
import numpy as np
def chunk_list(
lst: list, chunk_size: int
) -> Generator[list, None, None]:
"""Divide una lista in chunk di dimensione fissa."""
for i in range(0, len(lst), chunk_size):
yield lst[i:i + chunk_size]
def generate_embeddings_batch(
texts: list[str],
model_name: str = "all-MiniLM-L6-v2",
batch_size: int = 256,
device: str = "cpu" # "cuda" per GPU
) -> np.ndarray:
"""Genera embeddings in batch con progress tracking."""
model = SentenceTransformer(model_name, device=device)
all_embeddings = []
total_batches = (len(texts) + batch_size - 1) // batch_size
start = time.time()
for i, batch in enumerate(chunk_list(texts, batch_size)):
batch_emb = model.encode(
batch,
batch_size=batch_size,
normalize_embeddings=True,
show_progress_bar=False
)
all_embeddings.append(batch_emb)
elapsed = time.time() - start
rate = (i + 1) * batch_size / elapsed
print(
f"Batch {i+1}/{total_batches} - "
f"{rate:.0f} testi/sec"
)
return np.vstack(all_embeddings)
# Utilizzo
texts = [f"Documento numero {i}" for i in range(10_000)]
embeddings = generate_embeddings_batch(
texts,
batch_size=256,
device="cuda" # usa GPU se disponibile
)
print(f"Shape finale: {embeddings.shape}") # (10000, 384)
7. Istoricizarea înglobărilor în PostgreSQL
Acum că știm cum să generăm înglobări, să vedem cum să le salvăm în PostgreSQL cu pgvector și efectuați interogări de căutare de similaritate. Acesta este linkul practic către articolul 1 a seriei.
7.1 Dispunerea tabelului
-- Abilita pgvector
CREATE EXTENSION IF NOT EXISTS vector;
-- Tabella documenti con embedding
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(500) NOT NULL,
content TEXT NOT NULL,
source VARCHAR(255),
category VARCHAR(100),
embedding vector(384), -- dimensione del modello scelto
created_at TIMESTAMPTZ DEFAULT NOW(),
metadata JSONB DEFAULT '{}'::jsonb
);
-- Indice HNSW per ricerca veloce (cosine distance)
CREATE INDEX idx_documents_embedding
ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);
-- Indice su categoria per filtri combinati
CREATE INDEX idx_documents_category
ON documents (category);
7.2 Inserarea din Python
import psycopg2
from psycopg2.extras import execute_values
from sentence_transformers import SentenceTransformer
import numpy as np
# Configurazione
DB_CONFIG = {
"host": "localhost",
"port": 5432,
"dbname": "vectordb",
"user": "admin",
"password": "secret_password",
}
# 1. Genera embeddings
model = SentenceTransformer("all-MiniLM-L6-v2")
documenti = [
{
"title": "Introduzione a pgvector",
"content": "pgvector e un'estensione PostgreSQL per vettori...",
"source": "blog",
"category": "database"
},
{
"title": "RAG con LangChain",
"content": "Retrieval Augmented Generation combina retrieval...",
"source": "tutorial",
"category": "ai"
},
{
"title": "Python per Data Science",
"content": "Python e il linguaggio più usato per data science...",
"source": "guide",
"category": "programming"
},
]
# Genera embeddings per i contenuti
testi = [d["content"] for d in documenti]
embeddings = model.encode(testi, normalize_embeddings=True)
# 2. Salva in PostgreSQL
conn = psycopg2.connect(**DB_CONFIG)
cur = conn.cursor()
# Prepara i dati per batch insert
values = []
for doc, emb in zip(documenti, embeddings):
values.append((
doc["title"],
doc["content"],
doc["source"],
doc["category"],
emb.tolist() # converti numpy array in lista Python
))
# Inserimento batch efficiente
execute_values(
cur,
"""INSERT INTO documents
(title, content, source, category, embedding)
VALUES %s""",
values,
template="(%s, %s, %s, %s, %s::vector)"
)
conn.commit()
print(f"Inseriti {len(values)} documenti con embeddings")
cur.close()
conn.close()
7.3 Căutare de similaritate din Python
def similarity_search(
query: str,
top_k: int = 5,
category: str = None,
threshold: float = 0.3
) -> list[dict]:
"""Cerca documenti simili alla query."""
# Genera embedding della query
query_embedding = model.encode(
query, normalize_embeddings=True
).tolist()
conn = psycopg2.connect(**DB_CONFIG)
cur = conn.cursor()
# Query con filtro opzionale
if category:
cur.execute("""
SELECT id, title, content, category,
1 - (embedding <=> %s::vector) AS similarity
FROM documents
WHERE category = %s
AND 1 - (embedding <=> %s::vector) > %s
ORDER BY embedding <=> %s::vector
LIMIT %s
""", (
query_embedding, category,
query_embedding, threshold,
query_embedding, top_k
))
else:
cur.execute("""
SELECT id, title, content, category,
1 - (embedding <=> %s::vector) AS similarity
FROM documents
WHERE 1 - (embedding <=> %s::vector) > %s
ORDER BY embedding <=> %s::vector
LIMIT %s
""", (
query_embedding,
query_embedding, threshold,
query_embedding, top_k
))
results = []
for row in cur.fetchall():
results.append({
"id": row[0],
"title": row[1],
"content": row[2][:200], # troncato
"category": row[3],
"similarity": round(row[4], 4),
})
cur.close()
conn.close()
return results
# Esempio d'uso
risultati = similarity_search(
"come usare i vettori in un database",
top_k=3,
category="database"
)
for r in risultati:
print(f"[{r['similarity']}] {r['title']}")
7.4 Indexare: HNSW vs IVFFlat
Pentru seturile de date cu mai mult de câteva mii de documente, un index este esențial performanță acceptabilă. pgvector oferă două tipuri de index:
HNSW vs IVFFlat
| Caracteristică | HNSW | IVFFlat |
|---|---|---|
| Viteza de interogare | Foarte rapid | Rapid |
| Amintiți-vă | 95-99% | 85-95% |
| Construiește timp | Încet (minute) | Rapid (secunde) |
| Memorie | Ridicat (grafic în RAM) | Scăzut (centroizi) |
| Inserați/Actualizați | Bun (actualizare incrementală) | Necesită reconstrucție periodică |
| Recomandat pentru | Productie, calitate superioara | Prototipări, seturi de date statice |
-- HNSW (raccomandato per produzione)
-- m: connessioni per nodo (16-64, default 16)
-- ef_construction: qualità costruzione (64-512, default 64)
CREATE INDEX idx_hnsw_cosine
ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);
-- Per L2 distance
CREATE INDEX idx_hnsw_l2
ON documents
USING hnsw (embedding vector_l2_ops)
WITH (m = 16, ef_construction = 200);
-- IVFFlat (più veloce da costruire)
-- lists: numero di cluster (sqrt(N) come regola base)
CREATE INDEX idx_ivfflat_cosine
ON documents
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100); -- per ~10K documenti
-- Parametri di query per controllare recall vs velocità
SET hnsw.ef_search = 100; -- default 40, alza per più recall
SET ivfflat.probes = 10; -- default 1, alza per più recall
8. Înglobare pentru diferite tipuri de date
Încorporarile nu se limitează la text. Modelele moderne pot genera reprezentări grafică vectorială pentru imagini, audio, cod sursă și chiar date multimodale.
Înglobări multimodale: modele de tip de date
| Tip de date | Model | Dimensiuni | Caz de utilizare |
|---|---|---|---|
| Text | all-MiniLM-L6-v2, text-incorporare-3-mic | 384-3072 | Căutare semantică, RAG, clasificare |
| Imagini | CLIP (OpenAI), SigLIP (Google) | 512-768 | Căutare de imagini, clasificare vizuală |
| Audio | Şopteşte, CLAP | 512-1280 | Căutare audio, clasificare muzicală |
| Cod | CodeBERT, înglobare StarCoder | 768 | Căutarea codului, detectarea dublelor |
| Multimodal | CLIP, ImageBind (Meta) | 512-1024 | Căutare intermodală (text după imagine) |
# pip install transformers pillow
from transformers import CLIPProcessor, CLIPModel
from PIL import Image
import torch
import numpy as np
# Carica CLIP
model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
# Embedding di un'immagine
image = Image.open("foto_gatto.jpg")
inputs = processor(images=image, return_tensors="pt")
with torch.no_grad():
image_embedding = model.get_image_features(**inputs)
image_emb = image_embedding[0].numpy()
print(f"Image embedding: {image_emb.shape}") # (512,)
# Embedding di testo (nello STESSO spazio!)
text_inputs = processor(
text=["un gatto che dorme", "un cane che gioca"],
return_tensors="pt",
padding=True
)
with torch.no_grad():
text_embeddings = model.get_text_features(**text_inputs)
text_embs = text_embeddings.numpy()
# Calcola similarità cross-modale
from numpy.linalg import norm
for i, text in enumerate(["un gatto che dorme", "un cane che gioca"]):
sim = np.dot(image_emb, text_embs[i]) / (
norm(image_emb) * norm(text_embs[i])
)
print(f"Similarità '{text}': {sim:.4f}")
# "un gatto che dorme" avra similarità più alta con foto_gatto.jpg
Puterea CLIP este că textul și imaginile trăiesc în el același spațiu vectorial. Puteți căuta imagini cu o interogare de text sau puteți găsi text legat de o imagine. Aceasta deschide scenarii precum căutarea multimodală în PostgreSQL: salvați încorporarea CLIP în el tabel pgvector și cercuri cu interogări de text.
9. Evaluați calitatea înglobărilor
De unde știi dacă un model de încorporare este „bun”? Răspunsul depinde de sarcină există benchmark-uri și metrici obiective specifice, dar standardizate.
9.1 MTEB: Massive Text Embedding Benchmark
MTEB este reperul de referință pentru evaluarea modelelor de încorporare. Măsurați performanța pe peste 58 de sarcini grupate în 8 categorii:
- Recuperare: găsiți documente relevante în cazul unei interogări
- Similaritate semantică textuală (STS): cât de asemănătoare sunt două propoziții
- Clasificare: clasifica textele pe categorii
- Clustering: grupează texte similare
- Clasificarea perechilor: stabiliți dacă două texte sunt legate
- Reclasificare: reordonează rezultatele după relevanță
- Rezumat: calitatea rezumatelor
- BitextMining: găsiți traduceri paralele
from sentence_transformers import SentenceTransformer
from sentence_transformers.evaluation import (
InformationRetrievalEvaluator
)
# Prepara dataset di valutazione
queries = {
"q1": "come installare pgvector",
"q2": "cos'è la similarity search",
"q3": "embedding di immagini con CLIP",
}
corpus = {
"d1": "Guida installazione pgvector su Ubuntu",
"d2": "pgvector per PostgreSQL: setup Docker",
"d3": "Ricerca per similarità vettoriale",
"d4": "CLIP: modello multimodale per immagini e testo",
"d5": "Ricetta pasta alla carbonara",
}
# Mappatura query -> documenti rilevanti
relevant = {
"q1": {"d1": 1, "d2": 1}, # d1 e d2 rilevanti per q1
"q2": {"d3": 1},
"q3": {"d4": 1},
}
# Valuta il modello
model = SentenceTransformer("all-MiniLM-L6-v2")
evaluator = InformationRetrievalEvaluator(
queries=queries,
corpus=corpus,
relevant_docs=relevant,
name="custom-eval"
)
results = evaluator(model)
print(f"NDCG@10: {results['custom-eval_ndcg@10']:.4f}")
print(f"MAP@10: {results['custom-eval_map@10']:.4f}")
9.2 Evaluare intrinsecă vs. extrinsecă
Două abordări ale evaluării
| Tip | Ce măsoară | Exemplu | Când să utilizați |
|---|---|---|---|
| Intrinsec | Proprietățile vectorilor înșiși | Analogii, clustering, STS | Comparație rapidă între modele |
| extrinseci | Performanță la sarcina finală | Calitate RAG, precizie de cercetare | Decizia finală în producție |
Sfaturi practice
Nu te baza doar pe scorul MTEB. Un model poate avea un scor MTEB mare, dar performanțe slabe pe domeniul dvs. specific. Evaluați întotdeauna setul de date: creați un set mic de interogări și documente relevante din domeniul dvs. și măsurați nDCG și MAP. Acest lucru vă va oferi o estimare mult mai fiabilă a performanței reale.
10. Reducerea dimensionalității
Vectorii cu dimensiuni înalte sunt greu de vizualizat și pot fi scumpi în ceea ce privește stocarea și calculul. Tehnici de reducere a dimensionalității ele ajută atât la vizualizare, cât și la optimizare.
10.1 Tehnici de vizualizare
Tehnici de reducere dimensională
| Tehnică | Păstrează | Viteză | Utilizare tipică |
|---|---|---|---|
| PCA | Varianta globala | Foarte rapid | Reducerea dimensiunii pentru depozitare, preprocesare |
| t-SNE | Structura locală | Lent | Vizualizarea 2D a clusterelor |
| UMAP | Structura locală + globală | Medie | Vizualizare 2D, de asemenea pentru reducerea pre-indexării |
# pip install umap-learn matplotlib
import umap
import matplotlib.pyplot as plt
import numpy as np
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("all-MiniLM-L6-v2")
# Testi con categorie diverse
testi = [
# Database
"PostgreSQL e un database relazionale",
"MongoDB e un database NoSQL",
"Redis e un database in-memory",
# AI/ML
"Il deep learning usa reti neurali profonde",
"GPT e un modello di linguaggio",
"La regressione lineare e un algoritmo semplice",
# Cucina
"La pizza si cuoce nel forno a legna",
"Il tiramisu e un dolce italiano",
"La pasta alla carbonara usa uova e guanciale",
]
categorie = ["DB"]*3 + ["AI"]*3 + ["Cucina"]*3
colori = ["blue"]*3 + ["red"]*3 + ["green"]*3
# Genera embeddings (384 dimensioni)
embeddings = model.encode(testi, normalize_embeddings=True)
# Riduci a 2D con UMAP
reducer = umap.UMAP(n_components=2, random_state=42)
emb_2d = reducer.fit_transform(embeddings)
# Visualizza
plt.figure(figsize=(10, 8))
for i, (x, y) in enumerate(emb_2d):
plt.scatter(x, y, c=colori[i], s=100, zorder=5)
plt.annotate(testi[i][:30], (x, y),
fontsize=8, ha='left')
plt.title("Embeddings in 2D (UMAP)")
plt.savefig("embeddings_umap.png", dpi=150)
plt.show()
10.2 Înglobări de matrioșcă
O tehnică recentă și inovatoare: i Învățare privind reprezentarea matrioșcă (MRL) înglobările sunt antrenate astfel încât primele N componente ale vectorului să fie deja o înglobare valabil. Puteți trunchia vectorul de la 1536 la 512 sau 256 dimensiuni, menținând în același timp o calitate bună.
OpenAI text-embedding-3-small e text-embedding-3-large sprijin
această tehnică: puteți specifica parametrul dimensions pentru a obține vectori
mai compact fără a recalcula înglobările.
from openai import OpenAI
import numpy as np
import os
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
testo = "PostgreSQL come vector database per applicazioni AI"
# Genera lo stesso embedding a dimensioni diverse
for dim in [256, 512, 1024, 1536]:
response = client.embeddings.create(
input=[testo],
model="text-embedding-3-small",
dimensions=dim
)
emb = response.data[0].embedding
print(f"Dimensioni: {dim}, norma: {np.linalg.norm(emb):.4f}")
# In PostgreSQL: usa colonne con dimensione appropriata
# CREATE TABLE docs_compact (
# id BIGSERIAL PRIMARY KEY,
# content TEXT,
# embedding vector(256) -- più compatto, 6x meno storage
# );
11. Scalare costuri și strategii
La trecerea de la prototip la producție, costurile de generare și stocare înglobările devin un factor critic. Să vedem o analiză detaliată.
11.1 Costuri pe 1 milion de documente
Cost estimat: 1 milion de documente (în medie 500 de jetoane/doc)
| Model | Costul de generare | Dimensiunea vectorului | Depozitare (float32) | Total inițial |
|---|---|---|---|---|
| toate-MiniLM-L6-v2 | 0 USD (local) | 384 | ~1,4 GB | Numai timpul GPU/CPU |
| text-incorporare-3-mic | ~10 USD | 1536 | ~5,7 GB | ~10 USD + spațiu de stocare |
| text-incorporare-3-mare | ~65 USD | 3072 | ~11,4 GB | ~65 USD + spațiu de stocare |
| călătorie-3 | ~30 USD | 1024 | ~3,8 GB | ~30 USD + spațiu de stocare |
Formula de depozitare: N documente * dimensiune * 4 octeți (float32). Exemplu: 1M * 1536 * 4 = 5,7 GB numai pentru operatori.
11.2 Auto-găzduit vs API: compromis
Comparație auto-găzduită vs API
| astept | Auto-găzduit | API (OpenAI, Cohere) |
|---|---|---|
| Costul initial | Ridicat (GPU ~1-3 USD/oră) | Scăzut (plată-pe-utilizare) |
| Costul pe volum | Cel mai ieftin peste ~10 milioane doc | Liniar, scale cu volum |
| Latența | Scăzut (fără rețea) | 50-200 ms per apel |
| Confidențialitate | Datele rămân on-premise | Date trimise către terți |
| Întreţinere | Gestionare GPU, actualizări, monitorizare | Zero |
| calitate | Depinde de modelul ales | În general ridicat și consistent |
11.3 Strategii de stocare în cache
Încorporarea memoriei cache este esențială pentru a reduce costurile și latența. Dacă la fel textul este solicitat de mai multe ori, nu are sens să regenerați încorporarea.
import hashlib
import json
import psycopg2
from typing import Optional
import numpy as np
class EmbeddingCache:
"""Cache embeddings in PostgreSQL per evitare ricalcoli."""
def __init__(self, db_config: dict):
self.db_config = db_config
self._init_table()
def _init_table(self):
conn = psycopg2.connect(**self.db_config)
cur = conn.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS embedding_cache (
content_hash VARCHAR(64) PRIMARY KEY,
model_name VARCHAR(100) NOT NULL,
embedding vector(1536),
created_at TIMESTAMPTZ DEFAULT NOW()
)
""")
conn.commit()
cur.close()
conn.close()
def _hash(self, text: str, model: str) -> str:
"""Hash deterministico del contenuto + modello."""
key = f"{model}::{text}"
return hashlib.sha256(key.encode()).hexdigest()
def get(
self, text: str, model: str
) -> Optional[list[float]]:
"""Cerca embedding in cache."""
h = self._hash(text, model)
conn = psycopg2.connect(**self.db_config)
cur = conn.cursor()
cur.execute(
"SELECT embedding FROM embedding_cache "
"WHERE content_hash = %s",
(h,)
)
row = cur.fetchone()
cur.close()
conn.close()
return row[0] if row else None
def put(
self, text: str, model: str, embedding: list[float]
):
"""Salva embedding in cache."""
h = self._hash(text, model)
conn = psycopg2.connect(**self.db_config)
cur = conn.cursor()
cur.execute(
"INSERT INTO embedding_cache "
"(content_hash, model_name, embedding) "
"VALUES (%s, %s, %s::vector) "
"ON CONFLICT (content_hash) DO NOTHING",
(h, model, embedding)
)
conn.commit()
cur.close()
conn.close()
12. Conexiune cu seria PostgreSQL AI
Să recapitulăm modul în care înglobările se integrează în ecosistemul pe care îl construim in aceasta serie:
Fluxul complet: de la articolul 1 la articolul 3
| Pas | Articol | Acţiune |
|---|---|---|
| 1 | pgvector (Art. 1) | Configurați PostgreSQL cu pgvector, creați tabele cu coloane vectoriale |
| 2 | Înglobări (Art. 2 - aceasta) | Alegeți șablonul, generați încorporații, salvați-le în pgvector |
| 3 | RAG cu PostgreSQL (Art. 3) | Combinați recuperarea prin pgvector cu LLM pentru a răspunde la întrebări |
În articolul 1 am pregătit infrastructura: PostgreSQL cu pgvector instalat, tabele cu coloane vectoriale și indici HNSW configurați. În acest articol am completat golul fundamental: de unde provin acei vectori, cum să alegeți modelul potrivit și cum să îi generați eficient. În articolul următor vom construi o conductă RAG completă care folosește totul această stivă: documente indexate în pgvector, înglobări generate din mers pentru interogări, și un LLM care generează răspunsuri bazate pe contextul preluat.
13. Concluzii și Lista de verificare
Embedding-urile sunt componenta fundamentală cu care se leagă limbajul natural matematica bazelor de date vectoriale. Alegerea modelului potrivit, metrica distanței o strategie de scalare adecvată și eficientă sunt decizii care au un impact direct calitatea și costurile sistemului dvs. AI.
Lista de verificare: Alegerea modelului de încorporare potrivit
- Definiți sarcina: regăsire, clasificare, grupare, STS?
- Identificați limba: Numai engleză, multilingv sau specific domeniului?
- Evaluați constrângerile: buget, confidențialitate, latență, infrastructură disponibilă
- Alegeți 2-3 candidați din tabelul model (secțiunea 4.3)
- Creați un set de evaluare din domeniul dvs. (50-100 de interogări cu documente relevante)
- Măsuri nDCG și MAP pe setul dvs. de date pentru fiecare candidat
- Calculați costurile complet operațional pentru volumul așteptat (secțiunea 11)
- Testați dimensiunea mică: 512 dims sunt adesea suficiente pentru multe cazuri de utilizare
- Implementați memorarea în cache pentru a reduce costurile de regenerare
- Monitorizați calitatea în timp cu setul dvs. de evaluare
Articolul următor: RAG cu PostgreSQL
În următorul articol din serie vom construi unul Conducta RAG finalizată (Retrieval Augmented Generation) folosind PostgreSQL + pgvector ca bază de cunoștințe. Vom vedea cum să combinăm căutarea de similaritate cu fragmentarea inteligentă, cum să integrăm LLM-uri cum ar fi GPT-4 și Claude și cum să măsurați calitatea răspunsurilor generate.







