Visitar URL original
GitHub - vsvala/Wine-Recommendation-RAG-Fullstack-App: Wine recommendation RAG app using OpenAI embeddings, FAISS vector search, hybrid ranking, and GPT-4o-mini LLM explanations. Node.js + React. · GitHub
Skip to content

About

Wine recommendation RAG app using OpenAI embeddings, FAISS vector search, hybrid ranking, and GPT-4o-mini LLM explanations. Node.js + React.

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

10 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Wine Recommendation RAG System

A full-stack AI wine recommendation app that accepts natural language queries and returns ranked wine suggestions with sommelier-style explanations.

Example query: "bold red wine for steak under $30"

Built with Claude Code — Anthropic's AI coding assistant.


Architecture

┌─────────────────────────────────────────┐
│           User (Browser)                │
│     "bold red wine for steak $30"       │
└─────────────────┬───────────────────────┘
                  │ POST /recommend
┌─────────────────▼───────────────────────┐
│          React Frontend (Vite)          │
│   Search form → Wine cards + blurbs     │
└─────────────────┬───────────────────────┘
                  │
┌─────────────────▼───────────────────────┐
│         Express Backend (Node.js)       │
│                                         │
│  ┌─────────────┐    ┌────────────────┐  │
│  │ Redis Cache │◄───►   /recommend   │  │
│  │  (1h TTL)   │    │    pipeline    │  │
│  └─────────────┘    └───────┬────────┘  │
└────────────────────────────-│───────────┘
                              │
          ┌───────────────────▼───────────────────┐
          │        OpenAI Embeddings API           │
          │      text-embedding-3-small            │
          │   query string → 1536-dim vector       │
          └───────────────────┬───────────────────┘
                              │
          ┌───────────────────▼───────────────────┐
          │          FAISS Vector DB               │
          │   cosine similarity search             │
          │   returns top-20 wine candidates       │
          └───────────────────┬───────────────────┘
                              │
          ┌───────────────────▼───────────────────┐
          │           Hybrid Ranker                │
          │  0.35 semantic + 0.20 price_fit        │
          │  + 0.10 rating + 0.10 popularity       │
          │  → top-5 wines                         │
          └───────────────────┬───────────────────┘
                              │
          ┌───────────────────▼───────────────────┐
          │       GPT-4o-mini Explainer            │
          │  1 sommelier blurb per wine            │
          │  single API call for all 5 wines       │
          └───────────────────────────────────────┘

How Embeddings Work

Embeddings are the core of the RAG pipeline — they make semantic search possible.

"bold red wine for steak"
        │
        ▼
OpenAI text-embedding-3-small
        │
        ▼
[0.021, -0.134, 0.887, ...]   ← 1536 numbers (a vector)

The idea: every piece of text — whether a user query or a wine description — gets converted into a list of 1536 numbers. Texts that mean similar things end up as vectors that point in the same direction in that 1536-dimensional space.

At index build time (build_index.py):

  • Each wine's description field is sent to the Embeddings API
  • The returned vector is stored in the FAISS index
  • This runs once and is saved to disk

At query time (search.py):

  • The user's query is sent to the same Embeddings API
  • The returned query vector is compared against all stored wine vectors
  • FAISS returns the 20 wines whose vectors are most similar (cosine similarity)

Why cosine similarity? It measures the angle between vectors, not their length — so "cheap red" and "affordable red wine" land near the same wines even though the words differ.

The model used here is text-embedding-3-small — cheap (~$0.02 per million tokens), fast, and good enough for wine descriptions.


Tech Stack

Layer Technology
Frontend React 19, Vite 8
Backend Node.js, Express 5
Vector DB FAISS (via Python)
Embeddings OpenAI text-embedding-3-small
LLM OpenAI gpt-4o-mini
Cache Redis (ioredis)
Dataset Kaggle Wine Reviews (130k wines)

Project Structure

Wine-Recommendation-RAG-Fullstack-App/
├── back/
│   ├── index.js                  # Express server entry point
│   ├── routes/
│   │   └── recommend.js          # POST /recommend — main pipeline
│   ├── services/
│   │   ├── vectorSearch.js       # Calls search.py via child_process
│   │   ├── ranker.js             # Hybrid scoring engine
│   │   ├── explainer.js          # GPT-4o-mini explanation layer
│   │   └── cache.js              # Redis get/set wrapper
│   ├── scripts/
│   │   ├── filter_data.py        # Step 1: filter raw dataset
│   │   ├── build_index.py        # Step 2: embed wines + build FAISS index
│   │   └── search.py             # Step 3: query-time vector search
│   └── .env                      # API keys and config (not committed)
├── front/
│   └── src/
│       ├── App.jsx               # Search form + wine result cards
│       ├── App.css               # Component styles
│       └── index.css             # Design tokens, global reset
├── data/
│   ├── winemag-data-130k-v2.json # Raw Kaggle dataset (not committed)
│   ├── wines_filtered.json       # Filtered subset (not committed)
│   ├── wines.index               # FAISS index (not committed)
│   └── wines_ids.json            # Wine metadata for search results (not committed)
└── README.md

Prerequisites

  • Node.js 18+
  • Python 3.9+
  • Redis (running locally on default port 6379)
  • OpenAI API key with billing enabled

Python dependencies:

Use a virtual environment to keep these isolated from your system Python:

cd back
python3 -m venv venv
source venv/bin/activate   # Windows: venv\Scripts\activate
pip install numpy faiss-cpu openai

