Embedding Models
Embedding models turn text into vectors for semantic search. They power the
/v1/embeddings endpoint, the embed_text SDK calls, and the vectors you store
and query in the KV store.
Embedding models are encoders (BERT-family), not chat models. The engine
routes them automatically: loading an embedding model id through
model_load or embed_text resolves to the same shared encoder instance,
loaded once and reused across requests.
:::info Changed in 0.12.1
The text-embedding call was renamed from embed(...) to embed_text(...)
for consistency with the new embed_image(...) / embed_audio(...) variants
(e.g. Python engine.embed → engine.embed_text, Unity Embeddings.Embed →
Embeddings.EmbedText, Unreal Embed → EmbedText, C FFI atelico_embed →
atelico_embed_text). There are no compatibility aliases. The Godot
AtelicoCoreNode.embed(...) JSON call is unchanged; the new high-level
AtelicoEmbedderNode.embed_text(...) is described under
Multimodal Embedding.
:::
The model you pick determines the vector dimension your KV store must use
(embed_dim): MiniLM, bge-small, and granite-small are 384-dim; bge-base and
granite-english are 768-dim (see the table below).
Built-in models
These ids work out of the box — by short name or full HuggingFace repo id:
| Short name | HF repo id | Dim | Params | Notes |
|---|---|---|---|---|
all-MiniLM-L6-v2 | sentence-transformers/all-MiniLM-L6-v2 | 384 | 23M | Default; fast, ~90 MB |
bge-small-en-v1.5 | BAAI/bge-small-en-v1.5 | 384 | 33M | Higher quality English |
bge-base-en-v1.5 | BAAI/bge-base-en-v1.5 | 768 | 109M | Higher quality, larger |
granite-embedding-small-english-r2 | ibm-granite/granite-embedding-small-english-r2 | 384 | 47M | ModernBERT, 8K context |
granite-embedding-english-r2 | ibm-granite/granite-embedding-english-r2 | 768 | 149M | ModernBERT, 8K context |
granite-embedding-97m-multilingual-r2 | ibm-granite/granite-embedding-97m-multilingual-r2 | 384 | 97M | Multilingual |
granite-embedding-311m-multilingual-r2 | ibm-granite/granite-embedding-311m-multilingual-r2 | 768 | 311M | Multilingual |
Prefix with the backend as usual, e.g.
in-memory::sentence-transformers/all-MiniLM-L6-v2.
Any other BERT-family model
Models outside the built-in list (gte, e5, arctic, nomic — any
sentence-transformers checkpoint with model_type: "bert" or
"modernbert" in its config.json) load generically: the engine
detects the backbone from config.json and the pooling mode from the
sentence-transformers 1_Pooling/config.json (mean pooling if absent).
Embeddings are always L2-normalized.
A loadable snapshot needs at least:
config.json
tokenizer.json
model.safetensors (float weights) — OR — model.gguf (quantized)
1_Pooling/config.json (optional — pooling mode, defaults to mean)
Quantized (GGUF) embedding models
BERT embedders can ship as quantized GGUF instead of float safetensors. An 8-bit MiniLM is ~24 MB on disk and in RAM, versus ~90 MB for the float snapshot — worthwhile for download size and for on-device/iOS memory.
Drop a model.gguf (llama.cpp bert architecture, e.g. the Q8_0 build of
MiniLM) in place of model.safetensors, keeping the same config.json and
tokenizer.json next to it. The engine detects the GGUF automatically, reads
the linear projections as quantized tensors (embeddings and layer norms are
dequantized to F32), and serves it through the same embeddings API — no model
id change. When both model.safetensors and model.gguf are present, the
float weights win.
Notes:
- Only the BERT backbone has a quantized graph today; ModernBERT (Granite r2) must use safetensors.
config.jsonis still required (it's ~600 bytes; the size win is entirely in the weights file).
Cache layout & shipping offline
The engine resolves embedding models in this order — no network is touched if a local copy exists:
<cache>/embeddings/<model-id>/— canonical location (the asset downloader places them here)<cache>/models/<model-id>/— general model cache- HuggingFace download (materialized into
<cache>/embeddings/for next time)
To ship a game that works offline, place the snapshot under either directory of your configured model directory, e.g.:
<ModelDirectory>/embeddings/sentence-transformers/all-MiniLM-L6-v2/
├── config.json
├── tokenizer.json
├── model.safetensors # or model.gguf for the quantized build
└── 1_Pooling/config.json
Publishing to an object store works the same for both formats —
atelico-asset-publisher publish-model <id> materializes the snapshot
(safetensors or GGUF) under embeddings/<id>/ and uploads the tree.
Usage: embedding text
embed_text turns one or more strings into vectors. Warm-loading the model
first is optional — it loads lazily on the first embed_text call — but
pre-loading moves the one-time cost off your first request.
- Python
- Unity (C#)
- Unreal (C++)
- Godot (GDScript)
- HTTP
import atelico, json
engine = atelico.Engine()
# Optional warm-load (otherwise the first embed() pays the load cost)
engine.model_load("in-memory::sentence-transformers/all-MiniLM-L6-v2")
resp = json.loads(engine.embed_text(json.dumps({
"model": "in-memory::sentence-transformers/all-MiniLM-L6-v2",
"input": ["the cat sat on the mat", "a feline rests on the rug"],
})))
vectors = [d["embedding"] for d in resp["data"]] # two 384-dim vectors
engine.LoadModel("in-memory::sentence-transformers/all-MiniLM-L6-v2");
// Raw vectors:
string r = engine.Embeddings.EmbedText(JsonSerializer.Serialize(new {
model = "in-memory::sentence-transformers/all-MiniLM-L6-v2",
input = new[] { "the cat sat on the mat" },
}));
// Or a direct similarity score between two texts:
float sim = engine.Embeddings.Similarity(
"in-memory::sentence-transformers/all-MiniLM-L6-v2",
"the cat sat on the mat", "a feline rests on the rug");
atelico_model_load(Engine, "in-memory::sentence-transformers/all-MiniLM-L6-v2");
const FString Resp = Atelico->EmbedText(TEXT(R"({
"model": "in-memory::sentence-transformers/all-MiniLM-L6-v2",
"input": ["the cat sat on the mat"]
})"));
// Or a direct similarity score:
float Sim = Atelico->Similarity(
TEXT("in-memory::sentence-transformers/all-MiniLM-L6-v2"),
TEXT("the cat sat on the mat"), TEXT("a feline rests on the rug"));
@onready var core: AtelicoCoreNode = $AtelicoCore
func _ready() -> void:
core.model_load("in-memory::sentence-transformers/all-MiniLM-L6-v2")
var resp := JSON.parse_string(core.embed(JSON.stringify({
"model": "in-memory::sentence-transformers/all-MiniLM-L6-v2",
"input": "the cat sat on the mat",
})))
var vector: Array = resp.data[0].embedding
curl http://localhost:11434/v1/embeddings \
-H "Content-Type: application/json" \
-d '{
"model": "in-memory::sentence-transformers/all-MiniLM-L6-v2",
"input": ["the cat sat on the mat"]
}'
Image and audio embeddings
The same surface embeds images and audio, not just text:
embed_image(image_path) and embed_audio(audio_path) return a float vector
just like embed_text. With a cross-modal model (CLIP, SigLIP, CLAP,
ImageBind) text, image, and audio land in one shared space so a text query can
retrieve images or sounds. See
Multimodal Embedding for the models, the per-modality
ids, and full code in every SDK.
:::caution Audio format: WAV / RIFF only
embed_audio decodes WAV / RIFF files only — not MP3 or FLAC. Convert to
WAV first.
:::
:::note Models download on first use, but never silently mid-call
embed_* resolves weights from the on-device cache and does not trigger a
blocking download when they're missing — it fails / returns empty so you gate.
Check with is_model_cached(model_id) and fetch with
model_download_async(model_id) before embedding.
:::
Using embeddings for search
Embeddings on their own are just vectors. To build NPC memory, RAG, or lore lookups you store and query them:
- Vector Search & Memory (KV Store) — create a store,
insert your vectors, and retrieve the nearest ones. Set the store's
embed_dimto this model's dimension (384 or 768 above). - Hybrid & Lexical Search — add a BM25 full-text index and blend keyword + vector relevance.
Errors you might see
'<id>' is not a loadable embedding model: model_type '<x>' is not a supported encoder— the snapshot'sconfig.jsonnames an architecture the embedder can't run. Supported:bert,modernbert.failed to load embedding model '<id>': ...— the id is neither a built-in alias nor a resolvable snapshot. The message lists the built-in models and the expected cache layout. Unknown ids are an error — the engine never silently substitutes a different embedding model, because vectors from different models are not comparable.GGUF architecture 'bert' is an embedding encoder, not a chat model— you tried to load a quantized embedder as a chat model. Quantized embedders are supported, but through the embeddings API: name it as anembedding_model/embedtarget, not a chat model.