Tuxedo No. 2 – Cocktail recipes
The classic Tuxedo No. 2—gin, vermouth, orange bitters, and a dash of orange peel—served as a prototype for a larger ambition: an AI‑driven system that can...
Listen to Article
PlayingClick play to listen to audio narration
Table of Contents
Introduction
The classic Tuxedo No. 2—gin, vermouth, orange bitters, and a dash of orange peel—served as a prototype for a larger ambition: an AI‑driven system that can generate, validate, and recommend cocktail recipes at scale. Existing recipe sites store static data; they do not adapt to user taste, suggest substitutions, or ensure procedural correctness. The challenge is to build a system that understands cocktail chemistry, learns from user interactions, and presents step‑by‑step guidance in real time.
Why This Matters
Software engineers are increasingly asked to blend domain expertise with machine learning. A cocktail recommendation engine touches on natural language processing, collaborative filtering, and real‑time APIs—all within a single service. Mastering this domain provides a sandbox for experimenting with embedding models, vector stores, and modular micro‑services while delivering a tangible product that hobbyists and bar professionals can use directly.
How It Works
The flow from a user’s request to a ready‑to‑mix recipe follows a well‑defined pipeline. The diagram below illustrates the major components and data movements.
flowchart TD
A[User Request] --> B[API Gateway]
B --> C[Authentication Middleware]
C --> D[Request Router]
D --> E{Request Type}
E -->|Recipe Lookup| F[Recipe Service]
E -->|Recommendation| G[Similarity Engine]
E -->|Substitution| H[Substitution Service]
F --> I[PostgreSQL / Cocktail DB]
G --> J[Vector Store (Pinecone/Chroma)]
G --> K[Embedding Model (Sentence‑BERT)]
H --> L[Ingredient Compatibility DB]
I --> M[Response Formatter]
J --> M
K --> G
L --> M
M --> N[Client UI]
Step‑by‑step breakdown
- User Input – The client sends a query such as “show me gin‑forward cocktails under 150 ml” or “what can I replace bourbon with?”.
- API Gateway – Routes traffic, enforces rate limits, and aggregates metrics.
- Authentication – Validates JWT or API key; attaches user ID to the request context.
- Request Router – Dispatches to one of three internal services:
- Recipe Service for exact matches.
- Similarity Engine for AI‑driven recommendations.
- Substitution Service for ingredient swaps.
- Recipe Service – Queries a relational database (
cocktail_recipestable) and returns structured JSON. - Similarity Engine – Generates embeddings for each cocktail’s flavor profile using a sentence transformer, performs a nearest‑neighbor search against a vector index, and returns a ranked list.
- Substitution Service – consults a lookup table that groups ingredients by flavor compounds and returns compatible alternatives.
- Response Formatter – Assembles the final payload, enriches it with preparation steps, estimated pour times, and visual assets.
- Client UI – Renders the recipe, optionally triggers voice‑assist for hands‑free guidance.
Core Concepts
- Flavor Graph – Nodes represent ingredients; edges encode similarity based on shared flavor compounds (e.g., “citrus”, “herbaceous”). This graph drives substitution logic.
- Embedding Space – Each cocktail is encoded as a vector in a high‑dimensional space where Euclidean distance approximates perceptual similarity.
- Domain Constraints – Rules such as “no more than three spirit types per recipe” or “alcoholic strength ≤ 30 % ABV” are enforced at the model level.
- User Profile – Stores liked recipes, rating history, and explicit preferences (e.g., “I avoid anise”). The profile feeds collaborative filters.
- Preparation Ontology – Structured steps with estimated durations, equipment, and safety flags (e.g., “shake vigorously” → high turbulence flag).
Examples & Code Walkthrough
Flavor Similarity Calculator
class FlavorSimilarityCalculator:
"""Compute similarity between two cocktails based on ingredients and flavor compounds."""
def __init__(self, flavor_map):
# flavor_map: ingredient -> set of flavor tokens (e.g., {"gin": {"botanical", "citrus"}}
self.flavor_map = flavor_map
self.ingredient_index = {}
self._build_index()
def _build_index(self):
for ing, tokens in self.flavor_map.items():
self.ingredient_index[ing] = tokens
def similarity(self, cocktail_a, cocktail_b):
"""
cocktail_a / cocktail_b: dict with keys 'name' and 'ingredients' (list of strings)
Returns a dict with ingredient overlap, flavor overlap, and combined score.
"""
set_a = set(cocktail_a['ingredients'])
set_b = set(cocktail_b['ingredients'])
common = set_a & set_b
# Ingredient overlap ratio
total_ingredients = set_a | set_b
ingredient_score = len(common) / len(total_ingredients) if total_ingredients else 0.0
# Flavor overlap ratio
flavors_a = set().union(*(self.flavor_map.get(i, set()) for i in set_a))
flavors_b = set().union(*(self.flavor_map.get(i, set()) for i in set_b))
common_flavors = flavors_a & flavors_b
total_flavors = flavors_a | flavors_b
flavor_score = len(common_flavors) / len(total_flavors) if total_flavors else 0.0
combined = 0.6 * ingredient_score + 0.4 * flavor_score
return {
'ingredient_overlap': ingredient_score,
'flavor_overlap': flavor_score,
'combined_score': combined
}
Usage example:
flavor_db = {
"gin": {"botanical", "citrus"},
"dry_vermouth": {"herbal", "bitter"},
"orange_bitters": {"citrus", "bitter"},
"orange_peel": {"citrus", "aromatic"},
"bourbon": {"vanilla", "caramel"},
"rye": {"spicy", "vanilla"}
}
calc = FlavorSimilarityCalculator(flavor_db)
tuxedo = {'name': 'Tuxedo No. 2', 'ingredients': ['gin', 'dry_vermouth', 'orange_bitters', 'orange_peel']}
martini = {'name': 'Classic Martini', 'ingredients': ['gin', 'dry_vermouth']}
print(calc.similarity(tuxedo, martini))
Ingredient Line Parser
import re
class IngredientLineParser:
"""Parse a free‑form ingredient line into structured data."""
def __init__(self):
self.unit_map = {
'oz': 'fluid_ounce',
'ml': 'milliliter',
'tsp': 'teaspoon',
'tbsp': 'tablespoon',
'dash': 'dash',
'part': 'part'
}
def parse(self, line: str) -> dict | None:
"""
Expected format: "1 oz gin" or "2 parts orange bitters".
Returns a dict with amount, unit, original_text, ingredient_name.
"""
line = line.strip()
# Pattern: optional decimal number, whitespace, unit, whitespace, ingredient name
pattern = r'(\d+(?:\.\d+)?)\s*([a-zA-Z]+?)\s*(.+)'
match = re.match(pattern, line, re.IGNORECASE)
if not match:
return None
amount_str, unit_raw, ingredient_raw = match.groups()
unit = self.unit_map.get(unit_raw.lower(), unit_raw.lower())
return {
'amount': float(amount_str),
'unit': unit,
'original_text': line,
'ingredient_name': ingredient_raw.strip()
}
def parse_steps(self, block: str) -> list[dict]:
"""
Convert a block of numbered preparation steps into a list of step objects.
"""
steps = []
for line in block.splitlines():
line = line.strip()
m = re.match(r'(\d+)\.\s*(.+)', line)
if m:
step_num, instruction = m.groups()
steps.append({
'step_number': int(step_num),
'instruction': instruction,
'estimated_seconds': self._estimate_seconds(instruction)
})
return sorted(steps, key=lambda s: s['step_number'])
def _estimate_seconds(self, instruction: str) -> int:
# Very rough heuristic: count keywords that imply duration
if 'shake' in instruction.lower():
return 8
if 'stir' in instruction.lower():
return 6
if 'build' in instruction.lower() or 'pour' in instruction.lower():
return 3
return 2
Example usage:
parser = IngredientLineParser()
print(parser.parse("2 oz gin"))
# {'amount': 2.0, 'unit': 'fluid_ounce', 'original_text': '2 oz gin', 'ingredient_name': 'gin'}
steps = parser.parse_steps("""1. Pour gin and vermouth into a shaker.\n2. Add orange bitters.\n3. Shake until chilled.\n4. Strain into coupe.\n5. Express orange peel over drink.""")
print(steps)
API Endpoint (FastAPI)
from fastapi import FastAPI, HTTPException
from typing import List
app = FastAPI()
@app.get("/cocktails/{name}")
async def get_cocktail(name: str):
# Pseudo code: fetch from PostgreSQL
cocktail = await db.fetch_one("SELECT * FROM cocktail_recipes WHERE name = %s", name)
if not cocktail:
raise HTTPException(status_code=404, detail="Recipe not found")
return cocktail
@app.get("/recommendations")
async def recommend(user_id: str, limit: int = 10):
# Load user profile
profile = await db.fetch_one("SELECT * FROM user_preferences WHERE id = %s", user_id)
# Generate recommendations via similarity engine
recs = await similarity_engine.suggest(profile, limit)
return recs
@app.get("/substitute/{ingredient}")
async def substitute(ingredient: str):
alt = await sub_service.find_alternatives(ingredient)
return {"original": ingredient, "alternatives": alt}
Best Practices
- Separate Embedding Training – Keep the sentence transformer outside the request path; pre‑compute recipe vectors nightly and store them in a vector DB. This avoids latency spikes during peak traffic.
- Use Read‑Through/Write‑Through Caching – Cache frequent recipe lookups in Redis. Invalidation triggers on any update to the underlying PostgreSQL tables.
- Validate Input Early – Parse and normalize ingredient units at ingress; reject malformed lines before they reach business logic.
- Domain‑Driven Design – Keep cocktail, ingredient, and flavor entities in a dedicated module (
domain.cocktails) to enable independent testing and schema evolution. - Observability – Tag every request with
cocktail_id,user_id, andoperation_type. Use distributed tracing to spot bottlenecks in the similarity engine or substitution service.
Common Mistakes & Anti-Patterns
- Over‑reliance on a Single Similarity Metric – Using only ingredient count ignores subtle flavor interactions. Combine ingredient overlap, flavor token similarity, and embedding distance.
- Ignoring Unit Conversions – A recipe stored with “oz” cannot be directly compared to one using “ml”. Normalize all measurements to a canonical unit at ingestion.
- Blocking I/O in Synchronous Handlers – Performing vector searches or external API calls inside a FastAPI endpoint without async/await will starve other requests. Offload heavy computation to a background worker queue (e.g., Celery).
- Hard‑Coded Flavor Maps – Flavor groupings are domain‑specific and evolve with trends. Store them in a configurable JSON file or a small relational table that can be updated without code deployments.
Performance Considerations
- Embedding Generation – A 384‑dim Sentence‑BERT inference costs ~30 ms per cocktail on a single GPU. Batch processing 100 recipes adds ~3 s latency, which is acceptable for nightly updates but not for real‑time recommendations.
- Vector Search – Using HNSW on a 1 M‑dim index yields O(log N) lookup times (~1 ms). Ensure the index is updated incrementally to avoid re‑building from scratch.
- Database Load – The relational schema is accessed for write‑heavy operations (new recipe uploads). Use connection pooling and sharding if write throughput exceeds 200 TPS.
- Memory Footprint – The flavor graph can be stored as adjacency lists in a graph database (Neo4j) to enable fast traversal for substitution suggestions.
Real‑World Usage
- Bar Management Platforms – Companies like Pourly and Drinksify embed recommendation engines to suggest house specials based on inventory levels and customer feedback.
- Home Mixology Apps – Apps such as “Cocktail AI” use collaborative filtering to surface user‑generated recipes, leveraging the same similarity patterns described above.
- Enterprise Catering – Large event caterers run internal instances to generate menu variants that respect dietary restrictions while preserving brand flavor profiles.
Frequently Asked Questions (FAQ)
Q: How do we handle missing ingredient data?
A: Maintain a fallback mapping to a canonical “unknown” ingredient category. When a flavor token is missing, assign a neutral weight (e.g., 0.5) to avoid skewing similarity scores.
Q: Can the system suggest new recipes rather than just picking from existing ones?
A: The current architecture focuses on recombination. Adding generative capabilities would require a language model fine‑tuned on cocktail chemistry, which is a separate module.
Q: What is the latency budget for a recommendation request?
A: Under normal load, we target <200 ms end‑to‑end. This includes authentication, vector search, and serialization. Monitoring alerts if the p95 exceeds 300 ms.
Q: How do we ensure data consistency between relational and vector stores?
A: Use a message queue (Kafka) to emit events on any recipe upsert. A background service consumes these events, recomputes embeddings, and updates the vector index atomically.
Q: Are there any legal concerns with using copyrighted recipes?
A: Public domain and user‑submitted recipes are safe. For proprietary cocktails, obtain explicit licensing or restrict access to internal networks.
Conclusion
The Tuxedo No. 2 AI system demonstrates how a classic cocktail can serve as a blueprint for a scalable, data‑driven recommendation platform. By combining structured relational storage, flavor‑aware similarity calculations, and real‑time API services, engineers can deliver experiences that feel both personal and precise. The modular design allows future extensions—image recognition for ingredient identification, generative recipe drafting, and integrated nutrition analysis—while keeping the core logic testable, performant, and maintainable.
Written by Senior AI Research Scientist
Editorial staff persona reviewing transformer layers, neural networks fine-tuning, retrieval-augmented generation (RAG), and model evaluation metrics.