Vložení: teorie a praxe
Každý sémantický vyhledávací systém, každý kanál RAG a každá aplikace AI, která pracuje s jazykem přírodní má společnou základní složku: vložení. Já jsem ten překlad významu v číslech, most mezi světem textu a světem matematiky. Bez vložek, databáze nedokázala rozlišit „pes“ od „auto“ – s vloženými vložkami ví, že „pes“ a blíže ke „kočce“ než k „toustovači“.
V prvním článku této série jsme nakonfigurovali pgvector a naučil se šetřit a dotazovací vektory v PostgreSQL. Ale odkud tyto vektory pocházejí? Jak vygenerovat vložení kvalita? A především, jaký model z desítek dostupných vybrat? V tomto článku odpovídáme na všechny tyto otázky, od matematické teorie po praxi s Pythonem a PostgreSQL.
Přehled série
| # | Položka | Soustředit |
|---|---|---|
| 1 | pgvector | Instalace, operátoři, indexace |
| 2 | Jste zde - Vložení | Modely, vzdálenosti, generace |
| 3 | RAG s PostgreSQL | End-to-end potrubí RAG |
| 4 | Pokročilé vyhledávání podobností | Hybridní vyhledávání, filtrování |
| 5 | Indexování a výkon | HNSW, IVFFlat, ladění |
| 6 | RAG ve výrobě | Monitorování, škálování, CI/CD |
Co se naučíte
- Co je to vložení a proč je zásadní pro moderní AI
- Historický vývoj: od jednorázového kódování k Word2Vec, GloVe, BERT a Sentence Transformers
- Matematické vlastnosti vložení: vektorové analogie a sémantické shlukování
- Čtyři metriky vzdálenosti se vzorci a případy použití
- Jak generovat vložení pomocí Pythonu: místní a přes API
- Jak uložit a dotazovat vložení v PostgreSQL pomocí pgvector
- Multimodální vložení: text, obrázky, zvuk a kód
- Jak vyhodnotit kvalitu modelu vkládání (MTEB)
- Náklady a strategie škálování pro miliony dokumentů
1. Co jsou vložení
Un vkládání a hustou vektorovou reprezentaci objektu (slova, věty, dokument, obrázek) v souvislém prostoru se sníženou rozměrností. V praxi se jedná o pole čísel s plovoucí desetinnou čárkou, která zachycuje „význam“ daného objektu.
# 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
Klíčový poznatek je tento: v dobře trénovaném vektorovém prostoru geometrická vzdálenost mezi dvěma vektory odráží sémantickou podobnost mezi pojmy, které představují. Fráze s podobným významem budou mít blízké vektory, věty s různými významy budou daleko.
Charakteristika vložení
| Vlastnictví | Popis | Příklad |
|---|---|---|
| Hustý | Každý rozměr má nenulovou hodnotu | [0,023, -0,045, 0,089, ...] |
| Pokračovat | Skutečné hodnoty, ne diskrétní | Každá komponenta je float32/float16 |
| Pevná rozměrnost | Stejný model vždy vytváří vektory stejné délky | Rozměry 384, 768, 1536 nebo 3072 |
| Sémanticky významné | Vzdálenosti mezi vektory odrážejí významové vztahy | sim("kočka", "kočka") > sim("kočka", "auto") |
Pokud uvažujeme o vnořeném prostoru jako o mapě, podobné koncepty tvoří „sousedství“: zvířata v jedné oblasti, vozidla v jiné, emoce v jiné. Ale krása spočívá v tom, že tyto vztahy vznikají automaticky z tréninku, nepřicházejí naprogramované ručně.
2. Od slov k vektorům: Historický vývoj
Historie vkládání je vývojem stále sofistikovanějších nápadů, každého z nich který řeší omezení předchozího. Pochopení tohoto vývoje pomáhá pochopit proč moderní modely fungují tak dobře.
2.1 One-Hot Encoding (90. léta)
Nejjednodušší přístup: každé slovo je reprezentováno vektorem s pouze jednou 1 a všemi ostatní 0. Pokud má slovní zásoba V slov, má každý vektor V rozměry.
# 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)
Limity One-Hot Encoding
Výbušná rozměrnost: pro slovní zásobu 100 000 slov má každý vektor 100 000 rozměrů, téměř vše na nule. Žádné sémantické informace: všechny vektory jsou stejně vzdálené mezi nimi. "Kočka" je stejně vzdálená od "kočičí" jako od "zemětřesení". Tento přístup nezachycuje žádné významové vztahy mezi slovy.
2.2 TF-IDF (Termínová frekvence – Inverzní frekvence dokumentu)
O krok dále: místo 0/1 složky vektoru udávají, jak důležité slovo je v dokumentu ve srovnání s celým korpusem. Ale každý dokument se stává vektorem rozptýleným v rozměrnost slovní zásoby.
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 zlepšuje jednorázové kódování vážením slov podle důležitosti, ale trpí tímtéž zásadní problém: nechápe, že "kočka" a "kočka" jsou synonyma, protože si myslí pouze na přesné lexikální shody.
2.3 Word2Vec: The Revolution (2013)
V roce 2013 vydal Tomáš Mikolov a jeho tým v Googlu Word2Vec, který vše změnil. Geniální nápad: slovo je definováno kontextem, ve kterém se vyskytuje. Slova které se objevují v podobných kontextech, budou mít podobné reprezentace.
Word2Vec používá mělké neuronové sítě k učení hustých vektorů (obvykle 100-300 rozměrů) z velkých textových korpusů. Dvě architektury:
Architektury Word2Vec
| Architektura | Vstup | Výstupy | Popis |
|---|---|---|---|
| CBOW | Kontextová slova | Cílové slovo | Předpovídá ústřední slovo vzhledem k okolnímu kontextu |
| Přeskočit gram | Cílové slovo | Kontextová slova | Předpovídá okolní slova vzhledem k centrálnímu slovu |
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 Rukavice: Globální vektory (2014)
Stanford vyvinul GloVe (Global Vectors for Word Representation) s odlišným přístupem: místo neuronové sítě GloVe faktorizuje matice společného výskytu globální korpus. Kombinuje výhody globálních statistických metod (jako je LSA) s těmito místního kontextu Word2Vec.
GloVe minimalizuje nákladovou funkci, která zajišťuje, že bodový součin mezi dvěma vektory slov je úměrné logaritmu jejich pravděpodobnosti společného výskytu:
2.5 FastText: Vkládání podslov (2016)
Facebook AI Research (FAIR) rozšířil Word2Vec o FastText, kterou zastupuje každé slovo jako množinu n-gramů znaků. To řeší dva kritické problémy:
- Vzácná slova nebo slova mimo slovní zásobu (OOV): FastText může generovat vložení pro dříve neviděná slova složením vektorů dílčích segmentů
- Morfologie: morfologicky příbuzná slova (např. „běh“, „běh“, „běžec“) sdílejí n-gramy, a proto mají podobné vektory
Evoluce: Od řídkých k hustým zastoupením
| Metoda | Rok | Typ | Typické rozměry | Sémantika |
|---|---|---|---|---|
| Jeden horký | - | Roztroušeně | V (slovní zásoba) | Žádný |
| TF-IDF | 1972 | Roztroušeně | V (slovní zásoba) | Statistika |
| Word2Old | 2013 | Hustý | 100-300 | Místní kontextové |
| Rukavice | 2014 | Hustý | 50-300 | Globální + místní |
| FastText | 2016 | Hustý | 100-300 | Podslovo + kontext |
| BERT | 2018 | Hustý | 768 | Kontextová dynamika |
| Transformátory vět | 2019 | Hustý | 384-1024 | Celé věty |
3. Matematické vlastnosti vložení
Jedním z nejvíce fascinujících objevů Word2Vec je, že se vektorový prostor učí algebraické vztahy mezi pojmy. Aritmetické operace s vektory vytvářet sémanticky koherentní výsledky.
3.1 Vektorová analogie
Slavné přirovnání: král - muž + žena = královna. Ve vektorových termínech, rozdíl mezi „králem“ a „mužem“ zachycuje pojem „království“ a přidává jej k „ženě“ dostanete "královnu". Formálně:
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 Sémantické shlukování
Vložení přirozeně tvoří shluky ve vektorovém prostoru. Pokud promítneme vektory ve 2D (pomocí t-SNE nebo UMAP), pozorujeme, že slova stejné kategorie seskupují se: zvířata blízko zvířat, země blízko zemí, profese blízko k profesím.
Tato vlastnost je zásadní pro praktické aplikace: funguje podobnostní vyhledávání právě proto, že dokumenty na podobná témata mají těsné začlenění do vektorového prostoru.
4. Moderní vkládání: Kontextové vkládání
Word2Vec a GloVe generují a jeden vektor pro slovo, nezávislý na kontextu. Ale „škola“ má různé významy ve „školní lavici“ a „škole ryb“. THE kontextová vložení, představený s BERT v roce 2018, řeší tento problém: stejné slovo má různé vektory v závislosti na kontextu.
4.1 Vložení BERT
BERT (Bidirectional Encoder Representations from Transformers) zpracovává celou větu a výstupy vektor pro každý token. Pro vložení celé věty obvykle používáte:
- CLS tokeny: první speciální token [CLS] obsahuje souhrnnou reprezentaci věty
- Průměrné sdružování: průměr všech vektorů tokenů – obecně poskytuje lepší výsledky pro hledání podobnosti
BERT není optimální pro podobnostní vyhledávání
Původní BERT nebyl vycvičen, aby produkoval kvalitní vkládání vět. Token CLS a optimalizované pro klasifikaci, nikoli sémantickou podobnost. Pro hledání podobnosti, jsou potřeba specializované modely jako Sentence Transformers.
4.2 Sentence Transformers (SBERT)
V roce 2019 představili Reimers a Gurevych Sentence-BERT, dolaďující BERT s siamské vytvářet smysluplné vkládání vět. To způsobilo revoluci v podobnosti vyhledávání: poprvé bylo možné porovnávat věty s jednoduchou kosinusovou vzdáleností, získání vysoce kvalitních výsledků.
4.3 Modely vkládání: Komplexní srovnání
Porovnání modelů vkládání (2026)
| Model | Poskytovatelé | Rozměry | Skóre MTEB | Cena / 1 milion tokenu | Poznámky |
|---|---|---|---|---|---|
| text-embedding-3-small | OpenAI | 1536 | 62,3 | 0,02 USD | Dobrý poměr kvalita/cena |
| text-embedding-3-velký | OpenAI | 3072 | 64,6 | 0,13 USD | Maximální kvalita OpenAI |
| embed-v3 | Cohere | 1024 | 64,5 | 0,10 USD | Podporuje více než 100 jazyků |
| plavba - 3 | Voyage AI | 1024 | 67,1 | 0,06 USD | Nahoře k vyzvednutí |
| vše-MiniLM-L6-v2 | Objímání tváře | 384 | 56,3 | Uvolnit | Rychlé, lokální, kompaktní |
| all-mpnet-base-v2 | Objímání tváře | 768 | 57,8 | Uvolnit | Nejlepší základní open-source model |
| gte-large-en-v1.5 | Alibaba (HF) | 1024 | 65,4 | Uvolnit | Konkurenční s komerčními modely |
| bge-large-en-v1.5 | BAAI (HF) | 1024 | 64,2 | Uvolnit | Skvělé pro RAG |
Jak vybrat model
- Prototyp / omezený rozpočet: all-MiniLM-L6-v2 (zdarma, rychle, 384 slabě)
- Výroba, nízké náklady: text-embedding-3-small (OpenAI, $0,02/1 milion tokenu)
- Sběr v nejvyšší kvalitě: voyage-3 nebo gte-large-en-v1.5
- Vícejazyčné: Cohere embed-v3 (100+ jazyků)
- Vlastní hostování / soukromí: bge-large-en-v1.5 nebo gte-large-en-v1.5
5. Měření vzdáleností mezi vektory
Výběr metriky vzdálenosti přímo ovlivňuje kvalitu vyhledávání podobnosti. Podívejme se na čtyři hlavní metriky s jejich matematickými vzorci, jejich silnými stránkami a kdy je použít.
5.1 Kosinová podobnost
Nejpoužívanější metrika pro vkládání textu. Měří úhel mezi dvěma vektory, ignorování jejich velikosti (délky). Dva vektory směřující stejným směrem mají kosinusovou podobnost 1, ortogonální 0, protiklady -1.
V pgvector operátor <=> vypočítat kosinusová vzdálenost
(= 1 - kosinusová podobnost), kde 0 znamená totožné a 2 znamená protiklady.
5.2 Euklidovská vzdálenost (L2)
Vzdálenost „vzdušnou čarou“ mezi dvěma body v prostoru. Bere v úvahu obojí směru než velikosti vektorů.
V pgvector operátor <-> vypočítat vzdálenost L2.
5.3 Dot Product (Dot Product)
Bodový součin měří jak směr, tak velikost. Pro normalizované vektory (norma = 1), bodový součin je ekvivalentní kosinové podobnosti.
V pgvector operátor <#> vypočítat negativní vnitřní součin (kvůli kompatibilitě
s ORDER BY ASC).
5.4 Vzdálenost na Manhattan (L1)
Součet absolutních rozdílů komponenta po komponentě. Méně citlivé na odlehlé hodnoty vzhledem k euklidovské vzdálenosti.
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!
Kdy použít kterou metriku
| Metrický | operátor pgvector | Použijte Kdy | Vyhněte se Kdy |
|---|---|---|---|
| Malé věci | <=> |
Vkládání textu, když na velikosti nezáleží | Prostorová data, kde je významná velikost |
| L2 (euklidovský) | <-> |
Obrázky, číselná data, když záleží na velikosti | Vektory s různými měřítky mezi komponentami |
| Dot Product | <#> |
Vektory již normalizované (o něco lepší výkon) | Nenormalizované vektory (výsledky zkreslené velikostí) |
| Manhattan (L1) | Nenativní v pgvector | Řídká data, odolnost vůči odlehlým hodnotám | Obecné použití s hustým zapuštěním |
Praktické pravidlo
Pro 95 % případů s vkládáním textu použijte kosinusová vzdálenost
(<=> v pgvector). Moderní modely vkládání produkují vektory
již normalizované, díky čemuž jsou kosinové a bodové součiny prakticky ekvivalentní. Vzdálenost
Euklidovský má smysl pro prostorová data nebo když velikost vektoru nese informaci.
6. Vygenerujte vložení v Pythonu
Podívejme se nyní, jak generovat vložení pomocí tří různých přístupů: pomocí místních modelů Sentence Transformers, OpenAI API a HuggingFace Inference API. Každý přístup má konkrétní výhody a kompromisy.
6.1 Sentence Transformers (místní)
Nejflexibilnější a soukromý přístup: model běží na vašem počítači, žádná data odejde ze sítě, žádné náklady na volání 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 OpenAI nabízí vysoce kvalitní modely bez správy infrastruktury. Ideální pro středně objemovou výrobu.
# 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 HuggingFace Inference API
Kompromis mezi lokálními modely a komerčními API: přístup k tisícům modelů open-source přes API, s velkorysou bezplatnou úrovní.
# 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 Efektivní dávkové zpracování
Když potřebujete generovat vložení pro tisíce nebo miliony dokumentů, efektivita dávkové zpracování se stává kritickým.
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. Historicizujte vložení v PostgreSQL
Nyní, když víme, jak generovat vložení, podívejme se, jak je uložit v PostgreSQL pomocí pgvector a provádět podobné vyhledávací dotazy. Toto je praktický odkaz na článek 1 série.
7.1 Rozvržení tabulky
-- 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 Vkládání z Pythonu
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 Vyhledávání podobností z Pythonu
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 Indexování: HNSW vs IVFFlat
U datových sad s více než několika tisíci dokumenty je index nezbytný přijatelný výkon. pgvector nabízí dva typy indexů:
HNSW vs IVFFlat
| Charakteristický | HNSW | IVFFlat |
|---|---|---|
| Rychlost dotazu | Velmi rychle | Rychle |
| Odvolání | 95–99 % | 85–95 % |
| Čas budování | Pomalé (minuty) | Rychlé (sekundy) |
| Paměť | Vysoká (graf v RAM) | Nízká (centroidy) |
| Vložit/Aktualizovat | Dobré (přírůstková aktualizace) | Vyžaduje pravidelnou rekonstrukci |
| Doporučeno pro | Výroba, vysoká kvalita | Prototypování, statické datové sady |
-- 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. Vložení pro různé typy dat
Vkládání se neomezuje pouze na text. Moderní modely mohou generovat reprezentace vektorová grafika pro obrázky, zvuk, zdrojový kód a dokonce i multimodální data.
Multimodální vložení: Modely datových typů
| Typ dat | Model | Rozměry | Use Case |
|---|---|---|---|
| Text | all-MiniLM-L6-v2, text-embedding-3-small | 384-3072 | Sémantické vyhledávání, RAG, klasifikace |
| Obrázky | CLIP (OpenAI), SigLIP (Google) | 512-768 | Vyhledávání obrázků, vizuální klasifikace |
| Zvuk | Šeptej, tleskej | 512-1280 | Vyhledávání zvuku, klasifikace hudby |
| Kód | CodeBERT, vložení StarCoder | 768 | Hledání kódu, detekce duplicit |
| Multimodální | CLIP, ImageBind (Meta) | 512-1024 | Křížové vyhledávání (text podle obrázku) |
# 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
Síla CLIPu je v tom, že v něm žije text a obrázky stejný vektorový prostor. Můžete vyhledávat obrázky pomocí textového dotazu nebo najít text související s obrázkem. Toto otevře scénáře, jako je multimodální vyhledávání v PostgreSQL: uložte do něj vložení CLIP Tabulka pgvector a kruhy s textovými dotazy.
9. Vyhodnoťte kvalitu vložení
Jak poznáte, že je model vkládání „dobrý“? Odpověď závisí na úkolu existují specifické, ale standardizované benchmarky a objektivní metriky.
9.1 MTEB: Srovnávací test masivního vkládání textu
MTEB je referenčním měřítkem pro hodnocení modelů vkládání. Změřte výkon na 58+ úlohách seskupených do 8 kategorií:
- Získávání: najít relevantní dokumenty na základě dotazu
- Sémantická textová podobnost (STS): jak jsou si dvě věty podobné
- Klasifikace: třídit texty do kategorií
- Shlukování: seskupit podobné texty
- Klasifikace párů: určit, zda spolu dva texty souvisejí
- Změna pořadí: znovu seřadit výsledky podle relevance
- Shrnutí: kvalitu souhrnů
- Bitext Mining: najít paralelní překlady
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 Vnitřní vs vnější hodnocení
Dva přístupy k hodnocení
| Typ | Co měří | Příklad | Kdy použít |
|---|---|---|---|
| Vnitřní | Vlastnosti samotných vektorů | Analogie, shlukování, STS | Rychlé srovnání mezi modely |
| Vnější | Výkon na závěrečném úkolu | Kvalita RAG, přesnost výzkumu | Konečné rozhodnutí ve výrobě |
Praktické rady
Nespoléhejte jen na skóre MTEB. Model může mít vysoké skóre MTEB, ale ve vaší konkrétní doméně fungují špatně. Vždy vyhodnocujte svou datovou sadu: vytvořte malou sadu relevantních dotazů a dokumentů z vaší domény a měřte nDCG a MAP. Získáte tak mnohem spolehlivější odhad skutečného výkonu.
10. Redukce rozměrů
Vysokorozměrné vektory se obtížně vizualizují a mohou být drahé z hlediska úložiště a výpočtu. Techniky redukce rozměrů pomáhají při vizualizaci i optimalizaci.
10.1 Techniky vizualizace
Techniky zmenšování rozměrů
| Technika | Zachovat | Rychlost | Typické použití |
|---|---|---|---|
| PCA | Globální rozptyl | Velmi rychle | Zmenšení velikosti pro skladování, předzpracování |
| t-SNE | Místní struktura | Pomalý | 2D vizualizace shluků |
| UMAP | Lokální + globální struktura | Průměrný | 2D vizualizace, také pro redukci před indexací |
# 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 Vložení matrjošky
Nejnovější a inovativní technika: i Matryoshka Representation Learning (MRL) vložení jsou natrénována tak, že prvních N složek vektoru je již vložení platný. Vektor můžete zkrátit z 1536 na 512 nebo 256 rozměrů při zachování dobré kvality.
OpenAI text-embedding-3-small e text-embedding-3-large podporu
tato technika: můžete zadat parametr dimensions získat vektory
kompaktnější bez přepočítávání vložení.
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. Škálovací náklady a strategie
Při přechodu od prototypu k výrobě se náklady na generování a skladování vložení se stává kritickým faktorem. Podívejme se na podrobnou analýzu.
11.1 Náklady na 1 milion dokumentů
Odhad nákladů: 1 milion dokumentů (průměrně 500 tokenů/dokument)
| Model | Generační náklady | Velikost vektoru | Úložiště (float32) | Počáteční celkem |
|---|---|---|---|---|
| vše-MiniLM-L6-v2 | 0 $ (místní) | 384 | ~1,4 GB | Pouze čas GPU/CPU |
| text-embedding-3-small | ~10 $ | 1536 | ~5,7 GB | ~10 $ + úložiště |
| text-embedding-3-velký | ~65 $ | 3072 | ~11,4 GB | ~65 $ + úložiště |
| plavba - 3 | ~30 $ | 1024 | ~3,8 GB | ~30 $ + úložiště |
Vzorec úložiště: N dokumentů * velikost * 4 bajty (float32). Příklad: 1M * 1536 * 4 = 5,7 GB pouze pro operátory.
11.2 Self-hosted vs API: Kompromis
Self-hosted vs API srovnání
| čekám | Samoobslužný | API (OpenAI, Cohere) |
|---|---|---|
| Počáteční náklady | Vysoká (GPU ~ 1-3 $/hod.) | Nízká (platba za použití) |
| Cena za objem | Nejlevnější nad ~10 mil. doc | Lineární, měřítka s objemem |
| Latence | Nízká (žádná síť) | 50-200 ms na hovor |
| Soukromí | Data zůstávají na místě | Údaje zasílané třetím stranám |
| Údržba | Správa GPU, aktualizace, monitoring | Nula |
| kvalitní | Záleží na zvoleném modelu | Obecně vysoké a konzistentní |
11.3 Strategie ukládání do mezipaměti
Vkládání do mezipaměti je nezbytné pro snížení nákladů a latence. Pokud totéž text je požadován několikrát, nemá smysl vkládání znovu obnovovat.
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. Propojení s PostgreSQL AI Series
Pojďme si zrekapitulovat, jak se vestavby začleňují do ekosystému, který budujeme v této sérii:
Úplný tok: Od článku 1 k článku 3
| Krok | Položka | Akce |
|---|---|---|
| 1 | pgvector (článek 1) | Nakonfigurujte PostgreSQL pomocí pgvector, vytvořte tabulky s vektorovými sloupci |
| 2 | Vložení (článek 2 – tento) | Vyberte šablonu, vygenerujte vložení a uložte je do pgvector |
| 3 | RAG s PostgreSQL (článek 3) | Zkombinujte vyhledávání přes pgvector s LLM pro zodpovězení otázek |
V článku 1 jsme připravili infrastrukturu: PostgreSQL s nainstalovaným pgvectorem, tabulky s konfigurovanými vektorovými sloupci a indexy HNSW. V tomto článku jsme zaplnili mezeru základní: odkud tyto vektory pocházejí, jak vybrat správný model a jak je generovat efektivně. V příštím článku postavíme kompletní RAG potrubí, které využívá vše tento zásobník: dokumenty indexované v pgvector, vložení generovaná za běhu pro dotazy, a LLM, který generuje odpovědi na základě načteného kontextu.
13. Závěry a kontrolní seznam
Vložení je základní složkou, která spojuje přirozený jazyk s matematika vektorových databází. Výběr správného modelu, metrika vzdálenosti vhodná a účinná strategie škálování jsou rozhodnutí, která mají přímý dopad kvalitu a náklady vašeho systému AI.
Kontrolní seznam: Výběr správného modelu vkládání
- Definujte úkol: vyhledávání, klasifikace, shlukování, STS?
- Identifikujte jazyk: Pouze v angličtině, vícejazyčné nebo specifické pro doménu?
- Vyhodnoťte omezení: rozpočet, soukromí, latence, dostupná infrastruktura
- Vyberte 2-3 kandidáty z modelové tabulky (část 4.3)
- Vytvořte sadu hodnocení z vaší domény (50-100 dotazů s relevantními dokumenty)
- Měří nDCG a MAP na vaší datové sadě pro každého kandidáta
- Spočítejte si náklady plně funkční pro očekávaný objem (oddíl 11)
- Vyzkoušejte malou velikost: 512 stmívání často stačí pro mnoho případů použití
- Implementujte ukládání do mezipaměti snížit náklady na regeneraci
- Sledujte kvalitu v průběhu času s vaší sadou hodnocení
Další článek: RAG s PostgreSQL
V dalším článku série jeden postavíme Potrubí RAG dokončeno (Retrieval Augmented Generation) pomocí PostgreSQL + pgvector jako znalostní báze. Uvidíme, jak spojit podobnostní vyhledávání s inteligentním chunkingem, jak integrovat LLM jako GPT-4 a Claude a jak měřit kvalitu generovaných odpovědí.







