Embeddings

Embeddings

Zet tekst om in vectoren voor semantisch zoeken, retrieval (RAG), aanbevelingen, clustering en deduplicatie, met OpenAI's text-embedding-3-modellen tegen de helft van OpenAI's prijs. Dezelfde model-ID's, dezelfde request en response, dezelfde vectoren: code die je voor OpenAI schreef werkt zodra je de base URL en de key aanpast.

POST/v1/embeddings
ModelIDPer 1M inputtokensPrijs van OpenAIDimensies
Text Embedding 3 Smalltext-embedding-3-small$0.01$0.021,536, of minder
Text Embedding 3 Largetext-embedding-3-large$0.065$0.133,072, of minder

Je betaalt voor de tekst die je verstuurt, en de vectoren zijn gratis. Beide modellen lezen tot 8,192 tokens per input. Text Embedding 3 Small is het model om mee te beginnen: goedkoop, snel en goed voor de meeste zoek- en RAG-taken. Text Embedding 3 Large is het grotere, nauwkeurigere model, voor als retrievalkwaliteit zwaarder weegt dan opslag en prijs.

Snel aan de slag

import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.zurelay.com/v1",
api_key=os.environ["ZURELAY_API_KEY"],
)
response = client.embeddings.create(
model="text-embedding-3-small",
input="The food was delicious and the waiter was friendly.",
)
vector = response.data[0].embedding # a list of 1,536 floats
print(len(vector), response.usage.prompt_tokens) # 1536 10

Elke OpenAI SDK werkt: wijs hem naar https://api.zurelay.com/v1, gebruik je Zurelay-key en roep de embeddings-methode aan. Het antwoord is van OpenAI:

Antwoord (de vector ingekort)
{
"object": "list",
"data": [
{
"object": "embedding",
"index": 0,
"embedding": [-0.0022748, -0.0093183, 0.0158532, ..., -0.0028842]
}
],
"model": "text-embedding-3-small",
"usage": { "prompt_tokens": 10, "total_tokens": 10 }
}

data bevat één embedding per input, in de volgorde waarin je ze stuurde, elk met een index. usage.prompt_tokens is waarop de request is gefactureerd. De headers zeggen het ook: x-zurelay-model noemt het model en x-zurelay-input-tokens de tokens, en met x-request-id vind je de request terug in je requestlog.

Parameters

modelstringverplicht
text-embedding-3-small of text-embedding-3-large.
inputstring of arrayverplicht
Wat je kunt embedden: een string, een array van maximaal 2,048 strings, een array met token-ID's (integers uit OpenAI's cl100k_base-tokenizer), of een array van maximaal 2,048 van zulke arrays. Elke input is maximaal 8,192 tokens, en geen enkele mag een lege string zijn.
encoding_formatstring
float (de standaard): elke embedding is een reeks getallen. base64: elk is een base64-string van little-endian float32-getallen, kleiner en sneller te downloaden. Zie Base64.
dimensionsinteger
Maak de vector korter: 1 tot 1,536 bij text-embedding-3-small, 1 tot 3,072 bij text-embedding-3-large. Laat je het weg, dan krijg je de volledige lengte. Zie Kortere vectors.
userstring
Optioneel: een ID voor de eindgebruiker voor wie het verzoek bedoeld is, zoals in OpenAI’s API. Wordt geaccepteerd, maar verandert niets aan de vectors.

Batchen

Stuur veel teksten in één verzoek: input neemt een array van maximaal 2,048, en ze komen in dezelfde volgorde terug. Eén verzoek met duizend teksten is veel sneller dan duizend verzoeken, en kost hetzelfde: je betaalt per token, niet per verzoek. Houd elk verzoek in totaal onder 300,000 tokens.

# pip install tiktoken
import tiktoken
# The tokenizer text-embedding-3 counts with, to keep each request under the limits
enc = tiktoken.get_encoding("cl100k_base")
def batches(texts, max_inputs=2048, max_tokens=300_000):
batch, tokens = [], 0
for text in texts:
count = len(enc.encode(text))
if batch and (len(batch) == max_inputs or tokens + count > max_tokens):
yield batch
batch, tokens = [], 0
batch.append(text)
tokens += count
if batch:
yield batch
vectors = []
for batch in batches(texts):
response = client.embeddings.create(model="text-embedding-3-small", input=batch)
vectors += [item.embedding for item in response.data] # in the order sent

Splits documenten langer dan 8,192 tokens in stukken voordat je ze embedt. Voor zoeken en RAG vinden stukken van een paar honderd tokens, een alinea van twee, meestal betere matches dan hele documenten.

Kortere vectors met dimensions

De text-embedding-3-modellen zijn zo getraind dat een verkorte vector nog werkt: vraag je om dimensions, dan krijg je het eerste deel van de volledige vector, opnieuw genormaliseerd naar lengte 1. Kortere vectors kosten minder opslag en geheugen en zoeken sneller, voor iets minder nauwkeurigheid. Text Embedding 3 Large op 1,024 dimensies neemt een derde van de ruimte van de volledige 3,072 in.

