Skip to content

jayjfletcher/stretch

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

15 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Stretch - Laravel Elasticsearch Query Builder

A fluent, intuitive Laravel package for building Elasticsearch queries with comprehensive support for all major query types, aggregations, and advanced features.

Installation

Install the package via Composer:

composer require seclock/stretch

Publish the configuration file:

php artisan vendor:publish --tag="stretch-config"

Configuration

Configure your Elasticsearch connection in your .env file:

ELASTICSEARCH_HOST=localhost:9200
ELASTICSEARCH_USERNAME=your_username
ELASTICSEARCH_PASSWORD=your_password

Basic Usage

Simple Queries

use 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();

Bool Queries

// 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();

Aggregations

// 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();

Sorting and Pagination

$results = Stretch::index(['index_1', 'index_2'])
    ->match('title', 'Laravel')
    ->sort('created_at', 'desc')
    ->sort('_score', 'desc')
    ->size(20)
    ->from(0)
    ->execute();

Source Filtering

$results = Stretch::index(['index_1', 'index_2'])
    ->match('title', 'Laravel')
    ->source(['title', 'content', 'created_at'])
    ->execute();

Highlighting

$results = Stretch::index(['index_1', 'index_2'])
    ->match('content', 'elasticsearch')
    ->highlight([
        'content' => new \stdClass()
    ], [
        'pre_tags' => ['<strong>'],
        'post_tags' => ['</strong>']
    ])
    ->execute();

Advanced Features

Multi-Query Support

 // 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();

Caching

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]
],

Nested Queries

$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 and Fuzzy Queries

// 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();

Multiple Query Types

$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();

Index Management

// 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();

Document Operations

// 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);

Query Debugging

// 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

Available Query Types

  • Match Queries: match(), matchPhrase()
  • Term Queries: term(), terms(), exists(), wildcard(), fuzzy()
  • Range Queries: range() with gt(), gte(), lt(), lte()
  • Bool Queries: bool() with must(), should(), filter(), mustNot()
  • Nested Queries: nested()

Available Aggregations

  • Bucket Aggregations: terms(), dateHistogram(), range(), histogram()
  • Metric Aggregations: avg(), sum(), min(), max(), count(), cardinality()
  • Sub-Aggregations: subAggregation()

Configuration Options

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.

Testing

composer test

Contributing

Please see CONTRIBUTING for details.

Security

If you discover any security-related issues, please message instead of using the issue tracker.

License

The MIT License (MIT). Please see License File for more information.

About

Eloquent style wrapper for Elasticsearch

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages