Skip to main content

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?​