spatie/elasticsearch-query-builder
Lightweight fluent PHP query builder for Elasticsearch. Build searches, filters, and aggregations with a clean API, then execute via the official client. Designed to pair with Spatie’s search-string parser; covers common use cases and is easy to extend.
composer require spatie/elasticsearch-query-builder
use Spatie\ElasticsearchQueryBuilder\Builder;
use Spatie\ElasticsearchQueryBuilder\Queries\MatchQuery;
$client = Elastic\Elasticsearch\ClientBuilder::create()->build();
$results = (new Builder($client))
->index('products')
->addQuery(MatchQuery::create('name', 'laptop'))
->search();
$builder = new Builder($client)
->index('articles')
->addQuery(MatchQuery::create('title', 'laravel'))
->addQuery(RangeQuery::create('published_at')->gte('2023-01-01'))
->search();
Builder: Core class for constructing queriesQueries: Predefined query types (e.g., MatchQuery, RangeQuery)Aggregations: For analytics (e.g., TermsAggregation, AvgAggregation)Sorts: For ordering results$builder = new Builder($client)
->index('users')
->addQuery(MatchQuery::create('name', 'john'), 'must') // Full-text search
->addQuery(TermQuery::create('status', 'active'), 'filter') // Non-scoring filter
->search();
$builder = new Builder($client)
->index('orders')
->addAggregation(TermsAggregation::create('by_status', 'status'))
->addAggregation(AvgAggregation::create('avg_value', 'total'))
->search();
$nestedQuery = NestedQuery::create('comments', MatchQuery::create('comments.text', 'great'))
->innerHits(InnerHits::create('top_comments')->size(2));
$builder = new Builder($client)
->index('posts')
->addQuery($nestedQuery)
->search();
$builder = new Builder($client)
->index('products')
->from(10) // Skip first 10 results
->size(5) // Limit to 5 results
->search();
use Spatie\ElasticsearchQueryBuilder\MultiBuilder;
$multiBuilder = new MultiBuilder($client);
$multiBuilder->addSearch(
(new Builder($client))->index('users')->addQuery(MatchQuery::create('name', 'john'))
);
$multiBuilder->addSearch(
(new Builder($client))->index('products')->addQuery(MatchQuery::create('name', 'laptop'))
);
$results = $multiBuilder->search();
$this->app->singleton(ElasticsearchClient::class, function () {
return Elastic\Elasticsearch\ClientBuilder::create()->build();
});
class ProductRepository {
public function __construct(private Builder $builder) {}
public function search(string $query): array {
return $this->builder
->index('products')
->addQuery(MatchQuery::create('name', $query))
->search();
}
}
fields() to control returned fields:
$builder->fields(['name', 'price', 'description']);
Boolean Query Logic:
must (AND), must_not (NOT), should (OR), filter (non-scoring).must, which may not be intended.// ❌ Silent failure: defaults to 'must'
$builder->addQuery(MatchQuery::create('name', 'john'));
// ✅ Explicit
$builder->addQuery(MatchQuery::create('name', 'john'), 'must');
Aggregation Naming Conflicts:
// ❌ Overwrites previous 'genres' aggregation
$builder->addAggregation(TermsAggregation::create('genres', 'genre'));
$builder->addAggregation(TermsAggregation::create('genres', 'category'));
Nested Query Requirements:
nested. Elasticsearch will throw an error otherwise.// ❌ Fails if 'comments' is not a nested field
$builder->addQuery(NestedQuery::create('comments', MatchQuery::create('text', 'great')));
Pagination Limits:
from/size limit (typically 10,000). Exceeding this requires search_after for deep pagination.// ❌ May fail with "Search Too Large" error
$builder->from(10000)->size(10000)->search();
Fuzzy Matching Performance:
fuzziness values (e.g., fuzziness: 3) can significantly slow down queries.// ⚠️ Expensive for large datasets
$builder->addQuery(MatchQuery::create('name', 'john', fuzziness: 3));
$payload = $builder->getPayload();
// Log or dump $payload to verify the query structure
$client = Elastic\Elasticsearch\ClientBuilder::create()
->setLogger(new \Monolog\Logger('elasticsearch'))
->build();
explain for Debugging:
$builder->addQuery(MatchQuery::create('name', 'john'))->explain();
Query abstract class to create custom query types:
class CustomQuery extends Query {
public static function create(string $field, mixed $value): self {
return new self($field, $value);
}
public function build(): array {
return ['custom' => ['field' => $this->field, 'value' => $this->value]];
}
}
$builder->addAggregation(function (AggregationBuilder $aggregationBuilder) {
return $aggregationBuilder->terms('dynamic_agg', 'field_' . $this->dynamicField);
});
Builder class to modify default settings:
class CustomBuilder extends Builder {
public function __construct(Client $client) {
parent::__construct($client);
$this->defaultSize = 20; // Override default size
}
}
indices option if needed:
$builder->indices(['alias_name']);
$builder->routing('shard_key_value');
scroll method instead of search:
$scroll = $builder->scroll('1m')->search();
filter for Non-Scoring Queries:
// ✅ Faster for filtering (non-scoring)
$builder->addQuery(TermQuery::create('status', 'active'), 'filter');
$builder->fields(['id', 'name', 'price']);
wildcard Queries:
Wildcard queries (*) are resource-intensive. Use keyword subfields or ngram analyzers instead.How can I help you explore Laravel packages today?