Activate this venv (source back/venv/bin/activate) whenever running the scripts below.

Redis (via Docker):

docker run -d --name wine-redis -p 6379:6379 redis

Stop/start later with docker stop wine-redis / docker start wine-redis. Verify it's up with docker exec wine-redis redis-cli ping (should return PONG).

Docker Desktop credential helper error? If docker run fails with docker-credential-desktop: executable file not found in $PATH, Docker Desktop's docker-credential-desktop symlink is broken (common after an update). Fix by removing the "credsStore" line from ~/.docker/config.json — no credential store is needed to pull public images like redis.

Prefer not to use Docker? Install natively instead: brew install redis && brew services start redis.


Setup

1. Clone and install

# Backend
cd back
npm install

# Frontend
cd ../front
npm install

2. Configure environment

Create back/.env:

OPENAI_API_KEY=sk-...
REDIS_HOST=localhost
REDIS_PORT=6379
PORT=3001

3. Prepare the dataset

Download the Kaggle Wine Reviews dataset and place winemag-data-130k-v2.json in data/.

Filter the dataset (run once, with the venv from Prerequisites active):

source back/venv/bin/activate
python3 back/scripts/filter_data.py

This produces data/wines_filtered.json.

By default filters to wines with points >= 98 (~116 wines) for low-cost testing. Change the threshold to 93 in filter_data.py for the full ~11.5k dataset.

Build the FAISS index (run once, costs ~$0.01–0.02 in OpenAI credits for the full dataset):

OPENAI_API_KEY=sk-your-actual-key-here python3 back/scripts/build_index.py

Replace sk-your-actual-key-here with your real key. Don't leave angle brackets (<...>) in the command — the shell treats < as input redirection and fails with no such file or directory.

This produces data/wines.index and data/wines_ids.json.

4. Start Redis

docker start wine-redis   # or: docker run -d --name wine-redis -p 6379:6379 redis

See the Redis setup note under Prerequisites if you haven't created the container yet.


Running the App

# Terminal 1 — backend (port 3001)
cd back
npm run dev

# Terminal 2 — frontend (port 5173)
cd front
npm run dev

Open http://localhost:5173.


API

POST /recommend

Request:

{ "query": "light white wine for seafood under $25" }

Response:

{
  "wines": [
    {
      "title": "...",
      "variety": "Chardonnay",
      "country": "France",
      "price": 22,
      "points": 98,
      "score": 0.7341,
      "explanation": "A crisp, mineral-driven Chardonnay that pairs perfectly with delicate seafood."
    }
  ],
  "fromCache": false
}

GET /health

Returns { "status": "ok" }.


Hybrid Ranking

Results are re-ranked after vector search using a weighted formula:

final_score =
  0.35 × semantic_similarity   (cosine similarity from FAISS)
  0.20 × price_fit             (how well price matches budget hint in query)
  0.10 × rating                (normalised points 93–100)
  0.10 × popularity            (inverse price proxy)
  0.25 × food_pairing_score    (reserved — not yet implemented)

Budget hints are parsed from natural language: "under $30", "30€", "30 dollars".


Security

Measure Implementation
Secure HTTP headers helmet() middleware — sets X-Content-Type-Options, X-Frame-Options, etc.
Rate limiting express-rate-limit — max 10 requests/min per IP on /recommend
Input validation Query string capped at 500 characters before reaching OpenAI API
CORS Restricted to frontend origin via CORS_ORIGIN env variable
Env isolation Child process (Python) receives only OPENAI_API_KEY, not full process.env
Secrets .env is gitignored — use .env.example as template

Note: The dataset license (CC BY-NC-SA 4.0) prohibits commercial use. Do not deploy this app commercially without replacing the dataset.


Roadmap

Features

  • Upgrade to full 11.5k wine dataset — change points >= 98 to >= 93 in filter_data.py and rebuild the index
  • Implement food pairing score (0.25 weight currently unused in ranker)
  • Filter UI — dropdowns for country, variety, price range
  • Docker Compose for one-command startup
  • Unit tests for ranker scoring logic
  • Error boundary in React for graceful frontend failures

Performance

  • Python long-lived process — biggest win. Currently a new Python process starts on every request, loading the FAISS index from disk each time (~500ms–2s cold start). Fix: keep search.py running as a persistent process communicating via stdin/stdout JSON.
  • Embedding cache — on cache miss, the query is re-embedded every time even for near-identical queries. Cache the query vector in Redis alongside the result.
  • Streaming LLM responses — stream GPT-4o-mini tokens to the frontend so the user sees blurbs appear progressively instead of waiting for all 5 at once.
  • FAISS IVFFlat index — current IndexFlatIP does exact search over all vectors (fine for 116 wines). At 11.5k+ wines, switch to IndexIVFFlat for approximate nearest-neighbour search — much faster at scale with minimal accuracy loss.

Data Attribution

The wine dataset used in this project is sourced from Kaggle:

Wine Reviews by zynicide on Kaggle
Original data scraped from WineEnthusiast (June 2017)
License: CC BY-NC-SA 4.0

This dataset is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0. This means the dataset and any derivatives may not be used for commercial purposes. This project is for educational and non-commercial use only.

About

Wine recommendation RAG app using OpenAI embeddings, FAISS vector search, hybrid ranking, and GPT-4o-mini LLM explanations. Node.js + React.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages