This publication presents a retrieval-augmented generation (RAG) assistant that answers one
question: "I have this bottle of wine, what should I cook?" It retrieves facts about the wine
from 49 Wikipedia pages, picks matching recipes from 51 Wikibooks Cookbook recipes, and writes a
short answer that cites its sources. You can extend the knowledge base with your own recipes and
pairing notes. When the wine is not in its knowledge base, it says so instead of inventing facts. The assistant runs fully locally with qwen2.5:7b through Ollama, or
with OpenAI, Groq or Google Gemini, and passes all 8 questions of its end-to-end evaluation.
You will learn how the pipeline is built (three LLM calls and four filtered vector searches), why
each design decision was made, and how a small local model can be kept from hallucinating with
checks in code rather than in the prompt.
People are often given a bottle of wine as a gift and do not know what to cook with it.
This is a shame, because gift bottles are often special wines that need the right dish to show
their full potential. This project builds an assistant that solves exactly that problem, in one
direction only: wine → dish. The assistant also explains why the wine pairs well with the
dish, which is a great way to learn more about wine and how to pair it with food.
The objectives were to build an assistant that:
data/pairing_notes/_template.md, you can write down, in your own words, pairings you haveObjectives 1 to 4 are tested in the end-to-end evaluation, objective 5 is covered by the
evaluation and trace scripts; see Evaluation.
Wine pairing advice is easy to find, but it is a poor fit for the "gift bottle" situation. The
advice online is also usually very general, and it cannot include your own experiences that you
would like to recreate.
RAG addresses the first two problems by grounding the answer in retrieved documents [1, 2]. The
third is solved by using only openly licensed text (CC BY-SA 4.0).
Success criteria: the assistant must suggest a suitable dish for well-known wines, refuse to
describe wines it has no information about, and redirect questions outside its scope.
Retrieval-augmented generation was introduced by Lewis et al. [1] to combine a parametric
language model with a non-parametric document index; Gao et al. [2] survey the many variants
since. Two ideas from that literature shaped this project:
The gap this project fills is small but practical: most RAG tutorials use one retrieval step over
one document type. A pairing question needs two different kinds of knowledge (wine facts and
recipes) that never mention each other, so a single similarity search cannot connect them.
| Source | Pages | Category | License |
|---|---|---|---|
| Wikipedia | 5 | pairing principles (food pairing, acidity, sweetness, oak, tasting terms) | CC BY-SA 4.0 |
| Wikipedia | 7 | wine styles (red, white, rosé, sparkling, dessert, …) | CC BY-SA 4.0 |
| Wikipedia | 24 | grape varieties (13 red, 11 white) | CC BY-SA 4.0 |
| Wikipedia | 13 | well-known wines and regions (Barolo, Chablis, Rioja, …) | CC BY-SA 4.0 |
| Wikibooks Cookbook | 51 | home-cooking recipes, from boeuf bourguignon to pad thai | CC BY-SA 4.0 |
In total: 100 documents, about 1.1 million characters, split into 2,199 chunks with an average
length of 545 characters.
A download script fetches the pages through the MediaWiki API, removes reference and link
sections, and saves each page as Markdown with a metadata header (title, category, URL, revision
ID, license, download date). It respects Wikimedia's rate limits and writes an attribution file
listing every page.
Every question goes through three LLM calls and four vector searches:
question ──► 1. classify + wine search query (LLM)
│ "wine" → continue "dish" / "other" → fixed redirect, no search
▼
search wine knowledge with the question AND the rewritten query
├─ 4 chunks about the wine (grapes, regions, styles)
└─ 2 chunks of general pairing principles
▼
coverage check (code): is the named wine in the retrieved chunks?
▼
2. wine traits → food search query (LLM)
▼
search recipes ─► 4 recipe chunks
▼
3. answer from the 10 numbered chunks + coverage instruction (LLM)
Step 1: classify and rewrite. The LLM replies in a fixed three-line format:
TOPIC: wine
WINE: Barolo
SEARCH: Barolo food pairing, body, tannin, acidity
dish and other questions stop here with a fixed answer. If the model labels a question
wine but finds no wine and no search query, the code treats it as other.
Step 2: retrieve wine knowledge. Two searches with a ChromaDB metadata filter: one for
grape, style and region pages (4 chunks), and one for pairing principles (2 chunks). Both the
original question and the rewritten query are searched, and the best distinct chunks are kept,
so a poor rewrite cannot take over the results.
Step 3: coverage check. The code checks whether the named wine appears in the retrieved
chunks (accent-insensitive) and whether the best cosine distance is below 0.5. The result is one
of three instructions for the final prompt: covered, loose match, or "the knowledge base contains
NO information about Saperavi; say so and do not describe it".
Step 4: food search. The LLM reads the retrieved wine chunks and replies in two lines:
TRAITS (body, acidity, tannin, sweetness, flavors) and SEARCH (at most 12 words of dishes or
ingredients that suit those traits). For Chablis, the search became
Grilled fish, shellfish, goat cheese, salad. The recipes are searched with that query
(4 chunks).
Step 5: answer. The final prompt contains the coverage instruction, the 10 numbered chunks
and the question. The model describes the wine, suggests one or two of the retrieved recipes,
explains the pairing in terms of the wine's traits, and cites chunk numbers.
$ python src/app.py "I was given a bottle of Chablis, what should I cook?"
(wine search: Chablis food pairing, body, acidity)
(recipe search: Grilled fish, shellfish, goat cheese, salad)
(coverage: The wine knowledge covers this question)
Given your Chablis, you might want to consider a Greek Salad [7]. Chablis is known for its high
acidity, which can cut through the richness of the olive oil and the saltiness of the feta cheese
in the salad. [...]
If you prefer something heartier, you could also try Fish and Chips [10]. [...]
Sources:
[1] Chablis (AOC) (distance 0.40) https://en.wikipedia.org/wiki/Chablis_(AOC)
[3] Chardonnay (distance 0.46) https://en.wikipedia.org/wiki/Chardonnay
[7] Greek Salad (distance 0.49) https://en.wikibooks.org/wiki/Cookbook:Greek_Salad
...
Almost every decision below was made after a test question failed.
| Decision | Problem it solved | Alternative considered |
|---|---|---|
| Separate filtered searches for wine knowledge, pairing principles and recipes | One search for "Chianti" returned five Chianti chunks and no recipe. | One search with more results: still dominated by the wine pages. |
| LLM writes the food search query from the wine's traits | Searching the recipes for "Barolo" returned tiramisù: no recipe mentions wine names, and many wine pages (e.g. Chablis) never mention food. | Searching recipes with the question: random dishes. |
| Trait → food examples in the food-search prompt | With one wine → food example, the 7B model copied it for every wine (beef stew for Chablis). | More wine examples: still copied, and they leak facts into the search. |
| Coverage check in code | Asked about Saperavi, which is not in the knowledge base, the model described it from memory despite the prompt rule. | A stricter prompt only: ignored by the small model. |
| Scope check with three labels | "What's the capital of France?" was answered from general knowledge. A separate dish label keeps dish → wine questions (which mention wine) from being labeled wine. | Two labels (wine/other): the model has nowhere to put dish questions. |
| Search with the question and the rewrite | The rewrite for a curry question added Syrah and Grenache, words that were not in the question. | Rewrite only: one bad rewrite spoils the results. |
| Markdown-aware chunks of ≤ 800 characters with a "Title > Section" prefix | The embedding model reads at most 256 tokens; without the prefix, a chunk from "Viticulture" no longer says it is about Pinot noir. | Fixed-size chunks: split sentences and headings. |
| Two Wikipedia pages removed from the pairing principles | The trace script showed that "Tannin" and "Umami" filled the principle slots with plant biology and animal nutrition. | Keeping them: they wasted 2 of the 6 wine-knowledge chunks. |
| Component | Choice |
|---|---|
| Orchestration | LangChain (ChatPromptTemplate -> llm -> StrOutputParser) |
| Vector store | ChromaDB 1.0, persistent, cosine distance |
| Embeddings | sentence-transformers/all-MiniLM-L6-v2 [6] on CPU |
| Chunking | LangChain MarkdownHeaderTextSplitter + RecursiveCharacterTextSplitter (800 / 100 overlap) |
| LLM | Ollama qwen2.5:7b [7] (tested), or OpenAI, Groq, Google Gemini |
| Interface | Command line: one question, or an interactive session |
The local model runs with a context window of 8,192 tokens: Ollama's default is smaller and
silently truncates the prompt (about 3,000 tokens with 10 chunks). Temperature is 0 and random
seeds are fixed (42).
Environment: developed and tested on an Apple M1 with 16 GB of memory, macOS, Python 3.13 and
Ollama 0.40. No GPU is needed. The dependencies take about 2 GB because sentence-transformers
installs PyTorch. Building the index takes about a minute.
git clone https://github.com/KaiHoogenhoud/wine-pairing-rag-assistant.git cd wine-pairing-rag-assistant python3 -m venv .venv && source .venv/bin/activate pip install -r requirements.txt cp .env.example .env # then set OLLAMA_MODEL=qwen2.5:7b or an API key python src/app.py "A friend brought a Riesling, what can I make for dinner?"
The knowledge base is included in the repository, so no download is needed. Full instructions,
the configuration options and the project structure are in the repository README.
The questions cover a full-bodied red (Barolo), a crisp white (Chablis), an aromatic white
(Riesling), a light red (Pinot noir), a style without a grape (sweet dessert wine), a wine outside
the knowledge base (Saperavi), a dish → wine question and an off-topic question.
With qwen2.5:7b on the Apple M1, all 8 questions pass.
| Question | Checks | Suggested dishes | Time |
|---|---|---|---|
| Barolo | scope ✅ dish ✅ | roast leg of lamb, boeuf bourguignon | 41 s |
| Chablis | scope ✅ dish ✅ | Greek salad, fish and chips | 45 s |
| Riesling | scope ✅ dish ✅ | Thai green curry, pad thai | 45 s |
| Pinot noir | scope ✅ dish ✅ | risotto alla Milanese, roast leg of lamb | 42 s |
| Sweet dessert wine | scope ✅ dish ✅ | crème brûlée | 35 s |
| Saperavi (not in knowledge base) | scope ✅ dish ✅ coverage ✅ | roast leg of lamb, macaroni and cheese | 29 s |
| Thai green curry (dish → wine) | scope ✅ | redirected | 1 s |
| Capital of France (off-topic) | scope ✅ | redirected | 1 s |
The keyword check confirms that a sensible dish is suggested, not that the explanation is right,
so all answers were also read by hand and their claims checked against the source text.
This is a working prototype for learning and demonstration, run locally from the command line.
Running it as a service for home cooks would need:
--rebuild.data/pairing_notes/ folder and its template are ready,data/SOURCES.md.