From b3d86ea714113c0d6a2ebc61b2ad73a209a26994 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Sun, 5 Jul 2026 01:44:55 +0800 Subject: [PATCH 1/2] docs(readme): reorder DSL examples, add overviews, sharpen OOP - reorder DSL Examples: scenarios first, mechanisms last - rename Polymorphic parameters -> Flexible arguments - rewrite OOP section: extract clauses, add execution closure, drop self-proving toArray/toJson - replace Nested example with kNN; simplify Clause appending and Raw array - add design overview outside
for both DSL and Index sections - keep EN and ZH in sync Co-Authored-By: Claude --- README.md | 116 ++++++++++++++++++++++++++------------------------- README.zh.md | 115 +++++++++++++++++++++++++------------------------- 2 files changed, 117 insertions(+), 114 deletions(-) diff --git a/README.md b/README.md index ece8a86..1b8ddef 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,17 @@ [![Latest Version](https://img.shields.io/packagist/v/ykan/elastickit)](https://packagist.org/packages/ykan/elastickit) [![Total Downloads](https://img.shields.io/packagist/dt/ykan/elastickit)](https://packagist.org/packages/ykan/elastickit) +[![Tests](https://github.com/ykan821/ElasticKit/actions/workflows/ci.yml/badge.svg)](https://github.com/ykan821/ElasticKit/actions/workflows/ci.yml) +[![PHP](https://img.shields.io/packagist/php-v/ykan/elastickit)](https://packagist.org/packages/ykan/elastickit) [![License](https://img.shields.io/packagist/l/ykan/elastickit)](https://packagist.org/packages/ykan/elastickit) A PHP Elasticsearch DSL query builder covering queries, aggregations, CRUD, bulk writes, and zero-downtime rebuilds. +## Integrations + +- **[ElasticKit Laravel](https://github.com/ykan821/ElasticKitLaravel)** — Laravel integration + (native pagination, artisan rebuild). `composer require ykan/elastickit-laravel`. + ## Installation ``` @@ -50,46 +57,11 @@ $total = $results->total(); // null unless $trackTotalHits = true (see Paginatio ## DSL Examples +ElasticKit's DSL stays close to the native ES API to minimize cognitive load — if you know ES DSL, the transfer is smooth. Every query type is a dedicated `Node` class whose method names mirror ES parameters. +
Expand -### Polymorphic parameters - -The same method accepts four forms — string, array, closure, object: - -```php -$q->term('status', 'published'); // string -$q->term(['status' => 'published']); // array -$q->term(fn ($t) => $t->field('status')->value('published')); // closure -$q->term(Term::create('status', 'published')); // object -``` - -### OOP style - -Each query type is a dedicated Node class supporting chaining: - -```php -use ElasticKit\DSL\Query; -use ElasticKit\DSL\Queries\TermLevel\Term; -use ElasticKit\DSL\Queries\TermLevel\Range; -use ElasticKit\DSL\Queries\FullText\Match_; -use ElasticKit\DSL\Queries\Compound\Boolean; - -$bool = Boolean::create() - ->must(Match_::create('title', 'elasticsearch')) - ->filter(Term::create('status', 'published')->boost(1.5)); - -// incremental build -if ($filterByPrice) { - $bool->filter(Range::create('price', [10, 100])); -} - -$query = Query::create($bool); - -$query->toArray(); // ['query' => ['bool' => [...]]] -$query->toJson(); // '{"query":{"bool":{...}}}' -``` - ### Compound query ```php @@ -98,8 +70,7 @@ $results = ProductIndex::query() 'must' => fn ($q) => $q->match('title', 'elasticsearch'), 'filter' => fn ($q) => $q ->range('price', [10, 100]) - ->when($status, fn ($q) => $q->term('status', $status)) // conditional filter - ->term('status', 'published'), + ->when($status, fn ($q) => $q->term('status', $status)), // conditional filter ]) ->highlight('title') ->sort('price', 'asc') @@ -124,25 +95,29 @@ $results = ProductIndex::query() } ``` -### Clause appending (ClausesSupport) +### OOP style -The clauses of a `bool` query (must / should / filter / must_not) **append**, and accept the same four input forms as leaf queries: +Build each clause separately, then combine them into a query: ```php -// all four forms are equivalent, each produces one must clause -$q->bool(fn ($b) => $b->must(fn ($q) => $q->term('status', 'published'))); -$q->bool(['must' => fn ($q) => $q->term('status', 'published')]); -$q->bool('must', fn ($q) => $q->term('status', 'published')); +use ElasticKit\DSL\Queries\Compound\Boolean; +use ElasticKit\DSL\Queries\FullText\Match_; +use ElasticKit\DSL\Queries\TermLevel\Range; +use ElasticKit\DSL\Queries\TermLevel\Term; -// clauses accumulate (multiple calls and list form both append) -$q->bool(fn ($b) => $b->must(...)->must(...)); // must: [q1, q2] -$q->bool(['must' => [$q1, $q2]]); // same +// Build the clauses +$status = Term::create('status', 'published')->boost(1.5); +$title = Match_::create('title', 'elasticsearch'); -// contrast: minimum_should_match is a single-value property; later calls overwrite instead of append -$q->bool(fn ($b) => $b->minimumShouldMatch(1)->minimumShouldMatch(3)); // 3 -``` +// Combine into a bool query +$bool = Boolean::create()->must($title)->filter($status); +if ($filterByPrice) { + $bool->filter(Range::create('price', [10, 100])); +} -> `dis_max`, `span_or`, `span_near` and other array-clause containers behave the same way (queries / clauses append). +// Execute +$results = ProductIndex::query()->bool($bool)->size(20)->get(); +``` ### Aggregations @@ -157,22 +132,25 @@ $results = ProductIndex::query() $aggs = $results->aggregations(); ``` -### Nested query +### kNN search ```php $results = ProductIndex::query() - ->nested('comments', fn ($q) => $q->match('comments.body', 'great')) + ->knn(fn ($k) => $k + ->field('embedding') + ->queryVector([0.12, 0.45, 0.78, /* ... */]) + ->numCandidates(100)) + ->size(10) ->get(); ``` -### Raw DSL pass-through +### Raw array ```php -// supports raw arrays with nested closures; query/aggs/parameters can be passed all at once $query = Query::create([ 'query' => [ 'bool' => [ - 'must' => fn ($q) => $q->match('title', 'elasticsearch'), + 'must' => fn ($q) => $q->match('title', 'elasticsearch'), // array may nest closures 'filter' => fn ($q) => $q->term('status', 'published'), ], ], @@ -181,10 +159,34 @@ $query = Query::create([ ]); ``` +### Clause appending (ClausesSupport) + +The clauses of a `bool` query (must / should / filter / must_not) **append** — repeated calls accumulate, they don't overwrite: + +```php +$q->bool(fn ($b) => $b->must($q1)); // must: [q1] +$q->bool(fn ($b) => $b->must($q2)); // must: [q1, q2] +``` + +> `dis_max`, `span_or`, `span_near` and other array-clause containers behave the same way. + +### Flexible arguments + +The same method accepts multiple input forms — use whichever suits: + +```php +$q->term('status', 'published'); // string +$q->term(['status' => 'published']); // array +$q->term(fn ($t) => $t->field('status')->value('published')); // closure +$q->term(Term::create('status', 'published')); // object +``` +
## Index Examples +Around the `Index` base class, dedicated classes cover routine index operations — pagination, CRUD, bulk writes, index management, zero-downtime rebuilds, and event hooks. +
Expand diff --git a/README.zh.md b/README.zh.md index 8150afa..d8f367f 100644 --- a/README.zh.md +++ b/README.zh.md @@ -4,10 +4,16 @@ [![Latest Version](https://img.shields.io/packagist/v/ykan/elastickit)](https://packagist.org/packages/ykan/elastickit) [![Total Downloads](https://img.shields.io/packagist/dt/ykan/elastickit)](https://packagist.org/packages/ykan/elastickit) +[![Tests](https://github.com/ykan821/ElasticKit/actions/workflows/ci.yml/badge.svg)](https://github.com/ykan821/ElasticKit/actions/workflows/ci.yml) +[![PHP](https://img.shields.io/packagist/php-v/ykan/elastickit)](https://packagist.org/packages/ykan/elastickit) [![License](https://img.shields.io/packagist/l/ykan/elastickit)](https://packagist.org/packages/ykan/elastickit) PHP Elasticsearch DSL 查询构建库,覆盖查询、聚合、CRUD、批量写入、零停机重建。 +## 集成 + +- **[ElasticKit Laravel](https://github.com/ykan821/ElasticKitLaravel)** — Laravel 集成(原生分页、artisan 重建)。`composer require ykan/elastickit-laravel`。 + ## 安装 ``` @@ -50,46 +56,11 @@ $total = $results->total(); // 索引未设 $trackTotalHits = true 时为 null ## DSL 示例 +ElasticKit 的 DSL 尽量贴近 ES 原生 API,以减少认知负担——熟悉 ES DSL 的人可以平滑迁移。每个查询类型都是一个专门的 `Node` 类,方法名与 ES 参数对应。 +
展开查看 -### 多态参数 - -同一个方法支持字符串、数组、闭包、对象四种写法: - -```php -$q->term('status', 'published'); // string -$q->term(['status' => 'published']); // array -$q->term(fn ($t) => $t->field('status')->value('published')); // closure -$q->term(Term::create('status', 'published')); // object -``` - -### OOP 风格 - -每个查询类型都是独立的 Node 类,支持链式调用: - -```php -use ElasticKit\DSL\Query; -use ElasticKit\DSL\Queries\TermLevel\Term; -use ElasticKit\DSL\Queries\TermLevel\Range; -use ElasticKit\DSL\Queries\FullText\Match_; -use ElasticKit\DSL\Queries\Compound\Boolean; - -$bool = Boolean::create() - ->must(Match_::create('title', 'elasticsearch')) - ->filter(Term::create('status', 'published')->boost(1.5)); - -// 增量构建 -if ($filterByPrice) { - $bool->filter(Range::create('price', [10, 100])); -} - -$query = Query::create($bool); - -$query->toArray(); // ['query' => ['bool' => [...]]] -$query->toJson(); // '{"query":{"bool":{...}}}' -``` - ### 复合查询 ```php @@ -98,8 +69,7 @@ $results = ProductIndex::query() 'must' => fn ($q) => $q->match('title', 'elasticsearch'), 'filter' => fn ($q) => $q ->range('price', [10, 100]) - ->when($status, fn ($q) => $q->term('status', $status)) // 条件过滤 - ->term('status', 'published'), + ->when($status, fn ($q) => $q->term('status', $status)), // 条件过滤 ]) ->highlight('title') ->sort('price', 'asc') @@ -124,25 +94,29 @@ $results = ProductIndex::query() } ``` -### 子句追加(ClausesSupport) +### OOP 风格 -`bool` 查询的子句(must / should / filter / must_not)**累加追加**,并接受与叶子查询相同的 4 种输入形式: +先单独构造各个子句,再组合成查询: ```php -// 4 种输入形式等价,都产出一条 must -$q->bool(fn ($b) => $b->must(fn ($q) => $q->term('status', 'published'))); -$q->bool(['must' => fn ($q) => $q->term('status', 'published')]); -$q->bool('must', fn ($q) => $q->term('status', 'published')); +use ElasticKit\DSL\Queries\Compound\Boolean; +use ElasticKit\DSL\Queries\FullText\Match_; +use ElasticKit\DSL\Queries\TermLevel\Range; +use ElasticKit\DSL\Queries\TermLevel\Term; -// 子句累加(多次调用、列表形式都追加) -$q->bool(fn ($b) => $b->must(...)->must(...)); // must: [q1, q2] -$q->bool(['must' => [$q1, $q2]]); // 同上 +// 构造子句 +$status = Term::create('status', 'published')->boost(1.5); +$title = Match_::create('title', 'elasticsearch'); -// 对比:minimum_should_match 是单值属性,后调覆盖而非追加 -$q->bool(fn ($b) => $b->minimumShouldMatch(1)->minimumShouldMatch(3)); // 3 -``` +// 组合成 bool 查询 +$bool = Boolean::create()->must($title)->filter($status); +if ($filterByPrice) { + $bool->filter(Range::create('price', [10, 100])); +} -> `dis_max`、`span_or`、`span_near` 等其他数组子句容器同理(queries / clauses 累加)。 +// 执行 +$results = ProductIndex::query()->bool($bool)->size(20)->get(); +``` ### 聚合 @@ -157,22 +131,25 @@ $results = ProductIndex::query() $aggs = $results->aggregations(); ``` -### 嵌套查询 +### kNN 搜索 ```php $results = ProductIndex::query() - ->nested('comments', fn ($q) => $q->match('comments.body', 'great')) + ->knn(fn ($k) => $k + ->field('embedding') + ->queryVector([0.12, 0.45, 0.78, /* ... */]) + ->numCandidates(100)) + ->size(10) ->get(); ``` -### 原生 DSL 透传 +### 原生数组 ```php -// 支持原生数组嵌套闭包,query/aggs/参数可一次性传入 $query = Query::create([ 'query' => [ 'bool' => [ - 'must' => fn ($q) => $q->match('title', 'elasticsearch'), + 'must' => fn ($q) => $q->match('title', 'elasticsearch'), // 支持数组嵌套闭包 'filter' => fn ($q) => $q->term('status', 'published'), ], ], @@ -181,10 +158,34 @@ $query = Query::create([ ]); ``` +### 子句追加(ClausesSupport) + +`bool` 查询的子句(must / should / filter / must_not)**累加追加**——重复调用会累加,而非覆盖: + +```php +$q->bool(fn ($b) => $b->must($q1)); // must: [q1] +$q->bool(fn ($b) => $b->must($q2)); // must: [q1, q2] +``` + +> `dis_max`、`span_or`、`span_near` 等其他数组子句容器同理。 + +### 灵活入参 + +同一个方法接受多种入参形式——按场景选用: + +```php +$q->term('status', 'published'); // string +$q->term(['status' => 'published']); // array +$q->term(fn ($t) => $t->field('status')->value('published')); // closure +$q->term(Term::create('status', 'published')); // object +``` +
## Index 示例 +围绕 `Index` 基类,ElasticKit 为索引日常操作封装了专用类——分页、CRUD、批量写入、索引管理、零停机重建、事件监听。 +
展开查看 From f820e42a22150049287c0f32a38ca017ee0ec120 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Thu, 2 Jul 2026 18:25:46 +0800 Subject: [PATCH 2/2] refactor(dsl): move DeepClone to Support/, mark @internal Non-BC: the trait is internal clone plumbing (not a public extension point), now marked @internal. Co-Authored-By: Claude --- src/DSL/Agg.php | 1 + src/DSL/Node.php | 1 + src/DSL/{ => Support}/DeepClone.php | 6 +++--- 3 files changed, 5 insertions(+), 3 deletions(-) rename src/DSL/{ => Support}/DeepClone.php (90%) diff --git a/src/DSL/Agg.php b/src/DSL/Agg.php index 6696c4e..39cf7dc 100644 --- a/src/DSL/Agg.php +++ b/src/DSL/Agg.php @@ -7,6 +7,7 @@ use ElasticKit\DSL\Aggs\Bucket; use ElasticKit\DSL\Aggs\Metric; use ElasticKit\DSL\Aggs\Pipeline; +use ElasticKit\DSL\Support\DeepClone; use ElasticKit\DSL\Support\RegistersAgg; use stdClass; diff --git a/src/DSL/Node.php b/src/DSL/Node.php index 5e1b059..b795ecd 100644 --- a/src/DSL/Node.php +++ b/src/DSL/Node.php @@ -7,6 +7,7 @@ use ArgumentCountError; use BadMethodCallException; use Closure; +use ElasticKit\DSL\Support\DeepClone; use InvalidArgumentException; use LogicException; use stdClass; diff --git a/src/DSL/DeepClone.php b/src/DSL/Support/DeepClone.php similarity index 90% rename from src/DSL/DeepClone.php rename to src/DSL/Support/DeepClone.php index 7500d52..28cc673 100644 --- a/src/DSL/DeepClone.php +++ b/src/DSL/Support/DeepClone.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ElasticKit\DSL; +namespace ElasticKit\DSL\Support; use Closure; use ReflectionClass; @@ -12,8 +12,8 @@ * (own and inherited) is duplicated so the clone shares no references with its * original. New properties are covered automatically — no per-class __clone. * - * Closures are left shared (uncloneable; they capture context but are not - * mutated after creation). Scalars/null are copied by PHP's clone and skipped. + * + * @internal */ trait DeepClone {