title: NutriWeb
emoji: π₯
colorFrom: green
colorTo: blue
sdk: streamlit
sdk_version: 1.61.1
app_file: app.py
pinned: false
license: agpl-3.0
NutriWeb
Find a packaged food, and get a healthier alternative of the same kind that respects your allergens, your diet and your health profile.
Built on the full Open Food Facts product database (4.66M products), filtered to 796,542 US products.
Why this was rebuilt
The previous version ran on a hand-cleaned CSV subset of unknown provenance, downloaded from Google Drive at startup. Its recommendations weren't trustworthy, and the data turned out to be only one of three causes:
| Problem | Old approach | Now |
|---|---|---|
| Coverage | A static CSV subset, no lineage | 796,542 US products from the live upstream dataset, rebuildable in one command |
| Ingredient / allergen logic | ~90 hardcoded substrings matched with if key in name, so "sugar" fired on "sugar-free" |
Exact matching on Open Food Facts' canonical allergens_tags, additives_tags, ingredients_analysis_tags |
| Additive risk | A hand-written opinion list | EFSA overexposure ratings and the ANSES watch list, via the OFF additives taxonomy |
| Retrieval | FAISS index rebuilt on every query, with a positional slice applied to a filtered frame that misaligned scores against products | Category-constrained SQL candidate generation, ranked in NumPy |
| Relevance | No category constraint β a soda could return a candy bar | Candidates must share a category tag; regression-tested |
| Blood-pressure rule | sodium_100g > 5.0 β five grams per 100 g, a level no food reaches, so it never fired |
FDA-derived per-100g ceilings for salt and saturated fat |
| Passwords | Stored and compared in plaintext | bcrypt via passlib |
| Dependencies | torch + transformers + faiss β 2 GB | None of them; ~40 MB of pure Python and DuckDB |
What it does
Health score (0β100) β the single quantity used for ranking: 70% Nutri-Score 2023 + 30% NOVA processing group, minus a capped penalty for additives flagged by EFSA or ANSES.
Nutri-Score, computed where Open Food Facts has none. Only 44% of US products carry an OFF grade, because Nutri-Score is a European scheme and OFF usually cannot assign one without a category. NutriWeb ports the official 2023 algorithm from OFF's own reference implementation and computes the rest:
| products | share | |
|---|---|---|
| Published by Open Food Facts | 352,295 | 44.2% |
| Computed by NutriWeb | 290,908 | 36.5% |
| Total graded | 643,203 | 80.7% |
Validated against OFF's own grades on 317,265 products where both exist: 98.37% exact agreement, 99.73% within one letter, 95.99% exact numeric score.
Grades are never silently merged β the UI labels every grade with its source, and grades computed for products with no category are marked low confidence.
Recommendations β three stages:
- Candidates β products sharing a category tag, choosing the most specific tag whose pool clears a minimum size. Falls back to ingredient-tag overlap for the 49% of US products with no category.
- Hard filters β allergens (including traces), diet, and health-condition ceilings, applied in SQL before the limit so the pool is filtered, not truncated. These exclude; they are never traded off against a good score.
- Ranking β 45% health gain, 30% macro similarity, 20% ingredient overlap, 5% popularity. Every component is shown to the user.
Running it
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
# Build the catalog (one-off; ~7.2 GB download, then about a minute)
python pipeline/01_download.py # fetch food.parquet from the Hub
python pipeline/02_curate.py # filter, flatten, pivot -> DuckDB
python pipeline/03_score.py # Nutri-Score + health score + validation
python pipeline/04_audit.py # coverage report β read this
streamlit run app.py
01_download.py needs a Hugging Face token (hf auth login, or HF_TOKEN):
anonymous requests get rate-limited on a file that size.
Deploying to Spaces
Publish the built catalog so the Space doesn't rebuild from 7.2 GB on cold start:
python pipeline/05_publish.py <your-username>/nutriweb-us-catalog
Then set on the Space:
NUTRIWEB_CATALOG_REPO=<your-username>/nutriweb-us-catalogMONGODB_URI(optional) β as a repository secret, never in a file. Without it the app runs with session-only profiles.
Layout
app.py entry point; st.navigation
views/ search Β· product Β· recommend Β· compare Β· profile Β· insights
components/ badges, cards, nutrient tables
nutriweb/
data/catalog.py DuckDB access, FTS search
scoring/ nutriscore.py (2023 algorithm) Β· health_score.py Β· additives.py
reco/ engine.py Β· filters.py Β· similarity.py
profile/ model.py Β· auth.py
pipeline/ 01_download β 02_curate β 03_score β 04_audit β 05_publish
tests/ 64 tests: algorithm, filters, engine
Tests
pytest
Covers Nutri-Score variant selection and grade boundaries, the safety filters (an allergen must never reach a result), and engine guarantees β every recommendation is strictly healthier, shares the source category, and a soda never returns confectionery.
Data and licence
Product data Β© Open Food Facts contributors, licensed
ODbL. The Nutri-Score
implementation is ported from
openfoodfacts-server
(lib/ProductOpener/Nutriscore.pm), AGPL-3.0. This project is AGPL-3.0.
NutriWeb is an informational tool, not medical or dietary advice.