response = client.embeddings.create(
model="text-embedding-3-large",
input="The food was delicious and the waiter was friendly.",
dimensions=1024,
)
print(len(response.data[0].embedding)) # 1024
Gebruik één model en één maat voor alles wat je vergelijkt: vectors van verschillende lengte kun je niet vergelijken, en veel vector-databases leggen de lengte vast wanneer je de index aanmaakt. Kortere vectors kosten hetzelfde: de prijs is per input-token.

Base64

Met "encoding_format": "base64" komt elke embedding terug als een base64-string van little-endian float32-getallen in plaats van een JSON-array: kleiner, en sneller te downloaden en te parsen. Laat je encoding_format weg, dan vraagt OpenAI’s Python SDK zelf om base64 en decodeert die het voor je, dus je krijgt hoe dan ook gewone floats. Vraag je zelf om base64, dan krijg je de string, om zo te decoderen:

import base64
import numpy as np
response = client.embeddings.create(
model="text-embedding-3-small",
input=["First text", "Second text"],
encoding_format="base64",
)
# Each embedding is a base64 string of little-endian float32 numbers
vectors = [np.frombuffer(base64.b64decode(item.embedding), dtype="<f4") for item in response.data]

Embed je documenten één keer en sla de vectors op. Om te zoeken embed je de query met hetzelfde model en rangschik je de documenten op cosinusgelijkenis: hoe dichter bij 1, hoe dichter bij elkaar in betekenis, zelfs zonder gemeenschappelijke woorden. Vectors van text-embedding-3 hebben lengte 1, dus het inwendig product geeft dezelfde rangschikking.

# pip install openai numpy
import os
import numpy as np
from openai import OpenAI
client = OpenAI(base_url="https://api.zurelay.com/v1", api_key=os.environ["ZURELAY_API_KEY"])
MODEL = "text-embedding-3-small"
docs = [
"To change your password, open Settings, choose Security and click Change password.",
"Invoices are emailed on the first day of each month and can be downloaded under Billing.",
"Invite teammates from the Team page; each person signs in with their own email.",
"API keys can be limited to a monthly budget, a rate limit and a list of models.",
]
def embed(texts):
response = client.embeddings.create(model=MODEL, input=texts)
return np.array([item.embedding for item in response.data])
doc_vectors = embed(docs) # embed once, and store the vectors beside the text
def search(query, k=2):
query_vector = embed([query])[0]
# The vectors have length 1, so the dot product is the cosine similarity
scores = doc_vectors @ query_vector
best = np.argsort(-scores)[:k]
return [(docs[i], float(scores[i])) for i in best]
for text, score in search("How can I change my password?"):
print(f"{score:.3f} {text}") # the password article comes first

Voor meer dan een paar duizend documenten houd je de vectors in een vector-database van een vector-index (pgvector, Qdrant, Pinecone, Weaviate, Chroma en dergelijke) en laat je die de rangschikking doen.

Retrieval (RAG)

Retrieval-augmented generation beantwoordt een vraag uit je eigen documenten: zoek de passages die het dichtst bij de vraag liggen en geef ze samen met de vraag aan een chatmodel. Dezelfde Zurelay-key doet beide.

# After the search example above: retrieve, then answer from what was found
question = "How do I stop a key from spending too much?"
notes = "\n".join(text for text, _ in search(question))
answer = client.chat.completions.create(
model="gpt-6-luna",
messages=[
{
"role": "system",
"content": "Answer from these notes only. If they don't say, say so.\n\n" + notes,
},
{"role": "user", "content": question},
],
)
print(answer.choices[0].message.content)

LangChain, LlamaIndex en de AI SDK

Frameworks met een OpenAI-embeddingsklasse werken met de base-URL en key van Zurelay, en de model-ID’s blijven die van OpenAI.

LangChain

# pip install langchain-openai
import os
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small",
base_url="https://api.zurelay.com/v1",
api_key=os.environ["ZURELAY_API_KEY"],
)
query_vector = embeddings.embed_query("How can I change my password?")
doc_vectors = embeddings.embed_documents(["First document", "Second document"])

LangChain’s Python OpenAIEmbeddings tokeniseert teksten standaard zelf en stuurt de token-ID’s. Zurelay accepteert token-arrays, dus de standaardinstellingen werken zoals ze zijn.

LlamaIndex

# pip install llama-index-embeddings-openai
import os
from llama_index.core import Settings
from llama_index.embeddings.openai import OpenAIEmbedding
Settings.embed_model = OpenAIEmbedding(
model="text-embedding-3-small",
api_base="https://api.zurelay.com/v1",
api_key=os.environ["ZURELAY_API_KEY"],
)
vector = Settings.embed_model.get_text_embedding("How can I change my password?")

Met Settings.embed_model ingesteld embedt elke index en retriever die je bouwt via Zurelay. Voor het chatmodel zie de LlamaIndex-gids.

Vercel AI SDK

