Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Fulltext Laravel Package

swisnl/laravel-fulltext

swisnl/laravel-fulltext adds MySQL full-text search to Laravel Eloquent models. Create FULLTEXT indexes, run natural language or boolean searches, rank results by relevance, and integrate search directly into your models with a simple, fluent API.

Deep Wiki
Context7

Getting Started

The package gives you database-native fulltext search (MySQL FULLTEXT, PostgreSQL tsvector) without an external engine like Elasticsearch or Meilisearch. The mental model is: you declare indexes in config, the package generates a dedicated search table + migration, you attach a trait to your models, and you query through a search() scope.

  1. Install and publish config
composer require swisnl/laravel-fulltext
php artisan vendor:publish --provider="Swisnl\FulltextSearch\FulltextSearchServiceProvider"
  1. Declare your indexes in config/fulltext.php. This is the file you should read first — it drives everything else (migrations, indexing, searching):
'indexes' => [
    'posts' => [
        'model'   => App\Models\Post::class,
        'columns' => ['title', 'body'],
    ],
],
  1. Create the index tables (and the migration that backs them):
php artisan fulltext:migrate
  1. Make the model searchable:
use Swisnl\FulltextSearch\Searchable;

class Post extends Model
{
    use Searchable;

    protected $fulltextIndex = 'posts';
}
  1. Backfill existing rows, then search:
php artisan fulltext:index
$results = Post::search('laravel fulltext')->get();

First use case to build: a search box on an index page. search() returns an Eloquent builder, so ->paginate(), ->where('published', true), and API resources all compose normally. If a single search() scope and one config entry work end to end, you've validated the whole install.

Where to look when something is wrong, in order: config/fulltext.php → the trait (vendor/swisnl/laravel-fulltext/src/Searchable.php) for exact property/scope names → the generated index table in the DB → the artisan commands (php artisan list fulltext).

Implementation Patterns

Per-model index, scoped search. Keep one index per model so column weights and relevance tuning stay isolated. Query with normal constraints layered on:

Post::search($term)
    ->where('status', 'published')
    ->where('tenant_id', $tenantId)
    ->paginate(20);

The where clauses run on the underlying model query, not the index — so they filter after matching. That's usually what you want, but it means relevance ordering can't be trusted if you reorder afterwards.

Multi-model "global search" index. Because the index table carries model_type/model_id, you can point several models at one index and get a unified search result set. Hydrate the mixed results yourself (group by type, resolve models, map to results/DTOs) — don't try to return heterogeneous Eloquent collections directly to a view.

Weighting columns. Put the high-signal column first and/or configure weights so a title hit outranks a body hit. Weighting is a config concern, not a query concern — tune it once, don't scatter it through controllers.

Boolean-style queries for power users. Most fulltext backends accept +required, -excluded, "exact phrase", and prefix*. Build a small parser that escapes user input for the default mode and only switches to boolean mode when the user opts in (e.g. an "advanced search" toggle). Never pass raw search-box input straight into boolean syntax.

Keep the index in sync automatically, rebuild in bulk. Model events (create/update/delete) keep the index fresh for day-to-day edits. For imports, seeders, and data migrations, disable/queue syncing and use php artisan fulltext:index instead — it's far faster than firing one index write per row.

Chunked rebuilds in a job. For large tables, wrap the rebuild in a queued job with chunking so deploys and admin "reindex" buttons don't time out or lock the table:

Post::query()->chunkById(500, fn ($posts) => /* reindex batch */);

Search result DTOs. Rather than leaking the index model, map results into a SearchResult with title, url, excerpt, and score. This keeps the UI stable if you later swap the index implementation, and gives you a place to generate highlight snippets.

Testing pattern. Hit a real MySQL/Postgres connection in tests (SQLite has no MATCH … AGAINST). Use RefreshDatabase plus an explicit reindex in setUp() so tests don't depend on event-driven indexing order.

Gotchas and Tips

Minimum token length silently kills short queries. MySQL's innodb_ft_min_token_size defaults to 3, and InnoDB stopwords are ignored entirely. A user searching "IT" or "a" gets zero results with no error. Either raise the floor in the UI (min 3–4 chars) or lower the server variable and rebuild the index.

Stopwords are the #1 "search is broken" report. "the", "and", "of" return nothing. Log the actual query string sent to the DB when debugging; the user's input and the executed term are often different.

Driver behavior is not portable. MySQL fulltext and Postgres tsvector differ on stemming, stopwords, phrase handling, and scoring. If you support both, write driver-specific tests — don't assume a query that ranks well on MySQL ranks well on Postgres.

Transactions and indexing can race. If you write a model inside a transaction and immediately search in the same request, the index write may not be visible yet (or may be rolled back after the index was updated, depending on how the package hooks events). For read-after-write UX, search the DB directly or dispatch indexing after commit.

Bulk operations bypass model events. Post::where(...)->update([...]) and raw SQL never fire saved/deleted, so the index goes stale. Any bulk write path needs an explicit reindex step.

Soft deletes. Soft-deleted rows still match unless the trait's index sync observes deleted/restored. Verify this early — a search returning deleted content is a common first-week bug.

Accent and case sensitivity follow the column collation. café won't match cafe under utf8mb4_general_ci in the way users expect, and case behavior differs between drivers. Normalize input at write time if it matters (store a lowercased, unaccented search column and index that).

Rebuilds lock tables. Adding a fulltext index to a large table is a slow, blocking operation on MySQL. Schedule reindexing in a maintenance window or against a replica; never run fulltext:migrate casually in a deploy hook on a big table.

Don't override relevance accidentally. Chaining ->orderBy('created_at') or a default global scope ordering will discard the relevance ranking. If you need "relevance, then recency," apply the secondary sort explicitly and confirm it comes after the score.

Relevance scores aren't comparable across queries. A score of 8 for one search and 8 for another mean different things. Don't store scores in the DB, threshold them globally, or show them as a percentage.

Config cache. config/fulltext.php is cached in production; adding an index requires php artisan config:cache (or your deploy pipeline's equivalent) and a fulltext:migrate run, in that order.

Verify command and property names against your installed version. This package has evolved — check php artisan list fulltext and read the trait in vendor/ for the exact index property and scope signature before copying snippets from older blog posts.

Extension points worth knowing: the index model (override to add columns like tenant_id for multi-tenant partitioning), the trait's indexing hooks (queue them for async indexing), and per-index config options (weights, driver, connection) — use a separate DB connection for indexing so search load never competes with your primary writer.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo