Advanced Search Feature Guide
Last Updated: 2026-01-18 Status: Implemented Plan Reference: 041-advanced-search.md, 080-advanced-search-improvement.md, 142-search-service-refactoring.md
Overview
The Advanced Search feature provides a comprehensive search system for the client portal, enabling users to quickly find clients, projects, and invoices across the application. It includes full-text search capabilities, autocomplete suggestions, saved searches, search analytics, and role-based access control.
Table of Contents
- Accessing Search
- Search Features
- How It Works
- Accessibility Features
- Result Ranking
- Saved Searches
- Search Analytics
- API Endpoints
- Technical Architecture
- Related Features
Accessing Search
Navigation
The search feature is accessible through multiple entry points:
| Access Point | Location | URL |
|---|---|---|
| Main Search Page | Top navigation bar | /search |
| Quick Search | Global search icon in header | AJAX-powered dropdown |
| Saved Searches | Search page sidebar | /search/saved |
Permissions
| User Role | Permissions |
|---|---|
| Admin | Search all clients, projects, invoices system-wide |
| Client User | Search only accessible clients, their projects, and invoices |
Search Features
1. Global Search
The main search interface allows users to search across multiple entity types simultaneously.
URL: /search
Route Name: search.index
Features:
- Full-text search across titles, content, and keywords
- Filter by entity type (Clients, Projects, Invoices)
- Pagination for large result sets
- Relevance-based ordering
How to Use:
- Navigate to
/searchor click the search icon in the header - Enter at least 2 characters in the search box
- Optionally check the filter checkboxes to limit search to specific types
- Press Enter or click "Search"
- Results appear grouped by type with relevance ranking
2. Quick Search (Autocomplete)
Real-time search suggestions as you type.
URL: /search/quick
Route Name: search.quick
Features:
- Instant results with 200ms debounce
- Top 5 most relevant results
- Direct links to result pages
- Works via AJAX, no page reload
How to Use:
- Click the search box
- Start typing (minimum 2 characters)
- Suggestions appear below the input
- Click a suggestion to navigate directly to that item
3. Search Suggestions
Intelligent suggestions combining recent searches, popular searches, and entity matches.
URL: /search/suggestions
Route Name: search.suggestions
Suggestion Types:
- Recent: Your own recent search queries (clock icon)
- Popular: Trending searches across all users (trending-up icon)
- Entity: Direct matches to clients/projects/invoices (type-specific icon)
4. Trending Searches
View what others are searching for.
URL: /search/trending
Route Name: search.trending
Features:
- Top 10 searches from the last 7 days
- Search count and click metrics
- Useful for discovering popular content
Accessibility Features
The search system includes comprehensive accessibility features for users with disabilities.
Accessible Search Component
Use the <x-accessible-search> Blade component for a fully accessible search experience:
<x-accessible-search
placeholder="Search clients, projects, invoices..."
endpoint="/search/quick"
full-endpoint="/search"
/>
Keyboard Navigation
| Key | Action |
|---|---|
| Arrow Down | Move to next result |
| Arrow Up | Move to previous result |
| Enter | Select focused result |
| Escape | Close results dropdown |
Voice Search
The search component supports voice input using the Web Speech API:
- Click the microphone icon (if browser supports speech recognition)
- Speak your search query
- Results appear automatically
Browser Support: Chrome, Edge, Safari (partial)
Screen Reader Support
- ARIA live regions announce result counts
- Proper
role="search"androle="listbox"semantics aria-activedescendanttracks keyboard focus- Clear status announcements ("5 results found", "No results")
Result Ranking
Search results are ranked by a scoring algorithm that considers multiple factors.
Ranking Factors
| Factor | Points | Description |
|---|---|---|
| Exact Match | +100 | Title matches query exactly |
| Prefix Match | +50 | Title starts with query |
| Word Match | +25 | Query appears as a word in title |
| Recency | up to +30 | Recently updated items rank higher |
| Popularity | up to +20 | Frequently clicked items rank higher |
User Context Boost
When a user is authenticated, the system also considers:
- User's recent click history - Items matching frequently clicked types get boosted
- Personalization - Results are tailored to user's browsing patterns
SearchRankingService
use App\Services\Search\SearchRankingService;
$rankingService = app(SearchRankingService::class);
// Rank a collection of results
$ranked = $rankingService->rank($results, 'search query');
// Apply user context boost
$boosted = $rankingService->boostByUserContext($ranked, $userId);
Saved Searches
Users can save frequently used searches for quick access.
Managing Saved Searches
URL: /search/saved
Route Name: search.saved.index
Available Actions
| Action | Route | Description |
|---|---|---|
| View all | GET /search/saved | List all saved searches |
| Create | POST /search/saved | Save a new search |
| Update | PUT /search/saved/{id} | Update search name/settings |
| Delete | DELETE /search/saved/{id} | Remove a saved search |
| Set Default | POST /search/saved/{id}/default | Make this the default search |
| Toggle Alerts | POST /search/saved/{id}/toggle-alert | Enable/disable new result alerts |
| Execute | GET /search/saved/{id}/execute | Run the saved search |
Saved Search Options
| Option | Description |
|---|---|
| Name | Display name for the saved search |
| Query | The search term(s) |
| Filters | Type filters (clients/projects/invoices) |
| Is Default | Load this search by default when visiting search page |
| Alert on New | Receive notifications when new results match |
How to Save a Search
- Perform a search on the search page
- Click "Save Search" button
- Enter a name for the search
- Optionally enable "Alert on New Results"
- Click Save
Search Analytics
The system tracks search behavior to improve results and provide insights.
Tracked Data
| Metric | Description |
|---|---|
| Search History | User's search queries with timestamps |
| Click Tracking | Which results users click and position |
| Popular Searches | Aggregated search frequency by query |
| Result Counts | How many results each search returns |
Recording a Click
When a user clicks a search result, call:
POST /search/record-click
Parameters:
search_history_id: ID from the search sessionclicked_type: Entity type (client/project/invoice)clicked_id: ID of the clicked itemposition: Position in results (1-based)
API Endpoints
Search Routes Summary
| Method | Route | Name | Description |
|---|---|---|---|
| GET | /search | search.index | Main search page |
| GET | /search/results | search.results | Search with results (HTML or JSON) |
| GET | /search/quick | search.quick | Quick search (JSON) with ranking |
| GET | /search/federated | search.federated | Federated search across all types (JSON) |
| GET | /search/suggestions | search.suggestions | Autocomplete suggestions |
| GET | /search/trending | search.trending | Trending searches |
| POST | /search/record-click | search.record-click | Record result click |
| GET | /search/saved | search.saved.index | List saved searches |
| POST | /search/saved | search.saved.store | Create saved search |
| PUT | /search/saved/{id} | search.saved.update | Update saved search |
| DELETE | /search/saved/{id} | search.saved.destroy | Delete saved search |
| POST | /search/saved/{id}/default | search.saved.set-default | Set default |
| POST | /search/saved/{id}/toggle-alert | search.saved.toggle-alert | Toggle alerts |
| GET | /search/saved/{id}/execute | search.saved.execute | Execute saved search |
JSON Response Format
Search Results (GET /search/results?q=term):
{
"results": [...],
"total": 42,
"current_page": 1,
"last_page": 3
}
Quick Search (GET /search/quick?q=term):
{
"results": [
{
"id": 1,
"type": "Client",
"title": "Acme Corp",
"subtitle": "acme@example.com",
"url": "/admin/clients/1",
"_score": 125.5
}
],
"search_history_id": 42
}
Federated Search (GET /search/federated?q=term):
{
"results": [
{
"id": 1,
"type": "Client",
"title": "Acme Corp",
"subtitle": "acme@example.com",
"url": "/admin/clients/1",
"updated_at": "2026-01-12T10:30:00Z",
"_score": 125.5
},
{
"id": 5,
"type": "Project",
"title": "Acme Website Redesign",
"subtitle": "Acme Corp",
"url": "/admin/projects/5",
"updated_at": "2026-01-11T15:00:00Z",
"_score": 75.0
}
],
"facets": {
"clients": 1,
"projects": 1,
"invoices": 0
},
"total": 2,
"search_history_id": 43
}
Suggestions (GET /search/suggestions?q=ac):
{
"suggestions": [
{
"text": "acme",
"type": "recent",
"icon": "clock"
},
{
"text": "accounting",
"type": "popular",
"icon": "trending-up"
}
]
}
Technical Architecture
Core Components
app/
├── Http/Controllers/
│ ├── SearchController.php # Main search endpoints
│ └── SavedSearchController.php # Saved search management
├── Models/
│ ├── SearchIndex.php # Full-text search index
│ ├── SearchHistory.php # User search history
│ ├── SearchClick.php # Click tracking
│ ├── SearchSuggestion.php # Autocomplete suggestions
│ ├── SavedSearch.php # User's saved searches
│ └── PopularSearch.php # Aggregated popular searches
├── Services/
│ ├── SearchService.php # Core search orchestrator
│ └── Search/
│ ├── Contracts/
│ │ └── SearchableStrategy.php # Strategy interface
│ ├── Strategies/
│ │ ├── BaseSearchStrategy.php # Common search logic
│ │ ├── ClientSearchStrategy.php # Client-specific search
│ │ ├── ProjectSearchStrategy.php # Project-specific search
│ │ └── InvoiceSearchStrategy.php # Invoice-specific search
│ ├── SearchContext.php # Request context (user, limits)
│ ├── IndexingService.php # Index management
│ ├── SuggestionService.php # Suggestion generation
│ ├── SearchRankingService.php # Result ranking & scoring
│ └── SearchAnalyticsService.php # Analytics
└── resources/views/components/
└── accessible-search.blade.php # Accessible search component
Strategy Pattern Architecture
The SearchService uses the Strategy pattern to eliminate code duplication and enable easy extensibility for new searchable models.
SearchContext
A value object containing request context with cached user client IDs:
use App\Support\Search\SearchContext;
// Create context for a user with limit
$context = SearchContext::make($user, limit: 10, filters: ['status' => 'active']);
// Check if admin (skips user filtering)
$context->isAdminOrNoUser();
// Get cached client IDs (prevents repeated queries)
$context->getUserClientIds();
SearchableStrategy Interface
interface SearchableStrategy
{
public function getModelClass(): string; // e.g., Client::class
public function getKey(): string; // e.g., 'clients'
public function search(string $query, SearchContext $context): Collection;
public function getEagerLoads(): array; // Relations to eager load
public function getSelectColumns(): array; // Columns to select
public function buildFallbackQuery(string $query): Builder;
public function applyUserFilter(Builder $query, SearchContext $context): Builder;
}
Adding a New Searchable Model
- Create a new strategy class:
namespace App\Services\Search\Strategies;
use App\Models\Task;
use Illuminate\Database\Eloquent\Builder;
class TaskSearchStrategy extends BaseSearchStrategy
{
public function getModelClass(): string
{
return Task::class;
}
public function getKey(): string
{
return 'tasks';
}
public function getEagerLoads(): array
{
return ['project:id,name', 'assignee:id,name'];
}
public function getSelectColumns(): array
{
return ['id', 'project_id', 'title', 'status'];
}
public function buildFallbackQuery(string $query): Builder
{
return Task::query()
->where(function ($q) use ($query) {
$q->where('title', 'like', "%{$query}%")
->orWhere('description', 'like', "%{$query}%");
});
}
public function applyUserFilter(Builder $query, SearchContext $context): Builder
{
return $query->whereHas('project.client', function ($q) use ($context) {
$q->whereIn('id', $context->getUserClientIds());
});
}
}
- Register the strategy:
// In a service provider or SearchService constructor
$searchService->registerStrategy(new TaskSearchStrategy());
The new model will automatically be included in globalSearch() results and available via getAvailableTypes().
Database Tables
| Table | Purpose |
|---|---|
search_indices | Full-text searchable content |
search_history | User search query log |
search_clicks | Click-through tracking |
search_suggestions | Autocomplete terms |
saved_searches | User's saved search configurations |
popular_searches | Daily search frequency aggregates |
Search Index Model
The SearchIndex model uses polymorphic relationships to index any model:
// Indexed fields
- searchable_type (e.g., App\Models\Client)
- searchable_id
- title (main searchable text)
- content (body/description text)
- keywords (tags, categories)
- visibility (public/private)
- client_id (for scoping)
- project_id (for scoping)
Full-Text Search
For MySQL, the system uses MATCH...AGAINST with boolean mode:
- Automatic word stemming with
+word*syntax - Relevance scoring for result ordering
- Falls back to
LIKEqueries for SQLite (testing)
Role-Based Filtering
// In SearchIndex model
public function scopeForUser($query, User $user)
{
if ($user->isAdmin()) {
return $query;
}
$clientIds = $user->clients()->pluck('clients.id');
return $query->where(function ($q) use ($clientIds) {
$q->where('visibility', 'public')
->orWhereIn('client_id', $clientIds);
});
}
Related Features
Dependencies
| Feature | Relationship |
|---|---|
| Authentication | Search requires authenticated user |
| Clients Module | Clients are searchable entities |
| Projects Module | Projects are searchable entities |
| Invoices Module | Invoices are searchable entities |
| Notifications | Saved search alerts use notification system |
Complementary Features
| Feature | Description |
|---|---|
| Saved Filters | /filters - Save complex filter configurations |
| Activity Log | Track who searched for what (admin visibility) |
| Client Portal | Client users have scoped search access |
Artisan Commands
# Reindex all searchable models
php artisan search:reindex
# Reindex specific model
php artisan search:reindex --model=Client
# Clear stale index entries
php artisan search:cleanup
# Update suggestions from index
php artisan search:update-suggestions
Best Practices
For Users
- Use specific terms: More specific queries return better results
- Save frequent searches: Use saved searches for reports you run often
- Enable alerts: Get notified when new items match your saved search
- Use filters: Narrow results by type when you know what you're looking for
For Developers
- Implement
Searchabletrait: Add to models that should be indexed - Update index on model changes: Use model observers
- Set appropriate visibility: Mark content as public/private correctly
- Include client_id/project_id: Enable proper scoping for client users
Troubleshooting
Common Issues
| Issue | Solution |
|---|---|
| No results found | Ensure minimum 2 characters; check search index is populated |
| Missing results | Verify model implements toSearchableArray(); run reindex |
| Slow searches | Check MySQL FULLTEXT index exists; optimize query |
| Client sees admin content | Verify visibility and client_id set correctly in index |
Checking Index Status
use App\Services\Search\IndexingService;
$stats = app(IndexingService::class)->getStats();
// Returns: ['total' => 1234, 'by_type' => [...], 'last_indexed' => ...]
See Also
- Database Patterns - Eloquent query optimization
- Service Patterns - Service class architecture
- API Standards - API response formats
- Security - Access control patterns