A PHP library for building Elasticsearch DSL queries and managing indices. Targets ES 7.x.
Covers all query types (compound, full-text, term-level, geo, joining, span, shape, specialized), all 47 aggregation types, and 26 search request parameters.
- PHP 7.2+
- Elasticsearch 7.x
composer require ykan/elastx:^7use ykan\DSL\Query;
use ykan\DSL\Type\FullText\Match_;
$query = new Query();
$query->bool([
'must' => function (Query $q) {
$q->match('title', function (Match_ $m) {
$m->query('elasticsearch')->fuzziness('AUTO');
});
},
'filter' => function (Query $q) {
$q->range('price', [10, 100]);
$q->term('status', 'published');
},
'should' => function (Query $q) {
$q->match('category', 'database');
},
]);
$query->aggs('price_stats')->stats(['field' => 'price']);
$query->aggs('by_category')->terms(['field' => 'category', 'size' => 10]);
echo $query->toJson();Output:
{
"query": {
"bool": {
"must": [{ "match": { "title": { "query": "elasticsearch", "fuzziness": "AUTO" } } }],
"filter": [{ "range": { "price": { "gte": 10, "lte": 100 } } }, { "term": { "status": "published" } }],
"should": [{ "match": { "category": "database" } }]
}
},
"aggs": {
"price_stats": { "stats": { "field": "price" } },
"by_category": { "terms": { "field": "category", "size": 10 } }
}
}Retrieve aggregation results after search:
$results->aggregations(); // ['price_stats' => [...], 'by_category' => [...]]use ykan\Index\Index;
class ProductIndex extends Index
{
protected $name = 'products';
protected $mappings = [
'properties' => [
'title' => ['type' => 'text'],
'price' => ['type' => 'float'],
'status' => ['type' => 'keyword'],
],
];
}Register the ES client during bootstrap:
Index::setClient($client);use ykan\DSL\Query;
$results = ProductIndex::newQuery()
->bool('must', function (Query $q) {
$q->match('title', 'elasticsearch');
})
->sort('price', 'asc')
->size(20)
->get();
$results->total(); // int
$results->docs(); // array of _source
$results->ids(); // array of _id
$results->aggregations(); // aggregation resultsFor large result sets, use cursor() to iterate all matching documents via scroll:
$search = ProductIndex::newQuery()->match('title', 'test');
foreach ($search->cursor() as $doc) {
// process $doc
}$doc = ProductIndex::newDoc(1);
$doc->create(['title' => 'New Product', 'price' => 29.99]);
$doc->index(['title' => 'Updated', 'price' => 39.99]); // create or overwrite
$doc->source(); // _source array
$doc->get(); // full document with _id, _version, etc.
$doc->exists(); // bool
$doc->update(['price' => 39.99]);
$doc->delete();
$doc->retryOnConflict(3)->update(['price' => 39.99]);use ykan\Index\Bulk;
$bulk = new Bulk(new ProductIndex());
$bulk->index(1, ['title' => 'Product A']);
$bulk->index(2, ['title' => 'Product B']);
$bulk->delete(3);
$bulk->execute();use ykan\Index\Manager;
$manager = new Manager(new ProductIndex());
$manager->create();
$manager->exists(); // true
$manager->delete();
$manager->putMapping($mapping);
$manager->addAlias('products_alias');
$manager->removeAlias('products_alias');
$manager->swapAlias('products_alias', 'old_index');Zero-downtime rebuild: create new index, import data, swap alias.
use ykan\Index\Rebuild;
$rebuild = new Rebuild(new ProductIndex());
// Run with Index::source()
$rebuild->batchSize(500)->run();
// Rollback to previous index
$rebuild->rollback();
$rebuild->rollback('products_20260525_120000'); // or specify target
// Clean orphan indexes
$rebuild->orphans(); // ['products_20260527_100000', ...]
$rebuild->cleanOrphans();Provide data by overriding source(), or pass a custom source to run():
// Override in Index subclass
class ProductIndex extends Index
{
public function source(array $options = []): iterable
{
foreach (Product::all() as $product) {
yield $product->id => $product->toArray();
}
}
}
// Or pass at call time
$rebuild->source($rows)->run();MIT