Semantic Wine Search with Elixir, Bumblebee and Apple Silicon
See It in Action
Before diving into the code, try the live demo: garcon.fly.dev
Type something like:
-
"crisp citrus mineral"- finds fresh, zesty champagnes -
"rich toasty brioche"- returns aged, complex sparkling wines -
"cheap party wine"- budget-friendly proseccos and cavas
The app returns wines that match semantically, even when those exact words aren’t in the description. That’s the magic of embeddings.
What’s Under the Hood
Here’s the entire search logic:
defmodule Garcon.Search.Similarity do
alias Garcon.Search.Index
alias Garcon.ML.Embeddings
def search(query_text, opts \\ []) do
top_k = Keyword.get(opts, :top_k, 10)
query_embedding = Embeddings.generate_query_embedding(query_text)
Index.search(query_embedding, top_k)
end
end
That’s it. Three lines of actual logic:
- Generate an embedding from the user’s query
- Search the index for similar wines
- Return the top K results
The complexity is hidden in two places: the ML model that generates embeddings, and the index that makes similarity search fast. We’ll build both from scratch.
The Tech Stack
- Phoenix LiveView: Real-time interface with instant search results
- Bumblebee: Elixir library for ML models (Hugging Face)
- Nx: Tensors and numerical computing (Elixir’s “NumPy”)
- EMLX/EXLA: Nx backends for hardware acceleration (EMLX for Apple Silicon’s Metal GPU in dev, EXLA for production)
- Explorer: DataFrames for loading the CSV
- Agent: In-memory wine storage (simple and fast)
- DETS: Persistent embedding cache
We’ll start with a simple brute-force approach using Nx.dot for similarity search. This works great for learning the concepts, but we’ll quickly hit performance limits. Later in the series, we’ll explore how to scale this properly…
The Dataset
~130k wines extracted from Kaggle’s Wine Reviews dataset. Each wine has:
-
title: wine name -
description: tasting notes (our text for NLP) -
variety: grape variety (Champagne Blend, Prosecco, etc.) -
points: score out of 100 -
price: price in dollars
How It Works (Overview)
┌─────────────────────────────────────────────────────────────────┐
│ PREPARATION (once) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ CSV ──► Explorer DataFrame ──► Agent (memory) │
│ │
│ For each wine: │
│ description ──► e5 Model ──► embedding [384 floats] │
│ │ │
│ ▼ │
│ DETS (disk) │
│ │ │
│ ▼ │
│ Normalized Nx matrix │
│ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ SEARCH (real-time) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ "crisp citrus" ──► e5 Model ──► query embedding [384] │
│ │ │
│ ▼ │
│ Nx.dot(matrix, query) │
│ │ │
│ ▼ │
│ Similarity scores │
│ │ │
│ ▼ │
│ Top 10 wines + scores │
│ │
└─────────────────────────────────────────────────────────────────┘
Let’s Build It
Create a new Phoenix app:
mix phx.new garcon --no-ecto --no-mailer
cd garcon
We skip Ecto because we’ll store wines in memory with an Agent. Simpler for this use case.
Add the ML dependencies to mix.exs:
defp deps do
[
# ... existing deps ...
# ML stack
{:nx, "~> 0.9"},
{:exla, "~> 0.9"},
{:bumblebee, "~> 0.6"},
{:explorer, "~> 0.9"},
# Apple Silicon GPU (optional, dev only)
{:emlx, github: "elixir-nx/emlx", only: :dev}
]
end
Fetch dependencies:
mix deps.get
The first time you run the app, Bumblebee will download the ML model (~120MB). This happens once and gets cached in ~/.cache/bumblebee.
What We’ll Cover
- Embeddings explained - What are vectors and why they capture meaning
- Nx explained - Tensors, operations, and why it’s fast
- Bumblebee and models - Loading and running the e5 model
- Storage with ETS/DETS - Caching embeddings for performance
- Vectorized search - Brute-force cosine similarity with Nx
- LiveView interface - Real-time search with debouncing
- Recap - Putting it all together
- Scaling up - When brute-force isn’t enough anymore…
Prerequisites
- Elixir 1.15+
- A Mac with Apple Silicon for EMLX, otherwise EXLA works too
Need help using ML/AI in your systems? We specialize in Elixir systems that gets clever everyday. Get in touch.