Moteur de recherche sémantique pour un catalogue e-commerce. Le client décrit ce qu'il veut en langage naturel — le moteur comprend l'intention et remonte les bons produits.
"un truc pour mon frigo" → Framed Magnet 4x4
"cadeau pour ma mère" → Framed Canvas 8x10
"décorer mon salon avec du design moderne" → Canvas 16x20, Metal Panel 11x14
"album photo de notre voyage au Japon" → Photobook Hardcover 8.5x11
FR et EN indifféremment, même modèle. 100 % local, zéro API externe, zéro coût.
Une searchbar classique matche des mots-clés. Le client doit connaître le vocabulaire du catalogue (canvas_11x14, framed_magnet_4x4). En réalité, il pense en intention : "un truc déco pour mon salon", "quelque chose de carré pour Instagram".
Query "un truc pour mon frigo"
│
▼
1. EMBEDDING (~30 ms)
La phrase → 768 nombres (coordonnée sémantique).
Modèle multilingue : "frigo" et "fridge" atterrissent au même endroit.
│
▼
2. VECTOR SEARCH (~5 ms)
Trouver les 30 produits dont les coordonnées sont les plus proches.
SQLite + sqlite-vec, distance cosine.
│
▼
3. INTENT BOOST + RE-RANKING (~500 ms)
Règles déterministes FR/EN ("frigo" → boost magnets, malus wall art).
Cross-encoder relit chaque paire (query, produit) ensemble pour affiner.
│
▼
4. GENERATION optionnelle (~1 s)
Un LLM local rédige 3-4 phrases expliquant pourquoi ces produits matchent.
Latence totale : ~500 ms (sans génération). Exploitable comme searchbar.
frontend/ Vite + React + Tailwind — landing page, searchbar, modale de résultats
backend/ FastAPI — pipeline RAG complet
scripts/ Enrichissement catalogue (LLM) + indexation (embedding → SQLite)
data/ Catalogue brut (52 produits) + index vectoriel
tests/ Suite d'acceptation pytest (12 queries × 2 niveaux de strictness)
docs/ Design doc, flow pipeline, présentation
┌─────────────────┐ POST /search ┌──────────────────────────────────┐
│ Frontend │ ────────────────> │ Backend RAG │
│ localhost:3000 │ │ localhost:8000 │
│ │ JSON response │ │
│ SearchBarHero │ <──────────────── │ embed → search → boost → │
│ Modal + cards │ │ rerank → (LLM generation) │
└─────────────────┘ └──────────────────────────────────┘
│ │
┌──────┘ └──────┐
▼ ▼
┌───────────┐ ┌────────────┐
│ sqlite-vec│ │ Ollama │
│catalog.db │ │ qwen2.5:7b │
└───────────┘ └────────────┘
| Composant | Technologie | Rôle |
|---|---|---|
| Embedding | intfloat/multilingual-e5-base (768 dim) |
Texte → vecteur sémantique, bilingue FR/EN |
| Vector store | sqlite-vec (SQLite extension) |
Recherche par proximité cosine |
| Re-ranker | BAAI/bge-reranker-v2-m3 |
Affine le classement en lisant query + produit ensemble |
| Intent boost | Règles déterministes FR/EN | Détecte "frigo", "salon", "cadeau"… → boost/malus sur les specs |
| LLM | qwen2.5:7b via Ollama (local) |
Enrichissement offline + explication online (toggle) |
| Backend | FastAPI + Python | Pipeline + API REST |
| Frontend | Vite + React 19 + Tailwind 4 | Landing page + modale immersive |
# Venv Python
uv venv --python 3.11
source .venv/bin/activate
uv pip install -r backend/requirements.txt
# LLM local
brew install ollama
brew services start ollama
ollama pull qwen2.5:7b
# Enrichir + indexer le catalogue (~18 min la première fois, idempotent ensuite)
python scripts/enrich_catalog.py
python scripts/index_catalog.py
# Lancer
uvicorn backend.main:app --host 0.0.0.0 --port 8000cd frontend
pnpm install
pnpm dev # → http://localhost:3000# Health check
curl http://localhost:8000/health
# Recherche
curl -X POST http://localhost:8000/search \
-H "Content-Type: application/json" \
-d '{"query": "un truc pour mon frigo", "top_k": 6}'
# Tests d'acceptation
pytest tests/ -vLes fiches produit brutes sont techniques (canvas_11x14, DPI 300, 11 inches). Un modèle d'embedding n'en tirera rien de sémantique.
Le script enrich_catalog.py passe chaque produit dans un prompt strict à qwen qui génère :
- ideal_for : usages client ("décorer un salon", "cadeau pour un mariage"…)
- semantic_keywords : mots-clés FR/EN ("salon", "living room", "cadeau", "gift"…)
- context_phrases : phrases client complètes en FR et EN
- occasions, rooms_or_locations, photo_subjects, style_tags
Le prompt est contrôlé par archetype (un magnet ≠ un canvas) et par tier de prix (un print à 0,79 $ ne peut pas être tagué "premium/luxury"). Ça évite les hallucinations et donne au re-ranker du signal à discriminer.
{
"query": "un truc pour mon frigo",
"top_k": 6,
"use_llm": false,
"use_llm_expansion": false
}| Paramètre | Défaut | Description |
|---|---|---|
query |
requis | Texte libre (FR ou EN) |
top_k |
6 | Nombre de résultats |
use_llm |
false | Active l'explication en langage naturel (+~1 s) |
use_llm_expansion |
false | Décompose la query en 3 facettes pour queries complexes (+~1 s) |
{
"status": "ok",
"products_indexed": 52,
"ollama": { "reachable": true, "llm_model_available": true },
"llm_model": "qwen2.5:7b"
}- Design doc complet — architecture, décisions, risques, roadmap
- Flow pipeline détaillé — du clic au résultat, step by step
- Présentation weekly — pitch, démo, chiffres clés
Usage interne.