Meilisearch Integration for Website Search
Meilisearch — full-text search engine written in Rust. Response time under 50ms on most corpora, built-in typo support, fuzzy search, faceted filtering. Installs as separate process beside main application.
When Needed
Standard LIKE '%query%' in PostgreSQL stops working around 50,000 records — query time grows and database load increases. Meilisearch solves differently: builds inverted index separately, queries bypass the database.
Suitable for:
- Product catalogs with attribute filters
- Blogs and knowledge bases (10,000+ articles)
- User, address, product directories in B2B cabinets
- Documentation search
Integration Architecture
Browser → Backend API → Meilisearch HTTP API
↓
PostgreSQL (data source)
Indexer (Queued Job / Cron)
Meilisearch isn't main database replacement. Data lives in PostgreSQL, only search-needed data goes to Meilisearch. Sync via job queues on record changes or periodic index rebuild.
Installation and Configuration
Docker Compose:
services:
meilisearch:
image: getmeili/meilisearch:v1.7
environment:
MEILI_MASTER_KEY: "${MEILI_MASTER_KEY}"
MEILI_ENV: production
volumes:
- meili_data:/meili_data
ports:
- "7700:7700"
Index attribute configuration:
{
"searchableAttributes": ["name", "description", "sku", "brand"],
"filterableAttributes": ["category_id", "brand", "in_stock", "price"],
"sortableAttributes": ["price", "created_at", "rating"],
"rankingRules": ["words", "typo", "proximity", "attribute", "sort", "exactness"],
"typoTolerance": {
"enabled": true,
"minWordSizeForTypos": { "oneTypo": 5, "twoTypos": 9 }
}
}
Data Synchronization
Laravel Scout + official driver:
// composer require laravel/scout meilisearch/meilisearch-php
use Laravel\Scout\Searchable;
class Product extends Model
{
use Searchable;
public function toSearchableArray(): array
{
return [
'id' => $this->id,
'name' => $this->name,
'description' => strip_tags($this->description),
'brand' => $this->brand->name,
'category_id' => $this->category_id,
'price' => $this->price,
'in_stock' => $this->stock > 0,
];
}
}
Initial indexing:
php artisan scout:import "App\Models\Product"
Search with filters:
$results = Product::search($query)
->where('in_stock', true)
->where('category_id', $categoryId)
->orderBy('price')
->paginate(20);
Faceted Filtering
Meilisearch returns aggregations by facets in single request:
$client = new \Meilisearch\Client('http://localhost:7700', $apiKey);
$response = $client->index('products')->search($query, [
'filter' => ['category_id = 5', 'price 100 TO 5000'],
'facets' => ['brand', 'color', 'in_stock'],
'hitsPerPage' => 20,
'page' => 1,
]);
// $response->getFacetDistribution() — array with counts per facet
Browser Direct Search
Direct JavaScript requests to Meilisearch via Search-only API Key — key with limited rights (search only on specific indexes). Lowers latency, removes extra hop.
import { MeiliSearch } from 'meilisearch'
const client = new MeiliSearch({
host: 'https://search.example.com',
apiKey: SEARCH_ONLY_KEY,
})
const results = await client.index('products').search(query, {
filter: 'in_stock = true',
limit: 10,
})
Multilingual Search
Separate index per language (products_ru, products_en) or single with language filter. Meilisearch supports Russian stemming via dictionaries.
Timelines and Work Stages
| Stage | Details | Time |
|---|---|---|
| Infrastructure setup | Docker, SSL, API keys | 1 day |
| Index schema | Attributes, ranking, facets | 1 day |
| Backend integration | Scout / direct client, sync | 2–3 days |
| UI search component | Autocomplete, facets, pagination | 2–3 days |
| Testing | Load, relevance, edge cases | 1 day |
Total: 7–9 working days for typical catalog.
Monitoring
Meilisearch exposes metrics at /metrics in Prometheus format (if enabled). Key indicators: index size, indexing task time, queries per second. Indexing task status available at /tasks.







