diff --git a/README.md b/README.md
index ece8a86..1b8ddef 100644
--- a/README.md
+++ b/README.md
@@ -4,10 +4,17 @@
[](https://packagist.org/packages/ykan/elastickit)
[](https://packagist.org/packages/ykan/elastickit)
+[](https://github.com/ykan821/ElasticKit/actions/workflows/ci.yml)
+[](https://packagist.org/packages/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 @@
[](https://packagist.org/packages/ykan/elastickit)
[](https://packagist.org/packages/ykan/elastickit)
+[](https://github.com/ykan821/ElasticKit/actions/workflows/ci.yml)
+[](https://packagist.org/packages/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、批量写入、索引管理、零停机重建、事件监听。
+
展开查看
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
{