Skip to main content
Version: 0.12

Godot: Embeddings & Search

Embeddings, vector search, and hybrid search in Godot. For the concepts behind each step see the guide pages on Embedding Models, Vector Search, and Hybrid & Lexical Search.

Godot exposes the engine through nodes: AtelicoCoreNode for JSON-style embedding, AtelicoEmbedderNode for one-call text/image/audio vectors, AtelicoClassifierNode for classification, AtelicoKvStoreNode for searching a store, and AtelicoVectorMemoryStore for writable in-engine memory.

:::info Changed in 0.12.1 The classifier and embedder nodes now ship in the Godot extension. On AtelicoClassifierNode, predict(...) was renamed to predict_text(...) to sit alongside the new predict_image(...) / predict_audio(...); the high-level embedder calls are embed_text / embed_image / embed_audio. There are no compatibility aliases. (AtelicoCoreNode.embed(...), the JSON call shown above, is unchanged.) :::

Embed text

embed lives on AtelicoCoreNode:

@onready var core: AtelicoCoreNode = $AtelicoCore

func embed_one(text: String) -> Array:
var resp := JSON.parse_string(core.embed(JSON.stringify({
"model": "in-memory::sentence-transformers/all-MiniLM-L6-v2",
"input": text,
})))
return resp.data[0].embedding # 384-dim Array[float]

Search a KV store

AtelicoKvStoreNode creates and searches a store (vector, lexical, and hybrid), with embed_dim matching your model (384 for MiniLM/bge-small):

@onready var kv: AtelicoKvStoreNode = $AtelicoKvStore

func _ready() -> void:
kv.kvstore_create(JSON.stringify({
"store_id": "lore",
"db_path": "./data/lore",
"embed_dim": 384,
}))

func search(query_text: String) -> Array:
var qv: Array = embed_one(query_text)
return JSON.parse_string(kv.kvstore_query("lore", JSON.stringify({
"query_embedding": qv,
"limit": 5,
})))

:::note Writing entries AtelicoKvStoreNode is read/search-only — it has no insert. Populate the store from another binding (Python/Rust/C FFI) or the HTTP server sharing the same db_path — ideal for read-only lore you ship with the game. For memory your game writes at runtime, use AtelicoVectorMemoryStore below. :::

Writable memory: AtelicoVectorMemoryStore

For NPC memory the game records as it plays, AtelicoVectorMemoryStore is a LanceDB-backed node you can write to and query directly:

@onready var mem: AtelicoVectorMemoryStore = $AtelicoVectorMemoryStore

func _ready() -> void:
mem.connect_to_db("./data/npc-memory")

func remember(node: Dictionary) -> void:
mem.write_memory("memories", node)

func recall(filter: String) -> Array:
return mem.query_memory(...) # see the Godot API Reference for the exact args

See the Godot API Reference for the exact write_memory / query_memory dictionary fields, and kvstore_create_fts_index / kvstore_hybrid_query on AtelicoKvStoreNode for hybrid search.

One-call vectors: AtelicoEmbedderNode

AtelicoEmbedderNode turns a string, image file, or audio file straight into a PackedFloat32Array — no JSON. Call initialize() once, then embed_text / embed_image / embed_audio. With a cross-modal model id ("clip", "siglip", "clap") the vectors share one space.

@onready var embedder: AtelicoEmbedderNode = $AtelicoEmbedder

func _ready() -> void:
embedder.initialize()

func embed_examples() -> void:
var text_vec := embedder.embed_text("bge-small-en", "a friendly dragon")
var image_vec := embedder.embed_image("dinov2-small", "/abs/path/creature.png")
var audio_vec := embedder.embed_audio("clap", "/abs/path/roar.wav") # WAV/RIFF only

Each call returns an empty PackedFloat32Array if the model is not cached — they never trigger a blocking download. Gate with AtelicoEngineNode.is_model_cached(model) and, if missing, AtelicoEngineNode.model_download_async(model, token) before embedding.

Classify: AtelicoClassifierNode

AtelicoClassifierNode loads a trained classifier and predicts by modality. Each predict_* returns a JSON string with a top label, its probability, and a top list of the top-k {label, probability} pairs.

@onready var classifiers: AtelicoClassifierNode = $AtelicoClassifiers

func _ready() -> void:
classifiers.initialize()
classifiers.load_classifier("steering", "/abs/path/to/models/steering")

func classify(frame_path: String) -> void:
# top_k is optional: 0 returns just the top label (empty `top` list);
# a positive N returns the N best; omitting it returns all labels ranked.
var json := classifiers.predict_image("steering", frame_path, 0)
var result := JSON.parse_string(json)
print("label=%s" % result.label) # e.g. "left" / "right"

var text_json := classifiers.predict_text("sentiment", "I love this!", 3)
var audio_json := classifiers.predict_audio("ambience", "/abs/path/clip.wav", 3)

The image/audio embedder the classifier was trained with (e.g. "dinov2-small", "clap") must be cached first — gate it the same way. See Classifiers for training and the full result schema.