// npm install ai @ai-sdk/openai
import { createOpenAI } from "@ai-sdk/openai";
import { cosineSimilarity, embed, embedMany } from "ai";
const zurelay = createOpenAI({
baseURL: "https://api.zurelay.com/v1",
apiKey: process.env.ZURELAY_API_KEY,
});
const model = zurelay.embedding("text-embedding-3-small");
const { embeddings } = await embedMany({ model, values: ["First document", "Second document"] });
const { embedding } = await embed({ model, value: "How can I change my password?" });
console.log(cosineSimilarity(embedding, embeddings[0]));

Apps en vectorstores

Apps die OpenAI’s embeddings aanroepen met een base-URL die je kunt instellen, werken op dezelfde manier: zet die op https://api.zurelay.com/v1, gebruik je Zurelay-key en een van de model-ID’s hierboven. Dat geldt voor de OpenAI-embeddingengine van Open WebUI voor documenten, de RAG API van LibreChat, de Embeddings OpenAI-node van n8n en de OpenAI-compatibele modelprovider van Dify.

Dezelfde vectors als OpenAI

Verzoeken naar text-embedding-3-small en text-embedding-3-large draaien op OpenAI’s eigen modellen. We hebben elke route tegen OpenAI’s API gecontroleerd: voor dezelfde tekst en hetzelfde model komen de vectors overeen met die van OpenAI, met een cosinusgelijkenis van 0.9999 of meer. Een index die je met OpenAI’s API hebt gebouwd blijft dus werken wanneer je je query’s via Zurelay embedt, en een index die via Zurelay is gebouwd werkt met die van OpenAI. Wisselen tussen de twee vraagt geen nieuwe embedding.

Van model wisselen betekent opnieuw embedden

Vectors van verschillende modellen kun je niet vergelijken

Een vector van text-embedding-3-small zegt niets naast een vector van text-embedding-3-large, of van welk ander model dan ook: elk model plaatst tekst in zijn eigen ruimte. Om van model te wisselen embed je dus alles wat je hebt opgeslagen opnieuw met het nieuwe model, en query je daarna met dat model.

Om dezelfde reden wordt een embeddings-verzoek altijd beantwoord door het model dat je hebt opgegeven. Slimme routering geeft het nooit aan een ander model, wat je instellingen ook zijn: is het model down, dan krijg je 503 model_unavailable, om het opnieuw te proberen, in plaats van vectors die niet bij je index passen.

Limieten

  • Maximaal 2,048 inputs per verzoek.
  • Maximaal 8,192 tokens per input, en 300,000 in het hele verzoek.
  • Geen lege strings.
  • dimensions van 1 tot de volledige lengte van het model: 1,536 bij text-embedding-3-small, 3,072 bij text-embedding-3-large.
  • Een verzoek boven een limiet wordt geweigerd met een 400 die de limiet noemt, en kost niets.
  • Een key die tot bepaalde modellen beperkt is, moet het embedding-model op de lijst hebben. Zie Limieten en regels voor keys.

Betalen voor embeddings

Embeddings worden alleen op input-tokens afgerekend, geteld zoals OpenAI ze telt, en de vectors zijn gratis. Text Embedding 3 Small kost $0.01 per 1M tokens en Text Embedding 3 Large $0.065, de helft van OpenAI's prijs. Een miljoen teksten van elk 100 tokens, 100M tokens, kosten $1.00 om te embedden met Text Embedding 3 Small en $6.50 met Text Embedding 3 Large, tegenover $2.00 en $13.00 bij OpenAI.

Er is geen minimum per request: een request van 10 tokens wordt als 10 tokens afgerekend. Kortere dimensions en base64 kosten hetzelfde. De usage- en x-zurelay-input-tokens-header van elk antwoord zeggen waarop is afgerekend, elk request staat in je requestlog, en mislukte requests kosten niets.

Fouten

Fouten komen in OpenAI's vorm, dus de SDK's van OpenAI geven ze door zoals ze dat voor OpenAI doen.

StatusCodeWanneer
400invalid_requestEen veld ontbreekt of is onjuist: een lege string, meer dan 2,048 inputs, dimensions die geen geheel getal zijn, of een encoding_format anders dan float of base64. In de melding staat om welk veld het gaat.
400invalid_valuedimensions is groter dan de volledige lengte van het model.
400context_length_exceededEen invoer is groter dan 8,192 tokens, of de aanvraag groter dan 300,000. Splits de tekst, of stuur minder invoer in één keer.
503model_unavailableHet model is tijdelijk niet beschikbaar. Probeer het opnieuw na het aantal seconden in de retry-after-header.

Keys, tegoed, budgetten en rate limits geven dezelfde fouten als elk ander endpoint: zie Fouten. De SDKs van OpenAI proberen de fouten opnieuw waarbij dat zinvol is.

Probeer het eerst uit

Kies in de Playground van het dashboard een embeddingmodel, schrijf een paar teksten en embed ze: je ziet elke vector, de gefactureerde tokens en de cosinusgelijkenis van elk paar. Met de knop Code ophalen krijg je dezelfde request voor Python, Node.js of cURL.

Vragen, of ontbreekt er iets? Vraag het ons supportteam en je krijgt antwoord per e-mail, meestal binnen een minuut.