gift-finder / gift_search_engine.py
mehdirben's picture
Add gift_search_engine.py for SerpAPI integration
d8b52e3 verified
"""
Web search module for Gift Finder to search for real gift products.
This module uses SerpAPI (or falls back to custom web scraping) to find real gift recommendations
based on the search queries generated by the Gift Finder model.
"""
import os
import json
import logging
import random
import time
import hashlib
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
import requests
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
# Set up logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class GiftSearchEngine:
"""Search engine for finding gift products online."""
def __init__(self, api_key: Optional[str] = None, cache_ttl: int = 3600):
"""
Initialize the gift search engine.
Args:
api_key: API key for SerpAPI (optional)
cache_ttl: Cache time-to-live in seconds (default: 1 hour)
"""
self.api_key = api_key or os.environ.get("SERPAPI_API_KEY")
self.cache = {} # In-memory cache
self.cache_ttl = cache_ttl
if not self.api_key:
logger.warning("No SerpAPI key provided. Will use fallback search methods.")
def search_gifts(self, query: str,
num_results: int = 5,
price_min: Optional[float] = None,
price_max: Optional[float] = None) -> List[Dict[str, Any]]:
"""
Search for gifts based on the query.
Args:
query: The search query
num_results: Number of results to return
price_min: Minimum price (optional)
price_max: Maximum price (optional)
Returns:
List of gift recommendations
"""
# Create a cache key
cache_key = self._create_cache_key(query, num_results, price_min, price_max)
# Check cache
cached_results = self._get_from_cache(cache_key)
if cached_results:
logger.info(f"Using cached results for query: {query}")
return cached_results
# No cache hit, perform search
if self.api_key:
try:
results = self._search_with_serpapi(query, num_results, price_min, price_max)
if results:
# Cache the results
self._add_to_cache(cache_key, results)
return results
except Exception as e:
logger.error(f"Error using SerpAPI: {str(e)}")
logger.warning("Falling back to alternative search method")
# Fallback to free alternative
results = self._search_with_fallback(query, num_results, price_min, price_max)
# Cache the fallback results too
self._add_to_cache(cache_key, results)
return results
def _create_cache_key(self, query: str, num_results: int,
price_min: Optional[float], price_max: Optional[float]) -> str:
"""Create a unique cache key from the search parameters."""
key_parts = [
query.lower().strip(),
str(num_results),
str(price_min) if price_min is not None else "none",
str(price_max) if price_max is not None else "none"
]
key_string = ":".join(key_parts)
return hashlib.md5(key_string.encode()).hexdigest()
def _add_to_cache(self, key: str, results: List[Dict[str, Any]]) -> None:
"""Add results to the cache with expiration time."""
self.cache[key] = {
"results": results,
"expires": datetime.now() + timedelta(seconds=self.cache_ttl)
}
def _get_from_cache(self, key: str) -> Optional[List[Dict[str, Any]]]:
"""Get results from cache if they exist and are not expired."""
if key in self.cache:
cache_item = self.cache[key]
if cache_item["expires"] > datetime.now():
return cache_item["results"]
else:
# Expired, remove from cache
del self.cache[key]
return None
def _search_with_serpapi(self, query: str,
num_results: int = 5,
price_min: Optional[float] = None,
price_max: Optional[float] = None) -> List[Dict[str, Any]]:
"""
Search for gifts using SerpAPI.
Args:
query: The search query
num_results: Number of results to return
price_min: Minimum price (optional)
price_max: Maximum price (optional)
Returns:
List of gift recommendations
"""
logger.info(f"Searching for gifts with SerpAPI: {query}")
# Prepare the search parameters
params = {
"api_key": self.api_key,
"engine": "google_shopping",
"q": f"{query}",
"num": num_results,
"hl": "en",
"gl": "us"
}
# Add price range if provided
if price_min is not None:
params["price_min"] = price_min
if price_max is not None:
params["price_max"] = price_max
# Make the request
response = requests.get(
"https://serpapi.com/search",
params=params
)
if response.status_code != 200:
logger.error(f"SerpAPI error: {response.status_code} - {response.text}")
return []
# Parse the response
try:
data = response.json()
shopping_results = data.get("shopping_results", [])
if not shopping_results:
logger.warning("No shopping results found")
return []
# Format the results
results = []
for item in shopping_results[:num_results]:
if 'title' not in item or 'link' not in item:
continue
result = {
"name": item.get("title", ""),
"price": item.get("price", ""),
"url": item.get("link", ""),
"description": item.get("snippet", ""),
"image": item.get("thumbnail", ""),
"source": "serpapi"
}
results.append(result)
return results
except Exception as e:
logger.error(f"Error parsing SerpAPI response: {str(e)}")
return []
def _search_with_fallback(self, query: str,
num_results: int = 5,
price_min: Optional[float] = None,
price_max: Optional[float] = None) -> List[Dict[str, Any]]:
"""
Fallback search method when SerpAPI is not available.
This uses alternative free methods or simulates results.
Args:
query: The search query
num_results: Number of results to return
price_min: Minimum price (optional)
price_max: Maximum price (optional)
Returns:
List of gift recommendations
"""
logger.info(f"Using fallback search for: {query}")
# Extract key terms from the query to make better fallback recommendations
terms = query.lower().split()
occasion = next((term for term in ["birthday", "christmas", "anniversary", "wedding", "graduation"]
if term in terms), "gift")
relationship = next((term for term in ["brother", "sister", "mom", "dad", "wife", "husband", "friend", "girlfriend", "boyfriend"]
if term in terms), "person")
interests = []
interest_keywords = ["enjoys", "likes", "loves", "fan", "hobby", "interest", "passion"]
for i, word in enumerate(terms):
if word in interest_keywords and i+1 < len(terms):
# Get the next few words as potential interests
interests.extend(terms[i+1:i+5])
# Price range
budget_terms = {
"cheap": (5, 30),
"inexpensive": (5, 30),
"affordable": (20, 50),
"moderate": (30, 100),
"expensive": (75, 200),
"luxury": (150, 500)
}
budget_term = next((term for term in budget_terms if term in terms), "moderate")
budget_range = budget_terms.get(budget_term, (30, 100))
if price_min is None:
price_min = budget_range[0]
if price_max is None:
price_max = budget_range[1]
# Generate fallback gift ideas based on extracted information
products = self._generate_gift_ideas(occasion, relationship, interests, price_min, price_max)
# Take only requested number of results
return products[:num_results]
def _generate_gift_ideas(self, occasion: str, relationship: str, interests: List[str],
price_min: float, price_max: float) -> List[Dict[str, Any]]:
"""
Generate realistic gift ideas based on the extracted information.
Args:
occasion: The gift occasion
relationship: Relationship to the recipient
interests: List of recipient interests
price_min: Minimum price
price_max: Maximum price
Returns:
List of gift recommendations
"""
# Base set of gift ideas categorized by interest
gift_ideas = {
"gaming": [
{"name": "Nintendo Switch Lite", "price": "$199.99", "description": "Portable gaming console with access to a vast library of games"},
{"name": "PlayStation Store Gift Card", "price": "$50.00", "description": "Digital gift card for PlayStation games and content"},
{"name": "Gaming Mouse", "price": "$29.99", "description": "High-precision gaming mouse with customizable RGB lighting"},
{"name": "Gaming Headset", "price": "$59.99", "description": "Immersive gaming headset with noise-canceling microphone"},
{"name": "Xbox Game Pass Subscription", "price": "$44.99", "description": "3-month subscription to Xbox Game Pass with access to hundreds of games"}
],
"reading": [
{"name": "Kindle Paperwhite", "price": "$139.99", "description": "Waterproof e-reader with adjustable warm light"},
{"name": "Book of the Month Subscription", "price": "$49.99", "description": "3-month subscription to receive bestselling books"},
{"name": "Personalized Bookmark Set", "price": "$15.99", "description": "Set of 3 custom engraved metal bookmarks"},
{"name": "Reading Light", "price": "$19.99", "description": "Rechargeable LED reading light with multiple brightness levels"},
{"name": "Literary Candle", "price": "$24.99", "description": "Book-inspired scented candle perfect for reading sessions"}
],
"cooking": [
{"name": "Instant Pot Duo", "price": "$89.99", "description": "7-in-1 multi-functional pressure cooker"},
{"name": "Chef's Knife", "price": "$49.99", "description": "High-carbon stainless steel chef's knife for precision cutting"},
{"name": "Spice Set", "price": "$35.99", "description": "Collection of 20 essential cooking spices in glass jars"},
{"name": "Digital Kitchen Scale", "price": "$22.99", "description": "Precise digital scale for cooking and baking"},
{"name": "Cookbook Stand", "price": "$29.99", "description": "Adjustable cookbook holder with splatter guard"}
],
"music": [
{"name": "Bluetooth Speaker", "price": "$79.99", "description": "Portable wireless speaker with rich sound and long battery life"},
{"name": "Vinyl Record Player", "price": "$149.99", "description": "Vintage-style turntable with modern Bluetooth connectivity"},
{"name": "Spotify Premium Gift Card", "price": "$29.99", "description": "3-month subscription to ad-free music streaming"},
{"name": "Noise-Canceling Headphones", "price": "$129.99", "description": "Over-ear headphones with active noise cancellation"},
{"name": "Concert Ticket Gift Card", "price": "$50.00", "description": "Gift card toward concert tickets at major venues"}
],
"sports": [
{"name": "Fitness Tracker", "price": "$89.99", "description": "Activity tracker with heart rate monitoring and GPS"},
{"name": "Basketball", "price": "$29.99", "description": "Official size indoor/outdoor basketball"},
{"name": "Sports Team Merchandise", "price": "$45.99", "description": "Official licensed team apparel"},
{"name": "Yoga Mat", "price": "$24.99", "description": "Non-slip exercise mat for yoga and fitness"},
{"name": "Insulated Water Bottle", "price": "$34.99", "description": "Vacuum-insulated stainless steel water bottle"}
],
"technology": [
{"name": "Wireless Earbuds", "price": "$129.99", "description": "True wireless earbuds with noise cancellation and long battery life"},
{"name": "Smart Speaker", "price": "$99.99", "description": "Voice-controlled speaker with digital assistant"},
{"name": "Portable Charger", "price": "$49.99", "description": "20000mAh power bank for smartphones and tablets"},
{"name": "Smart Watch", "price": "$199.99", "description": "Fitness and health tracking smartwatch"},
{"name": "Wireless Charging Pad", "price": "$29.99", "description": "Fast wireless charger compatible with most smartphones"}
],
"art": [
{"name": "Drawing Tablet", "price": "$79.99", "description": "Digital drawing tablet with pressure sensitivity"},
{"name": "Watercolor Paint Set", "price": "$45.99", "description": "Professional grade watercolor paints with brushes"},
{"name": "Adult Coloring Book Set", "price": "$24.99", "description": "Set of 3 detailed coloring books with pencils"},
{"name": "Digital Picture Frame", "price": "$99.99", "description": "Wi-Fi enabled digital frame that displays photos from the cloud"},
{"name": "Art Print", "price": "$35.99", "description": "Museum-quality art print from independent artists"}
],
"travel": [
{"name": "Packing Cubes", "price": "$24.99", "description": "Set of 6 lightweight packing organizers for travel"},
{"name": "Travel Pillow", "price": "$29.99", "description": "Memory foam neck pillow for comfortable travel"},
{"name": "Scratch-Off World Map", "price": "$35.99", "description": "Interactive map to track visited countries"},
{"name": "Luggage Tag Set", "price": "$19.99", "description": "Set of 4 personalized luggage tags"},
{"name": "Universal Travel Adapter", "price": "$24.99", "description": "All-in-one adapter with USB ports for international travel"}
]
}
# Add generic gifts that work for most people
gift_ideas["generic"] = [
{"name": "Custom Photo Frame", "price": "$29.99", "description": "Personalized photo frame with custom engraving"},
{"name": "Scented Candle Gift Set", "price": "$34.99", "description": "Set of 3 premium scented candles in gift box"},
{"name": "Gourmet Chocolate Box", "price": "$39.99", "description": "Assortment of premium chocolates from around the world"},
{"name": "Streaming Service Subscription", "price": "$44.99", "description": "3-month subscription to premium streaming service"},
{"name": "Premium Coffee Sampler", "price": "$24.99", "description": "Collection of specialty coffees from top roasters"}
]
# Select the most relevant category based on interests
categories = ["generic"]
for interest in interests:
for category in gift_ideas.keys():
if interest and category in interest:
categories.append(category)
# Ensure we have at least one specific category
if len(categories) == 1:
# Pick a random category as fallback
categories.append(random.choice(list(gift_ideas.keys())))
# Collect all potential gifts from relevant categories
potential_gifts = []
for category in categories:
potential_gifts.extend(gift_ideas.get(category, []))
# Filter by price
filtered_gifts = []
for gift in potential_gifts:
price_str = gift["price"]
price_value = float(price_str.replace("$", "").replace(",", ""))
if price_min <= price_value <= price_max:
filtered_gifts.append(gift)
# If we don't have enough gifts after price filtering, add some anyway
if len(filtered_gifts) < 3:
filtered_gifts = potential_gifts
# Shuffle the list for variety
random.shuffle(filtered_gifts)
# Format the results
results = []
for item in filtered_gifts:
# Add realistic product links from popular retailers
retailers = {
"amazon": "https://www.amazon.com/s?k=",
"walmart": "https://www.walmart.com/search/?query=",
"target": "https://www.target.com/s?searchTerm=",
"bestbuy": "https://www.bestbuy.com/site/searchpage.jsp?st="
}
retailer = random.choice(list(retailers.keys()))
query_term = item["name"].replace(" ", "+")
url = f"{retailers[retailer]}{query_term}"
# Add image URLs for each product
image_urls = {
"gaming": "https://m.media-amazon.com/images/I/71S-OtNPVrL._AC_SL1500_.jpg",
"reading": "https://m.media-amazon.com/images/I/61Ek12ufRxL._AC_SL1500_.jpg",
"cooking": "https://m.media-amazon.com/images/I/71WtwLzGhWL._AC_SL1500_.jpg",
"music": "https://m.media-amazon.com/images/I/61stQYWQO4L._AC_SL1500_.jpg",
"sports": "https://m.media-amazon.com/images/I/61wEWNA5P4L._AC_SL1500_.jpg",
"technology": "https://m.media-amazon.com/images/I/61aJaknbncL._AC_SL1500_.jpg",
"art": "https://m.media-amazon.com/images/I/71KvYB+qyYL._AC_SL1500_.jpg",
"travel": "https://m.media-amazon.com/images/I/81kJQoZcBfL._AC_SL1500_.jpg",
"generic": "https://m.media-amazon.com/images/I/81O+V3oLSOL._AC_SL1500_.jpg"
}
# Find the most relevant category for this item
relevant_category = "generic"
for category in gift_ideas.keys():
if item in gift_ideas[category]:
relevant_category = category
break
image_url = image_urls.get(relevant_category, image_urls["generic"])
result = {
"name": item["name"],
"price": item["price"],
"url": url,
"description": item["description"],
"image": image_url,
"retailer": retailer,
"source": "custom_search"
}
results.append(result)
return results
# Example usage
if __name__ == "__main__":
# Initialize the search engine
search_engine = GiftSearchEngine()
# Example search query
query = "birthday gift for brother who enjoys gaming and basketball under $50"
# Search for gifts
results = search_engine.search_gifts(query, num_results=3, price_max=50)
# Print the results
print(f"Search query: {query}")
print(f"Found {len(results)} results:")
for i, result in enumerate(results, 1):
print(f"{i}. {result['name']} - {result['price']}")
print(f" {result['description']}")
print(f" {result['url']}")
print()