recipes: nutrition calculator with BLS/USDA matching, manual overwrites, and skip
Dual-source nutrition system using BLS (German, primary) and USDA (English, fallback) with ML embedding matching (multilingual-e5-small / all-MiniLM-L6-v2), hybrid substring-first search, and position-aware scoring heuristics. Includes per-recipe and global manual ingredient overwrites, ingredient skip/exclude, referenced recipe nutrition (base refs + anchor tags), section-name dedup, amino acid tracking, and reactive client-side calculator with NutritionSummary component.
This commit is contained in:
25
src/models/NutritionOverwrite.ts
Normal file
25
src/models/NutritionOverwrite.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
/**
|
||||
* Global nutrition overwrites — manually map ingredient names to BLS/USDA entries.
|
||||
* Checked during nutrition generation before embedding search.
|
||||
* Can also mark ingredients as excluded (skipped).
|
||||
*/
|
||||
const NutritionOverwriteSchema = new mongoose.Schema({
|
||||
// The normalized ingredient name this overwrite matches (German, lowercase)
|
||||
ingredientNameDe: { type: String, required: true },
|
||||
// Optional English name for display
|
||||
ingredientNameEn: { type: String },
|
||||
// What to map to
|
||||
source: { type: String, enum: ['bls', 'usda', 'skip'], required: true },
|
||||
fdcId: { type: Number },
|
||||
blsCode: { type: String },
|
||||
nutritionDbName: { type: String },
|
||||
// Whether this ingredient should be excluded from nutrition calculation
|
||||
excluded: { type: Boolean, default: false },
|
||||
}, { timestamps: true });
|
||||
|
||||
NutritionOverwriteSchema.index({ ingredientNameDe: 1 }, { unique: true });
|
||||
|
||||
delete mongoose.models.NutritionOverwrite;
|
||||
export const NutritionOverwrite = mongoose.model('NutritionOverwrite', NutritionOverwriteSchema);
|
||||
@@ -163,6 +163,25 @@ const RecipeSchema = new mongoose.Schema(
|
||||
}
|
||||
},
|
||||
|
||||
// Nutrition calorie/macro mapping for each ingredient
|
||||
nutritionMappings: [{
|
||||
sectionIndex: { type: Number, required: true },
|
||||
ingredientIndex: { type: Number, required: true },
|
||||
ingredientName: { type: String },
|
||||
ingredientNameDe: { type: String },
|
||||
source: { type: String, enum: ['bls', 'usda', 'manual'] },
|
||||
fdcId: { type: Number },
|
||||
blsCode: { type: String },
|
||||
nutritionDbName: { type: String },
|
||||
matchConfidence: { type: Number },
|
||||
matchMethod: { type: String, enum: ['exact', 'embedding', 'manual', 'none'] },
|
||||
gramsPerUnit: { type: Number },
|
||||
defaultAmountUsed: { type: Boolean, default: false },
|
||||
unitConversionSource: { type: String, enum: ['direct', 'density', 'usda_portion', 'estimate', 'manual', 'none'] },
|
||||
manuallyEdited: { type: Boolean, default: false },
|
||||
excluded: { type: Boolean, default: false },
|
||||
}],
|
||||
|
||||
// Translation metadata for tracking changes
|
||||
translationMetadata: {
|
||||
lastModifiedGerman: {type: Date},
|
||||
@@ -177,6 +196,6 @@ RecipeSchema.index({ "translations.en.translationStatus": 1 });
|
||||
|
||||
import type { RecipeModelType } from '$types/types';
|
||||
|
||||
let _recipeModel: mongoose.Model<RecipeModelType>;
|
||||
try { _recipeModel = mongoose.model<RecipeModelType>("Recipe"); } catch { _recipeModel = mongoose.model<RecipeModelType>("Recipe", RecipeSchema); }
|
||||
export const Recipe = _recipeModel;
|
||||
// Delete cached model on HMR so schema changes (e.g. new fields) are picked up
|
||||
delete mongoose.models.Recipe;
|
||||
export const Recipe = mongoose.model<RecipeModelType>("Recipe", RecipeSchema);
|
||||
|
||||
Reference in New Issue
Block a user