A fluent, intuitive Laravel package for building Elasticsearch queries with comprehensive support for all major query types, aggregations, and advanced features.
Install the package via Composer:
composer require seclock/stretchPublish the configuration file:
php artisan vendor:publish --tag="stretch-config"Configure your Elasticsearch connection in your .env file:
ELASTICSEARCH_HOST=localhost:9200
ELASTICSEARCH_USERNAME=your_username
ELASTICSEARCH_PASSWORD=your_passworduse JayI\Stretch\Facades\Stretch;
// Simple match query
$results = Stretch::index(['index_1', 'index_2'])
->match('title', 'Laravel')
->execute();
// Term query
$results = Stretch::index(['index_1', 'index_2'])
->term('status', 'published')
->size(10)
->execute();
// Range query
$results = Stretch::index(['index_1', 'index_2'])
->range('created_at')
->gte('2024-01-01')
->lte('2024-12-31')
->execute();// Complex bool query
$results = Stretch::index(['index_1', 'index_2'])
->bool(function ($bool) {
$bool->must([
fn($q) => $q->match('title', 'Laravel'),
fn($q) => $q->term('category', 'tutorial')
]);
$bool->filter(fn($q) => $q->range('published_at')->gte('2024-01-01'));
$bool->should(fn($q) => $q->term('featured', true));
$bool->minimumShouldMatch(1);
})
->execute();// Terms aggregation
$results = Stretch::index(['index_1', 'index_2'])
->match('content', 'elasticsearch')
->aggregation('categories', fn($agg) =>
$agg->terms('category.keyword')->size(10)
)
->execute();
// Date histogram with sub-aggregations
$results = Stretch::index(['index_1', 'index_2'])
->aggregation('monthly_stats', fn($agg) =>
$agg->dateHistogram('created_at', 'month')
->subAggregation('avg_score', fn($sub) => $sub->avg('score'))
->subAggregation('doc_count', fn($sub) => $sub->count())
)
->execute();$results = Stretch::index(['index_1', 'index_2'])
->match('title', 'Laravel')
->sort('created_at', 'desc')
->sort('_score', 'desc')
->size(20)
->from(0)
->execute();$results = Stretch::index(['index_1', 'index_2'])
->match('title', 'Laravel')
->source(['title', 'content', 'created_at'])
->execute();$results = Stretch::index(['index_1', 'index_2'])
->match('content', 'elasticsearch')
->highlight([
'content' => new \stdClass()
], [
'pre_tags' => ['<strong>'],
'post_tags' => ['</strong>']
])
->execute(); // Execute multiple searches in a single request
// add method accepts a name and Closure/ElasticQueryBuilder instance
$results = Stretch::multi()
->add('postQuery', fn ($q) => $q->index('posts')->match('title', 'Laravel')->size(10))
->add('userQuery', fn ($q) => $q->index('users')->term('status', 'active'))
->add('eventLogQuery', fn ($q) => $q->index(['logs', 'events'])->range('timestamp')->gte('2024-01-01'))
->execute();
// Access individual responses
$postsResults = $results['responses']['postQuery'];
$usersResults = $results['responses']['userQuery'];
$eventLogResults = $results['responses']['eventLogQuery'];
// Or use a query builder instance directly
$postQuery = Stretch::index('posts')->match('title', 'Laravel');
Stretch::multi()
->add('posts', $postQuery)
->execute();Stretch supports query result caching using Laravel's cache system with flexible TTL (stale-while-revalidate pattern):
// Enable caching for a query
$results = Stretch::index('posts')
->match('title', 'Laravel')
->cache()
->execute();
// Configure cache TTL (uses Laravel's flexible caching)
// First value: fresh period, Second value: stale period
$results = Stretch::index('posts')
->match('title', 'Laravel')
->cache()
->setCacheTtl([300, 600]) // Fresh for 5 min, stale for 10 min
->execute();
// Use a custom cache prefix
$results = Stretch::index('posts')
->match('title', 'Laravel')
->cache()
->setCachePrefix('search:')
->execute();
// Use a specific cache store
$results = Stretch::index('posts')
->match('title', 'Laravel')
->cache()
->setCacheStore('redis')
->execute();
// Clear cache before executing (force fresh results)
$results = Stretch::index('posts')
->match('title', 'Laravel')
->cache()
->clearCache()
->execute();
// Full example with all options
$results = Stretch::index('posts')
->match('title', 'Laravel')
->term('status', 'published')
->cache()
->setCacheTtl([600, 1200])
->setCachePrefix('es:posts:')
->setCacheStore('redis')
->execute();Cache keys are automatically generated based on the query structure and index name, ensuring different queries produce different cache entries.
Caching also works with multi-queries:
$results = Stretch::multi()
->add('posts', fn ($q) => $q->match('title', 'Laravel'))
->add('users', fn ($q) => $q->term('status', 'active'))
->cache()
->setCacheTtl([300, 600])
->execute();Configure default cache settings in config/stretch.php:
'cache' => [
'driver' => 'default', // Cache driver to use
'prefix' => '', // Prefix for all cache keys
'ttl' => [300, 600], // Default TTL [fresh, stale]
],$results = Stretch::index(['index_1', 'index_2'])
->nested('comments', function ($nested) {
$nested->bool(function ($bool) {
$bool->must(fn($q) => $q->match('comments.message', 'great'));
$bool->filter(fn($q) => $q->range('comments.created_at')->gte('2024-01-01'));
});
})
->execute();// Wildcard query
$results = Stretch::index(['index_1', 'index_2'])
->wildcard('title', 'Larave*')
->execute();
// Fuzzy query
$results = Stretch::index(['index_1', 'index_2'])
->fuzzy('title', 'Laravl', ['fuzziness' => 'AUTO'])
->execute();$results = Stretch::index(['index_1', 'index_2'])
->bool(function ($bool) {
$bool->must(fn($q) => $q->match('title', 'Laravel'));
$bool->should([
fn($q) => $q->term('featured', true),
fn($q) => $q->range('views')->gte(1000),
fn($q) => $q->exists('premium_content')
]);
$bool->filter(fn($q) => $q->terms('tags', ['php', 'web-development']));
$bool->mustNot(fn($q) => $q->term('status', 'draft'));
})
->execute();// Check if index exists
$exists = Stretch::indexExists('posts');
// Create an index
Stretch::createIndex('posts', [
'settings' => [
'number_of_shards' => 1,
'number_of_replicas' => 0
],
'mappings' => [
'properties' => [
'title' => ['type' => 'text'],
'content' => ['type' => 'text'],
'created_at' => ['type' => 'date']
]
]
]);
// Delete an index
Stretch::deleteIndex('posts');
// Get cluster health
$health = Stretch::health();// Index a document
$result = Stretch::indexDocument('posts', [
'title' => 'My Laravel Post',
'content' => 'This is a great post about Laravel',
'created_at' => now()->toISOString()
]);
// Update a document
$result = Stretch::updateDocument('posts', '123', [
'title' => 'Updated Laravel Post'
]);
// Delete a document
$result = Stretch::deleteDocument('posts', '123');
// Bulk operations
$operations = [
['index' => ['_index' => 'posts', '_id' => '1']],
['title' => 'First Post', 'content' => 'Content 1'],
['index' => ['_index' => 'posts', '_id' => '2']],
['title' => 'Second Post', 'content' => 'Content 2']
];
$result = Stretch::bulk($operations);// Get the raw query array
$query = Stretch::index(['index_1', 'index_2'])
->match('title', 'Laravel')
->bool(function ($bool) {
$bool->filter(fn($q) => $q->term('status', 'published'));
})
->toArray();
dd($query); // Inspect the generated Elasticsearch query- Match Queries:
match(),matchPhrase() - Term Queries:
term(),terms(),exists(),wildcard(),fuzzy() - Range Queries:
range()withgt(),gte(),lt(),lte() - Bool Queries:
bool()withmust(),should(),filter(),mustNot() - Nested Queries:
nested()
- Bucket Aggregations:
terms(),dateHistogram(),range(),histogram() - Metric Aggregations:
avg(),sum(),min(),max(),count(),cardinality() - Sub-Aggregations:
subAggregation()
The package supports extensive configuration options:
- Connection settings (hosts, authentication, SSL)
- Query defaults (size, timeout)
- Aggregation settings
- Logging and debugging
- Caching configuration
Check the published config file for all available options.
composer testPlease see CONTRIBUTING for details.
If you discover any security-related issues, please message instead of using the issue tracker.
The MIT License (MIT). Please see License File for more information.