Public Access
fix: matcher exclusion words + precision floor for clean grocery matching
- Add exclusion words: soda, rotisserie, tuna/tonno/salmon/sardine/anchovy to prevent beverages, prepared poultry, and seafood-in-oil from matching raw cooking ingredients - Add min precision floor (0.45): grocery sig-word count must be ≤ 2× the ingredient's sig-word count, catching long branded products that pass the word-overlap recall check but are clearly wrong category matches (e.g. "Garlic Herb Rotisserie Chicken" precision=0.25 now rejected) Result: all previously wrong matches now show '—' (no match) rather than a wrong product; correct matches unchanged Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,34 @@ _STOP_WORDS = frozenset({
|
|||||||
"and", "with", "for", "the",
|
"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
|
@dataclass
|
||||||
class MatchResult:
|
class MatchResult:
|
||||||
@@ -127,8 +155,18 @@ def run_match_job(
|
|||||||
# 100% recall: every ingredient sig-word must appear in the grocery name.
|
# 100% recall: every ingredient sig-word must appear in the grocery name.
|
||||||
if not ing_sig.issubset(gsig):
|
if not ing_sig.issubset(gsig):
|
||||||
continue
|
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 penalises grocery items with many extra words.
|
||||||
precision = len(ing_sig) / len(gsig)
|
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
|
combined = (score / 100.0) * precision
|
||||||
if combined > best_combined:
|
if combined > best_combined:
|
||||||
best_combined = combined
|
best_combined = combined
|
||||||
|
|||||||
Reference in New Issue
Block a user