bonu / php-elasticsearch-builder
Elasticsearch query builder for PHP
Package info
github.com/bonu-dev/php-elasticsearch-builder
pkg:composer/bonu/php-elasticsearch-builder
Requires
- php: ^8.4
Requires (Dev)
- elasticsearch/elasticsearch: ^9.2
- friendsofphp/php-cs-fixer: ^3.88
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^12.0
- rector/rector: ^2.2
This package is auto-updated.
Last update: 2026-04-20 06:58:37 UTC
README
A clean, fluent, immutable, and type-safe query builder for Elasticsearch - built from the ground up to work seamlessly with the official Elasticsearch PHP client.
No extra dependencies. No magic. Just expressive, readable, and maintainable Elasticsearch queries in PHP.
use Elastic\Elasticsearch\ClientBuilder; use Bonu\ElasticsearchBuilder\QueryBuilder; use Bonu\ElasticsearchBuilder\Query\TermQuery; use Bonu\ElasticsearchBuilder\Query\BoolQuery; use Bonu\ElasticsearchBuilder\Query\MatchQuery; $builder = new QueryBuilder('products') ->query(new TermQuery('ean', 'foo_bar_123')->boost(12)) ->query(new BoolQuery() ->should(new MatchQuery('name', 'foo')) ->should(new MatchQuery('description', 'bar')) ->boost(5) ) ->size(20); $client = ClientBuilder::create()->build(); $products = $client->search($builder->build());
Features
- Fully fluent & chainable API
- Zero dependencies beyond the official Elasticsearch PHP SDK
- Easy creation of reusable composite queries
- 100% type-hinted and IDE-friendly
Requirements
- PHP ≥ 8.4
Installation
composer require bonu/php-elasticsearch-builder
Using query builder
The Bonu\ElasticsearchBuilder\QueryBuilder class provides a fluent interface for building Elasticsearch queries.
Constructor of this class accepts a single argument - name of the index to query, which may be used for searching in specific index.
Out of box it supports:
- Chaining queries using
query()method - Chaining aggregations using
aggregation()method - Configuring pagination of results using
from()andsize()methods
Queries
Caution
Queries are immutable.
This package comes with a set of ready-to-use queries which is documented below.
It is also possible to create reusable composite queries using abstract Bonu\ElasticsearchBuilder\Query\CompositeQuery class.
Example of composite query:
use Bonu\ElasticsearchBuilder\Query\BoolQuery; use Bonu\ElasticsearchBuilder\Query\TermQuery; use Bonu\ElasticsearchBuilder\Query\CompositeQuery; class PubliclyVisibleProductsQuery extends CompositeQuery { /** * @inheritDoc */ public function query(): QueryInterface { return new BoolQuery() ->must(new TermQuery('is_active', true)) ->mustNot(new TermQuery('is_out_of_stock', false)); } } $builder = new QueryBuilder('products') ->query(new PubliclyVisibleProductsQuery());
TermQuery
https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-term-query
use Bonu\ElasticsearchBuilder\Query\TermQuery; new TermQuery('field', 'value')->boost(10)
TermsQuery
https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-terms-query
use Bonu\ElasticsearchBuilder\Query\TermsQuery; new TermsQuery('status', ['active', 'pending'])->boost(5)
ExistsQuery
https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-exists-query
use Bonu\ElasticsearchBuilder\Query\ExistsQuery; new ExistsQuery('field')
MatchQuery
https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-match-query
use Bonu\ElasticsearchBuilder\Query\MatchQuery; // Default operator is OR new MatchQuery('field', 'some text') ->boost(2) ->analyzer('standard') // With AND operator new MatchQuery('field', 'some text', MatchQuery::OPERATOR_AND)
MatchPhraseQuery
https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-match-query-phrase
use Bonu\ElasticsearchBuilder\Query\MatchPhraseQuery; // Optional third argument is slop new MatchPhraseQuery('field', 'exact phrase', 2) ->boost(1.5) ->analyzer('standard')
BoolQuery
https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-bool-query
use Bonu\ElasticsearchBuilder\Query\BoolQuery; use Bonu\ElasticsearchBuilder\Query\TermQuery; use Bonu\ElasticsearchBuilder\Query\MatchQuery; new BoolQuery() ->must(new TermQuery('status', 'active')) ->filter(new TermQuery('stock', 1)) ->should(new MatchQuery('title', 'awesome product')) ->mustNot(new TermQuery('blocked', true)) ->boost(3)
NestedQuery
https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-nested-query
use Bonu\ElasticsearchBuilder\Query\NestedQuery; use Bonu\ElasticsearchBuilder\Query\MatchQuery; // Query nested field path, inner query must be provided via ->query() new NestedQuery('variants') ->query(new MatchQuery('variants.name', 'red'))
RangeQuery
https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-range-query
Range queries can be used for filtering by multiple data types. For this reason, each data type has its own query class to fully support type-hinting.
use Bonu\ElasticsearchBuilder\Query\NumericRangeQuery; use Bonu\ElasticsearchBuilder\Query\DatetimeRangeQuery; new NumericRangeQuery('price', gte: 100) ->boost(10); new DatetimeRangeQuery('created_at', lt: date('Y-m-d'), format: 'yyyy-MM-dd', timeZone: 'Europe/Prague') ->boost(20);
Aggregations
Caution
Aggregations are immutable.
Similar to queries, it is also possible to create reusable composite aggregations using abstract Bonu\ElasticsearchBuilder\Aggregation\CompositeAggregation class.
Example of composite aggregation:
use Bonu\ElasticsearchBuilder\Aggregation\TermsAggregation; use Bonu\ElasticsearchBuilder\Aggregation\NestedAggregation; use Bonu\ElasticsearchBuilder\Aggregation\CompositeAggregation; class CategoryBrandAggregation extends CompositeAggregation { /** * @param string|\Stringable $name */ public function __construct( private readonly string | Stringable $name, ) {} /** * @inheritDoc */ public function aggregation(): AggregationInterface { return new NestedAggregation($this->name, 'products') ->aggregation(new TermsAggregation('by_brand', 'products.brand_id')); } }
ContainerAggregation
Container aggregation is used to group sub-aggregations. It requires either global or at least one filter (query) to be set, but not both.
use Bonu\ElasticsearchBuilder\Query\TermQuery; use Bonu\ElasticsearchBuilder\Aggregation\TermsAggregation; use Bonu\ElasticsearchBuilder\Aggregation\ContainerAggregation; // Container with a filter new ContainerAggregation('my_container') ->query(new TermQuery('status', 'active')) ->aggregation(new TermsAggregation('by_brand', 'brand.keyword')); // Global container new ContainerAggregation('global_container') ->asGlobal() ->aggregation(new TermsAggregation('all_brands', 'brand.keyword'));
TermsAggregation
https://www.elastic.co/docs/reference/aggregations/search-aggregations-bucket-terms-aggregation
use Bonu\ElasticsearchBuilder\Aggregation\TermsAggregation; use Bonu\ElasticsearchBuilder\Query\TermQuery; // Top 10 brands, filtered to active products new TermsAggregation('by_brand', 'brand.keyword') ->size(10) ->query(new TermQuery('status', 'active')); // Make the aggregation global (ignores the top-level query) new TermsAggregation('all_categories', 'category.keyword') ->asGlobal();
StatsAggregation
https://www.elastic.co/docs/reference/aggregations/search-aggregations-metrics-stats-aggregation
use Bonu\ElasticsearchBuilder\Aggregation\StatsAggregation; use Bonu\ElasticsearchBuilder\Query\TermQuery; // Basic stats for the price field, filtered by currency new StatsAggregation('price_stats', 'price') ->query(new TermQuery('currency', 'USD')); // Make the aggregation global (ignores the top-level query) new StatsAggregation('global_price_stats', 'price') ->asGlobal();
NestedAggregation
https://www.elastic.co/docs/reference/aggregations/search-aggregations-bucket-nested-aggregation
use Bonu\ElasticsearchBuilder\Aggregation\NestedAggregation; new NestedAggregation('categories', 'products') ->aggregation(new StatsAggregation('product_price', 'products.price'))
MultiTermsAggregation
use Bonu\ElasticsearchBuilder\Aggregation\MultiTermsAggregation; new MultiTermsAggregation('foo', ['product', 'category']) ->size(10) ->query(new TermQuery('status', 'active'));
HistogramAggregation
https://www.elastic.co/docs/reference/aggregations/search-aggregations-bucket-histogram-aggregation
use Bonu\ElasticsearchBuilder\Aggregation\HistogramAggregation; // Prices in $10 intervals new HistogramAggregation('price_histogram', 'price', 10); // With custom interval and min_doc_count new HistogramAggregation('price_histogram', 'price', 50, 1);
<<<<<<< HEAD
SumAggregation
https://www.elastic.co/docs/reference/aggregations/search-aggregations-metrics-sum-aggregation
use Bonu\ElasticsearchBuilder\Aggregation\SumAggregation; use Bonu\ElasticsearchBuilder\Query\TermQuery; // Sum of all prices new SumAggregation('total_price', 'price'); // Filtered sum new SumAggregation('active_total', 'price') ->query(new TermQuery('status', 'active')); // Global sum (ignores the top-level query) new SumAggregation('global_total', 'price') ->asGlobal();
CardinalityAggregation
use Bonu\ElasticsearchBuilder\Aggregation\CardinalityAggregation; // Count unique values new CardinalityAggregation('unique_brands', 'brand.keyword'); // With precision threshold for better accuracy on high-cardinality fields new CardinalityAggregation('unique_brands', 'brand.keyword', 1000); // Filtered cardinality new CardinalityAggregation('active_unique_brands', 'brand.keyword') ->query(new TermQuery('status', 'active'));
DateHistogramAggregation
use Bonu\ElasticsearchBuilder\Aggregation\DateHistogramAggregation; // Monthly buckets using calendar interval new DateHistogramAggregation('sales_over_time', 'date', calendarInterval: 'month'); // Fixed 30-day intervals with date format new DateHistogramAggregation('monthly_sales', 'date', fixedInterval: '30d', format: 'yyyy-MM-dd'); // With all options: calendar interval, min_doc_count, format, time zone, offset new DateHistogramAggregation( 'hourly_activity', 'timestamp', calendarInterval: 'hour', minDocCount: 1, format: 'yyyy-MM-dd HH:mm', timeZone: 'Europe/Prague', offset: '+6h', );
Sorts
FieldSort
use Bonu\ElasticsearchBuilder\Sort\FieldSort; use Bonu\ElasticsearchBuilder\Sort\SortDirectionEnum; new FieldSort('my_field', SortDirectionEnum::ASC)
ScoreSort
use Bonu\ElasticsearchBuilder\Sort\ScoreSort; use Bonu\ElasticsearchBuilder\Sort\SortDirectionEnum; new ScoreSort(SortDirectionEnum::DESC)
License
This package is licensed under the MIT License.