meilisearch/meilisearch-php
Official PHP client for Meilisearch, the open‑source search engine. Connect to Meilisearch or Meilisearch Cloud to index documents, configure indexes, and run fast, typo‑tolerant searches. Supports customizable HTTP clients and common PHP tooling.
## Getting Started
### Minimal Setup in Laravel
1. **Install the package** with Guzzle (recommended for Laravel):
```bash
composer require meilisearch/meilisearch-php guzzlehttp/guzzle http-interop/http-factory-guzzle
// config/meilisearch.php
return [
'host' => env('MEILISEARCH_HOST', 'http://127.0.0.1:7700'),
'api_key' => env('MEILISEARCH_MASTER_KEY', 'masterKey'),
];
app/Services/MeilisearchService.php):
namespace App\Services;
use Meilisearch\Client;
class MeilisearchService
{
protected $client;
public function __construct()
{
$this->client = new Client(config('meilisearch.host'), config('meilisearch.api_key'));
}
public function index(string $name)
{
return $this->client->index($name);
}
}
AppServiceProvider:
public function register()
{
$this->app->singleton(MeilisearchService::class, function ($app) {
return new MeilisearchService();
});
}
use App\Services\MeilisearchService;
public function indexMovies(MeilisearchService $meilisearch)
{
$index = $meilisearch->index('movies');
$movies = [
['id' => 1, 'title' => 'Inception', 'genres' => ['Sci-Fi', 'Action']],
];
$index->addDocuments($movies);
return response()->json(['status' => 'success']);
}
$hits = $meilisearch->index('products')->search('smartphon')->getHits();
public function searchProducts(string $query)
{
return $this->index('products')->search($query)->getHits();
}
$results = $meilisearch->index('products')->search('laptop', [
'filter' => ['price > 500 AND stock > 0'],
'facets' => ['category', 'brand'],
]);
public function search(Request $request)
{
$query = $request->input('q');
$filters = $request->input('filters', []);
return $this->searchProducts($query, $filters);
}
$index = $meilisearch->index('users');
$index->updateDocuments([
['id' => 1, 'name' => 'Updated Name'],
['id' => 2, 'email' => 'new@example.com'],
]);
public function syncUsers()
{
$users = User::all()->map(fn ($user) => [
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
]);
$this->index('users')->addDocuments($users);
}
$task = $index->addDocuments($documents);
$status = $task->waitUntilCompleted(); // Blocks until done
public function indexDocuments()
{
IndexDocumentsJob::dispatch($documents);
}
// IndexDocumentsJob.php
public function handle()
{
$task = $this->meilisearch->index('products')->addDocuments($this->documents);
$task->waitUntilCompleted();
}
$index->updateSettings([
'searchableAttributes' => ['title', 'description'],
'filterableAttributes' => ['category', 'price'],
]);
public function configureIndex()
{
$settings = config('meilisearch.index_settings.products');
$this->index('products')->updateSettings($settings);
}
Use Laravel Events:
// UserObserver.php
public function saved(User $user)
{
event(new UserUpdated($user));
}
// Listen to UserUpdated
public function handle(UserUpdated $event)
{
$this->meilisearch->index('users')->updateDocuments([$event->user->toArray()]);
}
Cache Search Results:
public function search(string $query)
{
return Cache::remember("meilisearch:{$query}", now()->addMinutes(5), function () use ($query) {
return $this->meilisearch->index('products')->search($query)->getHits();
});
}
Rate Limiting:
Route::middleware(['throttle:10,1'])->group(function () {
Route::get('/search', [SearchController::class, 'index']);
});
Environment-Specific Configs:
.env for different Meilisearch instances:
MEILISEARCH_HOST=production ? 'https://meilisearch.prod.com' : 'http://localhost:7700'
MEILISEARCH_MASTER_KEY=production ? env('PROD_MEILI_KEY') : 'masterKey'
Testing:
$mock = Mockery::mock(MeilisearchService::class);
$mock->shouldReceive('index')->andReturnSelf();
$mock->shouldReceive('search')->andReturn(new SearchResult([], 0, 0, 0, 'test'));
$this->app->instance(MeilisearchService::class, $mock);
Async Operations:
addDocuments() and updateDocuments() return a Task object. Always check its status or wait for completion:
$task = $index->addDocuments($docs);
if (!$task->isCompleted()) {
$task->waitUntilCompleted(); // Blocks
// OR use callbacks:
$task->onCompleted(function () { /* ... */ });
}
Index Creation:
$index = $client->index('products', [
'primaryKey' => 'id',
'searchableAttributes' => ['title', 'description'],
]);
Filterable Attributes:
filterableAttributes will cause silent failures. Always verify:
$settings = $index->getSettings();
if (!in_array('category', $settings['filterableAttributes'])) {
$index->updateFilterableAttributes(array_merge($settings['filterableAttributes'], ['category']));
}
Typo Tolerance:
$index->updateTypoTolerance(['enabled' => false]);
Pagination:
offset and limit for pagination, but avoid large offsets (e.g., offset=10000):
$hits = $index->search('query', ['offset' => 0, 'limit' => 20])->getHits();
Special Characters in Queries:
$filter = str_replace(['\\', '"'], ['\\\\', '\\"'], $userInput);
$index->search('query', ['filter' => $filter]);
Enable Debugging:
$client = new Client($host, $key, new GuzzleHttpClient([
'debug' => fopen('meilisearch_debug.log', 'w'),
]));
Task Status:
$task = $index->addDocuments($docs);
$status = $task->getStatus
How can I help you explore Laravel packages today?