""" Advanced Search and Filters Manager This module provides advanced search functionality, filtering capabilities, and search result management for the AniWorld web interface. """ from typing import Dict, List, Any, Optional, Set import re from datetime import datetime, timedelta from flask import Blueprint, request, jsonify import json class AdvancedSearchManager: """Manages advanced search and filtering functionality.""" def __init__(self, app=None): self.app = app self.search_history = [] self.saved_searches = {} self.filter_presets = { 'recent': { 'name': 'Recently Added', 'filters': { 'date_added': {'operator': 'last_days', 'value': 7} } }, 'downloading': { 'name': 'Currently Downloading', 'filters': { 'status': {'operator': 'equals', 'value': 'downloading'} } }, 'completed': { 'name': 'Completed Series', 'filters': { 'status': {'operator': 'equals', 'value': 'completed'} } }, 'high_rated': { 'name': 'Highly Rated', 'filters': { 'rating': {'operator': 'greater_than', 'value': 8.0} } } } def init_app(self, app): """Initialize with Flask app.""" self.app = app def get_search_js(self): """Generate JavaScript code for advanced search functionality.""" return f""" // AniWorld Advanced Search Manager class AdvancedSearchManager {{ constructor() {{ this.currentFilters = {{}}; this.searchHistory = []; this.savedSearches = {{}}; this.filterPresets = {json.dumps(self.filter_presets)}; this.searchResults = []; this.sortBy = 'name'; this.sortOrder = 'asc'; this.init(); }} init() {{ this.createSearchInterface(); this.setupSearchEvents(); this.loadSearchHistory(); this.loadSavedSearches(); this.setupKeyboardShortcuts(); }} createSearchInterface() {{ this.createSearchBar(); this.createAdvancedFilters(); this.createSearchResults(); this.createQuickFilters(); }} createSearchBar() {{ const existingSearch = document.querySelector('.advanced-search-bar'); if (existingSearch) return; const searchContainer = document.createElement('div'); searchContainer.className = 'advanced-search-bar mb-4'; searchContainer.innerHTML = `
`; // Insert at the top of main content const mainContent = document.querySelector('.main-content, .container-fluid'); if (mainContent) {{ mainContent.insertBefore(searchContainer, mainContent.firstChild); }} }} createAdvancedFilters() {{ const filtersModal = document.createElement('div'); filtersModal.id = 'advanced-filters-modal'; filtersModal.className = 'modal fade'; filtersModal.innerHTML = ` `; document.body.appendChild(filtersModal); // Populate filter presets this.populateFilterPresets(); }} createSearchResults() {{ const existingResults = document.querySelector('.search-results-container'); if (existingResults) return; const resultsContainer = document.createElement('div'); resultsContainer.className = 'search-results-container'; resultsContainer.innerHTML = `
0 results
`; const mainContent = document.querySelector('.main-content, .container-fluid'); if (mainContent) {{ mainContent.appendChild(resultsContainer); }} }} createQuickFilters() {{ const quickFiltersContainer = document.createElement('div'); quickFiltersContainer.className = 'quick-filters mb-3'; quickFiltersContainer.innerHTML = `
Quick Filters:
`; const searchBar = document.querySelector('.advanced-search-bar'); if (searchBar) {{ searchBar.parentNode.insertBefore(quickFiltersContainer, searchBar.nextSibling); }} }} setupSearchEvents() {{ // Search input events const searchInput = document.getElementById('search-input'); if (searchInput) {{ searchInput.addEventListener('input', this.handleSearchInput.bind(this)); searchInput.addEventListener('keypress', (e) => {{ if (e.key === 'Enter') {{ this.performSearch(); }} }}); }} // Search button document.getElementById('search-btn')?.addEventListener('click', this.performSearch.bind(this)); // Advanced filters button document.getElementById('advanced-filters-btn')?.addEventListener('click', this.showAdvancedFilters.bind(this)); // Clear search button document.getElementById('clear-search-btn')?.addEventListener('click', this.clearSearch.bind(this)); // Save search button document.getElementById('save-search-btn')?.addEventListener('click', this.saveCurrentSearch.bind(this)); // Sort controls document.getElementById('sort-by')?.addEventListener('change', this.handleSortChange.bind(this)); document.getElementById('sort-order-btn')?.addEventListener('click', this.toggleSortOrder.bind(this)); // Quick filter tags document.addEventListener('click', (e) => {{ if (e.target.classList.contains('filter-tag')) {{ this.applyFilterPreset(e.target.dataset.preset); }} }}); // Filter date change document.getElementById('filter-date-added')?.addEventListener('change', (e) => {{ const customRange = document.getElementById('custom-date-range'); if (e.target.value === 'custom') {{ customRange.classList.remove('d-none'); }} else {{ customRange.classList.add('d-none'); }} }}); // Apply filters button document.getElementById('apply-filters-btn')?.addEventListener('click', this.applyAdvancedFilters.bind(this)); // Clear filters button document.getElementById('clear-filters-btn')?.addEventListener('click', this.clearAllFilters.bind(this)); }} setupKeyboardShortcuts() {{ document.addEventListener('keydown', (e) => {{ if (e.ctrlKey || e.metaKey) {{ switch(e.key) {{ case 'f': e.preventDefault(); document.getElementById('search-input')?.focus(); break; case 'k': e.preventDefault(); this.showAdvancedFilters(); break; }} }} }}); }} handleSearchInput(e) {{ const query = e.target.value; // Show suggestions after 2 characters if (query.length >= 2) {{ this.showSearchSuggestions(query); }} else {{ this.hideSearchSuggestions(); }} }} showSearchSuggestions(query) {{ // Implement search suggestions // This would call an API to get suggestions fetch(`/api/search/suggestions?q=${{encodeURIComponent(query)}}`) .then(response => response.json()) .then(data => {{ this.displaySuggestions(data.suggestions); }}) .catch(error => console.error('Error fetching suggestions:', error)); }} displaySuggestions(suggestions) {{ // Display search suggestions dropdown let suggestionsDropdown = document.getElementById('search-suggestions'); if (!suggestionsDropdown) {{ suggestionsDropdown = document.createElement('div'); suggestionsDropdown.id = 'search-suggestions'; suggestionsDropdown.className = 'search-suggestions dropdown-menu show'; const searchInput = document.getElementById('search-input'); searchInput.parentNode.appendChild(suggestionsDropdown); }} suggestionsDropdown.innerHTML = suggestions.map(suggestion => ` ${{suggestion.text}} ${{suggestion.type}} `).join(''); // Add click handlers suggestionsDropdown.querySelectorAll('.suggestion-item').forEach(item => {{ item.addEventListener('click', (e) => {{ e.preventDefault(); document.getElementById('search-input').value = item.dataset.value; this.performSearch(); this.hideSearchSuggestions(); }}); }}); }} hideSearchSuggestions() {{ const suggestionsDropdown = document.getElementById('search-suggestions'); if (suggestionsDropdown) {{ suggestionsDropdown.remove(); }} }} async performSearch() {{ const query = document.getElementById('search-input').value; const searchType = document.getElementById('search-type').value; if (!query.trim()) {{ this.clearResults(); return; }} // Add to search history this.addToSearchHistory(query); // Show loading this.showSearchLoading(); try {{ const searchParams = {{ query: query, type: searchType, filters: this.currentFilters, sort_by: this.sortBy, sort_order: this.sortOrder }}; const response = await fetch('/api/search', {{ method: 'POST', headers: {{ 'Content-Type': 'application/json' }}, body: JSON.stringify(searchParams) }}); const data = await response.json(); if (data.success) {{ this.searchResults = data.results; this.displaySearchResults(data); this.updateSearchInfo(query, data.total_results); }} else {{ this.showSearchError(data.error); }} }} catch (error) {{ console.error('Search error:', error); this.showSearchError('Search failed. Please try again.'); }} }} showSearchLoading() {{ const resultsContainer = document.getElementById('search-results'); if (resultsContainer) {{ resultsContainer.innerHTML = `
Searching...

Searching...

`; }} }} displaySearchResults(data) {{ const resultsContainer = document.getElementById('search-results'); if (!resultsContainer) return; if (data.results.length === 0) {{ resultsContainer.innerHTML = `

No results found. Try adjusting your search terms or filters.

`; return; }} const resultsHTML = data.results.map(item => `
${{item.name}}
${{item.genre || 'Unknown'}} ${{item.year || 'N/A'}} ${{item.episodes || 0}} episodes ${{item.rating ? ` ${{item.rating}}` : ''}}
`).join(''); resultsContainer.innerHTML = resultsHTML; // Update pagination if needed this.updatePagination(data); }} updateSearchInfo(query, totalResults) {{ const resultsCount = document.querySelector('.results-count'); const searchQuery = document.querySelector('.search-query'); if (resultsCount) {{ resultsCount.textContent = `${{totalResults}} result${{totalResults === 1 ? '' : 's'}}`; }} if (searchQuery) {{ searchQuery.textContent = query ? `for "${{query}}"` : ''; }} }} showSearchError(error) {{ const resultsContainer = document.getElementById('search-results'); if (resultsContainer) {{ resultsContainer.innerHTML = `
${{error}}
`; }} }} clearResults() {{ const resultsContainer = document.getElementById('search-results'); if (resultsContainer) {{ resultsContainer.innerHTML = ''; }} this.updateSearchInfo('', 0); }} clearSearch() {{ document.getElementById('search-input').value = ''; document.getElementById('search-type').value = 'all'; this.currentFilters = {{}}; this.clearResults(); this.updateActiveFilters(); this.hideSearchSuggestions(); }} applyFilterPreset(presetName) {{ const preset = this.filterPresets[presetName]; if (preset) {{ this.currentFilters = {{ ...preset.filters }}; this.updateActiveFilters(); this.performSearch(); }} }} showAdvancedFilters() {{ const modal = document.getElementById('advanced-filters-modal'); if (modal) {{ const bsModal = new bootstrap.Modal(modal); bsModal.show(); }} }} applyAdvancedFilters() {{ // Collect filter values from the modal const filters = {{}}; // Genre filter const genre = document.getElementById('filter-genre'); if (genre.selectedOptions.length > 0) {{ filters.genre = Array.from(genre.selectedOptions).map(o => o.value); }} // Year range const yearFrom = document.getElementById('filter-year-from').value; const yearTo = document.getElementById('filter-year-to').value; if (yearFrom || yearTo) {{ filters.year_range = {{ from: yearFrom, to: yearTo }}; }} // Rating range const ratingMin = document.getElementById('filter-rating-min').value; const ratingMax = document.getElementById('filter-rating-max').value; if (ratingMin || ratingMax) {{ filters.rating_range = {{ min: ratingMin, max: ratingMax }}; }} // Status const status = document.getElementById('filter-status'); if (status.selectedOptions.length > 0) {{ filters.status = Array.from(status.selectedOptions).map(o => o.value); }} // Episode count range const episodesMin = document.getElementById('filter-episodes-min').value; const episodesMax = document.getElementById('filter-episodes-max').value; if (episodesMin || episodesMax) {{ filters.episodes_range = {{ min: episodesMin, max: episodesMax }}; }} // Date added const dateAdded = document.getElementById('filter-date-added').value; if (dateAdded) {{ if (dateAdded === 'custom') {{ const dateFrom = document.getElementById('filter-date-from').value; const dateTo = document.getElementById('filter-date-to').value; if (dateFrom || dateTo) {{ filters.date_range = {{ from: dateFrom, to: dateTo }}; }} }} else {{ filters.date_added = dateAdded; }} }} this.currentFilters = filters; this.updateActiveFilters(); // Close modal and perform search const modal = bootstrap.Modal.getInstance(document.getElementById('advanced-filters-modal')); modal.hide(); this.performSearch(); }} clearAllFilters() {{ this.currentFilters = {{}}; // Clear form fields document.getElementById('filter-genre').selectedIndex = -1; document.getElementById('filter-year-from').value = ''; document.getElementById('filter-year-to').value = ''; document.getElementById('filter-rating-min').value = ''; document.getElementById('filter-rating-max').value = ''; document.getElementById('filter-status').selectedIndex = -1; document.getElementById('filter-episodes-min').value = ''; document.getElementById('filter-episodes-max').value = ''; document.getElementById('filter-date-added').value = ''; document.getElementById('filter-date-from').value = ''; document.getElementById('filter-date-to').value = ''; this.updateActiveFilters(); this.performSearch(); }} updateActiveFilters() {{ const activeFiltersContainer = document.getElementById('active-filters'); if (!activeFiltersContainer) return; const filterTags = []; for (const [key, value] of Object.entries(this.currentFilters)) {{ let filterText = ''; switch(key) {{ case 'genre': filterText = `Genre: ${{value.join(', ')}}`; break; case 'year_range': filterText = `Year: ${{value.from || '?'}} - ${{value.to || '?'}}`; break; case 'rating_range': filterText = `Rating: ${{value.min || '?'}} - ${{value.max || '?'}}`; break; case 'status': filterText = `Status: ${{value.join(', ')}}`; break; case 'episodes_range': filterText = `Episodes: ${{value.min || '?'}} - ${{value.max || '?'}}`; break; case 'date_added': filterText = `Added: ${{value}}`; break; case 'date_range': filterText = `Date: ${{value.from || '?'}} - ${{value.to || '?'}}`; break; }} if (filterText) {{ filterTags.push(` ${{filterText}} `); }} }} if (filterTags.length > 0) {{ activeFiltersContainer.innerHTML = `
Active filters: ${{filterTags.join('')}}
`; }} else {{ activeFiltersContainer.innerHTML = ''; }} }} removeFilter(key) {{ delete this.currentFilters[key]; this.updateActiveFilters(); this.performSearch(); }} handleSortChange(e) {{ this.sortBy = e.target.value; this.performSearch(); }} toggleSortOrder() {{ this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc'; const btn = document.getElementById('sort-order-btn'); const icon = btn.querySelector('i'); if (this.sortOrder === 'desc') {{ icon.className = 'fas fa-sort-alpha-up'; }} else {{ icon.className = 'fas fa-sort-alpha-down'; }} this.performSearch(); }} addToSearchHistory(query) {{ // Remove if already exists this.searchHistory = this.searchHistory.filter(item => item.query !== query); // Add to beginning this.searchHistory.unshift({{ query: query, timestamp: Date.now(), filters: {{ ...this.currentFilters }} }}); // Keep only last 10 if (this.searchHistory.length > 10) {{ this.searchHistory = this.searchHistory.slice(0, 10); }} this.saveSearchHistory(); this.updateSearchHistoryMenu(); }} loadSearchHistory() {{ const stored = localStorage.getItem('aniworld_search_history'); if (stored) {{ try {{ this.searchHistory = JSON.parse(stored); }} catch (error) {{ console.error('Error loading search history:', error); this.searchHistory = []; }} }} this.updateSearchHistoryMenu(); }} saveSearchHistory() {{ localStorage.setItem('aniworld_search_history', JSON.stringify(this.searchHistory)); }} updateSearchHistoryMenu() {{ const menu = document.getElementById('search-history-menu'); if (!menu) return; if (this.searchHistory.length === 0) {{ menu.innerHTML = '
  • No recent searches
  • '; return; }} const historyItems = this.searchHistory.map(item => `
  • ${{item.query}} ${{this.formatTimestamp(item.timestamp)}}
  • `).join(''); menu.innerHTML = `
  • ${{historyItems}}
  • Clear History
  • `; // Add click handlers menu.querySelectorAll('.history-item').forEach(item => {{ item.addEventListener('click', (e) => {{ e.preventDefault(); document.getElementById('search-input').value = item.dataset.query; this.currentFilters = JSON.parse(item.dataset.filters); this.updateActiveFilters(); this.performSearch(); }}); }}); }} clearSearchHistory() {{ this.searchHistory = []; this.saveSearchHistory(); this.updateSearchHistoryMenu(); }} saveCurrentSearch() {{ const query = document.getElementById('search-input').value; if (!query.trim()) return; const name = prompt('Enter a name for this search:'); if (!name) return; const searchId = Date.now().toString(); this.savedSearches[searchId] = {{ name: name, query: query, filters: {{ ...this.currentFilters }}, created: Date.now() }}; this.saveSavedSearches(); this.populateSavedSearches(); this.showToast('Search saved successfully', 'success'); }} loadSavedSearches() {{ const stored = localStorage.getItem('aniworld_saved_searches'); if (stored) {{ try {{ this.savedSearches = JSON.parse(stored); }} catch (error) {{ console.error('Error loading saved searches:', error); this.savedSearches = {{}}; }} }} }} saveSavedSearches() {{ localStorage.setItem('aniworld_saved_searches', JSON.stringify(this.savedSearches)); }} populateSavedSearches() {{ const container = document.getElementById('saved-searches-list'); if (!container) return; if (Object.keys(this.savedSearches).length === 0) {{ container.innerHTML = '

    No saved searches

    '; return; }} const searches = Object.entries(this.savedSearches).map(([id, search]) => `
    ${{search.name}}
    ${{search.query}}
    ${{this.formatTimestamp(search.created)}}
    `).join(''); container.innerHTML = searches; }} loadSavedSearch(searchId) {{ const search = this.savedSearches[searchId]; if (!search) return; document.getElementById('search-input').value = search.query; this.currentFilters = {{ ...search.filters }}; this.updateActiveFilters(); this.performSearch(); // Close modal const modal = bootstrap.Modal.getInstance(document.getElementById('advanced-filters-modal')); if (modal) {{ modal.hide(); }} }} deleteSavedSearch(searchId) {{ if (confirm('Are you sure you want to delete this saved search?')) {{ delete this.savedSearches[searchId]; this.saveSavedSearches(); this.populateSavedSearches(); }} }} populateFilterPresets() {{ const container = document.getElementById('filter-presets'); if (!container) return; const presets = Object.entries(this.filterPresets).map(([key, preset]) => ` `).join(''); container.innerHTML = presets; }} formatTimestamp(timestamp) {{ const date = new Date(timestamp); const now = new Date(); const diff = now - date; if (diff < 60000) return 'Just now'; if (diff < 3600000) return `${{Math.floor(diff / 60000)}}m ago`; if (diff < 86400000) return `${{Math.floor(diff / 3600000)}}h ago`; return date.toLocaleDateString(); }} showToast(message, type = 'info') {{ // Create and show a toast notification const toast = document.createElement('div'); toast.className = `toast align-items-center text-white bg-${{type === 'error' ? 'danger' : type}}`; toast.innerHTML = `
    ${{message}}
    `; let toastContainer = document.querySelector('.toast-container'); if (!toastContainer) {{ toastContainer = document.createElement('div'); toastContainer.className = 'toast-container position-fixed bottom-0 end-0 p-3'; document.body.appendChild(toastContainer); }} toastContainer.appendChild(toast); const bsToast = new bootstrap.Toast(toast); bsToast.show(); toast.addEventListener('hidden.bs.toast', () => {{ if (toast.parentNode) {{ toastContainer.removeChild(toast); }} }}); }} }} // Initialize advanced search when DOM is loaded document.addEventListener('DOMContentLoaded', () => {{ window.advancedSearch = new AdvancedSearchManager(); }}); """ def get_css(self): """Generate CSS for advanced search functionality.""" return """ /* Advanced Search Styles */ .advanced-search-bar { background: var(--bs-light); border-radius: 8px; padding: 1rem; border: 1px solid var(--bs-border-color); } .search-suggestions { position: absolute; top: 100%; left: 0; right: 0; z-index: 1000; max-height: 300px; overflow-y: auto; } .suggestion-item { display: flex; justify-content: between; align-items: center; } .suggestion-item i { margin-right: 0.5rem; width: 16px; } .quick-filters .filter-tag.active { background-color: var(--bs-primary); color: white; } .active-filters .badge { display: inline-flex; align-items: center; } .active-filters .btn-close { --bs-btn-close-bg: none; } .search-result-item { transition: transform 0.2s ease, box-shadow 0.2s ease; } .search-result-item:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.1); } .search-info .results-count { font-weight: 600; color: var(--bs-primary); } .sort-controls { display: flex; align-items: center; gap: 0.5rem; } #advanced-filters-modal .modal-dialog { max-width: 800px; } #advanced-filters-modal h6 { color: var(--bs-primary); border-bottom: 1px solid var(--bs-border-color); padding-bottom: 0.5rem; margin-bottom: 1rem; } .saved-search-item { background: var(--bs-light); } .saved-search-item:hover { background: var(--bs-secondary-bg); } /* Search loading animation */ .search-results .spinner-border { width: 3rem; height: 3rem; } /* Responsive design */ @media (max-width: 768px) { .advanced-search-bar .row { gap: 0.5rem; } .advanced-search-bar .col-md-3, .advanced-search-bar .col-md-6 { flex: 1 1 100%; max-width: 100%; } .quick-filters { flex-direction: column; align-items: flex-start; } .quick-filters > div { flex-wrap: wrap; } .search-result-item .row { flex-direction: column; } .search-result-item .text-end { text-align: start !important; margin-top: 0.5rem; } .sort-controls { justify-content: space-between; width: 100%; } } /* Dark theme support */ [data-bs-theme="dark"] .advanced-search-bar { background: var(--bs-dark); border-color: var(--bs-border-color-translucent); } [data-bs-theme="dark"] .search-suggestions { background: var(--bs-dark); border-color: var(--bs-border-color-translucent); } [data-bs-theme="dark"] .saved-search-item { background: var(--bs-dark); } /* Animation for search results */ @keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .search-result-item { animation: fadeInUp 0.3s ease-out; } /* Accessibility improvements */ .search-suggestions .dropdown-item:focus { background-color: var(--bs-primary); color: var(--bs-white); } @media (prefers-reduced-motion: reduce) { .search-result-item, .search-result-item:hover { transition: none; transform: none; } .search-result-item { animation: none; } } """ def search_series(self, query: str, search_type: str = 'all', filters: Dict[str, Any] = None, sort_by: str = 'name', sort_order: str = 'asc') -> Dict[str, Any]: """Search for series based on query and filters.""" try: # This would implement actual search logic # For now, return mock data results = [ { 'id': '1', 'name': 'Attack on Titan', 'genre': 'Action', 'year': 2013, 'episodes': 75, 'rating': 9.0, 'status': 'completed' }, { 'id': '2', 'name': 'Death Note', 'genre': 'Thriller', 'year': 2006, 'episodes': 37, 'rating': 9.1, 'status': 'completed' } ] # Apply search query filtering if query: results = [r for r in results if query.lower() in r['name'].lower()] # Apply filters if provided if filters: if 'genre' in filters: genres = filters['genre'] if isinstance(filters['genre'], list) else [filters['genre']] results = [r for r in results if r.get('genre') in genres] if 'status' in filters: statuses = filters['status'] if isinstance(filters['status'], list) else [filters['status']] results = [r for r in results if r.get('status') in statuses] if 'year_range' in filters: year_range = filters['year_range'] if year_range.get('from'): results = [r for r in results if r.get('year', 0) >= int(year_range['from'])] if year_range.get('to'): results = [r for r in results if r.get('year', 0) <= int(year_range['to'])] # Apply sorting if sort_by in ['name', 'year', 'rating', 'episodes']: reverse = (sort_order == 'desc') results.sort(key=lambda x: x.get(sort_by, ''), reverse=reverse) return { 'success': True, 'results': results, 'total_results': len(results), 'query': query, 'filters': filters or {} } except Exception as e: return { 'success': False, 'error': str(e), 'results': [], 'total_results': 0 } def get_search_suggestions(self, query: str) -> List[Dict[str, Any]]: """Get search suggestions for a query.""" suggestions = [] # Mock suggestions - replace with actual implementation if 'attack' in query.lower(): suggestions.append({ 'text': 'Attack on Titan', 'value': 'Attack on Titan', 'type': 'Series', 'icon': 'fa-film' }) if 'action' in query.lower(): suggestions.append({ 'text': 'Action', 'value': 'Action', 'type': 'Genre', 'icon': 'fa-tags' }) return suggestions # Create search API blueprint search_bp = Blueprint('search', __name__, url_prefix='/api') # Global search manager instance search_manager = AdvancedSearchManager() @search_bp.route('/search', methods=['POST']) def search(): """Perform search with query and filters.""" try: data = request.get_json() query = data.get('query', '') search_type = data.get('type', 'all') filters = data.get('filters', {}) sort_by = data.get('sort_by', 'name') sort_order = data.get('sort_order', 'asc') result = search_manager.search_series(query, search_type, filters, sort_by, sort_order) return jsonify(result) except Exception as e: return jsonify({ 'success': False, 'error': str(e), 'results': [], 'total_results': 0 }), 500 @search_bp.route('/search/suggestions', methods=['GET']) def get_suggestions(): """Get search suggestions.""" try: query = request.args.get('q', '') suggestions = search_manager.get_search_suggestions(query) return jsonify({ 'success': True, 'suggestions': suggestions }) except Exception as e: return jsonify({ 'success': False, 'error': str(e), 'suggestions': [] }), 500 # Export the search manager advanced_search_manager = AdvancedSearchManager()