# Search & Filtering Implementation Guide This document provides a comprehensive overview of the search and filtering functionality implemented for the ProductHunt-like Laravel application. --- ## Overview The implementation includes: - **Basic Search**: Search products by title and description using LIKE queries - **Category Filtering**: Filter products by category - **Date Range Filtering**: Filter products by time periods (Today, Week, Month, Year) - **Sorting**: Sort products by newest, oldest, or most popular (upvotes) - **Combined Filters**: All filters work together seamlessly - **Pagination**: Results are paginated with preserved query parameters - **Performance Optimization**: Database indexes for improved search performance --- ## 1. Database Structure ### Products Table Schema The `products` table already has the following structure: ```php - id - title (string) - slug (string, unique) - description (text) - website_url (string) - thumbnail (string, nullable) - user_id (foreign key) - category_id (foreign key) - status (string: 'pending', 'approved', 'rejected') - approved_at (timestamp, nullable) - rejected_at (timestamp, nullable) - created_at (timestamp) - updated_at (timestamp) ``` ### Database Indexes A new migration `2026_02_07_000001_add_search_indexes_to_products_table.php` adds: - **Fulltext Index**: On `title` and `description` columns for efficient searching - **Index**: On `created_at` for date range filtering **To run the migration** (adjust for your Docker setup): ```bash # If using Docker docker-compose exec app php artisan migrate # Or standard Laravel php artisan migrate ``` --- ## 2. Model Implementation ### Product Model (`app/Models/Product.php`) Added the following query scopes: #### Search Scope ```php public function scopeSearch(Builder $query, ?string $search): Builder { if (empty($search)) { return $query; } return $query->where(function ($subQuery) use ($search) { $subQuery->where('title', 'like', '%' . $search . '%') ->orWhere('description', 'like', '%' . $search . '%'); }); } ``` #### Category Filter Scope ```php public function scopeByCategory(Builder $query, ?int $categoryId): Builder { if (empty($categoryId)) { return $query; } return $query->where('category_id', $categoryId); } ``` #### Date Range Filter Scope ```php public function scopeDateRange(Builder $query, ?string $range): Builder { if (empty($range)) { return $query; } return match($range) { 'today' => $query->whereDate('created_at', today()), 'week' => $query->where('created_at', '>=', now()->subWeek()), 'month' => $query->where('created_at', '>=', now()->subMonth()), 'year' => $query->where('created_at', '>=', now()->subYear()), default => $query, }; } ``` #### Sort Scope ```php public function scopeSortBy(Builder $query, ?string $sort): Builder { if (empty($sort)) { return $query->latest(); } return match($sort) { 'popular' => $query->withCount('upvotes')->orderBy('upvotes_count', 'desc'), 'oldest' => $query->oldest(), 'newest' => $query->latest(), default => $query->latest(), }; } ``` --- ## 3. Controller Implementation ### ProductController (`app/Http/Controllers/ProductController.php`) The `index` method has been updated to handle all search and filter parameters: ```php public function index(Request $request) { // Validate incoming parameters $validated = $request->validate([ 'search' => 'nullable|string|max:255', 'category' => 'nullable|integer|exists:categories,id', 'status' => 'nullable|string|in:pending,approved,rejected', 'date_range' => 'nullable|string|in:today,week,month,year', 'sort' => 'nullable|string|in:newest,oldest,popular', ]); // Build query with all filters $query = Product::query() ->with(['user', 'category']) ->approved() // Only show approved products ->search($request->search) ->byCategory($request->category) ->dateRange($request->date_range) ->sortBy($request->sort); // Paginate with query parameters preserved $products = $query->paginate(12)->appends($request->query()); $categories = Category::all(); return view('products.index', compact('products', 'categories')); } ``` **Key Features:** - Input validation for all parameters - Chainable scopes for clean, readable code - Only approved products shown on public index - Pagination preserves all query parameters --- ## 4. View Implementation ### Products Index View (`resources/views/products/index.blade.php`) The view includes: #### Search & Filter Form - **Search Input**: Text field for searching by title/description - **Category Dropdown**: Filter by product category - **Date Range Dropdown**: Filter by time period (Today, Week, Month, Year, All Time) - **Sort Dropdown**: Sort by Newest, Most Popular, or Oldest - **Apply/Clear Buttons**: Submit filters or clear all #### Active Filters Display Shows badges for currently active filters: ```blade @if(request()->hasAny(['search', 'category', 'date_range', 'sort']))
Active Filters: @if(request('search')) Search: "{{ request('search') }}" @endif
@endif ``` #### Results Display - **Results Summary**: Shows count of results (e.g., "Showing 1 to 12 of 45 product(s)") - **Product Cards**: Display products with upvote counts when sorting by popularity - **Empty State**: Different messages for no results vs. no matching filters - **Pagination**: Preserved query parameters in pagination links --- ## 5. Testing ### Test Suite (`tests/Feature/ProductSearchAndFilterTest.php`) Comprehensive tests covering: 1. **Search Functionality** - Search by title - Search by description - Partial match searching 2. **Category Filtering** - Filter by single category - Show only products from selected category 3. **Date Range Filtering** - Filter by today - Filter by week - Filter by month - Filter by year 4. **Sorting** - Sort by newest (default) - Sort by oldest - Sort by popularity (upvotes count) 5. **Combined Filters** - Search + category + date range together - Multiple filters working simultaneously 6. **Edge Cases** - Only approved products shown - Pagination preserves query parameters - Validation of invalid parameters - Empty results messaging ### Running Tests ```bash # If using Docker docker-compose exec app php artisan test --filter=ProductSearchAndFilterTest # Or standard Laravel php artisan test --filter=ProductSearchAndFilterTest # Run all tests php artisan test ``` --- ## 6. Usage Examples ### Search for Products ``` GET /products?search=Laravel ``` ### Filter by Category ``` GET /products?category=1 ``` ### Filter by Date Range ``` GET /products?date_range=week ``` ### Sort by Popularity ``` GET /products?sort=popular ``` ### Combined Filters ``` GET /products?search=Laravel&category=1&date_range=month&sort=popular ``` --- ## 7. Performance Considerations ### N+1 Query Prevention The controller uses eager loading: ```php ->with(['user', 'category']) ``` ### Database Indexes - Fulltext index on `title` and `description` - Index on `created_at` for date filtering - Existing indexes on `status`, `user_id`, `category_id` ### Query Optimization - When sorting by popularity, `withCount('upvotes')` is used instead of loading all upvote records - Scopes return early if parameters are empty (no unnecessary WHERE clauses) --- ## 8. Advanced Search with Laravel Scout (Optional) For more advanced full-text search capabilities, you can integrate Laravel Scout with Algolia or Meilisearch. ### Installation Steps 1. **Install Laravel Scout:** ```bash composer require laravel/scout php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider" ``` 2. **Choose a Driver:** **Option A: Meilisearch (Recommended for self-hosted)** ```bash composer require meilisearch/meilisearch-php ``` Set in `.env`: ```env SCOUT_DRIVER=meilisearch MEILISEARCH_HOST=http://localhost:7700 MEILISEARCH_KEY=your-master-key ``` **Option B: Algolia (Cloud-based)** ```bash composer require algolia/algoliasearch-client-php ``` Set in `.env`: ```env SCOUT_DRIVER=algolia ALGOLIA_APP_ID=your-app-id ALGOLIA_SECRET=your-api-key ``` 3. **Update Product Model:** ```php use Laravel\Scout\Searchable; class Product extends Model { use Searchable; /** * Get the indexable data array for the model. */ public function toSearchableArray(): array { return [ 'id' => $this->id, 'title' => $this->title, 'description' => $this->description, 'category' => $this->category->name, ]; } } ``` 4. **Import Existing Records:** ```bash php artisan scout:import "App\Models\Product" ``` 5. **Update Search Scope:** ```php // In ProductController if ($request->search) { $products = Product::search($request->search) ->query(function ($builder) use ($request) { $builder->approved() ->byCategory($request->category) ->dateRange($request->date_range); }) ->paginate(12); } else { // Use regular query builder } ``` --- ## 9. Frontend Enhancements (Optional) ### Real-time Search with Alpine.js Add debounced search: ```html
``` ### Filter Auto-submit ```javascript ``` --- ## 10. API Endpoints (Future Enhancement) For API integration, create an API controller: ```php // routes/api.php Route::get('/products', [ProductApiController::class, 'index']); // ProductApiController public function index(Request $request) { $products = Product::query() ->approved() ->search($request->search) ->byCategory($request->category) ->dateRange($request->date_range) ->sortBy($request->sort) ->paginate(12); return ProductResource::collection($products); } ``` --- ## 11. Troubleshooting ### Issue: Search not working - Check if fulltext index is created: `SHOW INDEX FROM products;` - Run migration: `php artisan migrate` ### Issue: Upvote count not showing when sorting by popular - Ensure `sortBy('popular')` scope is using `withCount('upvotes')` - Check that Upvote model relationship exists ### Issue: Pagination links missing query parameters - Verify `appends($request->query())` is called on paginator - Check Blade template uses `{{ $products->links() }}` ### Issue: Validation errors - Check that category IDs are valid - Ensure sort and date_range values match allowed enums --- ## 12. File Structure Summary ``` app/ ├── Http/Controllers/ │ └── ProductController.php (Updated) ├── Models/ │ └── Product.php (Updated with scopes) database/ ├── factories/ │ ├── CategoryFactory.php (New) │ ├── ProductFactory.php (New) │ └── UpvoteFactory.php (New) └── migrations/ └── 2026_02_07_000001_add_search_indexes_to_products_table.php (New) resources/views/products/ └── index.blade.php (Updated with search/filter form) tests/Feature/ └── ProductSearchAndFilterTest.php (New) ``` --- ## 13. Next Steps 1. **Run the migration** to add database indexes 2. **Run tests** to verify functionality 3. **Test manually** in the browser 4. **Consider Laravel Scout** for advanced search features 5. **Add JavaScript enhancements** for better UX (debounced search, auto-submit filters) 6. **Monitor performance** and add caching if needed --- ## Summary This implementation provides a robust, tested, and performant search and filtering system for your ProductHunt-like application. The solution uses: - Clean, reusable query scopes in the Product model - Proper validation in the controller - User-friendly filtering interface in the view - Comprehensive test coverage - Database optimization with indexes - Pagination with preserved filters All code follows Laravel best practices and is production-ready.