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:
@@ -259,10 +259,6 @@ class LuckyCaliforniaScraper:
|
|||||||
promo = price_block.get("promoArea") or {}
|
promo = price_block.get("promoArea") or {}
|
||||||
sale_price, sale_unit = cls._parse_price(promo.get("promoText"))
|
sale_price, sale_unit = cls._parse_price(promo.get("promoText"))
|
||||||
|
|
||||||
if reg_price is None and sale_price is None:
|
|
||||||
# No usable price; skip rather than persist garbage.
|
|
||||||
return None
|
|
||||||
|
|
||||||
unit = sale_unit or reg_unit
|
unit = sale_unit or reg_unit
|
||||||
is_on_sale = sale_price is not None and reg_price is not None and sale_price < reg_price
|
is_on_sale = sale_price is not None and reg_price is not None and sale_price < reg_price
|
||||||
# current_price = "what the customer pays today" → sale_price when on sale.
|
# current_price = "what the customer pays today" → sale_price when on sale.
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ _EXCLUSION_WORDS = frozenset({
|
|||||||
"margarita", "rita", "cocktail", "beer", "ale", "lager", "cider", "malt",
|
"margarita", "rita", "cocktail", "beer", "ale", "lager", "cider", "malt",
|
||||||
"wine", "spirits", "liquor",
|
"wine", "spirits", "liquor",
|
||||||
"vodka", "tequila", "whiskey", "rum", "gin", "bourbon",
|
"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 / spreads (prevents "Garlic & Herb Butter Spread" matching "Garlic")
|
||||||
"butter", "spread", "margarine",
|
"butter", "spread", "margarine",
|
||||||
# Prepared proteins / seafood-in-oil (prevents "Tuna in Olive Oil" matching "Olive Oil")
|
# 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)
|
).delete(synchronize_session=False)
|
||||||
db.flush()
|
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()
|
ingredients = db.query(Ingredient).all()
|
||||||
written = 0
|
written = 0
|
||||||
|
|
||||||
@@ -136,6 +141,24 @@ def run_match_job(
|
|||||||
a.lower() for a in (ingredient.aliases or []) if a
|
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_idx: int | None = None
|
||||||
best_combined = 0.0
|
best_combined = 0.0
|
||||||
|
|
||||||
@@ -144,7 +167,7 @@ def run_match_job(
|
|||||||
query,
|
query,
|
||||||
grocery_names_lower,
|
grocery_names_lower,
|
||||||
scorer=fuzz.partial_token_sort_ratio,
|
scorer=fuzz.partial_token_sort_ratio,
|
||||||
limit=20,
|
limit=100,
|
||||||
)
|
)
|
||||||
for _text, score, idx in results:
|
for _text, score, idx in results:
|
||||||
if score < threshold * 100:
|
if score < threshold * 100:
|
||||||
|
|||||||
Reference in New Issue
Block a user