PHPackages                             fab2s/searchable - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. fab2s/searchable

ActiveLibrary[Database &amp; ORM](/categories/database)

fab2s/searchable
================

Laravel searchable models based on FullText indexes with phonetic matching

1.0.0(2mo ago)354.7k—7.9%MITPHPPHP ^8.1CI passing

Since Feb 21Pushed 2mo ago1 watchersCompare

[ Source](https://github.com/fab2s/Searchable)[ Packagist](https://packagist.org/packages/fab2s/searchable)[ RSS](/packages/fab2s-searchable/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (1)Dependencies (5)Versions (3)Used By (0)

Searchable
==========

[](#searchable)

[![CI](https://github.com/fab2s/Searchable/actions/workflows/ci.yml/badge.svg)](https://github.com/fab2s/Searchable/actions/workflows/ci.yml)[![QA](https://github.com/fab2s/Searchable/actions/workflows/qa.yml/badge.svg)](https://github.com/fab2s/Searchable/actions/workflows/qa.yml)[![codecov](https://camo.githubusercontent.com/f72a3d3aa7214e8f8b7389e241dfddce32d79c7169024c6702c8894513512998/68747470733a2f2f636f6465636f762e696f2f67682f66616232732f53656172636861626c652f67726170682f62616467652e7376673f746f6b656e3d444b4654345a39414d4c)](https://codecov.io/gh/fab2s/Searchable)[![PHPStan](https://camo.githubusercontent.com/83dd3d35cebed0eab9ee97ff1a5849c1344cda6a8ee9cac2cda20f5aa55b67bd/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048505374616e2d6c6576656c253230392d627269676874677265656e2e7376673f7374796c653d666c6174)](https://phpstan.org/)[![PRs Welcome](https://camo.githubusercontent.com/7d9ed3c8f22eceb1711573169b1390cc0b1194467340dc815205060c162b5309/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5052732d77656c636f6d652d627269676874677265656e2e7376673f7374796c653d666c6174)](http://makeapullrequest.com)[![License: MIT](https://camo.githubusercontent.com/08cef40a9105b6526ca22088bc514fbfdbc9aac1ddbf8d4e6c750e3a88a44dca/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d626c75652e737667)](https://opensource.org/licenses/MIT)

**Add fulltext search to your Eloquent models in minutes — no external services, no Scout driver, just your existing database.**

This package keeps things simple: it concatenates model fields into a single indexed column and uses native fulltext capabilities (`MATCH...AGAINST` on MySQL, `tsvector/tsquery` on PostgreSQL) for fast prefix-based search, ideal for autocomplete.

Why Searchable?
---------------

[](#why-searchable)

If you need fast autocomplete or simple search and already run MySQL/MariaDB or PostgreSQL, you don't need a separate search engine.

SearchableLaravel Scout + DriverInfrastructureYour existing databaseExternal service (Algolia, Meilisearch, Typesense, ...)SetupAdd a trait, run one commandInstall driver, configure credentials, manage process/serviceSyncAutomatic on Eloquent `save`Queue workers, manual importsQuery integrationStandard Eloquent scopes &amp; builder — composes with `where`, `join`, `orderBy`, etc.Separate `::search()` API with limited query builder supportPhonetic matchingBuilt-in, pluggable algorithms (also provides typo tolerance)Depends on the external serviceScalabilityPerforms well even with millions of rows thanks to single-column native fulltext indexesDesigned for very large-scale, multi-field searchBest forAutocomplete, name/title/email search, up to millions of rowsMulti-field search, weighted ranking, facets, advanced typo toleranceSearchable is not a replacement for a dedicated search engine — it's a lightweight alternative for the many cases where one isn't needed. The single-column approach is what makes it fast: native fulltext indexes on one column scale well, whereas indexing many columns separately (especially on MySQL) is where dedicated engines pull ahead.

Requirements
------------

[](#requirements)

- PHP 8.1+
- Laravel 10.x / 11.x / 12.x
- MySQL / MariaDB or PostgreSQL
- `ext-intl` PHP extension

Installation
------------

[](#installation)

```
composer require fab2s/searchable
```

The service provider is auto-discovered.

Quick start
-----------

[](#quick-start)

Implement `SearchableInterface` on your model, use the `Searchable` trait, and list the fields to index:

```
use fab2s\Searchable\SearchableInterface;
use fab2s\Searchable\Traits\Searchable;

class Contact extends Model implements SearchableInterface
{
    use Searchable;

    protected $searchables = [
        'first_name',
        'last_name',
        'email',
    ];
}
```

Then run the artisan command to add the column and fulltext index:

```
php artisan searchable:enable
```

That's it. The `searchable` column is automatically populated on every save.

> **Choosing fields wisely:** The quality of matching depends directly on which fields you index. This package is designed for fast, simple autocomplete — not complex full-text search. Keep `$searchables` focused on the few fields users actually type into a search box (names, titles, emails). Adding large or numerous fields dilutes relevance and increases storage. If you need weighted fields, facets, or advanced ranking, consider a dedicated search engine instead.

Searching
---------

[](#searching)

The trait provides a `search` scope that handles everything automatically:

```
$results = Contact::search($request->input('q'))->get();
```

It composes with other query builder methods:

```
$results = Contact::search('john')
    ->where('active', true)
    ->limit(10)
    ->get();
```

Results are ordered by relevance (DESC) by default. Pass `null` to disable:

```
$results = Contact::search('john', null)->latest()->get();
```

The driver is detected automatically from the query's connection. The scope picks up the model's `tsConfig` and `phonetic` settings.

> For IDE autocompletion, add a `@method` annotation to your model:
>
> ```
> /**
>  * @method static Builder search(string|array $search, ?string $order = 'DESC')
>  */
> class Contact extends Model implements SearchableInterface
> ```

### Empty search terms

[](#empty-search-terms)

When the search input is empty or contains only operators/whitespace, the `search` scope is a no-op — no `WHERE` or `ORDER BY` clause is added. This means you can safely pass user input without checking for empty strings:

```
// Safe — returns all contacts (unfiltered) when $q is empty
$results = Contact::search($request->input('q', ''))
    ->where('active', true)
    ->get();
```

### Advanced usage with SearchQuery

[](#advanced-usage-with-searchquery)

For more control (table aliases in joins, custom field name), use `SearchQuery` directly:

```
use fab2s\Searchable\SearchQuery;

$search = new SearchQuery('DESC', 'searchable', 'english', phonetic: true);
$query  = Contact::query();

$search->addMatch($query, $request->input('q'), 'contacts');

$results = $query->get();
```

This is particularly useful when searching across joined tables. The third argument to `addMatch` is a table alias that prefixes the searchable column, preventing ambiguity:

```
$search = new SearchQuery;
$query  = Contact::query()
    ->join('companies', 'contacts.company_id', '=', 'companies.id')
    ->select('contacts.*');

// search in contacts
$search->addMatch($query, $request->input('q'), 'contacts');

// you could also search in companies with a second SearchQuery instance
// (new SearchQuery)->addMatch($query, $request->input('q'), 'companies');

$results = $query->get();
```

Configuration
-------------

[](#configuration)

Every option can be set by declaring a property on your model. The trait picks them up automatically and falls back to sensible defaults when omitted:

PropertyTypeDefaultDescription`$searchableField``string``'searchable'`Column name for the searchable content`$searchableFieldDbType``string``'string'`Migration column type (`string`, `text`)`$searchableFieldDbSize``int``500`Column size (applies to `string` type)`$searchables``array``[]`Model fields to index`$searchableTsConfig``string``'english'`PostgreSQL text search configuration`$searchablePhonetic``bool``false`Enable phonetic matching`$searchablePhoneticAlgorithm``class-string`— (metaphone)Custom phonetic encoder class```
class Contact extends Model implements SearchableInterface
{
    use Searchable;

    protected array $searchables = ['first_name', 'last_name', 'email'];
    protected string $searchableTsConfig = 'french';
    protected bool $searchablePhonetic = true;
    protected int $searchableFieldDbSize = 1000;
}
```

Each property has a corresponding getter method (`getSearchableField()`, `getSearchableFieldDbType()`, etc.) defined in `SearchableInterface`. You can override those methods instead if you need computed values.

### Custom content

[](#custom-content)

Override `getSearchableContent()` to control what gets indexed. The `$additional` parameter lets you inject extra data (decrypted fields, computed values, etc.):

```
public function getSearchableContent(string $additional = ''): string
{
    $extra = implode(' ', [
        $this->decrypt('phone'),
        $this->some_computed_value,
    ]);

    return parent::getSearchableContent($extra);
}
```

### PostgreSQL text search configuration

[](#postgresql-text-search-configuration)

By default, PostgreSQL uses the `english` text search configuration. Set `$searchableTsConfig` to change it:

```
protected string $searchableTsConfig = 'french';
```

The `search` scope picks this up automatically. When using `SearchQuery` directly, pass the same value:

```
$search = new SearchQuery('DESC', 'searchable', 'french');
```

### Phonetic matching

[](#phonetic-matching)

Enable phonetic matching to find results despite spelling variations (eg. "jon" matches "john", "smyth" matches "smith"). This uses PHP's `metaphone()` to append phonetic codes to the same searchable field — no extra column or extension needed.

```
protected bool $searchablePhonetic = true;
```

That's all — both storage and the `search` scope handle it automatically. Stored content becomes `john smith jn sm0`, and a search for `jon` produces the term `jn` which matches.

When using `SearchQuery` directly, pass the phonetic flag:

```
$search = new SearchQuery('DESC', 'searchable', 'english', phonetic: true);
```

### Custom phonetic algorithm

[](#custom-phonetic-algorithm)

The default `metaphone()` works well for English. For other languages, set `$searchablePhoneticAlgorithm` to any class implementing `PhoneticInterface`:

```
use fab2s\Searchable\Phonetic\PhoneticInterface;

class MyEncoder implements PhoneticInterface
{
    public static function encode(string $word): string
    {
        // your encoding logic
    }
}
```

Then reference it on your model:

```
use fab2s\Searchable\Phonetic\Phonetic;

class Contact extends Model implements SearchableInterface
{
    use Searchable;

    protected array $searchables = ['first_name', 'last_name'];
    protected bool $searchablePhonetic = true;
    protected string $searchablePhoneticAlgorithm = Phonetic::class;
}
```

The trait resolves the class to a closure internally — no method override needed.

When using `SearchQuery` directly, pass the encoder as a closure:

```
$search = new SearchQuery('DESC', 'searchable', 'french', phonetic: true, phoneticAlgorithm: Phonetic::encode(...));
```

### Built-in French encoders

[](#built-in-french-encoders)

Two French phonetic algorithms are included, optimized PHP ports from [Talisman](https://github.com/Yomguithereal/talisman) (MIT):

ClassAlgorithmDescription`Phonetic`[Phonetic Français](http://www.roudoudou.com/phonetic.php)Comprehensive French phonetic algorithm by Edouard Berge. Handles ligatures, silent letters, nasal vowels, and many French-specific spelling rules.`Soundex2`[Soundex2](http://sqlpro.developpez.com/cours/soundex/)French adaptation of Soundex. Simpler and faster than `Phonetic`, produces 4-character codes.Both implement `PhoneticInterface` and handle Unicode normalization (accents, ligatures like œ and æ) internally.

```
use fab2s\Searchable\Phonetic\Phonetic;
use fab2s\Searchable\Phonetic\Soundex2;

Phonetic::encode('jean');   // 'JAN'
Soundex2::encode('dupont'); // 'DIPN'
```

### Phonetic encoder benchmarks

[](#phonetic-encoder-benchmarks)

Measured on a set of 520 French words, 1000 iterations each (PHP 8.4):

EncoderPer wordThroughputmetaphone~2 µs~500k/sSoundex2~35 µs~28k/sPhonetic~51 µs~20k/sPHP's native `metaphone()` is a C extension and unsurprisingly the fastest. Both French encoders are pure PHP with extensive regex-based rule sets, yet fast enough for typical use — encoding 1000 words takes under 50ms.

Automatic setup after migrations
--------------------------------

[](#automatic-setup-after-migrations)

The package listens to Laravel's `MigrationsEnded` event and automatically runs `searchable:enable` after every successful `up` migration. This means:

- After `php artisan migrate`, the searchable column and fulltext index are added to any new Searchable model.
- After `php artisan migrate:fresh`, they are recreated along with the rest of your schema.
- Rollbacks (`down`) and pretended migrations (`--pretend`) are ignored.

This is fully automatic — no configuration needed. If you need to re-index existing records, run the command manually with `--index`.

The Enable command
------------------

[](#the-enable-command)

```
# Add searchable column + index to all models using the Searchable trait
php artisan searchable:enable

# Target a specific model
php artisan searchable:enable --model=App/Models/Contact

# Also (re)index existing records
php artisan searchable:enable --model=App/Models/Contact --index

# Scan a custom directory for models
php artisan searchable:enable --root=app/Domain/Models
```

The command detects the database driver and creates the appropriate index:

- **MySQL**: `ALTER TABLE ... ADD FULLTEXT`
- **PostgreSQL**: `CREATE INDEX ... USING GIN(to_tsvector(...))`

### Adding Searchable to an existing model

[](#adding-searchable-to-an-existing-model)

You can add the Searchable feature to a model with pre-existing data at any time. After implementing `SearchableInterface` and using the `Searchable` trait, run the enable command with `--index` to set up the column, create the fulltext index, and populate it for all existing records:

```
php artisan searchable:enable --model=App/Models/Contact --index
```

You can also run it without `--model` to process all Searchable models at once. Indexing is optimized with batch processing to handle large tables efficiently.

### When to re-index

[](#when-to-re-index)

The searchable column is automatically kept in sync on every Eloquent `save`. Manual re-indexing is only needed when:

- **Adding Searchable to a model with existing data** — existing rows have no searchable content yet.
- **Changing `$searchables`** — after adding or removing fields from the index, existing rows still contain the old content.
- **Mass imports that bypass Eloquent** — raw SQL inserts, `DB::insert()`, or bulk imports that skip model events won't populate the searchable column.

In all these cases, run:

```
# re-index a specific model
php artisan searchable:enable --model=App/Models/Contact --index

# or re-index all Searchable models
php artisan searchable:enable --index
```

Contributing
------------

[](#contributing)

Contributions are welcome. Feel free to open issues and submit pull requests.

```
# fix code style
composer fix

# run tests
composer test

# run tests with coverage
composer cov

# static analysis (src, level 9)
composer stan

# static analysis (tests, level 5)
composer stan-tests
```

License
-------

[](#license)

`Searchable` is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).

###  Health Score

45

—

FairBetter than 93% of packages

Maintenance83

Actively maintained with recent releases

Popularity32

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity44

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

86d ago

Major Versions

0.0.1.x-dev → 1.0.02026-02-21

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/7323989?v=4)[fab2s](/maintainers/fab2s)[@fab2s](https://github.com/fab2s)

---

Top Contributors

[![fab2s](https://avatars.githubusercontent.com/u/7323989?v=4)](https://github.com/fab2s "fab2s (27 commits)")

---

Tags

autocompleteautocompletionfulltextfulltext-indexesfulltext-searchlaravelmariadbmysqlpgsqlphoneticsearchsearchlaravelautocompletemysqlmariadbpgsqleloquentsearchablefulltext searchphoneticfulltext-index

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/fab2s-searchable/health.svg)

```
[![Health](https://phpackages.com/badges/fab2s-searchable/health.svg)](https://phpackages.com/packages/fab2s-searchable)
```

###  Alternatives

[jedrzej/searchable

Searchable trait for Laravel's Eloquent models - filter your models using request parameters

127259.1k5](/packages/jedrzej-searchable)[ramadan/easy-model

A Laravel package for enjoyably managing database queries.

101.6k](/packages/ramadan-easy-model)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
