Skip to main content
Version: 0.12

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", ""))

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.