prasadnu's picture
feat: add Search Personalization demo module
b4d5c9a
Raw
History Blame
7.34 kB
"""
Image mapping utilities for product catalog.
This module provides functions to map product image filenames to their
actual file paths in the product_samples directory structure, with
fallback handling for missing images and caching for performance.
"""
import os
from pathlib import Path
from typing import Dict, Optional
from functools import lru_cache
# Base path for product images (relative to project root)
PRODUCT_SAMPLES_BASE = "search_personalization/product_samples"
PLACEHOLDER_IMAGE = "search_personalization/product_samples/product_image_coming_soon.png"
THUMBNAIL_DIR = Path("search_personalization/product_samples/.thumbnails")
# In-memory cache for image path existence checks
_image_cache: Dict[str, str] = {}
def get_thumbnail_path(product: Dict) -> str:
"""
Get a small thumbnail for fast UI rendering (~200px wide, ~70% quality).
Generates on first access, then serves from disk cache.
Args:
product: Product dictionary containing 'category' and 'image' fields
Returns:
Path to the thumbnail image, or placeholder if original doesn't exist.
"""
original = get_image_path(product)
if original == PLACEHOLDER_IMAGE:
return PLACEHOLDER_IMAGE
rel = Path(original).relative_to(PRODUCT_SAMPLES_BASE)
thumb = THUMBNAIL_DIR / rel
if thumb.exists():
return str(thumb)
# Generate thumbnail
try:
from PIL import Image
thumb.parent.mkdir(parents=True, exist_ok=True)
img = Image.open(original)
img.thumbnail((200, 200))
img.save(thumb, "JPEG", quality=70)
return str(thumb)
except Exception:
return original
def get_image_path(product: Dict, use_cache: bool = True) -> str:
"""
Map product image filename to actual file path in product_samples directory.
This function constructs the full path to a product image based on the
product's category and image filename. If the image file doesn't exist,
it returns the path to a placeholder image.
Args:
product: Product dictionary containing 'category' and 'image' fields
use_cache: Whether to use cached image path results (default: True)
Returns:
String path to the image file, either the actual product image or
the placeholder image if the product image is not found
Examples:
>>> product = {"category": "accessories", "image": "abc123.jpg"}
>>> get_image_path(product)
'product_samples/accessories/abc123.jpg'
>>> product = {"category": "apparel", "image": "missing.jpg"}
>>> get_image_path(product) # If missing.jpg doesn't exist
'product_samples/product_image_coming_soon.png'
Requirements: 9.4
"""
# Extract category and image from product
category = product.get('category', '')
image_filename = product.get('image', '')
# If no image filename provided, return placeholder
if not image_filename:
return PLACEHOLDER_IMAGE
# If no category provided, return placeholder
if not category:
return PLACEHOLDER_IMAGE
# Create cache key
cache_key = f"{category}/{image_filename}"
# Check cache first if enabled
if use_cache and cache_key in _image_cache:
return _image_cache[cache_key]
# Construct the expected image path
image_path = os.path.join(PRODUCT_SAMPLES_BASE, category, image_filename)
# Check if the image file exists
if os.path.exists(image_path) and os.path.isfile(image_path):
# Cache the result
if use_cache:
_image_cache[cache_key] = image_path
return image_path
# Image doesn't exist, return placeholder
if use_cache:
_image_cache[cache_key] = PLACEHOLDER_IMAGE
return PLACEHOLDER_IMAGE
@lru_cache(maxsize=1000)
def get_image_path_cached(category: str, image_filename: str) -> str:
"""
Cached version of image path lookup using functools.lru_cache.
This function provides an alternative caching mechanism using Python's
built-in LRU cache decorator. It's more efficient for repeated lookups
of the same category/image combinations.
Args:
category: Product category (e.g., 'accessories', 'apparel', 'footwear')
image_filename: Image filename (e.g., 'abc123.jpg')
Returns:
String path to the image file or placeholder
Note:
This function uses @lru_cache which caches up to 1000 unique
category/filename combinations. The cache persists for the
lifetime of the application.
"""
# If no image filename provided, return placeholder
if not image_filename:
return PLACEHOLDER_IMAGE
# If no category provided, return placeholder
if not category:
return PLACEHOLDER_IMAGE
# Construct the expected image path
image_path = os.path.join(PRODUCT_SAMPLES_BASE, category, image_filename)
# Check if the image file exists
if os.path.exists(image_path) and os.path.isfile(image_path):
return image_path
# Image doesn't exist, return placeholder
return PLACEHOLDER_IMAGE
def clear_image_cache() -> None:
"""
Clear the in-memory image path cache.
This function clears both the manual cache dictionary and the
LRU cache used by get_image_path_cached. Useful for testing
or when the file system changes.
"""
global _image_cache
_image_cache.clear()
get_image_path_cached.cache_clear()
def get_cache_info() -> Dict[str, any]:
"""
Get information about the current cache state.
Returns:
Dictionary containing cache statistics:
- manual_cache_size: Number of entries in the manual cache
- lru_cache_info: Statistics from the LRU cache
"""
return {
'manual_cache_size': len(_image_cache),
'lru_cache_info': get_image_path_cached.cache_info()._asdict()
}
def preload_image_cache(products: list) -> int:
"""
Preload the image cache with paths for a list of products.
This function can be used to warm up the cache when loading
products from OpenSearch, improving performance for subsequent
image path lookups.
Args:
products: List of product dictionaries
Returns:
Number of image paths successfully cached
Example:
>>> products = [
... {"category": "accessories", "image": "img1.jpg"},
... {"category": "apparel", "image": "img2.jpg"}
... ]
>>> count = preload_image_cache(products)
>>> print(f"Cached {count} image paths")
"""
cached_count = 0
for product in products:
category = product.get('category', '')
image_filename = product.get('image', '')
if category and image_filename:
# Use the cached version to populate the LRU cache
get_image_path_cached(category, image_filename)
cached_count += 1
return cached_count
def validate_placeholder_exists() -> bool:
"""
Check if the placeholder image file exists.
Returns:
True if placeholder image exists, False otherwise
"""
return os.path.exists(PLACEHOLDER_IMAGE) and os.path.isfile(PLACEHOLDER_IMAGE)