provydon/laravel-pgsearch

PostgreSQL-friendly search for Eloquent (ILIKE + normalization; optional FTS/trigram later).

Installs: 1 082

Dependents: 0

Suggesters: 0

Security: 0

Stars: 1

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/provydon/laravel-pgsearch

1.6.0 2026-02-21 02:38 UTC

This package is auto-updated.

Last update: 2026-02-21 02:43:09 UTC


README

Smart PostgreSQL search for Laravel with text normalization and relationship support.

PHP Version Laravel Version PostgreSQL License Tests

โœจ Why This Package?

  • ๐ŸŽฏ Smart matching: Find "Jane Doe" even when stored as "Jane-Doe"
  • ๐Ÿ“ฑ Phone numbers: Search "1234567890" matches "(123) 456-7890"
  • ๐Ÿ”— Relationships: Search across related models seamlessly
  • โšก PostgreSQL optimized: Uses ILIKE and REGEXP_REPLACE for performance
  • ๐Ÿ›ก๏ธ Safe fallback: Works on non-PostgreSQL databases (no-op)

๐Ÿš€ Quick Start

Install

composer require provydon/laravel-pgsearch

Use Immediately

// Search users
User::query()->pgSearch('john doe', ['name', 'email'])->get();

// Search with relationships  
Post::query()->pgSearch('jane', ['title', 'user.name'])->get();

// Phone number search
User::query()->pgSearch('1234567890', ['phone'])->get();

// Or use the helper function
pg_search(User::query(), 'john doe', ['name', 'email'])->get();

That's it! No configuration needed.

๐Ÿ’– Support

If this package helped you, consider supporting its development:

Buy Me A Coffee

๐Ÿ“– Usage Examples

Basic Search

// Single column
User::query()->pgSearch('john', ['name'])->get();

// Multiple columns
User::query()->pgSearch('example', ['name', 'email'])->get();

Relationship Search

// Search posts by author name
Post::query()->pgSearch('jane doe', ['title', 'user.name'])->get();

// Search orders by customer info
Order::query()->pgSearch('smith', ['number', 'customer.name', 'customer.email'])->get();

Advanced Options

// Disable text normalization
User::query()->pgSearch('exact-match', ['name'], ['normalize' => false])->get();

// Disable best-match ordering (default: true)
User::query()->pgSearch('office', ['name'], ['order_by_best_match' => false])->get();

// Chain with other query methods
User::query()
    ->where('active', true)
    ->pgSearch('john', ['name'])
    ->orderBy('created_at')
    ->paginate(15);

Ordering and Custom Sort

When order_by_best_match is enabled (default), results are ranked by relevance: exact phrase match (100) > normalized match (50) > word token match (10). This prevents generic words (e.g. "office") from returning the wrong row when multiple rows match.

Custom ordering: Chain your orderBy after pgSearch() so relevance is primary and your column is the tiebreaker:

// โœ“ Relevance first, then created_at
User::query()->pgSearch('john', ['name'])->orderBy('created_at', 'desc')->get();

// โœ— Your order wins; relevance only as tiebreaker
User::query()->orderBy('created_at')->pgSearch('john', ['name'])->get();

To disable best-match ordering entirely, pass ['order_by_best_match' => false] or set it in config.

Helper Function

For convenience, you can also use the pg_search() helper function:

// Using the helper function
$users = pg_search(User::query(), 'john doe', ['name', 'email'])->get();

// With options
$users = pg_search(User::query(), 'john', ['name'], ['normalize' => false])->get();

// In controllers
public function search(Request $request)
{
    $query = User::query()->where('active', true);
    
    if ($request->has('search')) {
        $query = pg_search($query, $request->search, ['name', 'email']);
    }
    
    return $query->paginate(15);
}

๐Ÿ”ง Configuration (Optional)

Publish config to customize behavior:

php artisan vendor:publish --tag=pgsearch-config
// config/pgsearch.php
return [
    'normalize' => true, // Enable smart text matching (punctuation-stripped)

    'order_by_best_match' => true, // Order results by relevance (exact match > normalized > word matches)

    // Word-based matching (on normalized text)
    // When enabled, the search term is split into tokens and each
    // significant word is also searched individually. This lets
    // "Lagos State" match a record that only contains "Lagos", etc.
    'word_based_matching' => true,

    // NEW: Common suffixes ignored as standalone tokens when doing
    // word-based matching. Useful for geographic names:
    // "Lagos State" โ†’ token "lagos" (since "state" is ignored).
    'ignore_suffixes' => [
        'state',
        'province',
        'region',
        'territory',
        'city',
        'town',
        'municipality',
    ],
];

๐Ÿง  How It Works

The package performs intelligent PostgreSQL searches:

Search Type SQL Example Matches
Direct name ILIKE '%john doe%' "John Doe", "JOHN DOE"
Normalized REGEXP_REPLACE(phone, '[^a-zA-Z0-9]', '', 'g') ILIKE '%1234567890%' "(123) 456-7890", "123-456-7890"

Real-World Examples

// These all find the same user:
User::query()->pgSearch('Jane Doe', ['name'])->get();      // Direct match
User::query()->pgSearch('jane doe', ['name'])->get();      // Case insensitive  
User::query()->pgSearch('janedoe', ['name'])->get();       // Normalized match

// Phone number variations:
User::query()->pgSearch('1234567890', ['phone'])->get();   // Finds all these:
// "(123) 456-7890", "123-456-7890", "123.456.7890", "123 456 7890"

๐Ÿ“‹ Requirements

  • Laravel: 10.0+, 11.0+, or 12.0+
  • PHP: 8.1+
  • Database: PostgreSQL (graceful fallback for others)

โšก Performance Tips

For frequently searched columns, add expression indexes to speed up normalized searches:

-- For phone number searches
CREATE INDEX users_phone_normalized_idx 
ON users (REGEXP_REPLACE(phone::text, '[^a-zA-Z0-9]', '', 'g'));

-- For name searches  
CREATE INDEX users_name_normalized_idx 
ON users (REGEXP_REPLACE(name::text, '[^a-zA-Z0-9]', '', 'g'));

Important: Use the exact same expression as in the search query for optimal performance.

๐Ÿงช Testing

# Create test database
createdb pg-search

# Run tests
composer test

๐Ÿ“ License

MIT License - see LICENSE for details.

Made with โค๏ธ for the Laravel community