Python: Embeddings & Search
A complete walkthrough of embeddings, vector search, and hybrid search in Python. For the concepts behind each step — model dimensions, the bring-your-own-vector model, hybrid scoring — see the guide pages on Embedding Models, Vector Search, and Hybrid & Lexical Search.
All KV-store calls take/return JSON strings.
:::info Changed in 0.12.1
embed(...) is now embed_text(...), alongside the new embed_image(...)
and embed_audio(...) calls. There are no compatibility aliases — rename any
0.12.0 embed call.
:::
Embed text
import atelico, json
engine = atelico.Engine()
MODEL = "in-memory::sentence-transformers/all-MiniLM-L6-v2"
resp = json.loads(engine.embed_text(json.dumps({
"model": MODEL,
"input": ["The guard saw a thief enter the keep at midnight."],
})))
vector = resp["data"][0]["embedding"] # 384-dim list[float]
# Or a direct cosine similarity between two strings:
sim = engine.embed_similarity(MODEL, "a knight's blade", "a warrior's sword")
Create a store, insert, query
embed_dim must match the model — 384 for MiniLM/bge-small, 768 for bge-base.
# 1. Create (persisted under db_path)
engine.kvstore_create(json.dumps({
"store_id": "npc-memory",
"db_path": "./data/npc-memory",
"embed_dim": 384,
}))
# 2. Insert — key_embedding is the vector you produced above
engine.kvstore_insert("npc-memory", json.dumps([{
"id": "mem-001",
"key_text": "The guard saw a thief enter the keep at midnight.",
"key_embedding": vector,
"facets": {"npc_id": "guard-01", "location": "keep"},
"priority": 0.8,
}]))
# 3. Query — embed the question, then search
q = json.loads(engine.embed_text(json.dumps({"model": MODEL, "input": ["who broke in?"]})))
results = json.loads(engine.kvstore_query("npc-memory", json.dumps({
"query_embedding": q["data"][0]["embedding"],
"query_text": "who broke in?",
"facet_filters": {"npc_id": "guard-01"},
"limit": 5,
})))
for r in results:
print(r["key_text"], r["similarity"], r["combined_score"])
print("entries:", engine.kvstore_count("npc-memory", ""))
Hybrid (vector + keyword) search
Add a BM25 full-text index once, then blend keyword and vector scores:
engine.kvstore_create_fts_index("npc-memory", "key_text")
response = json.loads(engine.kvstore_hybrid_query("npc-memory", json.dumps({
"embeddings": q["data"][0]["embedding"],
"text": "thief keep",
"fts_column": "key_text",
"limit": 5,
"vector_weight": 0.6,
"lexical_weight": 0.4,
})))
for row, trace in zip(response["results"], response["scores"]):
print(row["key_text"], trace["merged_score"])
See Hybrid & Lexical Search for the scoring details
and the lexical-only (kvstore_lexical_query) variant.
Embed images and audio
embed_image and embed_audio mirror embed_text but take a base64-encoded
file in the request and return the same EmbeddingResponse shape. With a
cross-modal model (CLIP, SigLIP, CLAP) the vectors share the text space, so a
text query embedded with the same model id retrieves them by cosine similarity.
import base64, json
# Image — model ids: "clip", "siglip", "dinov2-small", "dinov2-large"
img_b64 = base64.b64encode(open("car.png", "rb").read()).decode()
img = json.loads(engine.embed_image(json.dumps({"model": "clip", "image": img_b64})))
image_vec = img["data"][0]["embedding"]
# Audio — model id "clap". WAV / RIFF only (not MP3/FLAC).
aud_b64 = base64.b64encode(open("bark.wav", "rb").read()).decode()
aud = json.loads(engine.embed_audio(json.dumps({"model": "clap", "audio": aud_b64})))
audio_vec = aud["data"][0]["embedding"]
:::note Gate the download yourself
embed_image / embed_audio read the model from the on-device cache and never
launch a silent blocking download. Check engine.is_model_cached("clip") and,
if missing, engine.model_download_async("clip") before embedding.
:::
See Multimodal Embedding for the model table and a full text → image retrieval example.
Classify text, images, and audio
Load a trained classifier under an id, then predict by modality. Each call
returns a result with a top label, its probability, and a top list of the
top-k {label, probability} pairs.
engine.load_classifier("steering", "/abs/path/to/models/steering")
# top_k is optional: omit it for all labels ranked; pass 0 for just the top
# label (empty `top` list); pass N for the N best.
text = json.loads(engine.classifier_predict_text("sentiment", "I love this!", top_k=3))
print(text["label"], text["probability"])
# Image classifier over DINOv2-Small embeddings (labels e.g. "left"/"right").
img = json.loads(engine.classifier_predict_image("steering", "/abs/path/frame.png", top_k=0))
print(img["label"])
# Audio classifier — WAV / RIFF input only.
aud = json.loads(engine.classifier_predict_audio("ambience", "/abs/path/clip.wav", top_k=3))
print(aud["label"], aud["probability"])
The image and audio embedders the classifier was trained with (e.g.
"dinov2-small", "clap") must be cached first — gate exactly as above. See
Classifiers for training and the full result schema.