MariaDB Full-Text Search Is Probably Enough (I Know, I Know)
Before you spin up another service, try MATCH...AGAINST. I've been surprised more than once by how far native MariaDB full-text gets you.
Every time search comes up on a project, there's an immediate gravitational pull toward adding a dedicated search service. Meilisearch, Algolia, Elasticsearch — pick your poison. I've done it too. But the last three times I've actually stopped, benchmarked, and thought it through, MariaDB's native full-text search was the right call. That's worth writing down.
What MariaDB Full-Text Search Actually Does
This isn't fuzzy matching on LIKE '%keyword%'. That's a table scan and it deserves every bad thing said about it. Full-text search in MariaDB uses an inverted index — the same fundamental data structure Meilisearch uses — maintained by the InnoDB engine (or MyISAM, but you're using InnoDB). You define one or more FULLTEXT indexes on text columns, then query them with MATCH(col) AGAINST('term' IN BOOLEAN MODE) or in natural language mode.
The engine tokenizes your text, strips stopwords, builds the index, and at query time it scores results by relevance. You get ranking for free. You can do phrase matching, exclusions, required terms, wildcards. It's not magic, but it's also not nothing.
The real question is whether it's enough for your specific use case. More often than not, it is.
A Concrete Example: Product Search in Laravel
I built a custom e-commerce admin for a Pacific Northwest industrial supplier last year. Think thousands of SKUs, part numbers, descriptions ranging from two words to three paragraphs. The client wanted search across product name, description, and a notes field. Classic Meilisearch pitch, right?
Here's what the migration looks like:
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('sku')->unique();
$table->string('name');
$table->text('description')->nullable();
$table->text('internal_notes')->nullable();
$table->decimal('price', 10, 2);
$table->timestamps();
$table->fullText(['name', 'description', 'internal_notes']);
});
That fullText() call in Laravel's schema builder generates a FULLTEXT INDEX covering all three columns. One index, multi-column. InnoDB handles it.
Then the query, in a Laravel Eloquent scope:
public function scopeSearch(Builder $query, string $term): Builder
{
if (blank($term)) {
return $query;
}
// Sanitize and prepare the boolean mode query
$cleaned = preg_replace('/[+\-><()~*"@]+/', ' ', $term);
$words = array_filter(explode(' ', $cleaned));
$booleanTerm = implode(' ', array_map(fn($w) => '+' . $w . '*', $words));
return $query
->whereRaw(
'MATCH(name, description, internal_notes) AGAINST(? IN BOOLEAN MODE)',
[$booleanTerm]
)
->orderByRaw(
'MATCH(name, description, internal_notes) AGAINST(? IN BOOLEAN MODE) DESC',
[$booleanTerm]
);
}
The +word* pattern in boolean mode means: every word is required, and each word is a prefix match. So searching "hydr pump" matches "hydraulic pump" and "hydraulic pumping unit". You call it like any scope:
$results = Product::search($request->input('q'))
->where('active', true)
->paginate(25);
That's it. No separate process to manage, no index sync to worry about, no API key, no additional monthly cost. Results come back ranked. Pagination just works because it's still a regular query.
The Gotchas That Will Bite You
The minimum word length. By default, MariaDB's InnoDB full-text parser ignores words shorter than 3 characters. Searching for "pH" in a biotech context returns nothing. You can change innodb_ft_min_token_size (default 3) in my.cnf, but that requires a server restart and a full index rebuild. I hit this hard on a LIMS integration where assay names routinely had 2-character identifiers. Know your data before you commit.
The stopwords list. InnoDB ships with a built-in stopword list. Common words like "the", "is", "in" get stripped. This mostly helps, but it will surprise you. You can swap in a custom stopword table via innodb_ft_server_stopword_table, which I've done once. Or you can disable stopwords entirely. Either way, you need to know the list exists.
50% threshold in natural language mode. If a search term appears in more than 50% of your rows, InnoDB's natural language mode treats it as a stopword and ignores it. On small tables this bites constantly. Boolean mode does not have this limitation. Use boolean mode. Always.
Relevance scoring is opaque and coarse. The scores you get back are floats, but don't read too much into them. They're based on term frequency and the number of documents containing the term, roughly TF-IDF style. They're useful for ordering but not for hard cutoffs. You can't say "only show results with a relevance above X" reliably across different data sizes.
No typo tolerance. This is the real differentiator with Meilisearch. Type "hydrualic" and MariaDB returns nothing. Meilisearch handles that by default. If your users are typing into a search box on a consumer-facing site, typo tolerance matters enormously. If your users are your client's warehouse staff copying part numbers from a spec sheet, it matters a lot less.
Index size and write overhead. FULLTEXT indexes on large text columns are big and writes cost more because the index updates synchronously. On a write-heavy table with long text fields, measure the impact. For most catalogs, product databases, article tables — it's fine. For a table taking thousands of inserts per minute, benchmark first.
When I'd Reach for Meilisearch Instead
Meilisearch earns its place in a few specific situations.
First, consumer-facing search where typo tolerance is a competitive feature. An e-commerce storefront where customers are typing on mobile — you want "nike sho" to return Nike shoes. MariaDB won't do that without serious workaround gymnastics.
Second, when you need faceted search with counts. "Show me 47 results in 'Fittings', 12 in 'Valves'" — that's either complex SQL or a Meilisearch feature you turn on with two lines of config.
Third, when your search has to span multiple tables or data sources in ways that don't map cleanly to a single indexed table. Meilisearch's index is a flat document store; you denormalize once on write and query simply.
Fourth, when you're working with a very large dataset (tens of millions of documents) and need sub-100ms response times. MariaDB full-text on a properly indexed table of 500k rows is fast — I've seen sub-20ms on modest hardware. But at 50 million rows, or on a shared DB server already under load, dedicated search starts looking better.
Meilisearch is genuinely good software. It's easy to self-host, the API is sane, the Laravel Scout driver works well. I'm not knocking it. I'm just saying it's another service to run, monitor, keep in sync with your database, and debug when the index drifts. That cost is real and it's ongoing.
When Native Is Genuinely the Right Call
Internal tools. Admin panels. B2B applications where the user population is small and trained. Catalogs under a million records. CMS search. Any situation where your users know roughly what they're looking for and can spell it.
I've shipped MariaDB full-text search for a healthcare client's internal document search (staff searching clinical protocols), a real estate tool searching property descriptions and agent notes, and that industrial supplier I mentioned. All of them are still running on native full-text. None of them have needed Meilisearch. One of them, the real estate tool, has been in production for four years.
The decision tree I actually use: Does this need typo tolerance? Does it need facets? Does it span multiple heterogeneous data sources? If all three are no, I stay native and move on. Simpler architecture wins every time you can get away with it.
// One more thing worth knowing: you can check your relevance scores
// during development to sanity-check your index
$results = DB::select("
SELECT id, name,
MATCH(name, description, internal_notes)
AGAINST(? IN BOOLEAN MODE) AS relevance
FROM products
WHERE MATCH(name, description, internal_notes) AGAINST(? IN BOOLEAN MODE)
ORDER BY relevance DESC
LIMIT 10
", [$term, $term]);
Pull that into a dev-only route and you can watch the scores as you tune your term construction. Beats guessing.
The Bottom Line
Most applications search their own database. MariaDB knows that database better than any external service will, and it's already running. Reach for the dedicated search engine when you have a genuine reason, not just because it's what the tutorial used. The best service in your stack is the one you don't have to operate.
Need help shipping something like this? Get in touch.