glueful / meilisearch
Meilisearch full-text search integration for Glueful Framework
Installs: 2
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
Type:glueful-extension
pkg:composer/glueful/meilisearch
Requires
- php: ^8.3
- meilisearch/meilisearch-php: ^1.6
Requires (Dev)
- glueful/framework: dev-main
- phpstan/phpstan: ^1.0
- phpunit/phpunit: ^10.5
- squizlabs/php_codesniffer: ^3.6
README
Full-text search integration for the Glueful Framework using Meilisearch.
Overview
The Meilisearch extension provides seamless integration between Glueful Framework and Meilisearch, an open-source, lightning-fast search engine. This extension enables models to be searchable with minimal configuration while providing advanced search features like typo tolerance, filtering, faceting, and geo-search.
Features
- Searchable trait: Make any model searchable with a simple trait
- Automatic syncing: Keep search index in sync with database changes via model events
- Transaction-safe indexing: Indexing deferred until after database transactions commit
- Fluent query builder: Intuitive API for building complex search queries
- Filters and facets: Full support for Meilisearch filtering and faceted search
- Geo-search: Location-based search with radius and bounding box filters
- Pagination: Built-in pagination with metadata
- Queue support: Optional async indexing via queue workers
- Batch operations: Efficient bulk indexing with configurable batch sizes
- Index prefixing: Multi-tenant friendly with configurable index prefixes
- CLI commands: Index management and debugging tools
Installation
Installation (Recommended)
Install via Composer
composer require glueful/meilisearch
# Rebuild the extensions cache after adding new packages
php glueful extensions:cache
Glueful auto-discovers packages of type glueful-extension and boots their service providers.
Enable/disable in development (these commands edit config/extensions.php):
# Enable the extension (adds to config/extensions.php) php glueful extensions:enable Meilisearch # Disable the extension (comments out in config/extensions.php) php glueful extensions:disable Meilisearch # Preview changes without writing php glueful extensions:enable Meilisearch --dry-run # Create backup before editing php glueful extensions:enable Meilisearch --backup
Local Development Installation
If you're working locally (without Composer), place the extension in extensions/meilisearch, ensure config/extensions.php has local_path pointing to extensions (non-prod).
Verify Installation
Check status and details:
php glueful extensions:list php glueful extensions:info Meilisearch
Requirements
- PHP 8.3 or higher
- Glueful Framework 1.27.0 or higher
- Meilisearch server (v1.6+ recommended)
- meilisearch/meilisearch-php ^1.6 (installed automatically)
Installing Meilisearch Server
The extension requires a running Meilisearch server. Choose one of the following installation methods:
Docker (Recommended)
docker run -d -p 7700:7700 \ -v $(pwd)/meili_data:/meili_data \ -e MEILI_MASTER_KEY='your-master-key' \ getmeili/meilisearch:v1.6
Homebrew (macOS)
brew install meilisearch
meilisearch --master-key="your-master-key"
Binary Download
Download the latest binary for your platform from the Meilisearch releases page or follow the official installation guide.
# Example for Linux curl -L https://install.meilisearch.com | sh ./meilisearch --master-key="your-master-key"
Meilisearch Cloud
For production, consider Meilisearch Cloud for a fully managed solution.
Configuration
Set the following environment variables in your .env file:
# Meilisearch connection MEILISEARCH_HOST=http://127.0.0.1:7700 MEILISEARCH_KEY=your-master-key # Index prefix (optional, useful for multi-tenant or staging/production separation) MEILISEARCH_PREFIX=myapp_ # Queue configuration (optional, for async indexing) MEILISEARCH_QUEUE=false MEILISEARCH_QUEUE_CONNECTION=redis MEILISEARCH_QUEUE_NAME=search # Batch configuration MEILISEARCH_BATCH_SIZE=500 MEILISEARCH_BATCH_TIMEOUT=30 # Search defaults MEILISEARCH_DEFAULT_LIMIT=20 MEILISEARCH_SOFT_DELETE=true
Configuration File
The extension configuration is located at config/meilisearch.php:
<?php return [ 'host' => env('MEILISEARCH_HOST', 'http://127.0.0.1:7700'), 'key' => env('MEILISEARCH_KEY', null), 'prefix' => env('MEILISEARCH_PREFIX', ''), 'queue' => [ 'enabled' => (bool) env('MEILISEARCH_QUEUE', false), 'connection' => env('MEILISEARCH_QUEUE_CONNECTION', null), 'queue' => env('MEILISEARCH_QUEUE_NAME', 'search'), ], 'batch' => [ 'size' => (int) env('MEILISEARCH_BATCH_SIZE', 500), 'timeout' => (int) env('MEILISEARCH_BATCH_TIMEOUT', 30), ], 'soft_delete' => (bool) env('MEILISEARCH_SOFT_DELETE', true), 'search' => [ 'limit' => (int) env('MEILISEARCH_DEFAULT_LIMIT', 20), 'attributes_to_highlight' => ['*'], 'highlight_pre_tag' => '<em>', 'highlight_post_tag' => '</em>', ], ];
Usage
Making Models Searchable
Add the Searchable trait to any model. Implementing SearchableInterface is recommended for static analysis type checking:
<?php namespace App\Models; use Glueful\Database\ORM\Model; use Glueful\Extensions\Meilisearch\Contracts\SearchableInterface; use Glueful\Extensions\Meilisearch\Model\Searchable; class Post extends Model implements SearchableInterface { use Searchable; protected string $table = 'posts'; /** * Customize the data indexed in Meilisearch. */ public function toSearchableArray(): array { return [ 'id' => $this->uuid, 'title' => $this->title, 'body' => $this->body, 'author_name' => $this->author->name ?? null, 'tags' => $this->tags->pluck('name')->toArray(), 'category' => $this->category?->name, 'status' => $this->status, 'published_at' => $this->published_at?->timestamp, ]; } /** * Define filterable attributes. */ public function getSearchableFilterableAttributes(): array { return ['status', 'category', 'tags', 'author_name', 'published_at']; } /** * Define sortable attributes. */ public function getSearchableSortableAttributes(): array { return ['published_at', 'title']; } /** * Only index published posts. */ public function shouldBeSearchable(): bool { return $this->status === 'published'; } }
Basic Searching
// $context is an ApplicationContext instance // Simple search $results = Post::search($context, 'laravel tutorial')->get(); // Access results foreach ($results as $post) { echo $post->title; } // Get raw hits without model hydration $rawResults = Post::search($context, 'laravel')->raw();
Filtering
// Single filter $results = Post::search($context, 'php') ->where('status', 'published') ->get(); // Multiple filters $results = Post::search($context, 'api') ->where('status', 'published') ->where('published_at', '>=', strtotime('-30 days')) ->whereIn('category', ['tutorials', 'guides']) ->get(); // Using raw filter syntax $results = Post::search($context, 'docker') ->filter('status = "published" AND category IN ["tutorials", "guides"]') ->get();
Sorting
$results = Post::search($context, 'api design') ->orderBy('published_at', 'desc') ->get(); // Multiple sort criteria $results = Post::search($context, '') ->orderBy('category', 'asc') ->orderBy('published_at', 'desc') ->get();
Pagination
$results = Post::search($context, 'docker') ->where('status', 'published') ->paginate(page: 1, perPage: 15); // Access pagination metadata $meta = $results->paginationMeta(); // ['current_page' => 1, 'per_page' => 15, 'total' => 42, 'total_pages' => 3, 'has_more' => true]
Faceted Search
$results = Post::search($context, '') ->facets(['category', 'tags', 'author_name']) ->where('status', 'published') ->get(); // Access facet distribution $categoryFacets = $results->facets('category'); // ['tutorials' => 45, 'guides' => 23, 'news' => 12] // All facets $allFacets = $results->facets();
Geo-Search
For location-based models, add geo data to your searchable array:
class Store extends Model implements SearchableInterface { use Searchable; public function toSearchableArray(): array { return [ 'id' => $this->uuid, 'name' => $this->name, '_geo' => [ 'lat' => (float) $this->latitude, 'lng' => (float) $this->longitude, ], ]; } public function getSearchableFilterableAttributes(): array { return ['_geo', 'category']; } public function getSearchableSortableAttributes(): array { return ['_geo', 'name']; } }
Search by location:
// Find stores within 5km of a point $results = Store::search($context, 'coffee') ->whereGeoRadius(40.7128, -74.0060, 5000) ->get(); // Find within bounding box $results = Store::search($context, '') ->whereGeoBoundingBox([45.0, -73.0], [40.0, -74.0]) ->get(); // Sort by distance (nearest first) $results = Store::search($context, 'coffee') ->orderByGeo(40.7128, -74.0060, 'asc') ->get();
Highlighting
$results = Post::search($context, 'important topic') ->highlight(['title', 'body']) ->get(); // Access highlighted results in raw hits $rawResults = Post::search($context, 'important')->highlight(['title'])->raw(); foreach ($rawResults['hits'] as $hit) { echo $hit['_formatted']['title']; // Contains <em>important</em> }
Manual Indexing
// Index a single model $post = Post::find($context, $uuid); $post->searchableSync(); // Remove from index $post->searchableRemove(); // Batch indexing via BatchIndexer $indexer = app($context, BatchIndexer::class); $posts = Post::query($context)->where('status', 'published')->get(); $indexer->indexMany($posts);
Index Management
use Glueful\Extensions\Meilisearch\Indexing\IndexManager; $manager = app($context, IndexManager::class); // Create index with settings $manager->createIndex('posts'); // Update index settings $manager->updateSettings('posts', [ 'filterableAttributes' => ['status', 'category'], 'sortableAttributes' => ['published_at', 'title'], 'searchableAttributes' => ['title', 'body', 'tags'], ]); // Sync settings from model $manager->syncSettingsForModel(new Post([], $context)); // Get index statistics $stats = $manager->getStats('posts'); // Delete all documents from index $manager->flush('posts'); // Delete the index entirely $manager->deleteIndex('posts');
CLI Commands
Index Models
# Index all records for a model php glueful search:index --model=App\\Models\\Post # Index specific IDs php glueful search:index --model=App\\Models\\Post --id=uuid1,uuid2,uuid3 # Fresh index (clear before indexing) php glueful search:index --model=App\\Models\\Post --fresh
Check Index Status
# Show all indexes php glueful search:status # Show specific index stats php glueful search:status posts # Output as JSON php glueful search:status --json
Sync Index Settings
# Sync settings from model to Meilisearch php glueful search:sync --model=App\\Models\\Post # Dry run (show settings without applying) php glueful search:sync --model=App\\Models\\Post --dry-run
Flush Index
# Flush specific index php glueful search:flush posts # Flush all indexes php glueful search:flush --all # Skip confirmation php glueful search:flush posts --force
Debug Search
# Search an index php glueful search:search posts "search query" # With filters php glueful search:search posts "query" --filter="status = published" # Limit results php glueful search:search posts "query" --limit=5 # Raw JSON output php glueful search:search posts "query" --raw
API Endpoints
All endpoints are prefixed with /api/search and require authentication.
Search
GET /api/search?index={index}&q={query}- Universal searchGET /api/search/{index}?q={query}- Search specific index
Query parameters:
q- Search query (optional, empty returns all)filter- Filter expressionfacets- Attributes for facet distributionsort- Sort criterialimit- Maximum results (default: 20)offset- Pagination offset
Admin
GET /api/search/admin/status- Get all index status (requires admin middleware)
Transaction-Safe Indexing
The extension automatically defers indexing operations until after database transactions commit:
// Using db() helper with transaction() db($context)->transaction(function () use ($context) { $post = Post::create($context, [ 'title' => 'New Post', 'body' => 'Content here...', ]); // Indexing is deferred, not executed yet }); // After commit, the post is indexed // If transaction rolls back, nothing is indexed try { db($context)->transaction(function () use ($context) { $post = Post::create($context, ['title' => 'Will be rolled back']); throw new \Exception('Rollback!'); }); } catch (\Exception $e) { // Transaction rolled back - post is NOT indexed }
Queue Support
Enable queue-based indexing for better performance in production:
MEILISEARCH_QUEUE=true MEILISEARCH_QUEUE_CONNECTION=redis MEILISEARCH_QUEUE_NAME=search
When enabled, indexing operations are dispatched to the queue after transaction commit, ensuring both data consistency and non-blocking request handling.
Run the queue worker:
php glueful queue:work --queue=search
Primary Key Strategy
The extension uses id as the Meilisearch primary key field name for all indexes. The model's actual key (uuid or id) is mapped to this field:
- Models with
uuidproperty:uuidvalue stored asid - Models without
uuid:idvalue stored asid
This ensures consistent behavior across all searchable models and proper document hydration.
Performance Considerations
- Batch size: Configure
MEILISEARCH_BATCH_SIZEfor bulk operations (default: 500) - Queue indexing: Enable for production to avoid blocking requests
- Selective indexing: Use
shouldBeSearchable()to skip irrelevant records - Attribute selection: Define
getSearchableFilterableAttributes()andgetSearchableSortableAttributes()for optimal index settings
Troubleshooting
Common Issues
-
Models not appearing in search: Ensure
shouldBeSearchable()returns true and the model was saved after adding the trait. -
Filters not working: Verify the attribute is listed in
getSearchableFilterableAttributes()and runphp glueful search:sync. -
Sort not working: Verify the attribute is listed in
getSearchableSortableAttributes()and runphp glueful search:sync. -
Connection errors: Check
MEILISEARCH_HOSTandMEILISEARCH_KEYare correct. Verify Meilisearch is running. -
Index not found: The extension auto-creates indexes on first use. If issues persist, manually create with
search:index --fresh.
Debugging
# Check Meilisearch connection and indexes php glueful search:status # Test search directly php glueful search:search posts "test query" --raw # Verify index settings match model php glueful search:sync --model=App\\Models\\Post --dry-run
License
This extension is licensed under the same license as the Glueful Framework.
Support
For issues, feature requests, or questions:
- Create an issue in the repository
- See Meilisearch Documentation for search engine specifics