ifixkart-backend/app/services/CatalogSearchService.py

96 lines
2.8 KiB
Python

"""
@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)