Skip to main content

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=ollama and EMBEDDING_MODEL=qwen3-embedding:0.6b set.
  • curl, jq, and node available in your shell.
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).

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')

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.

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]
}

Step 3 — Embed a batch of texts​

inputs (an array) returns embeddings, vectors in input order (Embeddings).

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
}

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.

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

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.

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

What's next​