""" @service CatalogSearchService @purpose Token + phrase matching for catalog search without loading the full product set. """ import re from typing import List, Optional from sqlalchemy import and_, or_, select from sqlalchemy.sql import Select from app.models.BrandModel import Brand from app.models.ProductModel import Product, ProductVariant _TOKEN_RE = re.compile(r"[^\w+-]+", re.UNICODE) def tokenize_search(query: Optional[str]) -> List[str]: if not query: return [] tokens = [t for t in _TOKEN_RE.split(query.strip()) if len(t) >= 2] return tokens[:8] def product_search_id_query(search: str) -> Select: """ Returns a SELECT of matching product_id values. Matches the full phrase OR every token across name, description, SKU, and brand. """ phrase = f"%{search.strip()}%" tokens = tokenize_search(search) phrase_match = or_( Product.name.ilike(phrase), Product.description.ilike(phrase), ProductVariant.sku.ilike(phrase), ProductVariant.barcode.ilike(phrase), Brand.name.ilike(phrase), ) token_match = None if tokens: token_clauses = [] for token in tokens: term = f"%{token}%" token_clauses.append( or_( Product.name.ilike(term), Product.description.ilike(term), ProductVariant.sku.ilike(term), ProductVariant.barcode.ilike(term), Brand.name.ilike(term), ) ) token_match = and_(*token_clauses) where_clause = phrase_match if token_match is None else or_(phrase_match, token_match) return ( select(Product.product_id) .outerjoin(ProductVariant, ProductVariant.product_id == Product.product_id) .outerjoin(Brand, Product.brand_id == Brand.brand_id) .where(where_clause) .distinct() ) def apply_product_search(stmt: Select, search: Optional[str]) -> Select: if not search or not search.strip(): return stmt return stmt.where(Product.product_id.in_(product_search_id_query(search.strip()))) def sku_search_clause(search: Optional[str]): if not search or not search.strip(): return None phrase = f"%{search.strip()}%" tokens = tokenize_search(search) phrase_match = or_( Product.name.ilike(phrase), ProductVariant.sku.ilike(phrase), ProductVariant.barcode.ilike(phrase), ) if not tokens: return phrase_match token_match = and_( *[ or_( Product.name.ilike(f"%{token}%"), ProductVariant.sku.ilike(f"%{token}%"), ProductVariant.barcode.ilike(f"%{token}%"), ) for token in tokens ] ) return or_(phrase_match, token_match)