定制 fab2s/searchable 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

fab2s/searchable

Composer 安装命令:

composer require fab2s/searchable

包简介

Laravel searchable models based on FullText indexes with phonetic matching

README 文档

README

CI QA codecov PHPStan PRs Welcome License: 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?

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

Searchable Laravel Scout + Driver
Infrastructure Your existing database External service (Algolia, Meilisearch, Typesense, ...)
Setup Add a trait, run one command Install driver, configure credentials, manage process/service
Sync Automatic on Eloquent save Queue workers, manual imports
Query integration Standard Eloquent scopes & builder — composes with where, join, orderBy, etc. Separate ::search() API with limited query builder support
Phonetic matching Built-in, pluggable algorithms (also provides typo tolerance) Depends on the external service
Scalability Performs well even with millions of rows thanks to single-column native fulltext indexes Designed for very large-scale, multi-field search
Best for Autocomplete, name/title/email search, up to millions of rows Multi-field search, weighted ranking, facets, advanced typo tolerance

Searchable 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

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

Installation

composer require fab2s/searchable

The service provider is auto-discovered.

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

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<static> search(string|array $search, ?string $order = 'DESC')
 */
class Contact extends Model implements SearchableInterface

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

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

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:

Property Type Default Description
$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<string> [] Model fields to index
$searchableTsConfig string 'english' PostgreSQL text search configuration
$searchablePhonetic bool false Enable phonetic matching
$searchablePhoneticAlgorithm class-string<PhoneticInterface> — (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

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

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

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

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

Two French phonetic algorithms are included, optimized PHP ports from Talisman (MIT):

Class Algorithm Description
Phonetic Phonetic Français Comprehensive French phonetic algorithm by Edouard Berge. Handles ligatures, silent letters, nasal vowels, and many French-specific spelling rules.
Soundex2 Soundex2 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

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

Encoder Per word Throughput
metaphone ~2 µs ~500k/s
Soundex2 ~35 µs ~28k/s
Phonetic ~51 µs ~20k/s

PHP'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

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

# 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

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

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

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

Searchable is open-sourced software licensed under the MIT license.

fab2s/searchable 适用场景与选型建议

fab2s/searchable 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 56.42k 次下载、GitHub Stars 达 3, 最近一次更新时间为 2022 年 12 月 05 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「search」 「mysql」 「autocomplete」 「pgsql」 「laravel」 「eloquent」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 fab2s/searchable 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 fab2s/searchable 我们能提供哪些服务?
定制开发 / 二次开发

基于 fab2s/searchable 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 56.42k
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 3
  • 点击次数: 19
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 3
  • Watchers: 1
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2022-12-05