Download notebook · View source · Open in Colab
Recipe Text Search
Semantic text search compares embeddings of a query and stored descriptions. This example embeds dish descriptions from the Cookbook dataset, stores them with cuisine metadata, and searches them by natural-language query and cuisine filter.
Step 1: Install Dependencies
%pip install --quiet "aperturedb==0.5.1" sentence-transformers pandas python-dotenv
# Encode literal data without interpreting a leading dollar sign as a property.
def literal(value):
if isinstance(value, str) and value.startswith("$"):
return "$" + value
if isinstance(value, (list, tuple)):
return {"array": [literal(item) for item in value]}
if isinstance(value, dict):
if len(value) == 1 and ("date" in value or "_date" in value):
return {"to_date": literal(next(iter(value.values())))}
return {"json": value}
return value
Step 2: Connect to ApertureDB
Option A: ApertureDB Cloud The SDK connects with an API key from your Cloud instance. Obtain the key from Connect → Generate API Key, add it to a .env file, and run the Cloud configuration cell.
Option B: Local evaluation Follow the deployment guide, then configure the connection profile from that package.
See client configuration options for all connection methods and server setup options for deployment choices.
Option A: ApertureDB Cloud
# Create a .env file in this directory containing:
# APERTUREDB_KEY=your_key_here
from dotenv import load_dotenv
load_dotenv() # loads APERTUREDB_KEY into the environment
# create_connector() reads APERTUREDB_KEY automatically: no further config needed
Option B: Community Edition (local Docker)
# Configure the local profile from your versioned deployment package.
# https://docs.aperturedata.io/administration/deployment/DockerCompose
Step 3: Connect and Verify
from aperturedb.CommonLibrary import create_connector
client = create_connector()
response, _ = client.query([{"GetStatus": {}}])
client.print_last_response()
Step 4: Load the Cookbook Dataset
The Cookbook's images.adb.csv supplies dish names, captions, and cuisines. Each dish_name and caption pair becomes one description for the embedding model.
import pandas as pd
dishes = pd.read_csv("https://raw.githubusercontent.com/aperture-data/Cookbook/51b3c3d9f495f8fd77108a270b88fe8ab8cc0004/images.adb.csv")
dishes["description"] = dishes["dish_name"] + " - " + dishes["caption"]
print(f"Loaded {len(dishes)} dishes")
dishes[["dish_name", "food_tags", "description"]].head()
Step 5: Generate Embeddings
all-MiniLM-L6-v2 converts each description to a 384-dimensional vector. It can run on a CPU.
from sentence_transformers import SentenceTransformer
import numpy as np
model = SentenceTransformer("all-MiniLM-L6-v2")
embeddings = model.encode(dishes["description"].tolist(), normalize_embeddings=True)
print(f"Embedding shape: {embeddings.shape}") # (20, 384)
Step 6: Store Embeddings in ApertureDB
A DescriptorSet is ApertureDB's vector index. Each dish is stored as a Descriptor with metadata for hybrid filtering.
import uuid
SET_NAME = "recipe_text_search_" + uuid.uuid4().hex[:12]
client.query([{"AddDescriptorSet": {
"name": SET_NAME,
"dimensions": 384,
"engine": "HNSW",
"metric": "CS",
}}])
client.print_last_response()
for _, row in dishes.iterrows():
emb = embeddings[row.name].astype("float32")
client.query([{
"AddDescriptor": {
"set": SET_NAME,
"label": row["food_tags"],
"properties": {
"dish_name": row["dish_name"],
"cuisine": row["food_tags"],
"caption": row["caption"],
"recipe_url": row["Recipe URL"],
},
"if_not_found": ['$dish_name', '==', literal(row['dish_name'])],
}
}], [emb.tobytes()])
print(f"Stored {len(dishes)} recipe embeddings")
Step 7: Search by Natural Language Query
The query text is encoded with the same model used for the stored descriptions. FindDescriptor compares that vector with the dish embeddings.
def search_recipes(query_text, k=3, cuisine=None):
query_emb = model.encode([query_text], normalize_embeddings=True)[0].astype("float32")
find = {
"set": SET_NAME,
"k_neighbors": k,
"distances": True,
"results": {"all_properties": True},
}
if cuisine:
find["constraints"] = ["$cuisine", "==", literal(cuisine)]
response, _ = client.query([{"FindDescriptor": find}], [query_emb.tobytes()])
return response[0]["FindDescriptor"].get("entities", [])
results = search_recipes("spicy curry with rice")
print("Top matches for 'spicy curry with rice':")
for r in results:
print(f" {r['dish_name']:<45} [{r['cuisine']}] score={1 - r['_distance']:.3f}")
Step 8: Filter by Cuisine
The cuisine constraint limits returned descriptors to the selected cuisine as part of the KNN query.
results = search_recipes("noodles in broth", k=3, cuisine="Japanese")
print("Japanese dishes similar to 'noodles in broth':")
for r in results:
print(f" {r['dish_name']}")
print(f" {r['caption']}")
print(f" {r['recipe_url']}")
Cleanup
client.query([{"DeleteDescriptorSet": {"with_name": SET_NAME}}])
client.print_last_response()
What's Next?
- Quick Start: store and retrieve one image with metadata
- Work with Descriptors: update, delete, and bulk-load embeddings
- RAG pipelines: connect ApertureDB to LangChain for question answering
- ApertureDB Cloud: managed database instances