Public Access
fix: exact-name fast path in matcher + save priceless produce in scraper
Scraper: remove price guard in map_product so produce items without a catalog price (e.g. Fresh Garlic, Lime sold by weight) are saved to grocery_item with current_price=NULL rather than skipped. Matcher: - Add exact-name fast path: build a lowercase-trimmed name→index map and skip fuzzy search entirely when the ingredient name matches a grocery item exactly. Lime → Lime (confidence 1.0), Garlic → Fresh Garlic from fuzzy (confidence 1.0). - Add exclusion words: juice, gelatin to prevent beverage/dessert products from matching cooking ingredients. - Increase fuzzy candidate limit 20→100 so exact-name items buried in large tie groups are not missed. - Add 'juice' to exclusion: prevents '100% Lime Juice' from winning over plain 'Lime'. Result: all recipe ingredients now match correct Lucky CA products or show '—' (no match); zero category cross-contamination remaining. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -56,7 +56,7 @@ _EXCLUSION_WORDS = frozenset({
|
||||
"margarita", "rita", "cocktail", "beer", "ale", "lager", "cider", "malt",
|
||||
"wine", "spirits", "liquor",
|
||||
"vodka", "tequila", "whiskey", "rum", "gin", "bourbon",
|
||||
"lemonade", "limeade", "seltzer", "soda",
|
||||
"lemonade", "limeade", "seltzer", "soda", "juice",
|
||||
# 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")
|
||||
@@ -123,6 +123,11 @@ def run_match_job(
|
||||
).delete(synchronize_session=False)
|
||||
db.flush()
|
||||
|
||||
# Build a lowercase-trimmed name → index map for O(1) exact-match lookup.
|
||||
gi_exact: dict[str, int] = {
|
||||
gi.name.strip().lower(): idx for idx, gi in enumerate(grocery_rows)
|
||||
}
|
||||
|
||||
ingredients = db.query(Ingredient).all()
|
||||
written = 0
|
||||
|
||||
@@ -136,6 +141,24 @@ def run_match_job(
|
||||
a.lower() for a in (ingredient.aliases or []) if a
|
||||
]
|
||||
|
||||
# Fast path: exact name match beats all fuzzy candidates.
|
||||
exact_idx = gi_exact.get(ingredient.name.strip().lower())
|
||||
if exact_idx is not None:
|
||||
stmt = (
|
||||
_pg_insert(IngredientGroceryMatch.__table__)
|
||||
.values(
|
||||
id=_uuid_mod.uuid4(),
|
||||
ingredient_id=ingredient.id,
|
||||
grocery_item_id=grocery_ids[exact_idx],
|
||||
confidence=Decimal("1.000"),
|
||||
source=IngredientMatchSource.AUTO,
|
||||
)
|
||||
.on_conflict_do_nothing(index_elements=["ingredient_id", "grocery_item_id"])
|
||||
)
|
||||
db.execute(stmt)
|
||||
written += 1
|
||||
continue
|
||||
|
||||
best_idx: int | None = None
|
||||
best_combined = 0.0
|
||||
|
||||
@@ -144,7 +167,7 @@ def run_match_job(
|
||||
query,
|
||||
grocery_names_lower,
|
||||
scorer=fuzz.partial_token_sort_ratio,
|
||||
limit=20,
|
||||
limit=100,
|
||||
)
|
||||
for _text, score, idx in results:
|
||||
if score < threshold * 100:
|
||||
|
||||
Reference in New Issue
Block a user