Skip to content

Search Engine

Where this lives / How to run

  • Repo: Eliinova/the-one-otc-api
  • Scout config: config/scout.php
  • Searchable models: app/Models/Product.php, app/Models/Member.php, app/Models/Carrier.php, app/Models/Manufacturer.php
  • Local MeiliSearch: http://localhost:7700
  • Re-index command: php artisan scout:import "App\Models\Product"
EnvironmentMeiliSearch host
Localhttp://localhost:7700
StagingManaged; see /access for the full URL list and required credentials

EasyOTC uses MeiliSearch as its primary search engine to provide fast, typo-tolerant search functionality across products, members, carriers.

Overview

MeiliSearch is a powerful, fast, and easy-to-use search engine that provides:

  • Typo-tolerant search - Finds results even with spelling mistakes
  • Fast search results - Sub-50ms response times
  • Faceted search - Filter results by categories
  • Real-time indexing - Updates search index immediately
  • Simple integration - Easy to set up and maintain

Installation & Setup

1. Install MeiliSearch

bash
# Download and install MeiliSearch binary
curl -L https://install.meilisearch.com | sh

# Start MeiliSearch (run this in a separate terminal)
./meilisearch

MeiliSearch will be available at http://localhost:7700

2. Configure Environment Variables

Add these to your .env file:

env
# MeiliSearch Configuration
MEILISEARCH_HOST=http://localhost:7700
MEILISEARCH_KEY=your-search-key

3. Install Laravel Scout

The platform uses Laravel Scout for MeiliSearch integration:

bash
composer require laravel/scout

Configuration

Scout Configuration

Update config/scout.php:

php
'default' => env('SCOUT_DRIVER', 'meilisearch'),

'connections' => [
    'meilisearch' => [
        'driver' => 'meilisearch',
        'host' => env('MEILISEARCH_HOST', 'http://localhost:7700'),
        'key' => env('MEILISEARCH_KEY'),
    ],
],

Model Configuration

Models that use search have the Searchable trait:

php
use Laravel\Scout\Searchable;

class Product extends Model
{
    use Searchable;
    
    // Define searchable fields
    public function toSearchableArray()
    {
        return $this->toArray();
    }
    
    // Define searchable fields for filtering
    public function getSearchableFields()
    {
        return [
            'name',
            'description',
            'sku',
            'active_ingredient',
            'drug_class',
            'strength',
            'dosage_form',
        ];
    }
}

Searchable Models

Searchable Fields:

  • name - Product name
  • description - Product description
  • sku - Stock keeping unit
  • active_ingredient - Active ingredient
  • drug_class - Drug classification
  • strength - Drug strength
  • dosage_form - Form of medication

Search Features:

  • Full-text search across all fields
  • Filter by prescription requirements
  • Filter by availability and inventory
  • Filter by drug class and active ingredient

Searchable Fields:

  • name - Member full name (from user relationship)
  • email - Member email (from user relationship)
  • phone - Contact phone number
  • address - Street address
  • city - City
  • state - State/province
  • zip_code - Postal code

Search Features:

  • Search by name or email
  • Filter by carrier
  • Filter by active status
  • Location-based search

Searchable Fields:

  • name - Carrier name
  • slug - URL-friendly identifier

Search Features:

  • Search by name
  • Filter by active status

Searchable Fields:

  • name - Manufacturer name
  • description - Manufacturer description
  • country - Country of origin

Search Features:

  • Search by name or description
  • Filter by featured status
  • Filter by country

Search Implementation

php
// Search products
$products = Product::search('aspirin')->get();

// Search with filters
$products = Product::search('pain relief')
    ->where('requires_prescription', false)
    ->where('is_available', true)
    ->get();
php
// Search with multiple filters
$products = Product::search('headache')
    ->where('drug_class', 'analgesic')
    ->where('is_available', true)
    ->where('inventory_count', '>', 0)
    ->get();

// Search members by location
$members = Member::search('john')
    ->where('carrier_id', $carrierId)
    ->where('is_active', true)
    ->get();

Search Results

Search results include:

  • Relevance scoring - Results ranked by relevance
  • Highlighting - Matched terms highlighted
  • Faceted search - Filter options based on results
  • Pagination - Large result sets paginated

Indexing

Automatic Indexing

Models are automatically indexed when:

  • A new record is created
  • An existing record is updated
  • A record is deleted (removed from index)

Manual Indexing

bash
# Index all searchable models
php artisan scout:import

# Index specific model
php artisan scout:import "App\Models\Product"

# Flush all indexes
php artisan scout:flush

Queue-based Indexing

For better performance, configure queue-based indexing:

php
// In your model
public function shouldBeSearchable()
{
    return $this->is_available;
}

Search API Endpoints

Product Search

http
GET /api/products/search?q=aspirin&prescription=false&available=true

Query Parameters:

  • q - Search query
  • prescription - Filter by prescription requirement
  • available - Filter by availability
  • drug_class - Filter by drug class
  • active_ingredient - Filter by active ingredient
  • page - Page number for pagination
  • per_page - Results per page

Member Search

http
GET /api/members/search?q=john&carrier_id=1&active=true

Query Parameters:

  • q - Search query
  • carrier_id - Filter by carrier
  • active - Filter by active status
  • page - Page number for pagination
  • per_page - Results per page

Performance Optimization

Index Optimization

bash
# Optimize MeiliSearch indexes
curl -X POST 'http://localhost:7700/indexes/products/settings/searchable-attributes' \
  -H 'Content-Type: application/json' \
  -d '["name", "description", "sku", "active_ingredient"]'

Caching

Implement caching for frequently searched terms:

php
// Cache search results
$cacheKey = "search:products:{$query}:{$filters}";
$results = Cache::remember($cacheKey, 300, function () use ($query, $filters) {
    return Product::search($query)->where($filters)->get();
});

Monitoring & Maintenance

Health Check

bash
# Check MeiliSearch health
curl http://localhost:7700/health

Index Statistics

bash
# Get index statistics
curl http://localhost:7700/indexes/products/stats

Backup & Restore

bash
# Create backup
curl -X POST 'http://localhost:7700/snapshots' \
  -H 'Content-Type: application/json' \
  -d '{"name": "backup-2024-01-15"}'

# Restore from backup
curl -X POST 'http://localhost:7700/snapshots/backup-2024-01-15/restore'

Troubleshooting

Common Issues

Search not working:

  • Verify MeiliSearch is running
  • Check environment variables
  • Ensure models have Searchable trait
  • Check index exists and is populated

Slow search performance:

  • Optimize searchable attributes
  • Implement caching
  • Use queue-based indexing
  • Monitor MeiliSearch performance

Index out of sync:

  • Run manual indexing: php artisan scout:import
  • Check for failed jobs in queue
  • Verify model events are firing correctly

Debugging

php
// Enable Scout debugging
SCOUT_DRIVER=meilisearch
SCOUT_QUEUE=false

// Check search queries
Log::info('Search query', [
    'query' => $query,
    'filters' => $filters,
    'results' => $results->count()
]);

Best Practices

  1. Index only necessary fields - Don't index sensitive or large data
  2. Use appropriate filters - Leverage faceted search for better performance
  3. Implement caching - Cache frequent search results
  4. Monitor performance - Track search response times
  5. Regular maintenance - Backup indexes and monitor disk usage
  6. Security - Use API keys and restrict access to MeiliSearch admin

This search engine implementation provides fast, relevant search results across all major entities in EasyOTC, enhancing user experience and improving product discovery.