Vector embeddings uitgelegd voor developers
Dit artikel legt uit hoe vector embeddings werken en hoe je ze praktisch inzet voor semantische zoekopdrachten, specifiek voor developers die met Python en OpenAI's API werken.
Vereisten
- Python 3.10 of hoger - `openai` Python-package versie 1.35.0 of hoger - Een OpenAI API-key met toegang tot het `text-embedding-3-small` model - `numpy` versie 1.26.0 of hoger voor vectorberekeningen
Installeer de dependencies:
pip install openai==1.35.0 numpy==1.26.0
Stappen
1. Maak een embedding aan
Een embedding is een lijst van floating-point nummers die de semantische betekenis van een tekst representeert. OpenAI's `text-embedding-3-small` model zet tekst om naar een vector van 1536 dimensies.
from openai import OpenAI</p><p>client = OpenAI(api_key="jouw-api-key")</p><p>response = client.embeddings.create(
model="text-embedding-3-small",
input="De kat zit op de mat"
)</p><p>embedding = response.data[0].embedding
print(len(embedding)) Output: 15362. Sla embeddings op in een vector database
Voor productie gebruik je een dedicated vector database zoals Pinecone, Weaviate of pgvector. Voor lokale ontwikkeling volstaat een NumPy-array.
import numpy as np</p><p>Sla embeddings op als matrix
documents = [
"De kat zit op de mat",
"De hond rent door het park",
"Een vogel vliegt over het water"
]</p><p>embeddings = []
for doc in documents:
response = client.embeddings.create(
model="text-embedding-3-small",
input=doc
)
embeddings.append(response.data[0].embedding)</p><p>embedding_matrix = np.array(embeddings)
np.save('document_embeddings.npy', embedding_matrix)3. Bereken cosine similarity
Cosine similarity meet de hoek tussen twee vectoren. Een waarde van 1 betekent identiek, 0 betekent geen overlap. Dit is de standaard manier om semantische gelijkenis te bepalen.
def cosine_similarity(vec_a, vec_b):
dot_product = np.dot(vec_a, vec_b)
norm_a = np.linalg.norm(vec_a)
norm_b = np.linalg.norm(vec_b)
return dot_product / (norm_a * norm_b)</p><p>Test met twee gerelateerde zinnen
query = "Een poes ligt op de vloer"
query_response = client.embeddings.create(
model="text-embedding-3-small",
input=query
)
query_embedding = query_response.data[0].embedding</p><p>similarities = [
cosine_similarity(query_embedding, doc_emb)
for doc_emb in embeddings
]</p><p>print(similarities)
Output: [0.82, 0.31, 0.28] — de eerste zin is het meest gerelateerd4. Bouw een semantische zoekfunctie
Semantische zoekopdrachten vinden resultaten op basis van betekenis, niet op exacte woordovereenkomst. Dit werkt omdat embeddings woorden met vergelijkbare betekenis dicht bij elkaar plaatsen in de vectorruimte.
def semantic_search(query, embeddings, documents, top_k=3):
query_response = client.embeddings.create(
model="text-embedding-3-small",
input=query
)
query_embedding = query_response.data[0].embedding
similarities = [
cosine_similarity(query_embedding, doc_emb)
for doc_emb in embeddings
]
top_indices = np.argsort(similarities)[-top_k:][::-1]
results = []
for idx in top_indices:
results.append({
'document': documents[idx],
'similarity': similarities[idx]
})
return results</p><p>results = semantic_search("Een dier ligt te slapen", embeddings, documents)
for r in results:
print(f"{r['similarity']:.2f}: {r['document']}")
Output:
0.79: De kat zit op de mat
0.45: De hond rent door het park
0.33: Een vogel vliegt over het water5. Reduceer dimensionaliteit met PCA
1536 dimensies zijn veel voor visualisatie of opslag in geheugen. Principal Component Analysis (PCA) reduceert de dimensionaliteit terwijl de relatieve afstanden behouden blijven.
from sklearn.decomposition import PCA</p><p>pca = PCA(n_components=2)
reduced_embeddings = pca.fit_transform(embedding_matrix)</p><p>print(reduced_embeddings.shape) Output: (3, 2)6. Implementeer caching voor embeddings
Embeddings genereren kost API-calls en tijd. Cache ze lokaal om herhaalde berekeningen te voorkomen.
import json
import hashlib
from pathlib import Path</p><p>CACHE_FILE = Path("embedding_cache.json")</p><p>def get_cached_embedding(text):
if CACHE_FILE.exists():
cache = json.loads(CACHE_FILE.read_text())
else:
cache = {}
text_hash = hashlib.sha256(text.encode()).hexdigest()
if text_hash in cache:
return cache[text_hash]
response = client.embeddings.create(
model="text-embedding-3-small",
input=text
)
embedding = response.data[0].embedding
cache[text_hash] = embedding
CACHE_FILE.write_text(json.dumps(cache))
return embedding</p><p>Gebruik de cache
emb = get_cached_embedding("De kat zit op de mat") API call
emb_cached = get_cached_embedding("De kat zit op de mat") Uit cache7. Batch verwerking voor efficiëntie
Verwerk meerdere documenten in één API-call. Dit verlaagt de latency en kosten aanzienlijk.
texts = [
"Eerste document over kunstmatige intelligentie",
"Tweede document over machine learning",
"Derde document over natuurlijke taalverwerking"
]</p><p>batch_response = client.embeddings.create(
model="text-embedding-3-small",
input=texts
)</p><p>batch_embeddings = [item.embedding for item in batch_response.data]
print(len(batch_embeddings)) Output: 38. Implementeer een volledige zoekpipeline
Combineer alle stappen in een herbruikbare pipeline die je direct in een applicatie kunt integreren.
class EmbeddingSearch:
def __init__(self, model="text-embedding-3-small"):
self.client = OpenAI()
self.model = model
self.documents = []
self.embeddings = np.array([])
def add_documents(self, docs):
self.documents.extend(docs)
response = self.client.embeddings.create(
model=self.model,
input=docs
)
new_embeddings = np.array([item.embedding for item in response.data])
if self.embeddings.size == 0:
self.embeddings = new_embeddings
else:
self.embeddings = np.vstack([self.embeddings, new_embeddings])
def search(self, query, top_k=5):
response = self.client.embeddings.create(
model=self.model,
input=query
)
query_embedding = np.array(response.data[0].embedding)
similarities = [
cosine_similarity(query_embedding, doc_emb)
for doc_emb in self.embeddings
]
top_indices = np.argsort(similarities)[-top_k:][::-1]
return [
(self.documents[idx], similarities[idx])
for idx in top_indices
]</p><p>Gebruik de pipeline
search_engine = EmbeddingSearch()
search_engine.add_documents(documents)
results = search_engine.search("Hoe werkt een kat?", top_k=2)</p><p>for doc, score in results:
print(f"{score:.3f} - {doc}")Verificatie
Controleer of je embeddings correct werken door de dimensionaliteit en semantische relaties te testen:
python -c "
from openai import OpenAI
import numpy as np</p><p>client = OpenAI()
response = client.embeddings.create(
model='text-embedding-3-small',
input=['hond', 'kat', 'auto']
)
embeddings = np.array([item.embedding for item in response.data])</p><p>Check dimensionaliteit
assert embeddings.shape == (3, 1536), f'Fout: verwacht (3, 1536), kreeg {embeddings.shape}'</p><p>Check semantische relaties
def cos_sim(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))</p><p>hond_kat = cos_sim(embeddings[0], embeddings[1])
hond_auto = cos_sim(embeddings[0], embeddings[2])</p><p>print(f'Dimensionaliteit: {embeddings.shape[1]}')
print(f'Hond vs kat similarity: {hond_kat:.3f}')
print(f'Hond vs auto similarity: {hond_auto:.3f}')
assert hond_kat > hond_auto, 'Semantische relatie klopt niet'
print('Alle checks geslaagd')
"Veelgestelde problemen
1. API-fout: "model_not_found"
Je API-key heeft geen toegang tot het `text-embedding-3-small` model. Gebruik een oudere modelversie of upgrade je account.
Oplossing: Vervang `text-embedding-3-small` door `text-embedding-ada-002` en pas de dimensionaliteit aan van 1536 naar 3072.
2. Lage similarity scores voor gerelateerde teksten
Embeddings werken goed voor semantische gelijkenis, maar niet voor exacte woordovereenkomst. Twee zinnen met dezelfde woorden maar andere volgorde kunnen lage scores geven.
Oplossing: Combineer embeddings met een lexicale zoekmethode zoals BM25 voor hybride zoekopdrachten. Gebruik `text-embedding-3-large` voor betere semantische precisie.
3. Geheugenproblemen bij veel documenten
Het opslaan van honderdduizenden embeddings van 1536 dimensies kost veel RAM. Een miljoen documenten verbruikt ongeveer 6 GB.
Oplossing: Gebruik een dedicated vector database zoals Pinecone of Weaviate. Deze slaan embeddings op schijf op en gebruiken approximate nearest neighbor (ANN) algoritmen voor snelle zoekopdrachten.
De overstap van ruwe tekst naar embeddings is een fundamentele verschuiving: je code werkt niet langer met woorden, maar met posities in een continue vectorruimte — en die ruimte bepaalt uiteindelijk wat je zoekmachine wel en niet vindt.