Generating Embeddings
Convert text into vectors with the Embeddings endpoint: single and batch embedding, cosine similarity in TypeScript, and ranking a collection against a query.
Prerequisites
- SOAT running locally (Quick Start); Key Concepts; Configuration.
- Ollama running locally with an embedding model pulled:
ollama pull qwen3-embedding:0.6b
- The server must have
EMBEDDING_PROVIDER=ollamaandEMBEDDING_MODEL=qwen3-embedding:0.6bset. curl,jq, andnodeavailable in your shell.
- CLI
- SDK
- curl
export SOAT_BASE_URL=http://localhost:5047
import { Embeddings, createClient, createConfig } from '@soat/sdk';
export SOAT_BASE_URL=http://localhost:5047
Step 1 — Log in
Obtain an admin token (Users) and create the project the embeddings are billed to (Embeddings — Metering).
- CLI
- SDK
- curl
ADMIN_TOKEN=$(soat login-user --username admin --password Admin1234! | jq -r '.token')
export SOAT_TOKEN=$ADMIN_TOKEN
PROJECT_ID=$(soat create-project --name "Embeddings Project" | jq -r '.id')
import { SoatClient, Embeddings, createClient, createConfig } from '@soat/sdk';
const soat = new SoatClient({ baseUrl: 'http://localhost:5047' });
const { data: login } = await soat.users.loginUser({
body: { username: 'admin', password: 'Admin1234!' },
});
const TOKEN = login.token;
const client = createClient(
createConfig({
baseUrl: 'http://localhost:5047',
headers: { Authorization: `Bearer ${TOKEN}` },
})
);
const authed = new SoatClient({
baseUrl: 'http://localhost:5047',
token: TOKEN,
});
const { data: project } = await authed.projects.createProject({
body: { name: 'Embeddings Project' },
});
const PROJECT_ID = project!.id;
ADMIN_TOKEN=$(curl -s -X POST "$SOAT_BASE_URL/api/v1/users/login" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"Admin1234!"}' | jq -r '.token')
PROJECT_ID=$(curl -s -X POST "$SOAT_BASE_URL/api/v1/projects" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Embeddings Project"}' | jq -r '.id')
Step 2 — Embed a single text
input embeds one text; the response embedding is a float array of length EMBEDDING_DIMENSIONS (default 1024). See Embeddings — Single vs batch.
- CLI
- SDK
- curl
soat create-embeddings \
--project-id "$PROJECT_ID" \
--input "SOAT is an open-source platform for building AI-powered applications." \
| jq '{dimensions: (.embedding | length), first_values: .embedding[:3]}'
Expected output:
{
"dimensions": 1024,
"first_values": [0.032, -0.041, 0.018]
}
const { data } = await Embeddings.createEmbeddings({
client,
body: {
project_id: PROJECT_ID,
input: 'SOAT is an open-source platform for building AI-powered applications.',
},
});
console.log(data.embedding.length); // 1024
console.log(data.embedding.slice(0, 3)); // first three values
curl -s -X POST "$SOAT_BASE_URL/api/v1/embeddings" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"project_id":"'"$PROJECT_ID"'","input":"SOAT is an open-source platform for building AI-powered applications."}' \
| jq '{dimensions: (.embedding | length), first_values: .embedding[:3]}'
Step 3 — Embed a batch of texts
inputs (an array) returns embeddings, vectors in input order (Embeddings).
- CLI
- SDK
- curl
soat create-embeddings \
--project-id "$PROJECT_ID" \
--inputs '["Wooden cutting board", "Cast iron skillet", "Silicone spatula set", "Stainless steel mixing bowls"]' \
| jq '{count: (.embeddings | length), dims: (.embeddings[0] | length)}'
Expected output:
{
"count": 4,
"dims": 1024
}
const { data } = await Embeddings.createEmbeddings({
client,
body: {
project_id: PROJECT_ID,
inputs: [
'Wooden cutting board',
'Cast iron skillet',
'Silicone spatula set',
'Stainless steel mixing bowls',
],
},
});
console.log(data.embeddings.length); // 4
console.log(data.embeddings[0].length); // 1024
curl -s -X POST "$SOAT_BASE_URL/api/v1/embeddings" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"project_id":"'"$PROJECT_ID"'","inputs":["Wooden cutting board","Cast iron skillet","Silicone spatula set","Stainless steel mixing bowls"]}' \
| jq '{count: (.embeddings | length), dims: (.embeddings[0] | length)}'
Step 4 — Compute cosine similarity
Cosine similarity ranges from -1 (opposite) to 1 (identical), independent of magnitude. All embeddings from one deployment share a vector space, so it is meaningful across any two texts.
- CLI
- SDK
- curl
VECS=$(soat create-embeddings \
--project-id "$PROJECT_ID" \
--inputs '["I love cooking pasta", "My favourite dish is spaghetti"]' \
| jq '.embeddings')
node << EOF
const vecs = $VECS;
const [a, b] = vecs;
const dot = a.reduce((s, v, i) => s + v * b[i], 0);
const magA = Math.sqrt(a.reduce((s, v) => s + v * v, 0));
const magB = Math.sqrt(b.reduce((s, v) => s + v * v, 0));
console.log('similarity:', (dot / (magA * magB)).toFixed(4));
EOF
const cosineSimilarity = (a: number[], b: number[]): number => {
const dot = a.reduce((sum, v, i) => sum + v * b[i], 0);
const magA = Math.sqrt(a.reduce((sum, v) => sum + v * v, 0));
const magB = Math.sqrt(b.reduce((sum, v) => sum + v * v, 0));
return dot / (magA * magB);
};
// Embed both texts in one batch request
const { data } = await Embeddings.createEmbeddings({
client,
body: {
project_id: PROJECT_ID,
inputs: [
'I love cooking pasta',
'My favourite dish is spaghetti',
],
},
});
const [vecA, vecB] = data.embeddings;
console.log('similarity:', cosineSimilarity(vecA, vecB).toFixed(4));
// → ~0.93 (highly similar)
VECS=$(curl -s -X POST "$SOAT_BASE_URL/api/v1/embeddings" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"project_id":"'"$PROJECT_ID"'","inputs":["I love cooking pasta","My favourite dish is spaghetti"]}' \
| jq '.embeddings')
node << EOF
const vecs = $VECS;
const [a, b] = vecs;
const dot = a.reduce((s, v, i) => s + v * b[i], 0);
const magA = Math.sqrt(a.reduce((s, v) => s + v * v, 0));
const magB = Math.sqrt(b.reduce((s, v) => s + v * v, 0));
console.log('similarity:', (dot / (magA * magB)).toFixed(4));
EOF
Step 5 — Find the most similar text
Embed the query and the collection in one batch call (Embeddings key concepts), then sort by cosine similarity.
- CLI
- SDK
- curl
ALL=$(soat create-embeddings \
--project-id "$PROJECT_ID" \
--inputs '["something to cook with on the stove","Wooden cutting board","Cast iron skillet","Silicone spatula set","Stainless steel mixing bowls"]' \
| jq '.embeddings')
node << EOF
const vecs = $ALL;
const items = ['Wooden cutting board', 'Cast iron skillet', 'Silicone spatula set', 'Stainless steel mixing bowls'];
const query = vecs[0];
const rest = vecs.slice(1);
const cos = (a, b) => {
const dot = a.reduce((s, v, i) => s + v * b[i], 0);
const magA = Math.sqrt(a.reduce((s, v) => s + v * v, 0));
const magB = Math.sqrt(b.reduce((s, v) => s + v * v, 0));
return dot / (magA * magB);
};
items
.map((name, i) => ({ name, score: cos(query, rest[i]) }))
.sort((a, b) => b.score - a.score)
.forEach(r => console.log(r.score.toFixed(4), r.name));
EOF
Expected output (the skillet is closest to stovetop cooking):
0.8412 Cast iron skillet
0.7931 Silicone spatula set
0.7204 Stainless steel mixing bowls
0.6981 Wooden cutting board
const cosineSimilarity = (a: number[], b: number[]): number => {
const dot = a.reduce((sum, v, i) => sum + v * b[i], 0);
const magA = Math.sqrt(a.reduce((sum, v) => sum + v * v, 0));
const magB = Math.sqrt(b.reduce((sum, v) => sum + v * v, 0));
return dot / (magA * magB);
};
const query = 'something to cook with on the stove';
const items = [
'Wooden cutting board',
'Cast iron skillet',
'Silicone spatula set',
'Stainless steel mixing bowls',
];
// Embed query + all items in one batch call
const { data } = await Embeddings.createEmbeddings({
client,
body: { project_id: PROJECT_ID, inputs: [query, ...items] },
});
const [queryVec, ...itemVecs] = data.embeddings;
const ranked = items
.map((name, i) => ({ name, score: cosineSimilarity(queryVec, itemVecs[i]) }))
.sort((a, b) => b.score - a.score);
ranked.forEach((r) => console.log(r.score.toFixed(4), r.name));
// 0.8412 Cast iron skillet ← most similar to "cook on the stove"
// 0.7931 Silicone spatula set
// 0.7204 Stainless steel mixing bowls
// 0.6981 Wooden cutting board
ALL=$(curl -s -X POST "$SOAT_BASE_URL/api/v1/embeddings" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"project_id":"'"$PROJECT_ID"'","inputs":["something to cook with on the stove","Wooden cutting board","Cast iron skillet","Silicone spatula set","Stainless steel mixing bowls"]}' \
| jq '.embeddings')
node << EOF
const vecs = $ALL;
const items = ['Wooden cutting board', 'Cast iron skillet', 'Silicone spatula set', 'Stainless steel mixing bowls'];
const query = vecs[0];
const rest = vecs.slice(1);
const cos = (a, b) => {
const dot = a.reduce((s, v, i) => s + v * b[i], 0);
const magA = Math.sqrt(a.reduce((s, v) => s + v * v, 0));
const magB = Math.sqrt(b.reduce((s, v) => s + v * v, 0));
return dot / (magA * magB);
};
items
.map((name, i) => ({ name, score: cos(query, rest[i]) }))
.sort((a, b) => b.score - a.score)
.forEach(r => console.log(r.score.toFixed(4), r.name));
EOF
What's next
- Production-scale search — store vectors in a vector engine: Meilisearch (
userProvidedembedder, hybrid keyword + semantic), Qdrant or pgvector. - Re-embed on update — re-embed only documents whose text changed.
- Agents with knowledge — Agent with Persistent Memory: embeddings inject context before every generation.
- Knowledge search —
POST /api/v1/knowledge/searchqueries Documents and Memories with the same model (Knowledge).