diff --git a/backend/app/services/matcher.py b/backend/app/services/matcher.py index 78f17cb..7b37222 100644 --- a/backend/app/services/matcher.py +++ b/backend/app/services/matcher.py @@ -39,6 +39,34 @@ _STOP_WORDS = frozenset({ "and", "with", "for", "the", }) +# If any of these words appear in a grocery item's sig-words but NOT in the +# ingredient's sig-words, the match is rejected outright. Prevents category +# cross-contamination: "Garlic" must not match "Garlic Bread", "Lime" must not +# match "Lime Margarita", etc. +_EXCLUSION_WORDS = frozenset({ + # Baked goods / bread products + "bread", "loaf", "rolls", "bun", "buns", "croissant", + "cracker", "crackers", "cookie", "cookies", "cake", "cupcake", "muffin", "bagel", + # Chips / snack foods + "chips", + # Pasta / noodles + "pasta", "noodle", "noodles", "vermicelli", "spaghetti", "linguine", + "fettuccine", "penne", "rigatoni", "macaroni", "rotini", "orzo", + # Alcoholic / mixed beverages + "margarita", "rita", "cocktail", "beer", "ale", "lager", "cider", "malt", + "wine", "spirits", "liquor", + "vodka", "tequila", "whiskey", "rum", "gin", "bourbon", + "lemonade", "limeade", "seltzer", "soda", + # Butter / spreads (prevents "Garlic & Herb Butter Spread" matching "Garlic") + "butter", "spread", "margarine", + # Prepared proteins / seafood-in-oil (prevents "Tuna in Olive Oil" matching "Olive Oil") + "tuna", "tonno", "salmon", "sardine", "anchovy", + # Prepared poultry (prevents "Garlic Herb Rotisserie Chicken" matching "Garlic") + "rotisserie", + # Baby / personal care (belt-and-suspenders after stop-word rework) + "baby", "wipes", "diaper", +}) + @dataclass class MatchResult: @@ -127,8 +155,18 @@ def run_match_job( # 100% recall: every ingredient sig-word must appear in the grocery name. if not ing_sig.issubset(gsig): continue + # Category exclusion: reject if grocery has a disqualifying word + # (e.g. "bread", "chips", "margarita") absent from the ingredient. + bad_words = (gsig & _EXCLUSION_WORDS) - ing_sig + if bad_words: + continue # Precision penalises grocery items with many extra words. precision = len(ing_sig) / len(gsig) + # Hard floor: grocery must not have >2× the sig-words of the ingredient. + # Catches long branded products that sneak past exclusion words, e.g. + # "Garlic Herb Rotisserie Chicken" for "Garlic". + if precision < 0.45: + continue combined = (score / 100.0) * precision if combined > best_combined: best_combined = combined