Skip to main content
Back to ScopeForged

ScopeForged Documentation

Technical documentation, guides, and feature references for the ScopeForged client portal.

Search & Analytics/Advanced Search

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

  1. Accessing Search
  2. Search Features
  3. How It Works
  4. Accessibility Features
  5. Result Ranking
  6. Saved Searches
  7. Search Analytics
  8. API Endpoints
  9. Technical Architecture
  10. Related Features

The search feature is accessible through multiple entry points:

Access PointLocationURL
Main Search PageTop navigation bar/search
Quick SearchGlobal search icon in headerAJAX-powered dropdown
Saved SearchesSearch page sidebar/search/saved

Permissions

User RolePermissions
AdminSearch all clients, projects, invoices system-wide
Client UserSearch only accessible clients, their projects, and invoices

Search Features

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:

  1. Navigate to /search or click the search icon in the header
  2. Enter at least 2 characters in the search box
  3. Optionally check the filter checkboxes to limit search to specific types
  4. Press Enter or click "Search"
  5. 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:

  1. Click the search box
  2. Start typing (minimum 2 characters)
  3. Suggestions appear below the input
  4. 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)

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

KeyAction
Arrow DownMove to next result
Arrow UpMove to previous result
EnterSelect focused result
EscapeClose results dropdown

The search component supports voice input using the Web Speech API:

  1. Click the microphone icon (if browser supports speech recognition)
  2. Speak your search query
  3. Results appear automatically

Browser Support: Chrome, Edge, Safari (partial)

Screen Reader Support

  • ARIA live regions announce result counts
  • Proper role="search" and role="listbox" semantics
  • aria-activedescendant tracks 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

FactorPointsDescription
Exact Match+100Title matches query exactly
Prefix Match+50Title starts with query
Word Match+25Query appears as a word in title
Recencyup to +30Recently updated items rank higher
Popularityup to +20Frequently 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

ActionRouteDescription
View allGET /search/savedList all saved searches
CreatePOST /search/savedSave a new search
UpdatePUT /search/saved/{id}Update search name/settings
DeleteDELETE /search/saved/{id}Remove a saved search
Set DefaultPOST /search/saved/{id}/defaultMake this the default search
Toggle AlertsPOST /search/saved/{id}/toggle-alertEnable/disable new result alerts
ExecuteGET /search/saved/{id}/executeRun the saved search

Saved Search Options

OptionDescription
NameDisplay name for the saved search
QueryThe search term(s)
FiltersType filters (clients/projects/invoices)
Is DefaultLoad this search by default when visiting search page
Alert on NewReceive notifications when new results match
  1. Perform a search on the search page
  2. Click "Save Search" button
  3. Enter a name for the search
  4. Optionally enable "Alert on New Results"
  5. Click Save

Search Analytics

The system tracks search behavior to improve results and provide insights.

Tracked Data

MetricDescription
Search HistoryUser's search queries with timestamps
Click TrackingWhich results users click and position
Popular SearchesAggregated search frequency by query
Result CountsHow 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 session
  • clicked_type: Entity type (client/project/invoice)
  • clicked_id: ID of the clicked item
  • position: Position in results (1-based)

API Endpoints

Search Routes Summary

MethodRouteNameDescription
GET/searchsearch.indexMain search page
GET/search/resultssearch.resultsSearch with results (HTML or JSON)
GET/search/quicksearch.quickQuick search (JSON) with ranking
GET/search/federatedsearch.federatedFederated search across all types (JSON)
GET/search/suggestionssearch.suggestionsAutocomplete suggestions
GET/search/trendingsearch.trendingTrending searches
POST/search/record-clicksearch.record-clickRecord result click
GET/search/savedsearch.saved.indexList saved searches
POST/search/savedsearch.saved.storeCreate saved search
PUT/search/saved/{id}search.saved.updateUpdate saved search
DELETE/search/saved/{id}search.saved.destroyDelete saved search
POST/search/saved/{id}/defaultsearch.saved.set-defaultSet default
POST/search/saved/{id}/toggle-alertsearch.saved.toggle-alertToggle alerts
GET/search/saved/{id}/executesearch.saved.executeExecute 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

  1. 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());
        });
    }
}
  1. 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

TablePurpose
search_indicesFull-text searchable content
search_historyUser search query log
search_clicksClick-through tracking
search_suggestionsAutocomplete terms
saved_searchesUser's saved search configurations
popular_searchesDaily 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)

For MySQL, the system uses MATCH...AGAINST with boolean mode:

  • Automatic word stemming with +word* syntax
  • Relevance scoring for result ordering
  • Falls back to LIKE queries 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);
    });
}

Dependencies

FeatureRelationship
AuthenticationSearch requires authenticated user
Clients ModuleClients are searchable entities
Projects ModuleProjects are searchable entities
Invoices ModuleInvoices are searchable entities
NotificationsSaved search alerts use notification system

Complementary Features

FeatureDescription
Saved Filters/filters - Save complex filter configurations
Activity LogTrack who searched for what (admin visibility)
Client PortalClient 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

  1. Use specific terms: More specific queries return better results
  2. Save frequent searches: Use saved searches for reports you run often
  3. Enable alerts: Get notified when new items match your saved search
  4. Use filters: Narrow results by type when you know what you're looking for

For Developers

  1. Implement Searchable trait: Add to models that should be indexed
  2. Update index on model changes: Use model observers
  3. Set appropriate visibility: Mark content as public/private correctly
  4. Include client_id/project_id: Enable proper scoping for client users

Troubleshooting

Common Issues

IssueSolution
No results foundEnsure minimum 2 characters; check search index is populated
Missing resultsVerify model implements toSearchableArray(); run reindex
Slow searchesCheck MySQL FULLTEXT index exists; optimize query
Client sees admin contentVerify 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