diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 22331f7..f03c592 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -59,7 +59,7 @@ jobs:
run: composer install --no-interaction --prefer-dist
- name: PHPUnit (with ES validation)
- run: vendor/bin/phpunit --testsuite unit
+ run: vendor/bin/phpunit --testsuite integration
env:
ELASTICKIT_TEST_HOST: http://localhost:9200
@@ -79,7 +79,7 @@ jobs:
run: composer install --no-interaction --prefer-dist
- name: PHPStan
- run: vendor/bin/phpstan analyse
+ run: vendor/bin/phpstan analyse --memory-limit=256M
- name: PHP-CS-Fixer
run: PHP_CS_FIXER_IGNORE_ENV=1 vendor/bin/php-cs-fixer fix --dry-run --diff
diff --git a/.gitignore b/.gitignore
index 08df105..a311532 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,4 @@ Thumbs.db
.env
.php-cs-fixer.cache
.phpunit.result.cache
+.phpunit.cache/
diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php
index 47a69ba..2a924d8 100644
--- a/.php-cs-fixer.php
+++ b/.php-cs-fixer.php
@@ -1,11 +1,12 @@
in(__DIR__ . '/src');
+ ->in([__DIR__ . '/src', __DIR__ . '/tests']);
return (new PhpCsFixer\Config())
->setRules([
'@PSR12' => true,
'no_unused_imports' => true,
])
- ->setFinder($finder);
+ ->setFinder($finder)
+ ->setUnsupportedPhpVersionAllowed(true);
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 62f9b3f..02769c0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,14 +1,43 @@
# Changelog
+## [8.0.0-beta.5] - 2026-06-28
+
+### Added
+
+- `Index::$trackTotalHits` (int|bool, default `false`) — opt-in total-hit tracking per index; `true` counts every hit, an int caps the count, `false` omits it.
+- `Results::hasMorePages()` — next-page signal that works with or without a total (full-page heuristic when `track_total_hits` is false).
+- `PaginationTotalUnavailableException` (in `ElasticKit\Index\Exception`) — thrown by `Results::toPaginator()` when no total is available.
+
+### Changed
+
+- **BC:** `Index` now defaults `track_total_hits` to `false`, so Elasticsearch omits the hit total. `Results::total()`/`lastPage()` return `?int` (null when the total is unavailable), and `toPaginator()` throws unless the index sets `$trackTotalHits = true` (or a count cap). Set `protected int|bool $trackTotalHits = true;` on indexes that paginate.
+- `Results::isEmpty()` docblock now points to `hasMorePages()` for "has next page".
+
+### Fixed
+
+- `Node::toArray()` (and field-keyed overrides such as `Intervals`) throw `LogicException` when a field-keyed node has no field set, instead of an uncatchable typed-property `Error`.
+- `Agg` empty aggregation body serializes to `{}`, not `[]` (which Elasticsearch rejects).
+- `RangeSupport` rejects positional elements beyond the `[start, end]` shorthand instead of leaking them as numeric keys.
+- `SpanTerm::term()` emits Elasticsearch's `{value}` key (was `{term}`).
+- `Rebuild` alias-swap failures now delete the orphaned new index.
+
+### Deprecated
+
+- `DateHistogram::interval()` — Elasticsearch deprecated the bare `interval` key; use `calendarInterval()`/`fixedInterval()`.
+
+### Removed
+
+- Composer scripts (`analyse` / `cs-check` / `cs-fix`) — run the binaries via your Docker workflow instead.
+
## [8.0.0-beta.4] - 2026-06-07
-### 新增
+### Added
-- DSL 查询构建器,支持多态参数(字符串/数组/闭包/对象)
-- 全量查询类型覆盖:TermLevel、FullText、Compound、Geo、Joining、Span、Shape、Specialized
-- 聚合支持:Bucket、Metric、Pipeline 三大类
-- 搜索参数:sort、highlight、rescore、collapse、suggest、post_filter、knn 等
-- Index 层:CRUD、分页、游标遍历、批量写入(Bulk)、零停机重建(Rebuild)
-- 事件系统:搜索、批量操作、重建各阶段的事件监听
-- OOP 风格:每个查询/聚合类型独立 Node 类,支持链式调用和增量构建
-- 原生 DSL 透传:未覆盖的 ES 特性直接传数组
+- DSL query builder with polymorphic parameters (string/array/closure/object)
+- Full query-type coverage: TermLevel, FullText, Compound, Geo, Joining, Span, Shape, Specialized
+- Aggregations: Bucket, Metric, and Pipeline categories
+- Search parameters: sort, highlight, rescore, collapse, suggest, post_filter, knn, etc.
+- Index layer: CRUD, pagination, cursor iteration, bulk writes (Bulk), zero-downtime rebuild (Rebuild)
+- Event system: listeners for each phase of search, bulk operations, and rebuild
+- OOP style: each query/aggregation type is a dedicated Node class, supporting chaining and incremental building
+- Raw DSL pass-through: uncovered ES features can be passed directly as arrays
diff --git a/CLAUDE.md b/CLAUDE.md
index 61b6b43..6049386 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,59 +1,67 @@
# ElasticKit - Elasticsearch DSL Query Builder
-PHP Elasticsearch DSL 查询构建库。
+A PHP Elasticsearch DSL query builder library.
-> 本文件提交到仓库。本地环境变量放在 `CLAUDE.local.md`(已 gitignore),Claude Code 自动加载两者。
+> This file is committed to the repository. Local environment variables live in `CLAUDE.local.md` (gitignored); Claude Code loads both automatically.
-**版本管理:** a.b.c,a 对齐 ES 主版本,`^8` 即可。
+**Versioning:** a.b.c, where `a` tracks the ES major version. `^8` suffices.
-> master 对应 v8.x(ES 8.x,PHP 8.1+),7.x 分支独立维护(ES 7.x,PHP 7.2+)。两条线不互合并,CLAUDE.md 各分支独立维护。
+> `master` tracks v8.x (ES 8.x, PHP 8.1+); the 7.x branch is maintained separately (ES 7.x, PHP 7.2+). The two lines are never merged into each other; CLAUDE.md is maintained per branch.
-### 提交信息规范
+### Commit message conventions
-- **参数名锁定**:公开方法参数名是 API 的一部分(支持命名参数),minor 版本禁止重命名
+- **Parameter names are locked**: public method parameter names are part of the API (named arguments are supported); renaming is forbidden in minor versions
-[Conventional Commits](https://www.conventionalcommits.org/),中文描述:`feat(query): 新增 knn 向量搜索`
+[Conventional Commits](https://www.conventionalcommits.org/), with an English description: `feat(query): add knn vector search`
-Scope 可选:dsl / index / agg / query / docs。Breaking change 加 `!` 后缀。
+Scope is optional: dsl / index / agg / query / docs. Append `!` for breaking changes.
-### Changelog 规范
+### Changelog conventions
-[Keep a Changelog](https://keepachangelog.com),中文分类:
+[Keep a Changelog](https://keepachangelog.com), with English categories:
-- **新增** / **变更** / **弃用** / **移除** / **修复** / **安全**
-- 只记录对用户有影响的变更
-- 相关改动合并为一条
-- Breaking change 以 `**BC:**` 前缀标记
+- **Added** / **Changed** / **Deprecated** / **Removed** / **Fixed** / **Security**
+- Record only user-facing changes
+- Merge related changes into a single entry
+- Mark breaking changes with the `**BC:**` prefix
-### 发版流程
+### Release flow
-1. 跑全部测试
-2. 更新 CHANGELOG.md
-3. 提交并推送
-4. 确认版本号后打 tag 并推送
+1. Run the full test suite
+2. Update CHANGELOG.md
+3. Commit and push
+4. Confirm the version, then tag and push
-### PHPDoc 规范
+### PHPDoc conventions
-PSR-5 规范。
+Follow PSR-5.
-## 测试
+## TODO
-测试在 Docker 容器中运行,需要设置以下环境变量:
+- [ ] **Add integration contract tests for Span and Shape queries**: unit DSL tests exist, but there is no Elasticsearch execution coverage (other query families have `tests/Integration/Dsl/*ContractTest.php`)
+- [ ] **Cover Rebuild import-failure rollback**: the `createIndex`→`import` try/catch (deletes the new index on failure) is untested; only the alias-swap rollback path is covered
-| 变量 | 用途 |
+## Tests
+
+Tests run inside a Docker container and require these environment variables:
+
+| Variable | Purpose |
|---|---|
-| `PHP_CONTAINER` | Docker 容器名 |
-| `PROJECT_PATH` | 项目在容器内的路径 |
-| `PROXY_PORT` | HTTP 代理端口(推送用) |
+| `PHP_CONTAINER` | Docker container name |
+| `PROJECT_PATH` | Project path inside the container |
+| `PROXY_PORT` | HTTP proxy port (for pushing) |
+| `ELASTICKIT_TEST_HOST` | ES endpoint for integration tests (e.g. `https://localhost:9200`); integration tests are skipped when unset |
+
+## Pre-push checklist
```bash
docker exec $PHP_CONTAINER sh -c "cd $PROJECT_PATH && vendor/bin/phpunit --testsuite unit"
-docker exec $PHP_CONTAINER sh -c "cd $PROJECT_PATH && vendor/bin/phpunit --testsuite index"
+docker exec -e ELASTICKIT_TEST_HOST=https://elasticsearch:9200 $PHP_CONTAINER sh -c "cd $PROJECT_PATH && vendor/bin/phpunit --testsuite integration"
docker exec $PHP_CONTAINER sh -c "cd $PROJECT_PATH && vendor/bin/phpunit"
-docker exec $PHP_CONTAINER sh -c "cd $PROJECT_PATH && vendor/bin/phpstan analyse"
+docker exec $PHP_CONTAINER sh -c "cd $PROJECT_PATH && vendor/bin/phpstan analyse --memory-limit=256M"
docker exec $PHP_CONTAINER sh -c "cd $PROJECT_PATH && vendor/bin/phpmd src text phpmd.xml"
docker exec $PHP_CONTAINER sh -c "cd $PROJECT_PATH && PHP_CS_FIXER_IGNORE_ENV=1 vendor/bin/php-cs-fixer fix --diff"
-# GitHub 不可达时走代理
+# Route through the proxy when GitHub is unreachable
https_proxy=http://127.0.0.1:$PROXY_PORT http_proxy=http://127.0.0.1:$PROXY_PORT git push
```
diff --git a/README.md b/README.md
index 85a0b34..6f442f9 100644
--- a/README.md
+++ b/README.md
@@ -1,34 +1,36 @@
# ElasticKit
+> [中文](README.zh.md) | English
+
[](https://packagist.org/packages/ykan/elastickit)
[](https://packagist.org/packages/ykan/elastickit)
[](https://packagist.org/packages/ykan/elastickit)
-PHP Elasticsearch DSL 查询构建库,覆盖查询、聚合、CRUD、批量写入、零停机重建。
+A PHP Elasticsearch DSL query builder covering queries, aggregations, CRUD, bulk writes, and zero-downtime rebuilds.
-## 安装
+## Installation
```
composer require ykan/elastickit:^8
```
-> 需要 PHP 8.1+、Elasticsearch 8.x。依赖 `elasticsearch-php` 自动安装。ES 7.x 用户见 [7.x 分支](https://github.com/ykan821/ElasticKit/tree/7.x)。
+> Requires PHP 8.1+ and Elasticsearch 8.x. The `elasticsearch-php` dependency is installed automatically.
-## 快速开始
+## Quick Start
```php
use ElasticKit\Index\Index;
-// 1. 注册 Client
+// 1. Register the client
$client = \Elastic\Elasticsearch\ClientBuilder::create()
->setHosts(['http://localhost:9200'])->build();
Index::setClient($client);
-// 2. 定义索引
+// 2. Define an index
class ProductIndex extends Index
{
- protected $name = 'products';
- protected $mappings = [
+ protected string $name = 'products';
+ protected array $mappings = [
'properties' => [
'title' => ['type' => 'text'],
'price' => ['type' => 'float'],
@@ -37,23 +39,23 @@ class ProductIndex extends Index
];
}
-// 3. 搜索
+// 3. Search
$results = ProductIndex::query()
->match('title', 'elasticsearch')
->get();
$hits = $results->docs(); // [['title' => '...'], ...]
-$total = $results->total(); // 命中总数
+$total = $results->total(); // null unless $trackTotalHits = true (see Pagination & cursor)
```
-## DSL 示例
+## DSL Examples
-展开查看
+Expand
-### 多态参数
+### Polymorphic parameters
-同一个方法支持字符串、数组、闭包、对象四种写法:
+The same method accepts four forms — string, array, closure, object:
```php
$q->term('status', 'published'); // string
@@ -62,9 +64,9 @@ $q->term(fn ($t) => $t->field('status')->value('published')); // closure
$q->term(Term::create('status', 'published')); // object
```
-### OOP 风格
+### OOP style
-每个查询类型都是独立的 Node 类,支持链式调用:
+Each query type is a dedicated Node class supporting chaining:
```php
use ElasticKit\DSL\Query;
@@ -77,9 +79,9 @@ $bool = Boolean::create()
->must(Match_::create('title', 'elasticsearch'))
->filter(Term::create('status', 'published')->boost(1.5));
-// 增量构建
+// incremental build
if ($filterByPrice) {
- $bool->addFilter(Range::create('price', [10, 100]));
+ $bool->filter(Range::create('price', [10, 100]));
}
$query = Query::create($bool);
@@ -88,7 +90,7 @@ $query->toArray(); // ['query' => ['bool' => [...]]]
$query->toJson(); // '{"query":{"bool":{...}}}'
```
-### 复合查询
+### Compound query
```php
$results = ProductIndex::query()
@@ -96,7 +98,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)) // 条件过滤
+ ->when($status, fn ($q) => $q->term('status', $status)) // conditional filter
->term('status', 'published'),
])
->highlight('title')
@@ -122,7 +124,27 @@ $results = ProductIndex::query()
}
```
-### 聚合
+### Clause appending (ClausesSupport)
+
+The clauses of a `bool` query (must / should / filter / must_not) **append**, and accept the same four input forms as leaf queries:
+
+```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'));
+
+// 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
+
+// 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
+```
+
+> `dis_max`, `span_or`, `span_near` and other array-clause containers behave the same way (queries / clauses append).
+
+### Aggregations
```php
$results = ProductIndex::query()
@@ -135,7 +157,7 @@ $results = ProductIndex::query()
$aggs = $results->aggregations();
```
-### 嵌套查询
+### Nested query
```php
$results = ProductIndex::query()
@@ -143,10 +165,10 @@ $results = ProductIndex::query()
->get();
```
-### 原生 DSL 透传
+### Raw DSL pass-through
```php
-// 支持原生数组嵌套闭包,query/aggs/参数可一次性传入
+// supports raw arrays with nested closures; query/aggs/parameters can be passed all at once
$query = Query::create([
'query' => [
'bool' => [
@@ -161,35 +183,43 @@ $query = Query::create([
-## Index 示例
+## Index Examples
-展开查看
+Expand
-### 分页与游标
+### Pagination & cursor
```php
-// 分页
+// pagination
$results = ProductIndex::query()
->match('title', 'elasticsearch')
->paginate($page, $perPage);
$results->lastPage();
$results->items();
-$results->toPaginator(); // 转为框架分页器(需注册 Paginator Resolver)
+$results->toPaginator(); // convert to a framework paginator (requires registering a Paginator Resolver)
-// 游标遍历(大批量导出)
-foreach (ProductIndex::query()->cursor() as $batch) {
- foreach ($batch->docs() as $doc) {
+// batch iteration (large exports / batch processing; yields a Results per batch)
+foreach (ProductIndex::query()->chunk() as $results) {
+ foreach ($results->docs() as $doc) {
// ...
}
}
+
+// per-hit iteration (exports / per-row processing; yields one hit: _id/_score/_source)
+foreach (ProductIndex::query()->cursor() as $hit) {
+ $doc = $hit['_source'];
+ // ...
+}
```
-### 文档 CRUD
+> **Pagination needs totals, which are opt-in.** `Index` defaults `$trackTotalHits = false`, so `total()`/`lastPage()` return `null` and `toPaginator()` throws. Set `protected int|bool $trackTotalHits = true;` (or a count cap) on the index for page-count pagination, or use `hasMorePages()` / `chunk()` / `cursor()` for total-less iteration.
+
+### Document CRUD
```php
-ProductIndex::insert(1, ['title' => 'Hello', 'price' => 99.9]);
+ProductIndex::doc(1)->save(['title' => 'Hello', 'price' => 99.9]);
$doc = ProductIndex::doc(1);
$doc->source(); // ['title' => 'Hello', 'price' => 99.9]
@@ -198,7 +228,7 @@ $doc->update(['price' => 89.9]);
$doc->delete();
```
-### 批量操作
+### Bulk operations
```php
use ElasticKit\Index\Bulk;
@@ -210,28 +240,28 @@ $bulk->batchSize(500)
->index(2, ['title' => 'B', 'price' => 20])
->update(3, ['price' => 15])
->delete(4)
- ->execute();
+ ->flush();
```
-### 索引管理
+### Index management
```php
use ElasticKit\Index\Manager;
$manager = new Manager(new ProductIndex());
-$manager->create(); // 创建索引
+$manager->create(); // create the index
$manager->exists(); // bool
-$manager->putMapping(); // 更新 mapping
-$manager->delete(); // 删除索引
+$manager->putMapping(); // update the mapping
+$manager->delete(); // delete the index
```
-### 零停机重建
+### Zero-downtime rebuild
```php
use ElasticKit\Index\Rebuild;
-// 1. 在 Index 子类中定义数据源
+// 1. Define the data source in an Index subclass
class ProductIndex extends Index
{
public function source(array $context = []): iterable
@@ -242,48 +272,66 @@ class ProductIndex extends Index
}
}
-// 2. 执行重建(自动创建新索引 → 导入 → 切换别名)
+// 2. Run the rebuild (creates a new index -> imports -> swaps the alias)
$result = (new Rebuild(new ProductIndex()))
->batchSize(500)
->run();
-// $result = ['newIndex' => 'products_20260607', 'oldIndex' => 'products_20260601']
+// $result = ['newIndex' => 'products_20260607_120000', 'oldIndex' => 'products_20260601_090000']
-// 3. 清理旧索引或回滚
+// 3. Clean up old indices or roll back
(new Rebuild(new ProductIndex()))->clean($result['oldIndex']);
(new Rebuild(new ProductIndex()))->rollback($result['oldIndex']);
```
-### 事件监听
+### Event listening
```php
-use ElasticKit\Index\Event;
+use ElasticKit\Index\Support\Event;
+use ElasticKit\Index\Support\EventDispatcher;
-ProductIndex::listen('search.query.after', function (Event $e) {
+EventDispatcher::listen('search.query.after', function (Event $e) {
Log::info("Search on {$e->index}", [
'dsl' => $e->dsl,
'duration' => $e->duration,
]);
});
-ProductIndex::listen('search.*', function (Event $e) {
+EventDispatcher::listen('search.*', function (Event $e) {
Log::debug($e->name);
});
```
-## 文档
+## Long-lived processes
+
+`Index::setClient()`, `ClientManager`, `EventDispatcher`, and `Pagination` hold **static state**. In a long-lived worker (Swoole, RoadRunner, Laravel Octane) this state persists across requests, so a worker leaks the registered client, event listeners, and pagination resolvers between requests.
+
+Reset them between requests — e.g. in a request-terminated hook:
+
+```php
+use ElasticKit\Index\Support\ClientManager;
+use ElasticKit\Index\Support\EventDispatcher;
+use ElasticKit\Index\Support\Pagination;
+
+ClientManager::reset();
+EventDispatcher::reset();
+Pagination::reset();
+```
+
+PHP-FPM forks a worker per request, so this only affects persistent workers.
+
+## Documentation
-- [实践指南](docs/guide.md)——电商订单场景,从安装到上线的完整流程
-- [Index 文档](docs/index.md)——搜索、CRUD、批量操作、零停机重建、事件
-- [升级指南](docs/upgrade.md)——v7.x → v8.x 迁移说明
-- [更新日志](CHANGELOG.md)
-- [Elasticsearch 官方文档](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html)——查询类型和参数参考
+- [Guide](docs/guide.md) — an e-commerce order scenario, the full flow from install to production
+- [Index docs](docs/index.md) — search, CRUD, bulk operations, zero-downtime rebuild, events
+- [Changelog](CHANGELOG.md)
+- [Elasticsearch official docs](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html) — query types and parameter reference
-## AI 辅助开发
+## AI-assisted development
-本项目使用 AI 辅助开发,核心路径和测试经人工审查。
+This project is developed with AI assistance; core paths and tests are human-reviewed.
## License
diff --git a/README.zh.md b/README.zh.md
new file mode 100644
index 0000000..ffb7583
--- /dev/null
+++ b/README.zh.md
@@ -0,0 +1,338 @@
+# ElasticKit
+
+> 中文 | [English](README.md)
+
+[](https://packagist.org/packages/ykan/elastickit)
+[](https://packagist.org/packages/ykan/elastickit)
+[](https://packagist.org/packages/ykan/elastickit)
+
+PHP Elasticsearch DSL 查询构建库,覆盖查询、聚合、CRUD、批量写入、零停机重建。
+
+## 安装
+
+```
+composer require ykan/elastickit:^8
+```
+
+> 需要 PHP 8.1+、Elasticsearch 8.x。依赖 `elasticsearch-php` 自动安装。
+
+## 快速开始
+
+```php
+use ElasticKit\Index\Index;
+
+// 1. 注册 Client
+$client = \Elastic\Elasticsearch\ClientBuilder::create()
+ ->setHosts(['http://localhost:9200'])->build();
+Index::setClient($client);
+
+// 2. 定义索引
+class ProductIndex extends Index
+{
+ protected string $name = 'products';
+ protected array $mappings = [
+ 'properties' => [
+ 'title' => ['type' => 'text'],
+ 'price' => ['type' => 'float'],
+ 'status' => ['type' => 'keyword'],
+ ],
+ ];
+}
+
+// 3. 搜索
+$results = ProductIndex::query()
+ ->match('title', 'elasticsearch')
+ ->get();
+
+$hits = $results->docs(); // [['title' => '...'], ...]
+$total = $results->total(); // 索引未设 $trackTotalHits = true 时为 null(见分页与游标)
+```
+
+## DSL 示例
+
+
+展开查看
+
+### 多态参数
+
+同一个方法支持字符串、数组、闭包、对象四种写法:
+
+```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
+$results = ProductIndex::query()
+ ->bool([
+ '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'),
+ ])
+ ->highlight('title')
+ ->sort('price', 'asc')
+ ->size(20)
+ ->get();
+```
+
+```json
+{
+ "query": {
+ "bool": {
+ "must": [{ "match": { "title": "elasticsearch" } }],
+ "filter": [
+ { "range": { "price": { "gte": 10, "lte": 100 } } },
+ { "term": { "status": "published" } }
+ ]
+ }
+ },
+ "highlight": { "fields": { "title": {} } },
+ "sort": [{ "price": "asc" }],
+ "size": 20
+}
+```
+
+### 子句追加(ClausesSupport)
+
+`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'));
+
+// 子句累加(多次调用、列表形式都追加)
+$q->bool(fn ($b) => $b->must(...)->must(...)); // must: [q1, q2]
+$q->bool(['must' => [$q1, $q2]]); // 同上
+
+// 对比:minimum_should_match 是单值属性,后调覆盖而非追加
+$q->bool(fn ($b) => $b->minimumShouldMatch(1)->minimumShouldMatch(3)); // 3
+```
+
+> `dis_max`、`span_or`、`span_near` 等其他数组子句容器同理(queries / clauses 累加)。
+
+### 聚合
+
+```php
+$results = ProductIndex::query()
+ ->matchAll()
+ ->aggs('status_counts', fn ($agg) => $agg->terms('status'))
+ ->aggs('price_stats', fn ($agg) => $agg->stats('price'))
+ ->size(0)
+ ->get();
+
+$aggs = $results->aggregations();
+```
+
+### 嵌套查询
+
+```php
+$results = ProductIndex::query()
+ ->nested('comments', fn ($q) => $q->match('comments.body', 'great'))
+ ->get();
+```
+
+### 原生 DSL 透传
+
+```php
+// 支持原生数组嵌套闭包,query/aggs/参数可一次性传入
+$query = Query::create([
+ 'query' => [
+ 'bool' => [
+ 'must' => fn ($q) => $q->match('title', 'elasticsearch'),
+ 'filter' => fn ($q) => $q->term('status', 'published'),
+ ],
+ ],
+ 'size' => 20,
+ 'sort' => [['price' => 'asc']],
+]);
+```
+
+
+
+## Index 示例
+
+
+展开查看
+
+### 分页与游标
+
+```php
+// 分页
+$results = ProductIndex::query()
+ ->match('title', 'elasticsearch')
+ ->paginate($page, $perPage);
+
+$results->lastPage();
+$results->items();
+$results->toPaginator(); // 转为框架分页器(需注册 Paginator Resolver)
+
+// 分批遍历(大批量导出/批处理,每次 yield 一个 Results)
+foreach (ProductIndex::query()->chunk() as $results) {
+ foreach ($results->docs() as $doc) {
+ // ...
+ }
+}
+
+// 逐条遍历(导出/逐条加工,每次 yield 一个 hit:_id/_score/_source)
+foreach (ProductIndex::query()->cursor() as $hit) {
+ $doc = $hit['_source'];
+ // ...
+}
+```
+
+> **分页要总数,而总数默认关闭。** `Index` 默认 `$trackTotalHits = false`,`total()`/`lastPage()` 返 `null`、`toPaginator()` 抛异常。需要页码分页时在索引上设 `protected int|bool $trackTotalHits = true;`(或计数上限);否则用 `hasMorePages()` / `chunk()` / `cursor()` 做无总数遍历。
+
+### 文档 CRUD
+
+```php
+ProductIndex::doc(1)->save(['title' => 'Hello', 'price' => 99.9]);
+
+$doc = ProductIndex::doc(1);
+$doc->source(); // ['title' => 'Hello', 'price' => 99.9]
+
+$doc->update(['price' => 89.9]);
+$doc->delete();
+```
+
+### 批量操作
+
+```php
+use ElasticKit\Index\Bulk;
+
+$bulk = new Bulk(new ProductIndex());
+
+$bulk->batchSize(500)
+ ->index(1, ['title' => 'A', 'price' => 10])
+ ->index(2, ['title' => 'B', 'price' => 20])
+ ->update(3, ['price' => 15])
+ ->delete(4)
+ ->flush();
+```
+
+### 索引管理
+
+```php
+use ElasticKit\Index\Manager;
+
+$manager = new Manager(new ProductIndex());
+
+$manager->create(); // 创建索引
+$manager->exists(); // bool
+$manager->putMapping(); // 更新 mapping
+$manager->delete(); // 删除索引
+```
+
+### 零停机重建
+
+```php
+use ElasticKit\Index\Rebuild;
+
+// 1. 在 Index 子类中定义数据源
+class ProductIndex extends Index
+{
+ public function source(array $context = []): iterable
+ {
+ foreach (Db::table('products')->cursor() as $row) {
+ yield $row['id'] => $row;
+ }
+ }
+}
+
+// 2. 执行重建(自动创建新索引 → 导入 → 切换别名)
+$result = (new Rebuild(new ProductIndex()))
+ ->batchSize(500)
+ ->run();
+
+// $result = ['newIndex' => 'products_20260607_120000', 'oldIndex' => 'products_20260601_090000']
+
+// 3. 清理旧索引或回滚
+(new Rebuild(new ProductIndex()))->clean($result['oldIndex']);
+(new Rebuild(new ProductIndex()))->rollback($result['oldIndex']);
+```
+
+### 事件监听
+
+```php
+use ElasticKit\Index\Support\Event;
+use ElasticKit\Index\Support\EventDispatcher;
+
+EventDispatcher::listen('search.query.after', function (Event $e) {
+ Log::info("Search on {$e->index}", [
+ 'dsl' => $e->dsl,
+ 'duration' => $e->duration,
+ ]);
+});
+
+EventDispatcher::listen('search.*', function (Event $e) {
+ Log::debug($e->name);
+});
+```
+
+
+
+## 常驻进程
+
+`Index::setClient()`、`ClientManager`、`EventDispatcher`、`Pagination` 持有**静态状态**。在常驻 worker(Swoole、RoadRunner、Laravel Octane)中,这些状态跨请求保留,worker 会把已注册的客户端、事件监听器、分页解析器泄漏到下一个请求。
+
+请在请求之间重置它们——例如在请求终止钩子里:
+
+```php
+use ElasticKit\Index\Support\ClientManager;
+use ElasticKit\Index\Support\EventDispatcher;
+use ElasticKit\Index\Support\Pagination;
+
+ClientManager::reset();
+EventDispatcher::reset();
+Pagination::reset();
+```
+
+PHP-FPM 每请求 fork 一个 worker,因此本节仅影响常驻 worker。
+
+## 文档
+
+- [实践指南](docs/guide.zh.md)——电商订单场景,从安装到上线的完整流程
+- [Index 文档](docs/index.zh.md)——搜索、CRUD、批量操作、零停机重建、事件
+- [更新日志](CHANGELOG.md)
+- [Elasticsearch 官方文档](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html)——查询类型和参数参考
+
+## AI 辅助开发
+
+本项目使用 AI 辅助开发,核心路径和测试经人工审查。
+
+## License
+
+MIT
diff --git a/composer.json b/composer.json
index 058f8b6..003b25d 100644
--- a/composer.json
+++ b/composer.json
@@ -30,15 +30,5 @@
"name": "ykan",
"email": "smykggi@gmail.com"
}
- ],
- "scripts": {
- "analyse": "phpstan analyse",
- "cs-check": "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix --dry-run --diff",
- "cs-fix": "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix"
- },
- "config": {
- "audit": {
- "block-insecure": false
- }
- }
+ ]
}
diff --git a/docs/guide.md b/docs/guide.md
index b95f499..1d8b541 100644
--- a/docs/guide.md
+++ b/docs/guide.md
@@ -1,18 +1,18 @@
-# ElasticKit 实践指南
+# ElasticKit Practical Guide
-以电商订单模块为例,演示 ElasticKit 的完整使用流程。
+Using an e-commerce order module as an example, this guide walks through the complete workflow with ElasticKit.
-## 阶段 1:安装与配置
+## Phase 1: Installation & Configuration
-运营提了需求:订单要有查询页面,能搜订单号、按状态和日期筛选,还要一个销售统计看板。
+The operations team has a requirement: an order search page that searches by order number, filters by status and date, plus a sales analytics dashboard.
-安装:
+Install:
```
composer require ykan/elastickit:^8
```
-注册 ES Client:
+Register the ES client:
```php
// app/Providers/AppServiceProvider.php
@@ -30,9 +30,9 @@ public function boot(): void
}
```
-## 阶段 2:设计索引
+## Phase 2: Design the index
-订单数据分散在订单表、用户表、商家表。ES 不支持 join,**写入时把关联数据组装到一条文档里**。
+Order data is spread across the orders, users, and merchants tables. ES doesn't support joins, so **assemble related data into a single document at write time**.
```php
use ElasticKit\Index\Index;
@@ -40,14 +40,14 @@ use Illuminate\Support\Facades\Db;
class OrderIndex extends Index
{
- protected $name = 'orders';
+ protected string $name = 'orders';
- protected $mappings = [
+ protected array $mappings = [
'properties' => [
- 'order_no' => ['type' => 'keyword'], // 精确匹配
+ 'order_no' => ['type' => 'keyword'], // exact match
'status' => ['type' => 'keyword'], // pending/paid/shipped/completed
- 'user_name' => ['type' => 'keyword'], // 关联用户表
- 'merchant_name' => ['type' => 'keyword'], // 关联商家表
+ 'user_name' => ['type' => 'keyword'], // joined from users
+ 'merchant_name' => ['type' => 'keyword'], // joined from merchants
'total_amount' => ['type' => 'float'],
'paid_at' => ['type' => 'date'],
'created_at' => ['type' => 'date'],
@@ -56,7 +56,7 @@ class OrderIndex extends Index
public function source(array $context = []): iterable
{
- // 关联用户、商家表,组装查询所需的全部字段
+ // join users + merchants, assemble every field the search needs
$query = Db::table('orders')
->select([
'orders.*',
@@ -66,12 +66,12 @@ class OrderIndex extends Index
->leftJoin('users', 'orders.user_id', '=', 'users.id')
->leftJoin('merchants', 'orders.merchant_id', '=', 'merchants.id');
- // 增量同步时只查指定 ID
+ // for incremental sync, query only the given IDs
if (isset($context['ids'])) {
$query->whereIn('orders.id', $context['ids']);
}
- // yield 返回 [文档ID => 文档数据],Rebuild 内部用 Bulk 批量写入
+ // yield [docId => docData]; Rebuild writes them in bulk internally
foreach ($query->cursor() as $order) {
yield $order['id'] => [
'order_no' => $order['order_no'],
@@ -87,11 +87,11 @@ class OrderIndex extends Index
}
```
-> `user_name`、`merchant_name` 写入时从关联表组装,查询时不再需要 join。传 `['ids' => [...]]` 支持增量查询。
+> `user_name` and `merchant_name` are assembled from related tables at write time, so no join is needed at query time. Pass `['ids' => [...]]` for incremental queries.
-## 阶段 3:首次导入
+## Phase 3: Initial import
-索引设计好了,把现有订单导入 ES。
+With the index designed, import the existing orders into ES.
```php
use ElasticKit\Index\Rebuild;
@@ -103,13 +103,13 @@ $result = (new Rebuild(new OrderIndex()))
// $result = ['newIndex' => 'orders_20260607_120000', 'oldIndex' => null]
```
-Rebuild 自动完成:创建新索引(`orders_20260607_120000`)→ 从 `source()` 取数据 → Bulk 批量写入 → 将 `orders` 别名指向新索引。首次导入时 `oldIndex` 为 null。
+Rebuild does it all automatically: creates a new index (`orders_20260607_120000`) -> reads from `source()` -> bulk-writes via Bulk -> points the `orders` alias at the new index. On the first import `oldIndex` is null.
-## 阶段 4:搜索与筛选
+## Phase 4: Search & filtering
-运营要一个订单查询页面,条件多且动态。把条件构建封装到 Index 里,控制器只管调用。
+Operations wants an order search page with many, dynamic conditions. Encapsulate the condition building inside the Index; the controller just calls it.
-在 OrderIndex 中加一个搜索方法:
+Add a search method to OrderIndex:
```php
use ElasticKit\DSL\Query;
@@ -120,25 +120,25 @@ use ElasticKit\DSL\Queries\Compound\Boolean;
class OrderIndex extends Index
{
- // ... mappings 和 source() 同阶段 2
+ // ... mappings and source() as in Phase 2
public static function searchOrders(array $filters)
{
$bool = Boolean::create();
- // 精确筛选(不需要评分,放 filter)
+ // exact filters (no scoring needed -> put in filter)
if (!empty($filters['status'])) {
- $bool->addFilter(Term::create('status', $filters['status']));
+ $bool->filter(Term::create('status', $filters['status']));
}
if (!empty($filters['start_date']) && !empty($filters['end_date'])) {
- $bool->addFilter(Range::create('created_at', [$filters['start_date'], $filters['end_date']]));
+ $bool->filter(Range::create('created_at', [$filters['start_date'], $filters['end_date']]));
}
- // 关键词搜索(OR,放 should)
+ // keyword search (OR -> put in should)
if (!empty($filters['keyword'])) {
- $bool->addShould(Wildcard::create('order_no', "*{$filters['keyword']}*"));
- $bool->addShould(Wildcard::create('merchant_name', "*{$filters['keyword']}*"));
+ $bool->should(Wildcard::create('order_no', "*{$filters['keyword']}*"));
+ $bool->should(Wildcard::create('merchant_name', "*{$filters['keyword']}*"));
}
return static::query(Query::create($bool));
@@ -146,7 +146,7 @@ class OrderIndex extends Index
}
```
-控制器调用:
+Controller:
```php
// app/Http/Controllers/OrderController.php
@@ -160,34 +160,36 @@ public function index(Request $request)
}
```
-> 条件用 `if` 逐个判断,只有传了值才加查询。`addShould()` 实现 OR 搜索。深分页场景用 `cursor()` 替代 `paginate()`。
+> `toPaginator()` needs a hit total. Set `protected int|bool $trackTotalHits = true;` on `OrderIndex` — `Index` defaults to `false`, which omits the total and makes `toPaginator()` throw.
-运营还要导出筛选结果到 Excel。ES 默认 `max_result_window = 10000`,`from/size` 翻不到后面的数据,用 `cursor()` 基于 scroll 遍历:
+> Conditions are checked one by one with `if`; a clause is added only when a value is present. `should()` implements OR search. For deep pagination use `chunk()` (batches) or `cursor()` (per hit) instead of `paginate()`.
+
+Operations also wants to export the filtered results to Excel. ES defaults to `max_result_window = 10000`, so `from/size` can't reach later data; iterate with `cursor()` (scroll-based):
```php
public function export(array $filters)
{
$search = static::searchOrders($filters)->sort('created_at', 'desc');
- foreach ($search->cursor() as $batch) {
- foreach ($batch->docs() as $doc) {
- // 写入 Excel
+ foreach ($search->chunk() as $results) {
+ foreach ($results->docs() as $doc) {
+ // write to Excel
}
}
}
```
-## 阶段 5:聚合统计
+## Phase 5: Aggregation statistics
-管理看板需要按月统计销售额,按商家分组汇总。筛选条件和列表页共用 `searchOrders()`。
+The admin dashboard needs monthly sales totals, grouped by merchant. The filter conditions reuse `searchOrders()`.
```php
public function statistics(array $filters)
{
- // 复用 searchOrders 的筛选条件,size(0) 不返回文档只取聚合
+ // reuse searchOrders' filters; size(0) returns no docs, only aggregations
$search = static::searchOrders($filters)->size(0);
- // 按月统计销售额
+ // monthly sales totals
$search->aggs('monthly', function ($agg) {
$agg->dateHistogram([
'field' => 'created_at',
@@ -198,7 +200,7 @@ public function statistics(array $filters)
$agg->aggs('revenue', fn ($a) => $a->sum('total_amount'));
});
- // 按商家分组汇总
+ // group + total by merchant
$search->aggs('by_merchant', function ($agg) {
$agg->terms('merchant_name');
$agg->aggs('revenue', fn ($a) => $a->sum('total_amount'));
@@ -209,14 +211,14 @@ public function statistics(array $filters)
}
```
-## 阶段 6:增量同步
+## Phase 6: Incremental sync
-订单状态变更、商家改名,ES 要跟着更新。触发方式可以是 ORM 事件、消息队列、binlog 监听等,最终都是同一个流程:**拿到文档 ID 列表 → 推队列异步处理**。
+Order status changes, merchant renames — ES must follow. Triggers can be ORM events, message queues, binlog listeners, etc.; the flow is always the same: **collect the document ID list -> push to a queue for async processing**.
-推入队列(不直接更新 ES):
+Push to a queue (don't update ES directly):
```php
-// OrderIndex 中推队列
+// in OrderIndex, push to the queue
public static function syncOrders(array $ids)
{
foreach (array_chunk($ids, 100) as $chunk) {
@@ -224,11 +226,11 @@ public static function syncOrders(array $ids)
}
}
-// 通过 binlog 监听、ORM 事件等触发,取到 doc_id 后异步更新
+// triggered via binlog listener, ORM events, etc.; once you have doc_ids, update async
OrderIndex::syncOrders($orderIds);
```
-通用的 SyncEsJob,所有 Index 复用:
+A generic SyncEsJob, reused by every index:
```php
use ElasticKit\Index\Bulk;
@@ -245,25 +247,25 @@ class SyncEsJob
$bulk->index($id, $doc);
}
- $bulk->execute();
+ $bulk->flush();
$job->delete();
}
}
```
-## 阶段 7:Schema 演进
+## Phase 7: Schema evolution
-上线后产品要加字段,比如新增"备注"。在 OrderIndex 中加上 mappings 和 source:
+After launch the product adds a field, e.g. "remark". Add it to OrderIndex mappings and source:
```php
-// mappings 加字段
+// add the field to mappings
'remark' => ['type' => 'text'],
-// source 的 yield 加字段
+// add the field to source's yield
'remark' => $order['remark'],
```
-然后 Rebuild:
+Then Rebuild:
```php
$rebuildStartTime = now();
@@ -272,7 +274,8 @@ $rebuild = new Rebuild(new OrderIndex());
$result = $rebuild->batchSize(500)->run();
$rebuild->clean($result['oldIndex']);
-// Rebuild 期间 DB 仍在变更,新索引只是开始时刻的快照,按 updated_at 增量补全
+// During Rebuild the DB keeps changing; the new index is a snapshot of the start moment,
+// so top it up incrementally by updated_at
$orderIds = Db::table('orders')
->where('updated_at', '>=', $rebuildStartTime)
->pluck('id');
@@ -280,8 +283,8 @@ $orderIds = Db::table('orders')
OrderIndex::syncOrders($orderIds);
```
-`run()` 自动完成:创建新索引 → 导入 → 别名切换,零停机。
+`run()` does it all: creates a new index -> imports -> swaps the alias, zero downtime.
---
-→ [Index 文档](index.md)——完整 API 参考。
+→ [Index docs](index.md) — full API reference.
diff --git a/docs/guide.zh.md b/docs/guide.zh.md
new file mode 100644
index 0000000..747ef7d
--- /dev/null
+++ b/docs/guide.zh.md
@@ -0,0 +1,289 @@
+# ElasticKit 实践指南
+
+以电商订单模块为例,演示 ElasticKit 的完整使用流程。
+
+## 阶段 1:安装与配置
+
+运营提了需求:订单要有查询页面,能搜订单号、按状态和日期筛选,还要一个销售统计看板。
+
+安装:
+
+```
+composer require ykan/elastickit:^8
+```
+
+注册 ES Client:
+
+```php
+// app/Providers/AppServiceProvider.php
+
+use ElasticKit\Index\Index;
+use Elastic\Elasticsearch\ClientBuilder;
+
+public function boot(): void
+{
+ Index::setClient(
+ ClientBuilder::create()
+ ->setHosts(['http://localhost:9200'])
+ ->build()
+ );
+}
+```
+
+## 阶段 2:设计索引
+
+订单数据分散在订单表、用户表、商家表。ES 不支持 join,**写入时把关联数据组装到一条文档里**。
+
+```php
+use ElasticKit\Index\Index;
+use Illuminate\Support\Facades\Db;
+
+class OrderIndex extends Index
+{
+ protected string $name = 'orders';
+
+ protected array $mappings = [
+ 'properties' => [
+ 'order_no' => ['type' => 'keyword'], // 精确匹配
+ 'status' => ['type' => 'keyword'], // pending/paid/shipped/completed
+ 'user_name' => ['type' => 'keyword'], // 关联用户表
+ 'merchant_name' => ['type' => 'keyword'], // 关联商家表
+ 'total_amount' => ['type' => 'float'],
+ 'paid_at' => ['type' => 'date'],
+ 'created_at' => ['type' => 'date'],
+ ],
+ ];
+
+ public function source(array $context = []): iterable
+ {
+ // 关联用户、商家表,组装查询所需的全部字段
+ $query = Db::table('orders')
+ ->select([
+ 'orders.*',
+ 'users.name as user_name',
+ 'merchants.name as merchant_name',
+ ])
+ ->leftJoin('users', 'orders.user_id', '=', 'users.id')
+ ->leftJoin('merchants', 'orders.merchant_id', '=', 'merchants.id');
+
+ // 增量同步时只查指定 ID
+ if (isset($context['ids'])) {
+ $query->whereIn('orders.id', $context['ids']);
+ }
+
+ // yield 返回 [文档ID => 文档数据],Rebuild 内部用 Bulk 批量写入
+ foreach ($query->cursor() as $order) {
+ yield $order['id'] => [
+ 'order_no' => $order['order_no'],
+ 'status' => $order['status'],
+ 'user_name' => $order['user_name'],
+ 'merchant_name' => $order['merchant_name'],
+ 'total_amount' => (float) $order['total_amount'],
+ 'paid_at' => $order['paid_at'],
+ 'created_at' => $order['created_at'],
+ ];
+ }
+ }
+}
+```
+
+> `user_name`、`merchant_name` 写入时从关联表组装,查询时不再需要 join。传 `['ids' => [...]]` 支持增量查询。
+
+## 阶段 3:首次导入
+
+索引设计好了,把现有订单导入 ES。
+
+```php
+use ElasticKit\Index\Rebuild;
+
+$result = (new Rebuild(new OrderIndex()))
+ ->batchSize(500)
+ ->run();
+
+// $result = ['newIndex' => 'orders_20260607_120000', 'oldIndex' => null]
+```
+
+Rebuild 自动完成:创建新索引(`orders_20260607_120000`)→ 从 `source()` 取数据 → Bulk 批量写入 → 将 `orders` 别名指向新索引。首次导入时 `oldIndex` 为 null。
+
+## 阶段 4:搜索与筛选
+
+运营要一个订单查询页面,条件多且动态。把条件构建封装到 Index 里,控制器只管调用。
+
+在 OrderIndex 中加一个搜索方法:
+
+```php
+use ElasticKit\DSL\Query;
+use ElasticKit\DSL\Queries\TermLevel\Term;
+use ElasticKit\DSL\Queries\TermLevel\Range;
+use ElasticKit\DSL\Queries\TermLevel\Wildcard;
+use ElasticKit\DSL\Queries\Compound\Boolean;
+
+class OrderIndex extends Index
+{
+ // ... mappings 和 source() 同阶段 2
+
+ public static function searchOrders(array $filters)
+ {
+ $bool = Boolean::create();
+
+ // 精确筛选(不需要评分,放 filter)
+ if (!empty($filters['status'])) {
+ $bool->filter(Term::create('status', $filters['status']));
+ }
+
+ if (!empty($filters['start_date']) && !empty($filters['end_date'])) {
+ $bool->filter(Range::create('created_at', [$filters['start_date'], $filters['end_date']]));
+ }
+
+ // 关键词搜索(OR,放 should)
+ if (!empty($filters['keyword'])) {
+ $bool->should(Wildcard::create('order_no', "*{$filters['keyword']}*"));
+ $bool->should(Wildcard::create('merchant_name', "*{$filters['keyword']}*"));
+ }
+
+ return static::query(Query::create($bool));
+ }
+}
+```
+
+控制器调用:
+
+```php
+// app/Http/Controllers/OrderController.php
+public function index(Request $request)
+{
+ $results = OrderIndex::searchOrders($request->all())
+ ->sort('created_at', 'desc')
+ ->paginate();
+
+ return $results->toPaginator();
+}
+```
+
+> `toPaginator()` 需要命中总数。在 `OrderIndex` 上设 `protected int|bool $trackTotalHits = true;`——`Index` 默认 `false`,不返总数、`toPaginator()` 会抛异常。
+
+> 条件用 `if` 逐个判断,只有传了值才加查询。`should()` 实现 OR 搜索。深分页场景用 `chunk()`(按批)或 `cursor()`(逐条)替代 `paginate()`。
+
+运营还要导出筛选结果到 Excel。ES 默认 `max_result_window = 10000`,`from/size` 翻不到后面的数据,用 `cursor()` 基于 scroll 遍历:
+
+```php
+public function export(array $filters)
+{
+ $search = static::searchOrders($filters)->sort('created_at', 'desc');
+
+ foreach ($search->chunk() as $results) {
+ foreach ($results->docs() as $doc) {
+ // 写入 Excel
+ }
+ }
+}
+```
+
+## 阶段 5:聚合统计
+
+管理看板需要按月统计销售额,按商家分组汇总。筛选条件和列表页共用 `searchOrders()`。
+
+```php
+public function statistics(array $filters)
+{
+ // 复用 searchOrders 的筛选条件,size(0) 不返回文档只取聚合
+ $search = static::searchOrders($filters)->size(0);
+
+ // 按月统计销售额
+ $search->aggs('monthly', function ($agg) {
+ $agg->dateHistogram([
+ 'field' => 'created_at',
+ 'calendar_interval' => 'month',
+ 'format' => 'yyyy-MM',
+ 'time_zone' => 'Asia/Shanghai',
+ ]);
+ $agg->aggs('revenue', fn ($a) => $a->sum('total_amount'));
+ });
+
+ // 按商家分组汇总
+ $search->aggs('by_merchant', function ($agg) {
+ $agg->terms('merchant_name');
+ $agg->aggs('revenue', fn ($a) => $a->sum('total_amount'));
+ });
+
+ $results = $search->get();
+ return $results->aggregations();
+}
+```
+
+## 阶段 6:增量同步
+
+订单状态变更、商家改名,ES 要跟着更新。触发方式可以是 ORM 事件、消息队列、binlog 监听等,最终都是同一个流程:**拿到文档 ID 列表 → 推队列异步处理**。
+
+推入队列(不直接更新 ES):
+
+```php
+// OrderIndex 中推队列
+public static function syncOrders(array $ids)
+{
+ foreach (array_chunk($ids, 100) as $chunk) {
+ Queue::push(SyncEsJob::class, ['class' => static::class, 'ids' => $chunk]);
+ }
+}
+
+// 通过 binlog 监听、ORM 事件等触发,取到 doc_id 后异步更新
+OrderIndex::syncOrders($orderIds);
+```
+
+通用的 SyncEsJob,所有 Index 复用:
+
+```php
+use ElasticKit\Index\Bulk;
+
+class SyncEsJob
+{
+ public function fire($job, $data)
+ {
+ $class = $data['class'];
+ $index = new $class();
+ $bulk = (new Bulk($index))->batchSize(500);
+
+ foreach ($index->source(['ids' => $data['ids']]) as $id => $doc) {
+ $bulk->index($id, $doc);
+ }
+
+ $bulk->flush();
+ $job->delete();
+ }
+}
+```
+
+## 阶段 7:Schema 演进
+
+上线后产品要加字段,比如新增"备注"。在 OrderIndex 中加上 mappings 和 source:
+
+```php
+// mappings 加字段
+'remark' => ['type' => 'text'],
+
+// source 的 yield 加字段
+'remark' => $order['remark'],
+```
+
+然后 Rebuild:
+
+```php
+$rebuildStartTime = now();
+
+$rebuild = new Rebuild(new OrderIndex());
+$result = $rebuild->batchSize(500)->run();
+$rebuild->clean($result['oldIndex']);
+
+// Rebuild 期间 DB 仍在变更,新索引只是开始时刻的快照,按 updated_at 增量补全
+$orderIds = Db::table('orders')
+ ->where('updated_at', '>=', $rebuildStartTime)
+ ->pluck('id');
+
+OrderIndex::syncOrders($orderIds);
+```
+
+`run()` 自动完成:创建新索引 → 导入 → 别名切换,零停机。
+
+---
+
+→ [Index 文档](index.zh.md)——完整 API 参考。
diff --git a/docs/index.md b/docs/index.md
index 996048e..868e4fc 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,53 +1,53 @@
# Index
-Index 是抽象基类。继承它定义索引,注册 ES Client,然后查询。
+Index is an abstract base class. Extend it to define an index, register an ES client, then query.
-## 配置
+## Configuration
```php
use ElasticKit\Index\Index;
-// 创建官方 Client
+// create the official client
$client = \Elastic\Elasticsearch\ClientBuilder::create()
->setHosts(['http://localhost:9200'])
->build();
-// 注册为默认连接
+// register as the default connection
Index::setClient($client);
-// 多连接
+// multiple connections
Index::setClient($mainClient, 'main');
Index::setClient($logClient, 'logs');
```
-定义索引:
+Define an index:
```php
class ProductIndex extends Index
{
- protected $name = 'products'; // 索引名(必填)
- protected $mappings = [ // 索引 mappings
+ protected string $name = 'products'; // index name (required)
+ protected array $mappings = [ // index mappings
'properties' => [
'title' => ['type' => 'text'],
'price' => ['type' => 'float'],
'status' => ['type' => 'keyword'],
],
];
- protected $settings = [ // 索引 settings
+ protected array $settings = [ // index settings
'number_of_shards' => 1,
];
- protected $connection = 'main'; // 连接名(默认 'default')
+ protected string $connection = 'main'; // connection name (default 'default')
- public function rebuildName(): string // 重建后的真实索引名(可重写自定义)
+ public function rebuildName(): string // the real index name after rebuild (override to customize)
{
return $this->name . '_' . date('Ymd_His');
}
}
```
-## 搜索
+## Search
-`query()` 返回新的 Search 实例。链式调用 DSL 方法,然后执行:
+`query()` returns a new Search instance. Chain DSL methods, then execute:
```php
$results = ProductIndex::query()
@@ -56,149 +56,212 @@ $results = ProductIndex::query()
->size(20)
->get();
-$results->total(); // 命中数
-$results->docs(); // _source 数组
-$results->hits(); // 完整 hit 数组
-$results->aggregations(); // 聚合结果
+$results->total(); // hit count (null unless the index sets $trackTotalHits = true)
+$results->docs(); // array of _source
+$results->hits(); // full hit array
+$results->aggregations(); // aggregation results
```
```php
-// 仅返回第一条(内部设置 size=1)
+// return only the first (internally sets size=1)
$doc = ProductIndex::query()->match('title', 'test')->first();
-// 不获取文档,只统计数量
+// don't fetch docs, just count
$total = ProductIndex::query()->term('status', 'published')->count();
-// 聚合快捷方法(内部设置 size=0)
+// aggregation shortcuts (internally set size=0)
$avg = ProductIndex::query()->avg('price');
$max = ProductIndex::query()->max('price');
$min = ProductIndex::query()->min('price');
$sum = ProductIndex::query()->sum('price');
```
-## 分页
+## Pagination
```php
-// 手动分页
+use ElasticKit\Index\Support\Pagination;
+
+// manual pagination
$results = ProductIndex::query()->paginate($page, $perPage);
-// 自动从请求解析
-Index::setPageResolver(function () {
+// auto-resolve from the request
+Pagination::setPageResolver(function () {
return [request('page', 1), request('per_page', 20)];
});
$results = ProductIndex::query()->paginate();
-// 对接框架分页器
-Index::setPaginatorResolver(function ($results, $page, $perPage) {
+// wire up a framework paginator
+Pagination::setPaginatorResolver(function ($results, $page, $perPage) {
return new LengthAwarePaginator($results->docs(), $results->total(), $perPage, $page);
});
$results->toPaginator();
```
+> **Total tracking is opt-in.** `Index` defaults `$trackTotalHits = false`, so Elasticsearch omits the hit total: `total()`/`lastPage()` return `null` and `toPaginator()` throws. For length-aware pagination, enable it on the index — `true` counts every hit, an int caps the count (e.g. 5000):
+>
+> ```php
+> class ProductIndex extends Index
+> {
+> protected int|bool $trackTotalHits = true;
+> }
+> ```
+>
+> Otherwise use total-less pagination: `hasMorePages()` (full-page heuristic) or `chunk()` / `cursor()`. Deep pagination past 10,000 (`from + size > max_result_window`) is rejected by Elasticsearch — use `chunk()` (scroll) there.
+
## Scroll
-大数据集使用 scroll 分批获取:
+For large datasets, use scroll to fetch in batches:
```php
-// 首批(默认 size=1000)
+// first batch (default size=1000)
$results = ProductIndex::query()->size(500)->scroll();
$total = $results->total();
$scrollId = $results->scrollId();
-// 继续获取
+// keep fetching
while (count($results->docs()) > 0) {
- // 处理 $results->docs()...
+ // process $results->docs()...
$results = ProductIndex::query()->scroll($scrollId);
$scrollId = $results->scrollId();
}
-// 完成后清理
-ProductIndex::query()->clear($scrollId);
+// clear when done
+ProductIndex::query()->clear($results);
```
-## Cursor
+## Chunk / Cursor
-Cursor 把 scroll 封装成 PHP 生成器:
+Wraps scroll into a PHP generator; the scroll is cleared automatically.
+
+**chunk** iterates by batch, yielding a Results each time (with docs/hits/total etc.):
```php
-foreach (ProductIndex::query()->cursor() as $results) {
+foreach (ProductIndex::query()->chunk() as $results) {
foreach ($results->docs() as $doc) {
- // 处理
+ // process
}
}
-// scroll 自动清理
```
-## 文档 CRUD
+**cursor** iterates per hit, yielding one full hit each time (_id/_score/_source):
+
+```php
+foreach (ProductIndex::query()->cursor() as $hit) {
+ $doc = $hit['_source'];
+ $id = $hit['_id'];
+}
+```
+
+## Document CRUD
```php
$doc = ProductIndex::doc(1);
$doc->create(['title' => 'New Product', 'price' => 29.99]);
-$doc->source(); // 获取 _source 数组
+$doc->source(); // get the _source array
$doc->update(['price' => 39.99]);
-// 带冲突重试的更新
+// update with conflict retry
$doc->retryOnConflict(3)->update(['price' => 39.99]);
$doc->delete();
```
-`update()` 默认不使用 upsert 语义——文档不存在时会报错。传入 `true` 启用 upsert:
+`update()` does not use upsert semantics by default — it throws if the document doesn't exist. Pass `true` to enable upsert:
```php
-$doc->update(['price' => 39.99]); // 文档不存在时报错
-$doc->update(['price' => 39.99], true); // 文档不存在时自动创建
+$doc->update(['price' => 39.99]); // throws if the document doesn't exist
+$doc->update(['price' => 39.99], true); // creates it if it doesn't exist
```
-## 批量操作
+## Bulk operations
+
+Bulk is a buffer: `index()/create()/update()/delete()` only enqueue; **`flush()` is what sends**.
```php
use ElasticKit\Index\Bulk;
$bulk = new Bulk(new ProductIndex());
-$bulk->batchSize(500);
$bulk->index(1, ['title' => 'Product A']);
$bulk->index(2, ['title' => 'Product B']);
$bulk->delete(3);
-$bulk->execute(); // 执行所有操作,执行后清空状态
+$bulk->flush(); // send and clear the buffer
```
-## 零停机重建
+`batchSize(N)` enables **auto-flush**: when the buffer reaches N it sends automatically (default 0 = off, pure buffering). Use it for large imports to avoid piling up memory; after the loop you **still need `flush()` to send the tail**:
+
+```php
+$bulk = (new Bulk(new ProductIndex()))->batchSize(500);
+foreach ($docs as $id => $doc) {
+ $bulk->index($id, $doc); // auto-flushes at 500
+}
+$bulk->flush(); // the tail (< 500)
+```
-创建新索引 → 导入数据 → 切换别名。
+### Error handling
-`$name` 始终是应用面向的名称。应用不需要更改使用的名称——所有 CRUD、搜索、批量操作始终指向 `$name`。重建后,`$name` 变成别名,指向由 `rebuildName()` 生成的新索引。
+`flush()` throws a `RuntimeException` by default when the response contains errors. Use `onError()` to customize — the callback receives three raw materials and decides what to do:
+
+- `$response` — the raw ES response (`items[]` carry per-item status/error)
+- `$body` — the full original batch (successes included, native ES format)
+- `$newbulk` — a fresh Bulk bound to the same index + target, for re-sending failures
+
+Inside the callback **don't throw (return) → treated as handled, this batch is cleared and we continue; throw → abort, this batch is preserved** for the caller.
+
+```php
+// no onError → throws RuntimeException on error
+$bulk->flush();
+
+// with onError → handle failures yourself (you can re-send)
+$bulk->onError(function (array $response, array $body, Bulk $newbulk) {
+ // items[k] ↔ the k-th action; pick out the failures and re-send (simple alignment for a pure-index batch)
+ foreach ($response['items'] as $i => $item) {
+ $meta = $item[array_key_first($item)];
+ if (($meta['status'] ?? 200) >= 400) {
+ $newbulk->index($meta['_id'], $body[$i * 2 + 1]);
+ }
+ }
+ $newbulk->flush();
+})->flush();
+```
+
+> Errors from a `batchSize` auto-flush also go through `onError`.
+
+## Zero-downtime rebuild
+
+Create a new index → import data → swap the alias.
+
+`$name` is always the application-facing name. The application never needs to change which name it uses — all CRUD, search, and bulk operations always target `$name`. After a rebuild, `$name` becomes an alias pointing at the new index generated by `rebuildName()`.
```php
use ElasticKit\Index\Rebuild;
$rebuild = new Rebuild(new ProductIndex());
-// 重建:返回新旧索引名
+// rebuild: returns the new and old index names
$result = $rebuild->batchSize(500)->run();
// $result = ['newIndex' => 'products_20250601_120000', 'oldIndex' => 'products_20250531_090000']
-// 确认无误后清理旧索引
+// once confirmed, clean up the old index
$rebuild->clean($result['oldIndex']);
-// 或出问题时回滚
+// or roll back if something went wrong
$rebuild->rollback($result['oldIndex']);
```
-### 工作原理
+### How it works
-`run()` 自动检测当前状态:
+`run()` auto-detects the current state:
-1. **$name 已是别名**(后续重建):原子别名切换,零停机
-2. **$name 是真实索引**:抛出 `RuntimeException`,必须先手动删除或转换为别名模式
-3. **$name 不存在**:创建新索引并设置别名
+1. **$name is already an alias** (subsequent rebuilds): atomic alias swap, zero downtime
+2. **$name is a real index**: throws a `RuntimeException`; you must first delete it manually or convert to alias mode
+3. **$name doesn't exist**: creates a new index and sets up the alias
-重建后 `$name` 变成别名,指向 `rebuildName()` 生成的新索引。旧索引保留,由你决定 `clean()` 或 `rollback()`。
+After a rebuild `$name` becomes an alias pointing at the new index generated by `rebuildName()`. The old index is kept; you decide whether to `clean()` or `rollback()`.
-### 自定义命名
+### Custom naming
-重写 `rebuildName()` 自定义新索引命名:
+Override `rebuildName()` to customize the new index name:
```php
class ProductIndex extends Index
@@ -210,9 +273,9 @@ class ProductIndex extends Index
}
```
-### 数据源
+### Data source
-在 Index 子类中重写 `source()` 提供重建数据。基类未重写时会抛异常:
+Override `source()` in an Index subclass to feed the rebuild. The base class throws if not overridden:
```php
class ProductIndex extends Index
@@ -226,7 +289,7 @@ class ProductIndex extends Index
}
```
-也可以在调用时传入自定义数据源:
+You can also pass a custom data source at call time:
```php
$rebuild->source(function () {
@@ -234,33 +297,32 @@ $rebuild->source(function () {
})->run();
```
-`run()` 接受可选的 `$context` 参数,传递给 `source()`:
+`run()` accepts an optional `$context` parameter, forwarded to `source()`:
```php
$rebuild->run(['after' => '2024-01-01']);
```
-### 错误处理
+### Error handling
-导入错误会触发 `rebuild.import.failed` 事件(始终),并抛出异常(默认)。使用 `skipErrors()` 抑制异常但保留事件通知:
+Rebuild uses Bulk internally for the import; `onError()` works the same as in [Bulk operations > Error handling](#error-handling):
```php
-Index::listen('rebuild.import.failed', function (Event $e) {
- Log::warning("重建导入错误", $e->response);
-});
-
-$rebuild->skipErrors()->run(); // 通过事件记录错误,不中断
+$rebuild->onError(function (array $response, array $body, Bulk $newbulk) {
+ Log::warning("Rebuild import error", $response);
+ // to re-send failures use $body + $newbulk, see "Bulk operations > Error handling"
+})->run();
```
-> rebuild 期间 DB 仍在变更,新索引是开始时刻的快照,建议 rebuild 后通过 `updated_at` 增量同步补齐。
+> During a rebuild the DB keeps changing; the new index is a snapshot of the start moment — after the rebuild, top it up incrementally via `updated_at`.
>
-> 新增字段后,对尚未建立 mapping 的字段执行 sort、agg、collapse 等操作会报错,需评估是否先 `putMapping()` 再部署。修改/删除字段需分阶段部署。
+> After adding fields, running sort/agg/collapse on fields that don't have a mapping yet will error; evaluate whether to `putMapping()` before deploying. Modifying/removing fields requires a staged deployment.
-## 参考
+## Reference
### Manager
-ES indices API 的薄代理。`new Manager($index)`,不会给 Index 添加方法:
+A thin proxy over the ES indices API. `new Manager($index)`; adds no methods to Index:
```php
use ElasticKit\Index\Manager;
@@ -268,25 +330,26 @@ use ElasticKit\Index\Manager;
$manager = new Manager(new ProductIndex());
```
-### 事件
+### Events
```php
-use ElasticKit\Index\Event;
+use ElasticKit\Index\Support\Event;
+use ElasticKit\Index\Support\EventDispatcher;
-Index::listen('search.query.after', function (Event $e) {
+EventDispatcher::listen('search.query.after', function (Event $e) {
Log::info("{$e->name} on {$e->index}", ['duration' => $e->duration]);
});
-// 通配符
-Index::listen('search.*', function (Event $e) { ... });
-Index::listen('*', function (Event $e) { ... });
+// wildcards
+EventDispatcher::listen('search.*', function (Event $e) { /* ... */ });
+EventDispatcher::listen('*', function (Event $e) { /* ... */ });
```
-所有事件携带 `$name` 和 `$index`。
+All events carry `$name` and `$index`.
-### 自定义 Client
+### Custom client
-使用 `ClientBuilder` 配置客户端(主机、SSL、日志等):
+Use the `ClientBuilder` to configure the client (hosts, SSL, logging, etc.):
```php
$client = \Elastic\Elasticsearch\ClientBuilder::create()
@@ -297,52 +360,52 @@ $client = \Elastic\Elasticsearch\ClientBuilder::create()
Index::setClient($client);
```
-## 安全
+## Security
-以下方法接受原生 ES 参数,**不得**直接接收用户输入:
+The following methods accept raw ES parameters and **must never** receive user input directly:
-- `script()`, `scriptScore()`, `scriptFields()`, `runtimeMappings()` — Painless 脚本执行
-- `Bulk::target()` — 目标索引覆盖
-- `sort()` 使用 `_script` 类型 — 通过排序执行脚本
-- `postFilter()` — 原生查询透传
+- `script()`, `scriptScore()`, `scriptFields()`, `runtimeMappings()` — Painless script execution
+- `Bulk::target()` — target index override
+- `sort()` with the `_script` type — script execution via sorting
+- `postFilter()` — raw query pass-through
-务必在传入 DSL 方法前验证和过滤用户输入。
+Always validate and filter user input before passing it to DSL methods.
-## 速查表
+## Cheat sheet
-### Manager 方法
+### Manager methods
-| 方法 | 说明 |
+| Method | Description |
|------|------|
-| `create()` | 创建索引(含 mappings 和 settings) |
-| `delete()` | 删除索引 |
-| `exists()` | 检查索引是否存在 |
-| `get()` | 获取索引信息 |
-| `open()` | 打开索引 |
-| `close()` | 关闭索引 |
-| `putMapping()` | 更新索引 mappings(使用 Index 定义) |
-| `getMapping()` | 获取索引 mappings |
-| `putSettings($settings)` | 更新索引 settings |
-| `getSettings()` | 获取索引 settings |
-| `refresh()` | 刷新索引 |
-| `forceMerge()` | 强制合并索引段 |
-| `addAlias($alias)` | 添加别名 |
-| `removeAlias($alias)` | 移除别名 |
-| `swapAlias($alias, $target)` | 切换别名指向 |
-| `getAliases()` | 获取索引别名 |
-
-### 事件列表
-
-所有事件携带 `$name` 和 `$index`。`$action` 是调用方法名:`get`、`first`、`count`、`scroll` 或 `paginate`。
-
-| 事件 | 属性 |
+| `create()` | Create the index (with mappings and settings) |
+| `delete()` | Delete the index |
+| `exists()` | Check whether the index exists |
+| `get()` | Get index info |
+| `open()` | Open the index |
+| `close()` | Close the index |
+| `putMapping()` | Update the index mappings (uses the Index definition) |
+| `getMapping()` | Get the index mappings |
+| `putSettings($settings)` | Update the index settings |
+| `getSettings()` | Get the index settings |
+| `refresh()` | Refresh the index |
+| `forceMerge()` | Force-merge index segments |
+| `addAlias($alias)` | Add an alias |
+| `removeAlias($alias)` | Remove an alias |
+| `swapAlias($alias, $target)` | Swap where an alias points |
+| `getAliases()` | Get the index's aliases |
+
+### Event list
+
+All events carry `$name` and `$index`. `$action` is the called method name: `get`, `first`, `count`, `scroll`, or `paginate`.
+
+| Event | Properties |
|------|------|
| `search.query.before` | `$dsl`, `$action` |
| `search.query.after` | `$dsl`, `$response`, `$duration`, `$action` |
| `search.scroll.before` | `$action`, `$scrollId` |
| `search.scroll.after` | `$action`, `$scrollId`, `$response`, `$duration` |
-| `bulk.execute.before` | `$actions` |
-| `bulk.execute.after` | `$actions`, `$response`, `$duration` |
+| `bulk.flush.before` | `$actions` |
+| `bulk.flush.after` | `$actions`, `$response`, `$duration` |
| `manager.create.before` | |
| `manager.create.after` | `$response` |
| `manager.delete.before` | |
@@ -351,4 +414,3 @@ Index::setClient($client);
| `manager.swap_alias.after` | `$response` |
| `rebuild.run.before` | |
| `rebuild.run.after` | `$newIndex`, `$oldIndex` |
-| `rebuild.import.failed` | `$response` |
diff --git a/docs/index.zh.md b/docs/index.zh.md
new file mode 100644
index 0000000..2a40120
--- /dev/null
+++ b/docs/index.zh.md
@@ -0,0 +1,416 @@
+# Index
+
+Index 是抽象基类。继承它定义索引,注册 ES Client,然后查询。
+
+## 配置
+
+```php
+use ElasticKit\Index\Index;
+
+// 创建官方 Client
+$client = \Elastic\Elasticsearch\ClientBuilder::create()
+ ->setHosts(['http://localhost:9200'])
+ ->build();
+
+// 注册为默认连接
+Index::setClient($client);
+
+// 多连接
+Index::setClient($mainClient, 'main');
+Index::setClient($logClient, 'logs');
+```
+
+定义索引:
+
+```php
+class ProductIndex extends Index
+{
+ protected string $name = 'products'; // 索引名(必填)
+ protected array $mappings = [ // 索引 mappings
+ 'properties' => [
+ 'title' => ['type' => 'text'],
+ 'price' => ['type' => 'float'],
+ 'status' => ['type' => 'keyword'],
+ ],
+ ];
+ protected array $settings = [ // 索引 settings
+ 'number_of_shards' => 1,
+ ];
+ protected string $connection = 'main'; // 连接名(默认 'default')
+
+ public function rebuildName(): string // 重建后的真实索引名(可重写自定义)
+ {
+ return $this->name . '_' . date('Ymd_His');
+ }
+}
+```
+
+## 搜索
+
+`query()` 返回新的 Search 实例。链式调用 DSL 方法,然后执行:
+
+```php
+$results = ProductIndex::query()
+ ->match('title', 'elasticsearch')
+ ->sort('price', 'asc')
+ ->size(20)
+ ->get();
+
+$results->total(); // 命中数(索引未设 $trackTotalHits = true 时为 null)
+$results->docs(); // _source 数组
+$results->hits(); // 完整 hit 数组
+$results->aggregations(); // 聚合结果
+```
+
+```php
+// 仅返回第一条(内部设置 size=1)
+$doc = ProductIndex::query()->match('title', 'test')->first();
+
+// 不获取文档,只统计数量
+$total = ProductIndex::query()->term('status', 'published')->count();
+
+// 聚合快捷方法(内部设置 size=0)
+$avg = ProductIndex::query()->avg('price');
+$max = ProductIndex::query()->max('price');
+$min = ProductIndex::query()->min('price');
+$sum = ProductIndex::query()->sum('price');
+```
+
+## 分页
+
+```php
+use ElasticKit\Index\Support\Pagination;
+
+// 手动分页
+$results = ProductIndex::query()->paginate($page, $perPage);
+
+// 自动从请求解析
+Pagination::setPageResolver(function () {
+ return [request('page', 1), request('per_page', 20)];
+});
+$results = ProductIndex::query()->paginate();
+
+// 对接框架分页器
+Pagination::setPaginatorResolver(function ($results, $page, $perPage) {
+ return new LengthAwarePaginator($results->docs(), $results->total(), $perPage, $page);
+});
+$results->toPaginator();
+```
+
+> **总数追踪默认关闭。** `Index` 默认 `$trackTotalHits = false`,ES 不返命中总数:`total()`/`lastPage()` 返 `null`、`toPaginator()` 抛异常。需要带页码的分页时,在索引上开启——`true` 精确计数、int 限制计数上限(如 5000):
+>
+> ```php
+> class ProductIndex extends Index
+> {
+> protected int|bool $trackTotalHits = true;
+> }
+> ```
+>
+> 否则用无总数分页:`hasMorePages()`(满页启发式)或 `chunk()` / `cursor()`。深翻页超过 1 万(`from + size > max_result_window`)会被 ES 拒绝——那里用 `chunk()`(scroll)。
+
+## Scroll
+
+大数据集使用 scroll 分批获取:
+
+```php
+// 首批(默认 size=1000)
+$results = ProductIndex::query()->size(500)->scroll();
+$total = $results->total();
+$scrollId = $results->scrollId();
+
+// 继续获取
+while (count($results->docs()) > 0) {
+ // 处理 $results->docs()...
+ $results = ProductIndex::query()->scroll($scrollId);
+ $scrollId = $results->scrollId();
+}
+
+// 完成后清理
+ProductIndex::query()->clear($results);
+```
+
+## Chunk / Cursor
+
+把 scroll 封装成 PHP 生成器,scroll 自动清理。
+
+**chunk** 按批遍历,每次 yield 一个 Results(含 docs/hits/total 等):
+
+```php
+foreach (ProductIndex::query()->chunk() as $results) {
+ foreach ($results->docs() as $doc) {
+ // 处理
+ }
+}
+```
+
+**cursor** 逐条遍历,每次 yield 一个完整 hit(_id/_score/_source):
+
+```php
+foreach (ProductIndex::query()->cursor() as $hit) {
+ $doc = $hit['_source'];
+ $id = $hit['_id'];
+}
+```
+
+## 文档 CRUD
+
+```php
+$doc = ProductIndex::doc(1);
+
+$doc->create(['title' => 'New Product', 'price' => 29.99]);
+$doc->source(); // 获取 _source 数组
+$doc->update(['price' => 39.99]);
+
+// 带冲突重试的更新
+$doc->retryOnConflict(3)->update(['price' => 39.99]);
+
+$doc->delete();
+```
+
+`update()` 默认不使用 upsert 语义——文档不存在时会报错。传入 `true` 启用 upsert:
+
+```php
+$doc->update(['price' => 39.99]); // 文档不存在时报错
+$doc->update(['price' => 39.99], true); // 文档不存在时自动创建
+```
+
+## 批量操作
+
+Bulk 是一个缓冲区:`index()/create()/update()/delete()` 只入队,**`flush()` 才发送**。
+
+```php
+use ElasticKit\Index\Bulk;
+
+$bulk = new Bulk(new ProductIndex());
+$bulk->index(1, ['title' => 'Product A']);
+$bulk->index(2, ['title' => 'Product B']);
+$bulk->delete(3);
+$bulk->flush(); // 发送并清空缓冲
+```
+
+`batchSize(N)` 开启**自动 flush**:缓冲达到 N 时自动发送(默认 0 = 关闭,纯缓冲)。大导入用它避免一次性堆积内存;循环结束后**仍需 `flush()` 发送尾部**:
+
+```php
+$bulk = (new Bulk(new ProductIndex()))->batchSize(500);
+foreach ($docs as $id => $doc) {
+ $bulk->index($id, $doc); // 满 500 自动 flush
+}
+$bulk->flush(); // 尾部(< 500 那批)
+```
+
+### 错误处理
+
+`flush()` 默认在响应包含错误时抛出 `RuntimeException`。用 `onError()` 自定义处理——回调收到三样原材料,自行决定:
+
+- `$response` — ES 原始响应(`items[]` 带逐条 status/error)
+- `$body` — 完整原始批次(含成功项,native ES 格式)
+- `$newbulk` — 一个新的、绑定同索引+目标 的 Bulk,用于重投失败项
+
+回调内**不抛(返回)→ 视为已处理,本批清空、继续;抛出 → 中断、本批保留**给调用方。
+
+```php
+// 不设 onError → 有错误就抛 RuntimeException
+$bulk->flush();
+
+// 设 onError → 自行处理失败项(可重投)
+$bulk->onError(function (array $response, array $body, Bulk $newbulk) {
+ // items[k] ↔ 第 k 个 action;把失败的挑出来重投(此处为纯 index 批次的简易对齐)
+ foreach ($response['items'] as $i => $item) {
+ $meta = $item[array_key_first($item)];
+ if (($meta['status'] ?? 200) >= 400) {
+ $newbulk->index($meta['_id'], $body[$i * 2 + 1]);
+ }
+ }
+ $newbulk->flush();
+})->flush();
+```
+
+> `batchSize` 自动 flush 触发的错误同样走 `onError`。
+
+## 零停机重建
+
+创建新索引 → 导入数据 → 切换别名。
+
+`$name` 始终是应用面向的名称。应用不需要更改使用的名称——所有 CRUD、搜索、批量操作始终指向 `$name`。重建后,`$name` 变成别名,指向由 `rebuildName()` 生成的新索引。
+
+```php
+use ElasticKit\Index\Rebuild;
+
+$rebuild = new Rebuild(new ProductIndex());
+
+// 重建:返回新旧索引名
+$result = $rebuild->batchSize(500)->run();
+// $result = ['newIndex' => 'products_20250601_120000', 'oldIndex' => 'products_20250531_090000']
+
+// 确认无误后清理旧索引
+$rebuild->clean($result['oldIndex']);
+
+// 或出问题时回滚
+$rebuild->rollback($result['oldIndex']);
+```
+
+### 工作原理
+
+`run()` 自动检测当前状态:
+
+1. **$name 已是别名**(后续重建):原子别名切换,零停机
+2. **$name 是真实索引**:抛出 `RuntimeException`,必须先手动删除或转换为别名模式
+3. **$name 不存在**:创建新索引并设置别名
+
+重建后 `$name` 变成别名,指向 `rebuildName()` 生成的新索引。旧索引保留,由你决定 `clean()` 或 `rollback()`。
+
+### 自定义命名
+
+重写 `rebuildName()` 自定义新索引命名:
+
+```php
+class ProductIndex extends Index
+{
+ public function rebuildName(): string
+ {
+ return $this->name . '_v' . time();
+ }
+}
+```
+
+### 数据源
+
+在 Index 子类中重写 `source()` 提供重建数据。基类未重写时会抛异常:
+
+```php
+class ProductIndex extends Index
+{
+ public function source(array $context = []): iterable
+ {
+ foreach (Product::all() as $product) {
+ yield $product->id => $product->toArray();
+ }
+ }
+}
+```
+
+也可以在调用时传入自定义数据源:
+
+```php
+$rebuild->source(function () {
+ yield 1 => ['title' => 'test'];
+})->run();
+```
+
+`run()` 接受可选的 `$context` 参数,传递给 `source()`:
+
+```php
+$rebuild->run(['after' => '2024-01-01']);
+```
+
+### 错误处理
+
+Rebuild 内部使用 Bulk 执行导入,`onError()` 用法与 [批量操作 > 错误处理](#错误处理) 一致:
+
+```php
+$rebuild->onError(function (array $response, array $body, Bulk $newbulk) {
+ Log::warning("重建导入错误", $response);
+ // 需要重投失败项时用 $body + $newbulk,见「批量操作 > 错误处理」
+})->run();
+```
+
+> rebuild 期间 DB 仍在变更,新索引是开始时刻的快照,建议 rebuild 后通过 `updated_at` 增量同步补齐。
+>
+> 新增字段后,对尚未建立 mapping 的字段执行 sort、agg、collapse 等操作会报错,需评估是否先 `putMapping()` 再部署。修改/删除字段需分阶段部署。
+
+## 参考
+
+### Manager
+
+ES indices API 的薄代理。`new Manager($index)`,不会给 Index 添加方法:
+
+```php
+use ElasticKit\Index\Manager;
+
+$manager = new Manager(new ProductIndex());
+```
+
+### 事件
+
+```php
+use ElasticKit\Index\Support\Event;
+use ElasticKit\Index\Support\EventDispatcher;
+
+EventDispatcher::listen('search.query.after', function (Event $e) {
+ Log::info("{$e->name} on {$e->index}", ['duration' => $e->duration]);
+});
+
+// 通配符
+EventDispatcher::listen('search.*', function (Event $e) { ... });
+EventDispatcher::listen('*', function (Event $e) { ... });
+```
+
+所有事件携带 `$name` 和 `$index`。
+
+### 自定义 Client
+
+使用 `ClientBuilder` 配置客户端(主机、SSL、日志等):
+
+```php
+$client = \Elastic\Elasticsearch\ClientBuilder::create()
+ ->setHosts(['https://localhost:9200'])
+ ->setLogger($logger)
+ ->build();
+
+Index::setClient($client);
+```
+
+## 安全
+
+以下方法接受原生 ES 参数,**不得**直接接收用户输入:
+
+- `script()`, `scriptScore()`, `scriptFields()`, `runtimeMappings()` — Painless 脚本执行
+- `Bulk::target()` — 目标索引覆盖
+- `sort()` 使用 `_script` 类型 — 通过排序执行脚本
+- `postFilter()` — 原生查询透传
+
+务必在传入 DSL 方法前验证和过滤用户输入。
+
+## 速查表
+
+### Manager 方法
+
+| 方法 | 说明 |
+|------|------|
+| `create()` | 创建索引(含 mappings 和 settings) |
+| `delete()` | 删除索引 |
+| `exists()` | 检查索引是否存在 |
+| `get()` | 获取索引信息 |
+| `open()` | 打开索引 |
+| `close()` | 关闭索引 |
+| `putMapping()` | 更新索引 mappings(使用 Index 定义) |
+| `getMapping()` | 获取索引 mappings |
+| `putSettings($settings)` | 更新索引 settings |
+| `getSettings()` | 获取索引 settings |
+| `refresh()` | 刷新索引 |
+| `forceMerge()` | 强制合并索引段 |
+| `addAlias($alias)` | 添加别名 |
+| `removeAlias($alias)` | 移除别名 |
+| `swapAlias($alias, $target)` | 切换别名指向 |
+| `getAliases()` | 获取索引别名 |
+
+### 事件列表
+
+所有事件携带 `$name` 和 `$index`。`$action` 是调用方法名:`get`、`first`、`count`、`scroll` 或 `paginate`。
+
+| 事件 | 属性 |
+|------|------|
+| `search.query.before` | `$dsl`, `$action` |
+| `search.query.after` | `$dsl`, `$response`, `$duration`, `$action` |
+| `search.scroll.before` | `$action`, `$scrollId` |
+| `search.scroll.after` | `$action`, `$scrollId`, `$response`, `$duration` |
+| `bulk.flush.before` | `$actions` |
+| `bulk.flush.after` | `$actions`, `$response`, `$duration` |
+| `manager.create.before` | |
+| `manager.create.after` | `$response` |
+| `manager.delete.before` | |
+| `manager.delete.after` | `$response` |
+| `manager.swap_alias.before` | |
+| `manager.swap_alias.after` | `$response` |
+| `rebuild.run.before` | |
+| `rebuild.run.after` | `$newIndex`, `$oldIndex` |
diff --git a/docs/upgrade.md b/docs/upgrade.md
deleted file mode 100644
index 66ec6d0..0000000
--- a/docs/upgrade.md
+++ /dev/null
@@ -1,94 +0,0 @@
-# 升级指南
-
-## v7.x → v8.x
-
-### 环境要求
-
-| | v7.x | v8.x |
-|---|---|---|
-| PHP | 7.2+ | **8.1+** |
-| Elasticsearch | 7.x | **8.x** |
-| elasticsearch-php | ^7.0 | **^8.0** |
-
-```bash
-composer require ykan/elastickit:^8
-```
-
-### Client 配置
-
-命名空间从 `Elasticsearch\` 变更为 `Elastic\Elasticsearch\`:
-
-```php
-// v7.x
-$client = \Elasticsearch\ClientBuilder::create()
- ->setHosts(['http://localhost:9200'])
- ->build();
-
-// v8.x
-$client = \Elastic\Elasticsearch\ClientBuilder::create()
- ->setHosts(['https://localhost:9200'])
- ->setBasicAuthentication('elastic', 'password')
- ->build();
-```
-
-> ES 8.x 默认启用安全认证(TLS + 认证)。需使用 `https://` 并提供凭据。
-
-### 不兼容变更
-
-**1. 自定义 Client**
-
-`Client` 现在是 `final` 类——不可继承。使用 `ClientBuilder` 配置代替:
-
-```php
-// v7.x — 继承 Client 自定义
-class MyClient extends \Elasticsearch\Client { ... }
-
-// v8.x — 通过 ClientBuilder 配置
-$client = \Elastic\Elasticsearch\ClientBuilder::create()
- ->setHosts([...])
- ->setLogger($logger)
- ->setSSLVerification(false)
- ->build();
-```
-
-**2. 响应对象**
-
-elasticsearch-php 8.x 返回 Response 对象而非数组。ElasticKit 内部已处理——Index 层所有方法(search、get、bulk 等)仍返回数组。**用户代码无需修改。**
-
-如果你在 ElasticKit 之外直接使用 elasticsearch-php 客户端:
-
-```php
-// v7.x — 返回数组
-$response = $client->search($params);
-$total = $response['hits']['total']['value'];
-
-// v8.x — 返回 Response 对象,使用 asArray()
-$response = $client->search($params);
-$total = $response['hits']['total']['value']; // ArrayAccess 仍可读取
-$total = $response->asArray()['hits']['total']['value']; // 显式转换
-```
-
-**3. GeoPolygon 查询**
-
-`geo_polygon` 查询在 ES 8.x 中已移除。使用 `geo_shape` 代替:
-
-```php
-// v7.x
-$query->geoPolygon('location', $points);
-
-// v8.x
-$query->geoShape('location', function ($s) {
- $s->shape($polygon, 'envelope');
-});
-```
-
-### 无需修改
-
-以下在 v7.x 和 v8.x 中完全一致:
-
-- DSL 查询构建(match、term、bool、range 等)
-- 聚合构建
-- Index 层所有 API(Search、Doc、Bulk、Manager、Rebuild)
-- 事件系统
-- `Query::make()`、`Agg::make()`、`Query::when()`
-- Index 子类定义($name、$mappings、$settings)
diff --git "a/docs/\345\256\236\346\210\230\346\214\207\345\215\227\350\247\204\345\210\222.md" "b/docs/\345\256\236\346\210\230\346\214\207\345\215\227\350\247\204\345\210\222.md"
deleted file mode 100644
index de3d7c6..0000000
--- "a/docs/\345\256\236\346\210\230\346\214\207\345\215\227\350\247\204\345\210\222.md"
+++ /dev/null
@@ -1,225 +0,0 @@
-# 实战指南规划
-
-以电商订单系统为例,展示 ElasticKit 在真实业务中的完整使用流程。
-
-> 灵感来源:真实生产环境的 ES 集成经验,简化后用于教学。
-
-## 场景设定
-
-电商后台,订单数据分散在多张表,需要搜索、筛选、统计、导出。核心需求:
-
-- **订单列表**:按状态、日期、金额、品类筛选,支持搜索订单号/商品名
-- **统计看板**:按月/品类统计销售额,按商家分组
-- **数据导出**:筛选结果批量导出 Excel
-- **实时同步**:数据库变更后 ES 自动更新
-
-### 技术难点
-
-1. **数据分散在多张表**(订单 + 用户 + 商品 + 商家),写入时必须关联组装
-2. **订单有嵌套明细**(items),ES 用 nested 类型存储
-3. **筛选条件多且动态**(10+ 个可选条件),需要灵活的条件构建器
-4. **统计和列表要联动**(同一组条件,既要分页列表又要聚合数据)
-5. **数据一致性**(数据库更新后 ES 必须同步,处理冲突重试)
-
----
-
-## 踩坑记录
-
-> 从生产环境提炼。只收录非显而易见的实战教训,ES 基础知识不在此列。
-
-### 数据建模
-
-**1. nested 要慎重**
-
-nested 查询比普通字段慢很多。真实项目中用了 nested 存流水明细,后来条件查询改成了平铺字段,放弃了嵌套查询。教训:**如果不需要对嵌套文档做独立筛选,就不要用 nested**,平铺即可。
-
-### 查询
-
-**2. 聚合 N+1 反模式**
-
-多组统计数据(如按不同状态分别求和)不要发 N 次查询。用一次查询 + 多个 `filter` 聚合桶,或 `aggs()` 嵌套聚合。真实项目账单统计发了 11 次查询,其实可以合并为 1 次。
-
-**3. 深分页 10000 限制**
-
-ES 默认 `max_result_window = 10000`。应对:
-
-- 用户翻页:限制最大页数
-- 批量导出:用 `cursor()`(scroll API)
-- 注意 `chunk()` 如果内部用 from/size,同样受限
-
-### 增量同步
-
-**4. 队列投递要延迟**
-
-ORM afterSave 触发时,MySQL 事务可能还没提交。队列 job 立即执行会读到旧数据。解决:延迟投递,等事务提交。
-
-**5. 队列 job 不要先删除再执行**
-
-先删除任务再执行 ES 更新,失败时任务就丢了。正确:执行成功后再删除。
-
-**6. Bulk API 不支持 retry_on_conflict**
-
-ES 单文档 update 支持 `retry_on_conflict`,但 Bulk API 的 update action 不支持。并发写入版本冲突只能靠应用层重试。ElasticKit 的 `Bulk::retryOnConflict()` 封装了这个参数。
-
-**7. 间接关联更新**
-
-商品改名了,订单里的 `product_name` 也要改。但订单 source() 查的是订单表,不是商品表。解决:商品变更 → 查关联订单 ID → 批量更新订单文档。
-
-```
-商品表变更 → SELECT id FROM 订单表 WHERE product_id = ? → source(['ids' => $ids]) → Bulk update
-```
-
-**8. doc_as_upsert 默认策略**
-
-更新时统一用 `doc_as_upsert: true`(不存在则插入,存在则更新),避免区分 insert/update 两种逻辑。
-
-**9. Canal 配置技巧**
-
-- **字段过滤**:只监听业务字段,忽略 `updated_at` 等无关变更
-- **事件过滤**:只监听 UPDATE,忽略 INSERT/DELETE
-- **去重**:同一批次内相同文档 ID 合并,避免重复入队
-
-### 数据一致性
-
-**10. 一致性校验是必要的**
-
-不管同步多完善,总会丢数据(进程崩溃、网络超时、冲突重试耗尽)。定时对比 DB 和 ES 关键字段,不一致就修复。真实项目每小时跑一次。
-
----
-
-## 阶段规划
-
-### 阶段 1:安装与配置
-
-**业务背景**:项目立项,先把 ES 连上。
-
-**内容**:
-- composer 安装
-- 注册 ES Client(Laravel ServiceProvider)
-- 验证连接
-
----
-
-### 阶段 2:设计索引
-
-**业务背景**:订单数据分散在 4 张表,ES 不支持 join,写入时关联组装。
-
-**内容**:
-- OrderIndex 类定义
-- mappings 设计:基本字段 + nested(订单明细)+ 关联字段(用户/商家/品类)
-- `source()` 方法:多表 join 组装文档数据
-- 增量查询支持:`source(['ids' => [...]])`
-
-**踩坑提醒**:坑 1(nested 慎重)
-
-**索引字段**(简化版,保留核心模式):
-
-```
-基本字段:id, order_no, user_id, merchant_id, status, total_amount,
- paid_at, created_at, is_cancelled
-
-关联字段(写入时从其他表组装):user_name, merchant_name, category_name
-
-嵌套文档:items (product_id, product_name, price, quantity)
-```
-
----
-
-### 阶段 3:首次导入
-
-**业务背景**:索引设计好了,把现有数据灌进去。
-
-**内容**:
-- Rebuild::run() 全量导入
-- batchSize 调优
-- 查看结果验证
-
----
-
-### 阶段 4:搜索与筛选
-
-**业务背景**:运营要一个订单查询页面,条件多且动态。
-
-**内容**:
-- 条件构建器模式(从真实代码提炼)
-- `term`(精确匹配)/ `terms`(多值匹配)/ `range`(日期范围)/ `wildcard`(模糊搜索)
-- `bool should` 实现多字段 OR 搜索
-- `when()` 条件查询
-- 分页与排序
-
-**踩坑提醒**:坑 3(深分页限制)
-
----
-
-### 阶段 5:聚合统计
-
-**业务背景**:管理看板需要按月/品类统计销售额,筛选条件和列表页联动。
-
-**内容**:
-- 聚合 + 列表一次查询(同一条件,`size(0)` 只取聚合)
-- terms 桶聚合:按品类分组
-- date_histogram:按月分组,带时区和补零
-- stats/sum:金额统计
-- 嵌套聚合:品类 → 月份 → 销售额合计
-
-**踩坑提醒**:坑 2(聚合 N+1)
-
----
-
-### 阶段 6:增量同步
-
-**业务背景**:数据持续变更(发货、退款、改价),ES 要跟着更新。
-
-**内容**:
-- 同步流程:数据变更 → 提取 doc_ids → source(['ids']) 取最新数据 → Bulk update
-- 队列投递与延迟
-- 冲突重试
-- 间接关联更新(商品改名 → 关联订单全部更新)
-- doc_as_upsert 默认策略
-
-**踩坑提醒**:坑 4-9 全部涉及
-
----
-
-### 阶段 7:Schema 演进
-
-**业务背景**:产品要加字段(如新增"配送方式"),需要修改 mapping。
-
-**内容**:
-- 新增可空字段:`putMapping()` 直接加
-- Breaking 变更(改字段类型、改分词器):必须 Rebuild
-- 修改 `source()` 配合新字段
-- Rebuild 完整流程
-
----
-
-### 阶段 8:监控与数据一致性
-
-**业务背景**:上线后要监控搜索性能和同步延迟。
-
-**内容**:
-- 慢查询日志(事件监听)
-- 数据一致性校验:定时对比 DB 和 ES 关键字段,不一致则重新入队
-
-**踩坑提醒**:坑 10(一致性校验)
-
----
-
-## 覆盖的 ElasticKit 特性
-
-| 阶段 | 特性 |
-|---|---|
-| 1 | 安装、注册 Client |
-| 2 | Index 定义、mappings、nested、source()、multi-table join |
-| 3 | Rebuild::run()、Bulk、别名切换 |
-| 4 | term/terms/range/wildcard/bool/when、分页、排序 |
-| 5 | aggs/terms/stats/dateHistogram、嵌套聚合 |
-| 6 | Bulk update、source(['ids'])、冲突重试 |
-| 7 | putMapping、Rebuild(schema 变更) |
-| 8 | 事件监听、数据校验 |
-
-## 不在指南范围内的
-
-- Canal/binlog 监听部署(属于基础设施)
-- Laravel Queue 配置(属于框架知识)
-- ES 集群调优(属于运维范畴)
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index 400bc2b..284e2bc 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -1,5 +1,17 @@
parameters:
ignoreErrors:
+ -
+ message: '#^Parameter &\$store by\-ref type of method ElasticKit\\DSL\\Agg\:\:registerAgg\(\) expects array\, array\ given\.$#'
+ identifier: parameterByRef.type
+ count: 3
+ path: src/DSL/Agg.php
+
+ -
+ message: '#^Parameter &\$store by\-ref type of method ElasticKit\\DSL\\Query\:\:registerAgg\(\) expects array\, array\ given\.$#'
+ identifier: parameterByRef.type
+ count: 3
+ path: src/DSL/Query.php
+
-
message: '#^Call to an undefined method Elastic\\Elasticsearch\\ClientInterface\:\:bulk\(\)\.$#'
identifier: method.notFound
@@ -43,21 +55,39 @@ parameters:
path: src/Index/Doc.php
-
- message: '#^Call to an undefined method Elastic\\Elasticsearch\\ClientInterface\:\:index\(\)\.$#'
+ message: '#^Call to an undefined method Elastic\\Elasticsearch\\ClientInterface\:\:indices\(\)\.$#'
+ identifier: method.notFound
+ count: 17
+ path: src/Index/Manager.php
+
+ -
+ message: '#^Call to an undefined method Elastic\\Elasticsearch\\ClientInterface\:\:delete\(\)\.$#'
+ identifier: method.notFound
+ count: 2
+ path: src/Index/Rebuild.php
+
+ -
+ message: '#^Call to an undefined method Elastic\\Elasticsearch\\ClientInterface\:\:exists\(\)\.$#'
identifier: method.notFound
count: 1
- path: src/Index/Index.php
+ path: src/Index/Rebuild.php
-
- message: '#^Call to an undefined method Elastic\\Elasticsearch\\ClientInterface\:\:indices\(\)\.$#'
+ message: '#^Call to an undefined method Elastic\\Elasticsearch\\ClientInterface\:\:index\(\)\.$#'
identifier: method.notFound
- count: 17
- path: src/Index/Manager.php
+ count: 1
+ path: src/Index/Rebuild.php
-
message: '#^Call to an undefined method Elastic\\Elasticsearch\\ClientInterface\:\:indices\(\)\.$#'
identifier: method.notFound
- count: 4
+ count: 5
+ path: src/Index/Rebuild.php
+
+ -
+ message: '#^If condition is always false\.$#'
+ identifier: if.alwaysFalse
+ count: 1
path: src/Index/Rebuild.php
-
@@ -75,7 +105,7 @@ parameters:
-
message: '#^Call to an undefined method Elastic\\Elasticsearch\\ClientInterface\:\:scroll\(\)\.$#'
identifier: method.notFound
- count: 2
+ count: 1
path: src/Index/Search.php
-
diff --git a/phpunit.xml b/phpunit.xml
index 944f10f..a4f6eaf 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -1,15 +1,12 @@
-
-
-
- tests
- tests/Index
-
-
- tests/Index
-
-
+
+
+
+ tests
+ tests/Integration
+
+
+ tests/Integration
+
+
diff --git a/src/DSL/Agg.php b/src/DSL/Agg.php
index cb7c565..6696c4e 100644
--- a/src/DSL/Agg.php
+++ b/src/DSL/Agg.php
@@ -1,11 +1,14 @@
*/
- protected $subAggs = [];
+ protected array $_subAggs = [];
/**
- * Properties for array-based aggregation definitions.
- * Supports nested Query, Node, and Agg instances (resolved by resolveProperties).
+ * Properties for array-based aggregation definitions (raw DSL mode).
+ * Null when the aggregation is node-based or empty.
*
* @var array|null
*/
- protected $_properties;
+ protected ?array $_properties = null;
/**
* Static factory — thin proxy over the constructor.
@@ -56,7 +57,7 @@ class Agg
* @param mixed $agg
* @return static
*/
- public static function create($agg = [])
+ public static function create($agg = []): static
{
if ($agg instanceof static) {
return $agg;
@@ -83,7 +84,7 @@ public static function create($agg = [])
* @param Node $node
* @return $this
*/
- protected function node($node)
+ protected function node(Node $node): static
{
$this->_node = $node;
$this->_properties = null;
@@ -96,12 +97,12 @@ protected function node($node)
* Similar to Node::field(), the alias wraps the output:
* {"alias_name": {"terms": {"field": "status"}}}.
*
- * @param string $alias
+ * @param string $value
* @return $this
*/
- public function alias($alias)
+ public function alias(string $value): static
{
- $this->_alias = $alias;
+ $this->_alias = $value;
return $this;
}
@@ -110,7 +111,7 @@ public function alias($alias)
*
* @return string|null
*/
- public function getAlias()
+ public function getAlias(): ?string
{
return $this->_alias;
}
@@ -125,50 +126,11 @@ public function getAlias()
*
* @param string|Agg|array $alias
* @param callable|Agg|array|null $aggs
- * @return $this
- * @throws \BadMethodCallException if called with a string alias and no definition
+ * @return static
*/
- public function aggs($alias, $aggs = null)
+ public function aggs($alias, $aggs = null): static
{
- if ($aggs === null && !is_string($alias)) {
- $aggs = $alias;
- $alias = null;
- }
-
- if ($aggs instanceof Agg) {
- if ($alias !== null) {
- $aggs->alias($alias);
- }
- $this->subAggs[$alias ?? $aggs->getAlias()] = $aggs;
- return $this;
- }
-
- if (is_array($aggs)) {
- $childAgg = Agg::create($aggs);
- if ($alias !== null) {
- $childAgg->alias($alias);
- }
- $this->subAggs[$alias] = $childAgg;
- return $this;
- }
-
- if ($alias !== null && !isset($this->subAggs[$alias])) {
- $this->subAggs[$alias] = new Agg();
- $this->subAggs[$alias]->alias($alias);
- }
-
- if ($aggs instanceof \Closure) {
- $aggs($this->subAggs[$alias]);
- return $this;
- }
-
- if ($alias !== null) {
- throw new BadMethodCallException(
- sprintf('aggs() requires a second argument. Use aggs("%s", $definition) where $definition is a closure, array, or Agg instance.', $alias)
- );
- }
-
- return $this;
+ return $this->registerAgg($alias, $aggs, $this->_subAggs);
}
/**
@@ -177,18 +139,22 @@ public function aggs($alias, $aggs = null)
* @param array $properties
* @return array
*/
- protected function resolveProperties(array $properties)
+ protected function resolveProperties(array $properties): array
{
foreach ($properties as $key => $property) {
if ($property instanceof Query) {
- $properties[$key] = $property->toArray()['query'];
+ $properties[$key] = $property->toArray()['query'] ?? null;
} elseif ($property instanceof Agg) {
$properties[$key] = $property->toArray();
} elseif ($property instanceof Node) {
$properties[$key] = $property->toArray();
+ } elseif ($property instanceof \Closure) {
+ $properties[$key] = Query::create($property)->toArray()['query'] ?? null;
+ } elseif (is_array($property)) {
+ $properties[$key] = $this->resolveProperties($property);
}
}
- return $properties;
+ return array_filter($properties, fn ($v) => $v !== null);
}
/**
@@ -199,12 +165,12 @@ protected function resolveProperties(array $properties)
*
* @return array
*/
- public function toArray()
+ public function toArray(): array
{
if ($this->_properties !== null) {
$resolved = $this->resolveProperties($this->_properties);
if ($this->_alias !== null) {
- return [$this->_alias => $resolved];
+ return [$this->_alias => ($resolved === [] ? new stdClass() : $resolved)];
}
return $resolved;
}
@@ -215,15 +181,15 @@ public function toArray()
$inner[$this->_node->key()] = $this->_node->toArray();
}
- if (!empty($this->subAggs)) {
+ if (!empty($this->_subAggs)) {
$inner['aggs'] = [];
- foreach ($this->subAggs as $subAgg) {
+ foreach ($this->_subAggs as $subAgg) {
$inner['aggs'] += $subAgg->toArray();
}
}
if ($this->_alias !== null) {
- return [$this->_alias => $inner];
+ return [$this->_alias => ($inner === [] ? new stdClass() : $inner)];
}
return $inner;
@@ -236,9 +202,11 @@ public function toArray()
* @param int $depth
* @return string
*/
- public function toJson($flags = JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT, $depth = 512)
+ public function toJson(int $flags = JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_PRESERVE_ZERO_FRACTION, int $depth = 512): string
{
- return json_encode($this->toArray(), $flags, $depth);
+ $json = json_encode($this->toArray(), $flags, $depth);
+
+ return $json === false ? '' : $json;
}
/**
@@ -246,7 +214,7 @@ public function toJson($flags = JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT, $dep
*
* @return string
*/
- public function __toString()
+ public function __toString(): string
{
return $this->toJson();
}
diff --git a/src/DSL/Aggs/Bucket.php b/src/DSL/Aggs/Bucket.php
index e31eb30..7519c2a 100644
--- a/src/DSL/Aggs/Bucket.php
+++ b/src/DSL/Aggs/Bucket.php
@@ -1,5 +1,7 @@
node(Terms::create(is_string($params) ? ['field' => $params] : $params));
+ return $this->node(Terms::create(is_string($value) ? ['field' => $value] : $value));
}
/**
* Defines a single bucket that limits documents matching a query.
*
- * @param mixed $filter
+ * @param mixed $value
* @return static
*/
- public function filter($filter)
+ public function filter($value): static
{
- $instance = new FilterAgg();
- $instance->setFilter($filter);
+ $instance = new Filter();
+ $instance->filter($value);
return $this->node($instance);
}
/**
* Defines multiple buckets from multiple filters, one per filter expression.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function filters($params)
+ public function filters($value): static
{
- return $this->node(Filters::create($params));
+ return $this->node(Filters::create($value));
}
/**
* Groups documents into buckets based on combinations of filter expressions.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function adjacencyMatrix($params)
+ public function adjacencyMatrix($value): static
{
- return $this->node(AdjacencyMatrix::create($params));
+ return $this->node(AdjacencyMatrix::create($value));
}
/**
* Automatically determines bucket intervals for date fields based on document count.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function autoDateHistogram($params)
+ public function autoDateHistogram($value): static
{
- return $this->node(AutoDateHistogram::create(is_string($params) ? ['field' => $params] : $params));
+ return $this->node(AutoDateHistogram::create(is_string($value) ? ['field' => $value] : $value));
}
/**
* Extracts categories from text fields by tokenizing and grouping values.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function categorizeText($params)
+ public function categorizeText($value): static
{
- return $this->node(CategorizeText::create(is_string($params) ? ['field' => $params] : $params));
+ return $this->node(CategorizeText::create(is_string($value) ? ['field' => $value] : $value));
}
/**
* Creates composite buckets from multiple source values, supporting pagination.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function composite($params)
+ public function composite($value): static
{
- return $this->node(Composite::create($params));
+ return $this->node(Composite::create($value));
}
/**
* Groups documents into buckets by date interval (e.g. per day, per month).
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function dateHistogram($params)
+ public function dateHistogram($value): static
{
- return $this->node(DateHistogram::create(is_string($params) ? ['field' => $params] : $params));
+ return $this->node(DateHistogram::create(is_string($value) ? ['field' => $value] : $value));
}
/**
* Groups documents into buckets by user-defined date ranges.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function dateRange($params)
+ public function dateRange($value): static
{
- return $this->node(DateRange::create(is_string($params) ? ['field' => $params] : $params));
+ return $this->node(DateRange::create(is_string($value) ? ['field' => $value] : $value));
}
/**
* Limits any child aggregations to a diversified sample of top-scoring documents.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function diversifiedSampler($params)
+ public function diversifiedSampler($value): static
{
- return $this->node(DiversifiedSampler::create($params));
+ return $this->node(DiversifiedSampler::create($value));
}
/**
* Finds frequently co-occurring item sets in array fields.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function frequentItemSets($params)
+ public function frequentItemSets($value): static
{
- return $this->node(FrequentItemSets::create($params));
+ return $this->node(FrequentItemSets::create($value));
}
/**
* Groups documents into buckets by distance ranges from a geo point.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function geoDistance($params)
+ public function geoDistance($value): static
{
- return $this->node(GeoDistance::create(is_string($params) ? ['field' => $params] : $params));
+ return $this->node(GeoDistance::create(is_string($value) ? ['field' => $value] : $value));
}
/**
* Groups documents into grid cells using geohash prefixes.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function geoHashGrid($params)
+ public function geoHashGrid($value): static
{
- return $this->node(GeoHashGrid::create(is_string($params) ? ['field' => $params] : $params));
+ return $this->node(GeoHashGrid::create(is_string($value) ? ['field' => $value] : $value));
}
/**
* Groups documents into grid cells using H3 hexagon indexes.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function geohexGrid($params)
+ public function geohexGrid($value): static
{
- return $this->node(GeohexGrid::create(is_string($params) ? ['field' => $params] : $params));
+ return $this->node(GeohexGrid::create(is_string($value) ? ['field' => $value] : $value));
}
/**
* Groups documents into grid cells using geotile prefixes.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function geotileGrid($params)
+ public function geotileGrid($value): static
{
- return $this->node(GeotileGrid::create(is_string($params) ? ['field' => $params] : $params));
+ return $this->node(GeotileGrid::create(is_string($value) ? ['field' => $value] : $value));
}
/**
@@ -208,173 +210,173 @@ public function geotileGrid($params)
*
* @return static
*/
- public function globalAggregation()
+ public function global(): static
{
- return $this->node(new GlobalAgg());
+ return $this->node(new Global_());
}
/**
* Groups documents into buckets by numeric interval.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function histogram($params)
+ public function histogram($value): static
{
- return $this->node(Histogram::create(is_string($params) ? ['field' => $params] : $params));
+ return $this->node(Histogram::create(is_string($value) ? ['field' => $value] : $value));
}
/**
* Groups documents into buckets by IP address prefix.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function ipPrefix($params)
+ public function ipPrefix($value): static
{
- return $this->node(IpPrefix::create(is_string($params) ? ['field' => $params] : $params));
+ return $this->node(IpPrefix::create(is_string($value) ? ['field' => $value] : $value));
}
/**
* Groups documents into buckets by user-defined IP address ranges.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function ipRange($params)
+ public function ipRange($value): static
{
- return $this->node(IpRange::create($params));
+ return $this->node(IpRange::create($value));
}
/**
* Creates a single bucket for documents missing a field value.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function missing($params)
+ public function missing($value): static
{
- return $this->node(Missing::create(is_string($params) ? ['field' => $params] : $params));
+ return $this->node(Missing::create(is_string($value) ? ['field' => $value] : $value));
}
/**
* Groups documents into buckets by multiple field term combinations.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function multiTerms($params)
+ public function multiTerms($value): static
{
- return $this->node(MultiTerms::create($params));
+ return $this->node(MultiTerms::create($value));
}
/**
* Aggregates on nested documents within a parent document.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function nested($params)
+ public function nested($value): static
{
- return $this->node(Nested::create($params));
+ return $this->node(Nested::create($value));
}
/**
* Aggregates on parent documents from a child document context in a join relation.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function parent($params)
+ public function parent($value): static
{
- return $this->node(ParentAgg::create($params));
+ return $this->node(Parent_::create($value));
}
/**
* Limits any child aggregations to a random sample of documents.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function randomSampler($params)
+ public function randomSampler($value): static
{
- return $this->node(RandomSampler::create($params));
+ return $this->node(RandomSampler::create($value));
}
/**
* Groups documents into buckets by user-defined numeric ranges.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function range($params)
+ public function range($value): static
{
- return $this->node(Range::create(is_string($params) ? ['field' => $params] : $params));
+ return $this->node(Range::create(is_string($value) ? ['field' => $value] : $value));
}
/**
* Groups documents into buckets by rare field values with low document counts.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function rareTerms($params)
+ public function rareTerms($value): static
{
- return $this->node(RareTerms::create(is_string($params) ? ['field' => $params] : $params));
+ return $this->node(RareTerms::create(is_string($value) ? ['field' => $value] : $value));
}
/**
* Aggregates on parent documents from within a nested aggregation context.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function reverseNested($params = [])
+ public function reverseNested($value = []): static
{
- return $this->node(ReverseNested::create($params));
+ return $this->node(ReverseNested::create($value));
}
/**
* Finds field values that are unusually common in a subset compared to the whole index.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function significantTerms($params)
+ public function significantTerms($value): static
{
- return $this->node(SignificantTerms::create(is_string($params) ? ['field' => $params] : $params));
+ return $this->node(SignificantTerms::create(is_string($value) ? ['field' => $value] : $value));
}
/**
* Finds significant terms from text field content without needing a sub-field.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function significantText($params)
+ public function significantText($value): static
{
- return $this->node(SignificantText::create(is_string($params) ? ['field' => $params] : $params));
+ return $this->node(SignificantText::create(is_string($value) ? ['field' => $value] : $value));
}
/**
* Groups documents into time series buckets for time-series data.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function timeSeries($params)
+ public function timeSeries($value): static
{
- return $this->node(TimeSeries::create($params));
+ return $this->node(TimeSeries::create($value));
}
/**
* Groups documents into dynamically sized histogram buckets based on data distribution.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function variableWidthHistogram($params)
+ public function variableWidthHistogram($value): static
{
- return $this->node(VariableWidthHistogram::create(is_string($params) ? ['field' => $params] : $params));
+ return $this->node(VariableWidthHistogram::create(is_string($value) ? ['field' => $value] : $value));
}
}
diff --git a/src/DSL/Aggs/Bucket/AdjacencyMatrix.php b/src/DSL/Aggs/Bucket/AdjacencyMatrix.php
index efcd155..7bd2425 100644
--- a/src/DSL/Aggs/Bucket/AdjacencyMatrix.php
+++ b/src/DSL/Aggs/Bucket/AdjacencyMatrix.php
@@ -1,5 +1,7 @@
*/
- protected $_filters;
+ protected string $_key = 'adjacency_matrix';
/**
- * Filters used to create buckets.
+ * Add a named filter used to create buckets.
*
* @param string $key
* @param mixed $query
* @return static
*/
- public function filters($key, $query)
+ public function filters($key, $query): static
{
- $this->_filters[$key] = Query::create($query);
- return $this->addProperty('filters', $this->_filters);
+ $this->_properties ??= [];
+ $this->_properties['filters'][$key] = Query::create($query);
+ return $this;
}
/**
* Separator used to concatenate filter names. Defaults to &.
*
- * @param string $separator
+ * @param string $value
* @return static
*/
- public function separator($separator)
+ public function separator(string $value): static
{
- return $this->addProperty('separator', $separator);
- }
-
- /**
- * {@inheritdoc}
- */
- public function toArray()
- {
- $properties = $this->_properties;
-
- if (isset($properties['filters'])) {
- foreach ($properties['filters'] as $key => $filter) {
- if ($filter instanceof Query) {
- $properties['filters'][$key] = $filter->toArray()['query'];
- }
- }
- }
-
- $properties = $this->resolveProperties($properties);
-
- if ($this->_isPropertyField) {
- return [$this->_field => $properties];
- }
- return $properties;
+ return $this->addProperty('separator', $value);
}
}
diff --git a/src/DSL/Aggs/Bucket/AutoDateHistogram.php b/src/DSL/Aggs/Bucket/AutoDateHistogram.php
index f2c1172..518ec28 100644
--- a/src/DSL/Aggs/Bucket/AutoDateHistogram.php
+++ b/src/DSL/Aggs/Bucket/AutoDateHistogram.php
@@ -1,5 +1,7 @@
addProperty('buckets', $buckets);
+ return $this->addProperty('buckets', $value);
}
/**
* Date format pattern for bucket keys.
*
- * @param string $format
+ * @param string $value
* @return static
*/
- public function format($format)
+ public function format(string $value): static
{
- return $this->addProperty('format', $format);
+ return $this->addProperty('format', $value);
}
/**
* Time zone for bucketing.
*
- * @param string $timeZone
+ * @param string $value
* @return static
*/
- public function timeZone($timeZone)
+ public function timeZone(string $value): static
{
- return $this->addProperty('time_zone', $timeZone);
+ return $this->addProperty('time_zone', $value);
}
/**
* Minimum interval to use when automatically determining buckets.
*
- * @param string $minimumInterval
+ * @param string $value
* @return static
*/
- public function minimumInterval($minimumInterval)
+ public function minimumInterval(string $value): static
{
- return $this->addProperty('minimum_interval', $minimumInterval);
+ return $this->addProperty('minimum_interval', $value);
}
/**
* Value to use for documents missing the field value.
*
- * @param mixed $missing
+ * @param mixed $value
* @return static
*/
- public function missing($missing)
+ public function missing($value): static
{
- return $this->addProperty('missing', $missing);
+ return $this->addProperty('missing', $value);
}
}
diff --git a/src/DSL/Aggs/Bucket/CategorizeText.php b/src/DSL/Aggs/Bucket/CategorizeText.php
index c54f088..91bcdb5 100644
--- a/src/DSL/Aggs/Bucket/CategorizeText.php
+++ b/src/DSL/Aggs/Bucket/CategorizeText.php
@@ -1,5 +1,7 @@
addProperty('categorization_analyzer', $categorizationAnalyzer);
+ return $this->addProperty('categorization_analyzer', $value);
}
/**
* Filters applied to each token before categorization.
*
- * @param array $categorizationFilters
+ * @param array $value
* @return static
*/
- public function categorizationFilters($categorizationFilters)
+ public function categorizationFilters(array $value): static
{
- return $this->addProperty('categorization_filters', $categorizationFilters);
+ return $this->addProperty('categorization_filters', $value);
}
/**
* Maximum number of matched tokens to consider.
*
- * @param int $maxMatchedTokens
+ * @param int $value
* @return static
*/
- public function maxMatchedTokens($maxMatchedTokens)
+ public function maxMatchedTokens(int $value): static
{
- return $this->addProperty('max_matched_tokens', $maxMatchedTokens);
+ return $this->addProperty('max_matched_tokens', $value);
}
/**
* Maximum number of unique tokens to consider.
*
- * @param int $maxUniqueTokens
+ * @param int $value
* @return static
*/
- public function maxUniqueTokens($maxUniqueTokens)
+ public function maxUniqueTokens(int $value): static
{
- return $this->addProperty('max_unique_tokens', $maxUniqueTokens);
+ return $this->addProperty('max_unique_tokens', $value);
}
/**
* Minimum document count per bucket.
*
- * @param int $minDocCount
+ * @param int $value
* @return static
*/
- public function minDocCount($minDocCount)
+ public function minDocCount(int $value): static
{
- return $this->addProperty('min_doc_count', $minDocCount);
+ return $this->addProperty('min_doc_count', $value);
}
/**
* Minimum document count per shard.
*
- * @param int $shardMinDocCount
+ * @param int $value
* @return static
*/
- public function shardMinDocCount($shardMinDocCount)
+ public function shardMinDocCount(int $value): static
{
- return $this->addProperty('shard_min_doc_count', $shardMinDocCount);
+ return $this->addProperty('shard_min_doc_count', $value);
}
/**
* Number of categories to return from each shard.
*
- * @param int $shardSize
+ * @param int $value
* @return static
*/
- public function shardSize($shardSize)
+ public function shardSize(int $value): static
{
- return $this->addProperty('shard_size', $shardSize);
+ return $this->addProperty('shard_size', $value);
}
/**
* Similarity threshold for grouping categories.
*
- * @param float $similarityThreshold
+ * @param float $value
* @return static
*/
- public function similarityThreshold($similarityThreshold)
+ public function similarityThreshold(float $value): static
{
- return $this->addProperty('similarity_threshold', $similarityThreshold);
+ return $this->addProperty('similarity_threshold', $value);
}
/**
* Maximum number of categories to return.
*
- * @param int $size
+ * @param int $value
* @return static
*/
- public function size($size)
+ public function size(int $value): static
{
- return $this->addProperty('size', $size);
+ return $this->addProperty('size', $value);
}
}
diff --git a/src/DSL/Aggs/Bucket/Composite.php b/src/DSL/Aggs/Bucket/Composite.php
index 681ef95..c261f50 100644
--- a/src/DSL/Aggs/Bucket/Composite.php
+++ b/src/DSL/Aggs/Bucket/Composite.php
@@ -1,5 +1,7 @@
> $value
* @return static
*/
- public function sources($sources)
+ public function sources(array $value): static
{
- return $this->addProperty('sources', $sources);
+ return $this->addProperty('sources', $value);
}
/**
* Cursor value to resume pagination after a previous composite response.
*
- * @param mixed $after
+ * @param mixed $value
* @return static
*/
- public function after($after)
+ public function after($value): static
{
- return $this->addProperty('after', $after);
+ return $this->addProperty('after', $value);
}
/**
* Sort order for composite buckets.
*
- * @param mixed $order
+ * @param mixed $value
* @return static
*/
- public function order($order)
+ public function order($value): static
{
- return $this->addProperty('order', $order);
+ return $this->addProperty('order', $value);
}
/**
* Maximum number of composite buckets to return.
*
- * @param int $size
+ * @param int $value
* @return static
*/
- public function size($size)
- {
- return $this->addProperty('size', $size);
- }
-
- /**
- * {@inheritdoc}
- * @SuppressWarnings(PHPMD.IfStatementAssignment)
- */
- public function toArray()
+ public function size(int $value): static
{
- $properties = $this->_properties;
-
- if (isset($properties['sources'])) {
- foreach ($properties['sources'] as $key => $source) {
- if (($item = current($source)) instanceof Node) {
- $properties['sources'][$key] = $item->toArray();
- }
- }
- }
-
- $properties = $this->resolveProperties($properties);
-
- if ($this->_isPropertyField) {
- return [$this->_field => $properties];
- }
- return $properties;
+ return $this->addProperty('size', $value);
}
}
diff --git a/src/DSL/Aggs/Bucket/DateHistogram.php b/src/DSL/Aggs/Bucket/DateHistogram.php
index 2e1d01b..8aeab30 100644
--- a/src/DSL/Aggs/Bucket/DateHistogram.php
+++ b/src/DSL/Aggs/Bucket/DateHistogram.php
@@ -1,5 +1,7 @@
addProperty('calendar_interval', $calendarInterval);
+ return $this->addProperty('calendar_interval', $value);
}
/**
* Interval for bucketing. Deprecated in favor of calendar_interval or fixed_interval.
*
- * @param string $interval
+ * @deprecated ES deprecated the bare `interval` key; use calendarInterval() or fixedInterval() instead.
+ * @param string $value
* @return static
*/
- public function interval($interval)
+ public function interval(string $value): static
{
- return $this->addProperty('interval', $interval);
+ return $this->addProperty('interval', $value);
}
/**
* Fixed-unit interval for bucketing (e.g. 30d, 12h).
*
- * @param string $fixedInterval
+ * @param string $value
* @return static
*/
- public function fixedInterval($fixedInterval)
+ public function fixedInterval(string $value): static
{
- return $this->addProperty('fixed_interval', $fixedInterval);
+ return $this->addProperty('fixed_interval', $value);
}
/**
* Date format pattern for bucket keys.
*
- * @param string $format
+ * @param string $value
* @return static
*/
- public function format($format)
+ public function format(string $value): static
{
- return $this->addProperty('format', $format);
+ return $this->addProperty('format', $value);
}
/**
* Time zone for bucketing.
*
- * @param string $timeZone
+ * @param string $value
* @return static
*/
- public function timeZone($timeZone)
+ public function timeZone(string $value): static
{
- return $this->addProperty('time_zone', $timeZone);
+ return $this->addProperty('time_zone', $value);
}
/**
* Minimum number of documents in a bucket to be returned.
*
- * @param int $minDocCount
+ * @param int $value
* @return static
*/
- public function minDocCount($minDocCount)
+ public function minDocCount(int $value): static
{
- return $this->addProperty('min_doc_count', $minDocCount);
+ return $this->addProperty('min_doc_count', $value);
}
/**
* Extends the bucket range beyond the data bounds.
*
- * @param mixed $extendedBounds
+ * @param mixed $value
* @return static
*/
- public function extendedBounds($extendedBounds)
+ public function extendedBounds($value): static
{
- return $this->addProperty('extended_bounds', $extendedBounds);
+ return $this->addProperty('extended_bounds', $value);
}
/**
* Limits the bucket range to a bounded range.
*
- * @param mixed $hardBounds
+ * @param mixed $value
* @return static
*/
- public function hardBounds($hardBounds)
+ public function hardBounds($value): static
{
- return $this->addProperty('hard_bounds', $hardBounds);
+ return $this->addProperty('hard_bounds', $value);
}
/**
* Sort order for buckets.
*
- * @param mixed $order
+ * @param mixed $value
* @return static
*/
- public function order($order)
+ public function order($value): static
{
- return $this->addProperty('order', $order);
+ return $this->addProperty('order', $value);
}
/**
* Whether to return bucket keys as strings.
*
- * @param bool $keyed
+ * @param bool $value
* @return static
*/
- public function keyed($keyed)
+ public function keyed(bool $value): static
{
- return $this->addProperty('keyed', $keyed);
+ return $this->addProperty('keyed', $value);
}
/**
* Value to use for documents missing the field value.
*
- * @param mixed $missing
+ * @param mixed $value
* @return static
*/
- public function missing($missing)
+ public function missing($value): static
{
- return $this->addProperty('missing', $missing);
+ return $this->addProperty('missing', $value);
}
/**
* Offset for each bucket start time.
*
- * @param string $offset
+ * @param string $value
* @return static
*/
- public function offset($offset)
+ public function offset(string $value): static
{
- return $this->addProperty('offset', $offset);
+ return $this->addProperty('offset', $value);
}
}
diff --git a/src/DSL/Aggs/Bucket/DateRange.php b/src/DSL/Aggs/Bucket/DateRange.php
index e7a6638..e62b25d 100644
--- a/src/DSL/Aggs/Bucket/DateRange.php
+++ b/src/DSL/Aggs/Bucket/DateRange.php
@@ -1,5 +1,7 @@
addProperty('field', $field);
- }
+ protected string $_key = 'date_range';
/**
* Array of range definitions for bucketing.
*
- * @param array $ranges
+ * @param array $value
* @return static
*/
- public function ranges($ranges)
+ public function ranges(array $value): static
{
- return $this->addProperty('ranges', $ranges);
+ return $this->addProperty('ranges', $value);
}
/**
* Whether to return range buckets as a hash keyed by range key.
*
- * @param bool $keyed
+ * @param bool $value
* @return static
*/
- public function keyed($keyed)
+ public function keyed(bool $value): static
{
- return $this->addProperty('keyed', $keyed);
+ return $this->addProperty('keyed', $value);
}
/**
* Date format pattern for range keys.
*
- * @param string $format
+ * @param string $value
* @return static
*/
- public function format($format)
+ public function format(string $value): static
{
- return $this->addProperty('format', $format);
+ return $this->addProperty('format', $value);
}
/**
* Value to use for documents missing the field value.
*
- * @param mixed $missing
+ * @param mixed $value
* @return static
*/
- public function missing($missing)
+ public function missing($value): static
{
- return $this->addProperty('missing', $missing);
+ return $this->addProperty('missing', $value);
}
/**
* Time zone for date calculations.
*
- * @param string $timeZone
+ * @param string $value
* @return static
*/
- public function timeZone($timeZone)
+ public function timeZone(string $value): static
{
- return $this->addProperty('time_zone', $timeZone);
+ return $this->addProperty('time_zone', $value);
}
}
diff --git a/src/DSL/Aggs/Bucket/DiversifiedSampler.php b/src/DSL/Aggs/Bucket/DiversifiedSampler.php
index 781676d..938127a 100644
--- a/src/DSL/Aggs/Bucket/DiversifiedSampler.php
+++ b/src/DSL/Aggs/Bucket/DiversifiedSampler.php
@@ -1,5 +1,7 @@
addProperty('shard_size', $shardSize);
+ return $this->addProperty('shard_size', $value);
}
/**
* Maximum number of documents per unique value.
*
- * @param int $maxDocsPerValue
+ * @param int $value
* @return static
*/
- public function maxDocsPerValue($maxDocsPerValue)
+ public function maxDocsPerValue(int $value): static
{
- return $this->addProperty('max_docs_per_value', $maxDocsPerValue);
+ return $this->addProperty('max_docs_per_value', $value);
}
/**
* Execution hint for the aggregation.
*
- * @param string $executionHint
+ * @param string $value
* @return static
*/
- public function executionHint($executionHint)
+ public function executionHint(string $value): static
{
- return $this->addProperty('execution_hint', $executionHint);
+ return $this->addProperty('execution_hint', $value);
}
}
diff --git a/src/DSL/Aggs/Bucket/FilterAgg.php b/src/DSL/Aggs/Bucket/Filter.php
similarity index 54%
rename from src/DSL/Aggs/Bucket/FilterAgg.php
rename to src/DSL/Aggs/Bucket/Filter.php
index f2c8f54..0adf209 100644
--- a/src/DSL/Aggs/Bucket/FilterAgg.php
+++ b/src/DSL/Aggs/Bucket/Filter.php
@@ -1,5 +1,7 @@
_properties = $filter;
+ $this->_filter = $value;
return $this;
}
@@ -31,6 +36,6 @@ public function setFilter($filter)
*/
public function toArray()
{
- return Query::create($this->_properties)->toArray()['query'];
+ return Query::create($this->_filter)->toArray()['query'];
}
}
diff --git a/src/DSL/Aggs/Bucket/Filters.php b/src/DSL/Aggs/Bucket/Filters.php
index e6502f8..0e42e6e 100644
--- a/src/DSL/Aggs/Bucket/Filters.php
+++ b/src/DSL/Aggs/Bucket/Filters.php
@@ -1,5 +1,7 @@
addProperty('filters', $filters);
+ return $this->addProperty('filters', $value);
}
/**
* Key for the bucket that holds documents not matching any filter.
*
- * @param string $otherBucketKey
+ * @param string $value
* @return static
*/
- public function otherBucketKey($otherBucketKey)
+ public function otherBucketKey(string $value): static
{
- return $this->addProperty('other_bucket_key', $otherBucketKey);
+ return $this->addProperty('other_bucket_key', $value);
}
/**
* Whether to return buckets as a hash keyed by filter name.
*
- * @param bool $keyed
+ * @param bool $value
* @return static
*/
- public function keyed($keyed)
+ public function keyed(bool $value): static
{
- return $this->addProperty('keyed', $keyed);
+ return $this->addProperty('keyed', $value);
}
}
diff --git a/src/DSL/Aggs/Bucket/FrequentItemSets.php b/src/DSL/Aggs/Bucket/FrequentItemSets.php
index a5c944e..7a66431 100644
--- a/src/DSL/Aggs/Bucket/FrequentItemSets.php
+++ b/src/DSL/Aggs/Bucket/FrequentItemSets.php
@@ -1,5 +1,7 @@
addProperty('minimum_set_size', $minimumSetSize);
+ return $this->addProperty('minimum_set_size', $value);
}
/**
* Fields to analyze for frequent item sets.
*
- * @param array $fields
+ * @param array $value
* @return static
*/
- public function fields($fields)
+ public function fields(array $value): static
{
- return $this->addProperty('fields', $fields);
+ return $this->addProperty('fields', $value);
}
/**
* Maximum number of item sets to return.
*
- * @param int $size
+ * @param int $value
* @return static
*/
- public function size($size)
+ public function size(int $value): static
{
- return $this->addProperty('size', $size);
+ return $this->addProperty('size', $value);
}
/**
* Query to filter documents before analysis.
*
- * @param mixed $filter
+ * @param mixed $value
* @return static
*/
- public function filter($filter)
+ public function filter($value): static
{
- return $this->addProperty('filter', $filter);
+ return $this->addProperty('filter', $value);
}
}
diff --git a/src/DSL/Aggs/Bucket/GeoDistance.php b/src/DSL/Aggs/Bucket/GeoDistance.php
index 3e6dcdd..77411db 100644
--- a/src/DSL/Aggs/Bucket/GeoDistance.php
+++ b/src/DSL/Aggs/Bucket/GeoDistance.php
@@ -1,5 +1,7 @@
addProperty('field', $field);
- }
+ protected string $_key = 'geo_distance';
/**
* The central geo point from which distances are measured.
*
- * @param mixed $origin
+ * @param mixed $value
* @return static
*/
- public function origin($origin)
+ public function origin($value): static
{
- return $this->addProperty('origin', $origin);
+ return $this->addProperty('origin', $value);
}
/**
* Distance unit (e.g. km, mi, m).
*
- * @param string $unit
+ * @param string $value
* @return static
*/
- public function unit($unit)
+ public function unit(string $value): static
{
- return $this->addProperty('unit', $unit);
+ return $this->addProperty('unit', $value);
}
/**
* How to compute the distance (arc or plane).
*
- * @param string $distanceType
+ * @param string $value
* @return static
*/
- public function distanceType($distanceType)
+ public function distanceType(string $value): static
{
- return $this->addProperty('distance_type', $distanceType);
+ return $this->addProperty('distance_type', $value);
}
/**
* Array of distance range definitions for bucketing.
*
- * @param array $ranges
+ * @param array $value
* @return static
*/
- public function ranges($ranges)
+ public function ranges(array $value): static
{
- return $this->addProperty('ranges', $ranges);
+ return $this->addProperty('ranges', $value);
}
/**
* Whether to return range buckets as a hash keyed by range key.
*
- * @param bool $keyed
+ * @param bool $value
* @return static
*/
- public function keyed($keyed)
+ public function keyed(bool $value): static
{
- return $this->addProperty('keyed', $keyed);
+ return $this->addProperty('keyed', $value);
}
}
diff --git a/src/DSL/Aggs/Bucket/GeoHashGrid.php b/src/DSL/Aggs/Bucket/GeoHashGrid.php
index 6933f99..9e46e3b 100644
--- a/src/DSL/Aggs/Bucket/GeoHashGrid.php
+++ b/src/DSL/Aggs/Bucket/GeoHashGrid.php
@@ -1,5 +1,7 @@
addProperty('field', $field);
- }
+ protected string $_key = 'geohash_grid';
/**
* Geohash precision (length) for grid cells.
*
- * @param int $precision
+ * @param int $value
* @return static
*/
- public function precision($precision)
+ public function precision(int $value): static
{
- return $this->addProperty('precision', $precision);
+ return $this->addProperty('precision', $value);
}
/**
* Maximum number of geohash buckets to return.
*
- * @param int $size
+ * @param int $value
* @return static
*/
- public function size($size)
+ public function size(int $value): static
{
- return $this->addProperty('size', $size);
+ return $this->addProperty('size', $value);
}
/**
* Number of geohash buckets to return from each shard.
*
- * @param int $shardSize
+ * @param int $value
* @return static
*/
- public function shardSize($shardSize)
+ public function shardSize(int $value): static
{
- return $this->addProperty('shard_size', $shardSize);
+ return $this->addProperty('shard_size', $value);
}
}
diff --git a/src/DSL/Aggs/Bucket/GeohexGrid.php b/src/DSL/Aggs/Bucket/GeohexGrid.php
index fa8b446..5f93015 100644
--- a/src/DSL/Aggs/Bucket/GeohexGrid.php
+++ b/src/DSL/Aggs/Bucket/GeohexGrid.php
@@ -1,5 +1,7 @@
addProperty('field', $field);
- }
+ protected string $_key = 'geohex_grid';
/**
* H3 resolution for grid cells.
*
- * @param int $precision
+ * @param int $value
* @return static
*/
- public function precision($precision)
+ public function precision(int $value): static
{
- return $this->addProperty('precision', $precision);
+ return $this->addProperty('precision', $value);
}
/**
* Maximum number of hex buckets to return.
*
- * @param int $size
+ * @param int $value
* @return static
*/
- public function size($size)
+ public function size(int $value): static
{
- return $this->addProperty('size', $size);
+ return $this->addProperty('size', $value);
}
/**
* Number of hex buckets to return from each shard.
*
- * @param int $shardSize
+ * @param int $value
* @return static
*/
- public function shardSize($shardSize)
+ public function shardSize(int $value): static
{
- return $this->addProperty('shard_size', $shardSize);
+ return $this->addProperty('shard_size', $value);
}
}
diff --git a/src/DSL/Aggs/Bucket/GeotileGrid.php b/src/DSL/Aggs/Bucket/GeotileGrid.php
index 6f60cec..aa61d5a 100644
--- a/src/DSL/Aggs/Bucket/GeotileGrid.php
+++ b/src/DSL/Aggs/Bucket/GeotileGrid.php
@@ -1,5 +1,7 @@
addProperty('field', $field);
- }
+ protected string $_key = 'geotile_grid';
/**
* Zoom level (precision) for geotile grid cells.
*
- * @param int $precision
+ * @param int $value
* @return static
*/
- public function precision($precision)
+ public function precision(int $value): static
{
- return $this->addProperty('precision', $precision);
+ return $this->addProperty('precision', $value);
}
/**
* Maximum number of geotile buckets to return.
*
- * @param int $size
+ * @param int $value
* @return static
*/
- public function size($size)
+ public function size(int $value): static
{
- return $this->addProperty('size', $size);
+ return $this->addProperty('size', $value);
}
/**
* Number of geotile buckets to return from each shard.
*
- * @param int $shardSize
+ * @param int $value
* @return static
*/
- public function shardSize($shardSize)
+ public function shardSize(int $value): static
{
- return $this->addProperty('shard_size', $shardSize);
+ return $this->addProperty('shard_size', $value);
}
}
diff --git a/src/DSL/Aggs/Bucket/GlobalAgg.php b/src/DSL/Aggs/Bucket/Global_.php
similarity index 79%
rename from src/DSL/Aggs/Bucket/GlobalAgg.php
rename to src/DSL/Aggs/Bucket/Global_.php
index 9fd6f86..8efd193 100644
--- a/src/DSL/Aggs/Bucket/GlobalAgg.php
+++ b/src/DSL/Aggs/Bucket/Global_.php
@@ -1,5 +1,7 @@
addProperty('field', $field);
- }
+ protected string $_key = 'histogram';
/**
* Interval size for each bucket.
*
- * @param float $interval
+ * @param float $value
* @return static
*/
- public function interval($interval)
+ public function interval(float $value): static
{
- return $this->addProperty('interval', $interval);
+ return $this->addProperty('interval', $value);
}
/**
* Minimum number of documents in a bucket to be returned.
*
- * @param int $minDocCount
+ * @param int $value
* @return static
*/
- public function minDocCount($minDocCount)
+ public function minDocCount(int $value): static
{
- return $this->addProperty('min_doc_count', $minDocCount);
+ return $this->addProperty('min_doc_count', $value);
}
/**
* Extends the bucket range beyond the data bounds.
*
- * @param mixed $bounds
+ * @param mixed $value
* @return static
*/
- public function extendedBounds($bounds)
+ public function extendedBounds($value): static
{
- return $this->addProperty('extended_bounds', $bounds);
+ return $this->addProperty('extended_bounds', $value);
}
/**
* Sort order for buckets.
*
- * @param mixed $order
+ * @param mixed $value
* @return static
*/
- public function order($order)
+ public function order($value): static
{
- return $this->addProperty('order', $order);
+ return $this->addProperty('order', $value);
}
/**
* Whether to return bucket keys as strings.
*
- * @param bool $keyed
+ * @param bool $value
* @return static
*/
- public function keyed($keyed)
+ public function keyed(bool $value): static
{
- return $this->addProperty('keyed', $keyed);
+ return $this->addProperty('keyed', $value);
}
/**
* Value to use for documents missing the field value.
*
- * @param float $missing
+ * @param float $value
* @return static
*/
- public function missing($missing)
+ public function missing(float $value): static
{
- return $this->addProperty('missing', $missing);
+ return $this->addProperty('missing', $value);
}
/**
* Format pattern for bucket key values.
*
- * @param string $format
+ * @param string $value
* @return static
*/
- public function format($format)
+ public function format(string $value): static
{
- return $this->addProperty('format', $format);
+ return $this->addProperty('format', $value);
}
/**
* Script to compute the bucket value.
*
- * @param string|callable $script
+ * @param string|callable $value
* @return static
*/
- public function script($script)
+ public function script($value): static
{
- return $this->addProperty('script', $script);
+ return $this->addProperty('script', $value);
}
/**
* Offset for bucket starting values.
*
- * @param float $offset
+ * @param float $value
* @return static
*/
- public function offset($offset)
+ public function offset(float $value): static
{
- return $this->addProperty('offset', $offset);
+ return $this->addProperty('offset', $value);
}
/**
* Limits the bucket range to a bounded range.
*
- * @param mixed $hardBounds
+ * @param mixed $value
* @return static
*/
- public function hardBounds($hardBounds)
+ public function hardBounds($value): static
{
- return $this->addProperty('hard_bounds', $hardBounds);
+ return $this->addProperty('hard_bounds', $value);
}
}
diff --git a/src/DSL/Aggs/Bucket/IpPrefix.php b/src/DSL/Aggs/Bucket/IpPrefix.php
index 8d90deb..433ad0c 100644
--- a/src/DSL/Aggs/Bucket/IpPrefix.php
+++ b/src/DSL/Aggs/Bucket/IpPrefix.php
@@ -1,39 +1,32 @@
addProperty('field', $field);
- }
+ protected string $_key = 'ip_prefix';
/**
* Length of the network prefix.
*
- * @param int $length
+ * @param int $value
* @return static
*/
- public function prefixLength($length)
+ public function prefixLength(int $value): static
{
- return $this->addProperty('prefix_length', $length);
+ return $this->addProperty('prefix_length', $value);
}
/**
- * @param int $length
+ * @param int $value
* @return static
*/
- public function minPrefixLength($length)
+ public function minPrefixLength(int $value): static
{
- return $this->addProperty('min_prefix_length', $length);
+ return $this->addProperty('min_prefix_length', $value);
}
}
diff --git a/src/DSL/Aggs/Bucket/IpRange.php b/src/DSL/Aggs/Bucket/IpRange.php
index 46661ec..51f1cb3 100644
--- a/src/DSL/Aggs/Bucket/IpRange.php
+++ b/src/DSL/Aggs/Bucket/IpRange.php
@@ -1,5 +1,7 @@
addProperty('field', $field);
- }
+ protected string $_key = 'ip_range';
/**
* Array of IP range definitions for bucketing.
*
- * @param array $ranges
+ * @param array $value
* @return static
*/
- public function ranges($ranges)
+ public function ranges(array $value): static
{
- return $this->addProperty('ranges', $ranges);
+ return $this->addProperty('ranges', $value);
}
/**
* Whether to return range buckets as a hash keyed by range key.
*
- * @param bool $keyed
+ * @param bool $value
* @return static
*/
- public function keyed($keyed)
+ public function keyed(bool $value): static
{
- return $this->addProperty('keyed', $keyed);
+ return $this->addProperty('keyed', $value);
}
/**
* Value to use for documents missing the field value.
*
- * @param mixed $missing
+ * @param mixed $value
* @return static
*/
- public function missing($missing)
+ public function missing($value): static
{
- return $this->addProperty('missing', $missing);
+ return $this->addProperty('missing', $value);
}
}
diff --git a/src/DSL/Aggs/Bucket/Missing.php b/src/DSL/Aggs/Bucket/Missing.php
index bea6610..c445f03 100644
--- a/src/DSL/Aggs/Bucket/Missing.php
+++ b/src/DSL/Aggs/Bucket/Missing.php
@@ -1,5 +1,7 @@
addProperty('field', $field);
- }
+ protected string $_key = 'missing';
/**
* Value to treat as missing for the field.
*
- * @param mixed $missing
+ * @param mixed $value
* @return static
*/
- public function missing($missing)
+ public function missing($value): static
{
- return $this->addProperty('missing', $missing);
+ return $this->addProperty('missing', $value);
}
}
diff --git a/src/DSL/Aggs/Bucket/MultiTerms.php b/src/DSL/Aggs/Bucket/MultiTerms.php
index 4d3859d..98620b4 100644
--- a/src/DSL/Aggs/Bucket/MultiTerms.php
+++ b/src/DSL/Aggs/Bucket/MultiTerms.php
@@ -1,73 +1,75 @@
addProperty('terms', $terms, true);
+ return $this->addProperty('terms', $value, true);
}
/**
- * @param mixed $order
+ * @param mixed $value
* @return static
*/
- public function order($order)
+ public function order($value): static
{
- return $this->addProperty('order', $order);
+ return $this->addProperty('order', $value);
}
/**
- * @param int $size
+ * @param int $value
* @return static
*/
- public function size($size)
+ public function size(int $value): static
{
- return $this->addProperty('size', $size);
+ return $this->addProperty('size', $value);
}
/**
- * @param int $shardSize
+ * @param int $value
* @return static
*/
- public function shardSize($shardSize)
+ public function shardSize(int $value): static
{
- return $this->addProperty('shard_size', $shardSize);
+ return $this->addProperty('shard_size', $value);
}
/**
- * @param int $minDocCount
+ * @param int $value
* @return static
*/
- public function minDocCount($minDocCount)
+ public function minDocCount(int $value): static
{
- return $this->addProperty('min_doc_count', $minDocCount);
+ return $this->addProperty('min_doc_count', $value);
}
/**
- * @param int $shardMinDocCount
+ * @param int $value
* @return static
*/
- public function shardMinDocCount($shardMinDocCount)
+ public function shardMinDocCount(int $value): static
{
- return $this->addProperty('shard_min_doc_count', $shardMinDocCount);
+ return $this->addProperty('shard_min_doc_count', $value);
}
/**
- * @param string $collectMode
+ * @param string $value
* @return static
*/
- public function collectMode($collectMode)
+ public function collectMode(string $value): static
{
- return $this->addProperty('collect_mode', $collectMode);
+ return $this->addProperty('collect_mode', $value);
}
}
diff --git a/src/DSL/Aggs/Bucket/Nested.php b/src/DSL/Aggs/Bucket/Nested.php
index d30200b..5f185fc 100644
--- a/src/DSL/Aggs/Bucket/Nested.php
+++ b/src/DSL/Aggs/Bucket/Nested.php
@@ -1,5 +1,7 @@
addProperty('path', $path);
+ return $this->addProperty('path', $value);
}
/**
* Whether to return an empty bucket instead of an error for unmapped nested types.
*
- * @param bool $ignoreUnmapped
+ * @param bool $value
* @return static
*/
- public function ignoreUnmapped($ignoreUnmapped)
+ public function ignoreUnmapped(bool $value): static
{
- return $this->addProperty('ignore_unmapped', $ignoreUnmapped);
+ return $this->addProperty('ignore_unmapped', $value);
}
}
diff --git a/src/DSL/Aggs/Bucket/ParentAgg.php b/src/DSL/Aggs/Bucket/Parent_.php
similarity index 57%
rename from src/DSL/Aggs/Bucket/ParentAgg.php
rename to src/DSL/Aggs/Bucket/Parent_.php
index b60e61b..82b4154 100644
--- a/src/DSL/Aggs/Bucket/ParentAgg.php
+++ b/src/DSL/Aggs/Bucket/Parent_.php
@@ -1,5 +1,7 @@
addProperty('type', $type);
+ return $this->addProperty('type', $value);
}
}
diff --git a/src/DSL/Aggs/Bucket/RandomSampler.php b/src/DSL/Aggs/Bucket/RandomSampler.php
index de1b946..3e5217d 100644
--- a/src/DSL/Aggs/Bucket/RandomSampler.php
+++ b/src/DSL/Aggs/Bucket/RandomSampler.php
@@ -1,5 +1,7 @@
addProperty('probability', $probability);
+ return $this->addProperty('probability', $value);
}
/**
* Seed for the random number generator to produce repeatable samples.
*
- * @param int $seed
- * @return static
- */
- public function seed($seed)
- {
- return $this->addProperty('seed', $seed);
- }
-
- /**
- * Field used to maintain consistent random ordering across shards.
- *
- * @param string $field
+ * @param int $value
* @return static
*/
- public function field($field)
+ public function seed(int $value): static
{
- return $this->addProperty('field', $field);
+ return $this->addProperty('seed', $value);
}
}
diff --git a/src/DSL/Aggs/Bucket/Range.php b/src/DSL/Aggs/Bucket/Range.php
index b488c66..e87eaae 100644
--- a/src/DSL/Aggs/Bucket/Range.php
+++ b/src/DSL/Aggs/Bucket/Range.php
@@ -1,5 +1,7 @@
addProperty('field', $field);
- }
+ protected string $_key = 'range';
/**
* Array of range definitions for bucketing.
*
- * @param array $ranges
+ * @param array $value
* @return static
*/
- public function ranges($ranges)
+ public function ranges(array $value): static
{
- return $this->addProperty('ranges', $ranges);
+ return $this->addProperty('ranges', $value);
}
/**
* Whether to return range buckets as a hash keyed by range key.
*
- * @param bool $keyed
+ * @param bool $value
* @return static
*/
- public function keyed($keyed)
+ public function keyed(bool $value): static
{
- return $this->addProperty('keyed', $keyed);
+ return $this->addProperty('keyed', $value);
}
/**
* Script to compute the bucket value.
*
- * @param string|callable $script
+ * @param string|callable $value
* @return static
*/
- public function script($script)
+ public function script($value): static
{
- return $this->addProperty('script', $script);
+ return $this->addProperty('script', $value);
}
/**
* Value to use for documents missing the field value.
*
- * @param float $missing
+ * @param float $value
* @return static
*/
- public function missing($missing)
+ public function missing($value): static
{
- return $this->addProperty('missing', $missing);
+ return $this->addProperty('missing', $value);
}
}
diff --git a/src/DSL/Aggs/Bucket/RareTerms.php b/src/DSL/Aggs/Bucket/RareTerms.php
index 55d678e..ad0ae34 100644
--- a/src/DSL/Aggs/Bucket/RareTerms.php
+++ b/src/DSL/Aggs/Bucket/RareTerms.php
@@ -1,73 +1,66 @@
addProperty('field', $field);
- }
+ protected string $_key = 'rare_terms';
/**
- * @param int $maxDocCount
+ * @param int $value
* @return static
*/
- public function maxDocCount($maxDocCount)
+ public function maxDocCount(int $value): static
{
- return $this->addProperty('max_doc_count', $maxDocCount);
+ return $this->addProperty('max_doc_count', $value);
}
/**
- * @param mixed $precision
+ * @param mixed $value
* @return static
*/
- public function precision($precision)
+ public function precision($value): static
{
- return $this->addProperty('precision', $precision);
+ return $this->addProperty('precision', $value);
}
/**
- * @param mixed $include
+ * @param mixed $value
* @return static
*/
- public function include($include)
+ public function include($value): static
{
- return $this->addProperty('include', $include);
+ return $this->addProperty('include', $value);
}
/**
- * @param mixed $exclude
+ * @param mixed $value
* @return static
*/
- public function exclude($exclude)
+ public function exclude($value): static
{
- return $this->addProperty('exclude', $exclude);
+ return $this->addProperty('exclude', $value);
}
/**
- * @param mixed $missing
+ * @param mixed $value
* @return static
*/
- public function missing($missing)
+ public function missing($value): static
{
- return $this->addProperty('missing', $missing);
+ return $this->addProperty('missing', $value);
}
/**
- * @param int $shardSize
+ * @param int $value
* @return static
*/
- public function shardSize($shardSize)
+ public function shardSize(int $value): static
{
- return $this->addProperty('shard_size', $shardSize);
+ return $this->addProperty('shard_size', $value);
}
}
diff --git a/src/DSL/Aggs/Bucket/ReverseNested.php b/src/DSL/Aggs/Bucket/ReverseNested.php
index ca434eb..39c8d64 100644
--- a/src/DSL/Aggs/Bucket/ReverseNested.php
+++ b/src/DSL/Aggs/Bucket/ReverseNested.php
@@ -1,5 +1,7 @@
addProperty('path', $path);
+ return $this->addProperty('path', $value);
}
}
diff --git a/src/DSL/Aggs/Bucket/SignificantTerms.php b/src/DSL/Aggs/Bucket/SignificantTerms.php
index 54fe10c..17fe658 100644
--- a/src/DSL/Aggs/Bucket/SignificantTerms.php
+++ b/src/DSL/Aggs/Bucket/SignificantTerms.php
@@ -1,5 +1,7 @@
addProperty('field', $field);
- }
+ protected string $_key = 'significant_terms';
/**
* Maximum number of significant terms to return.
*
- * @param int $size
+ * @param int $value
* @return static
*/
- public function size($size)
+ public function size(int $value): static
{
- return $this->addProperty('size', $size);
+ return $this->addProperty('size', $value);
}
/**
* Number of candidate terms to return from each shard.
*
- * @param int $shardSize
+ * @param int $value
* @return static
*/
- public function shardSize($shardSize)
+ public function shardSize(int $value): static
{
- return $this->addProperty('shard_size', $shardSize);
+ return $this->addProperty('shard_size', $value);
}
/**
* Minimum document count for a term to be returned.
*
- * @param int $minDocCount
+ * @param int $value
* @return static
*/
- public function minDocCount($minDocCount)
+ public function minDocCount(int $value): static
{
- return $this->addProperty('min_doc_count', $minDocCount);
+ return $this->addProperty('min_doc_count', $value);
}
/**
* Minimum document count for a term to be considered on each shard.
*
- * @param int $shardMinDocCount
+ * @param int $value
* @return static
*/
- public function shardMinDocCount($shardMinDocCount)
+ public function shardMinDocCount(int $value): static
{
- return $this->addProperty('shard_min_doc_count', $shardMinDocCount);
+ return $this->addProperty('shard_min_doc_count', $value);
}
/**
* Terms to include in the aggregation.
*
- * @param mixed $include
+ * @param mixed $value
* @return static
*/
- public function include($include)
+ public function include($value): static
{
- return $this->addProperty('include', $include);
+ return $this->addProperty('include', $value);
}
/**
* Terms to exclude from the aggregation.
*
- * @param mixed $exclude
+ * @param mixed $value
* @return static
*/
- public function exclude($exclude)
+ public function exclude($value): static
{
- return $this->addProperty('exclude', $exclude);
+ return $this->addProperty('exclude', $value);
}
/**
* Query to filter the background document set for significance calculation.
*
- * @param mixed $backgroundFilter
+ * @param mixed $value
* @return static
*/
- public function backgroundFilter($backgroundFilter)
+ public function backgroundFilter($value): static
{
- return $this->addProperty('background_filter', $backgroundFilter);
+ return $this->addProperty('background_filter', $value);
}
/**
* Execution hint for the aggregation mechanism.
*
- * @param string $executionHint
+ * @param string $value
* @return static
*/
- public function executionHint($executionHint)
+ public function executionHint(string $value): static
{
- return $this->addProperty('execution_hint', $executionHint);
+ return $this->addProperty('execution_hint', $value);
}
}
diff --git a/src/DSL/Aggs/Bucket/SignificantText.php b/src/DSL/Aggs/Bucket/SignificantText.php
index d07832b..cd78ec1 100644
--- a/src/DSL/Aggs/Bucket/SignificantText.php
+++ b/src/DSL/Aggs/Bucket/SignificantText.php
@@ -1,5 +1,7 @@
addProperty('field', $field);
- }
+ protected string $_key = 'significant_text';
/**
* Maximum number of significant terms to return.
*
- * @param int $size
+ * @param int $value
* @return static
*/
- public function size($size)
+ public function size(int $value): static
{
- return $this->addProperty('size', $size);
+ return $this->addProperty('size', $value);
}
/**
* Number of candidate terms to return from each shard.
*
- * @param int $shardSize
+ * @param int $value
* @return static
*/
- public function shardSize($shardSize)
+ public function shardSize(int $value): static
{
- return $this->addProperty('shard_size', $shardSize);
+ return $this->addProperty('shard_size', $value);
}
/**
* Minimum document count for a term to be returned.
*
- * @param int $minDocCount
+ * @param int $value
* @return static
*/
- public function minDocCount($minDocCount)
+ public function minDocCount(int $value): static
{
- return $this->addProperty('min_doc_count', $minDocCount);
+ return $this->addProperty('min_doc_count', $value);
}
/**
* Minimum document count for a term to be considered on each shard.
*
- * @param int $shardMinDocCount
+ * @param int $value
* @return static
*/
- public function shardMinDocCount($shardMinDocCount)
+ public function shardMinDocCount(int $value): static
{
- return $this->addProperty('shard_min_doc_count', $shardMinDocCount);
+ return $this->addProperty('shard_min_doc_count', $value);
}
/**
* Terms to include in the aggregation.
*
- * @param mixed $include
+ * @param mixed $value
* @return static
*/
- public function include($include)
+ public function include($value): static
{
- return $this->addProperty('include', $include);
+ return $this->addProperty('include', $value);
}
/**
* Terms to exclude from the aggregation.
*
- * @param mixed $exclude
+ * @param mixed $value
* @return static
*/
- public function exclude($exclude)
+ public function exclude($value): static
{
- return $this->addProperty('exclude', $exclude);
+ return $this->addProperty('exclude', $value);
}
/**
* Query to filter the background document set for significance calculation.
*
- * @param mixed $backgroundFilter
+ * @param mixed $value
* @return static
*/
- public function backgroundFilter($backgroundFilter)
+ public function backgroundFilter($value): static
{
- return $this->addProperty('background_filter', $backgroundFilter);
+ return $this->addProperty('background_filter', $value);
}
/**
* Whether to filter duplicate text before analysis.
*
- * @param bool $filter
+ * @param bool $value
* @return static
*/
- public function filterDuplicateText($filter)
+ public function filterDuplicateText(bool $value): static
{
- return $this->addProperty('filter_duplicate_text', $filter);
+ return $this->addProperty('filter_duplicate_text', $value);
}
}
diff --git a/src/DSL/Aggs/Bucket/Terms.php b/src/DSL/Aggs/Bucket/Terms.php
index ad6531d..d7bf761 100644
--- a/src/DSL/Aggs/Bucket/Terms.php
+++ b/src/DSL/Aggs/Bucket/Terms.php
@@ -1,5 +1,7 @@
addProperty('field', $field);
- }
+ protected string $_key = 'terms';
/**
* Maximum number of term buckets to return.
*
- * @param int $size
+ * @param int $value
* @return static
*/
- public function size($size)
+ public function size(int $value): static
{
- return $this->addProperty('size', $size);
+ return $this->addProperty('size', $value);
}
/**
* Sort order for term buckets.
*
- * @param mixed $order
+ * @param mixed $value
* @return static
*/
- public function order($order)
+ public function order($value): static
{
- return $this->addProperty('order', $order);
+ return $this->addProperty('order', $value);
}
/**
* Minimum document count for a term bucket to be returned.
*
- * @param int $minDocCount
+ * @param int $value
* @return static
*/
- public function minDocCount($minDocCount)
+ public function minDocCount(int $value): static
{
- return $this->addProperty('min_doc_count', $minDocCount);
+ return $this->addProperty('min_doc_count', $value);
}
/**
* Number of term buckets to return from each shard.
*
- * @param int $shardSize
+ * @param int $value
* @return static
*/
- public function shardSize($shardSize)
+ public function shardSize(int $value): static
{
- return $this->addProperty('shard_size', $shardSize);
+ return $this->addProperty('shard_size', $value);
}
/**
* Whether to show document count error for each term.
*
- * @param bool $show
+ * @param bool $value
* @return static
*/
- public function showTermDocCountError($show)
+ public function showTermDocCountError(bool $value): static
{
- return $this->addProperty('show_term_doc_count_error', $show);
+ return $this->addProperty('show_term_doc_count_error', $value);
}
/**
* Minimum document count for a term to be considered on each shard.
*
- * @param int $shardMinDocCount
+ * @param int $value
* @return static
*/
- public function shardMinDocCount($shardMinDocCount)
+ public function shardMinDocCount(int $value): static
{
- return $this->addProperty('shard_min_doc_count', $shardMinDocCount);
+ return $this->addProperty('shard_min_doc_count', $value);
}
/**
* Value to use for documents missing the field value.
*
- * @param mixed $missing
+ * @param mixed $value
* @return static
*/
- public function missing($missing)
+ public function missing($value): static
{
- return $this->addProperty('missing', $missing);
+ return $this->addProperty('missing', $value);
}
/**
* Collection mode for the aggregation (breadth_first or depth_first).
*
- * @param string $collectMode
+ * @param string $value
* @return static
*/
- public function collectMode($collectMode)
+ public function collectMode(string $value): static
{
- return $this->addProperty('collect_mode', $collectMode);
+ return $this->addProperty('collect_mode', $value);
}
/**
* Terms to include in the aggregation.
*
- * @param mixed $include
+ * @param mixed $value
* @return static
*/
- public function include($include)
+ public function include($value): static
{
- return $this->addProperty('include', $include);
+ return $this->addProperty('include', $value);
}
/**
* Terms to exclude from the aggregation.
*
- * @param mixed $exclude
+ * @param mixed $value
* @return static
*/
- public function exclude($exclude)
+ public function exclude($value): static
{
- return $this->addProperty('exclude', $exclude);
+ return $this->addProperty('exclude', $value);
}
/**
* Execution hint for the aggregation mechanism.
*
- * @param string $executionHint
+ * @param string $value
* @return static
*/
- public function executionHint($executionHint)
+ public function executionHint(string $value): static
{
- return $this->addProperty('execution_hint', $executionHint);
+ return $this->addProperty('execution_hint', $value);
}
}
diff --git a/src/DSL/Aggs/Bucket/TimeSeries.php b/src/DSL/Aggs/Bucket/TimeSeries.php
index 2497ae8..0d0b144 100644
--- a/src/DSL/Aggs/Bucket/TimeSeries.php
+++ b/src/DSL/Aggs/Bucket/TimeSeries.php
@@ -1,46 +1,39 @@
addProperty('field', $field);
- }
+ protected string $_key = 'time_series';
/**
- * @param string $calendarInterval
+ * @param string $value
* @return static
*/
- public function calendarInterval($calendarInterval)
+ public function calendarInterval(string $value): static
{
- return $this->addProperty('calendar_interval', $calendarInterval);
+ return $this->addProperty('calendar_interval', $value);
}
/**
- * @param string $fixedInterval
+ * @param string $value
* @return static
*/
- public function fixedInterval($fixedInterval)
+ public function fixedInterval(string $value): static
{
- return $this->addProperty('fixed_interval', $fixedInterval);
+ return $this->addProperty('fixed_interval', $value);
}
/**
- * @param mixed $missing
+ * @param mixed $value
* @return static
*/
- public function missing($missing)
+ public function missing($value): static
{
- return $this->addProperty('missing', $missing);
+ return $this->addProperty('missing', $value);
}
}
diff --git a/src/DSL/Aggs/Bucket/VariableWidthHistogram.php b/src/DSL/Aggs/Bucket/VariableWidthHistogram.php
index 1f4d3ed..df6091e 100644
--- a/src/DSL/Aggs/Bucket/VariableWidthHistogram.php
+++ b/src/DSL/Aggs/Bucket/VariableWidthHistogram.php
@@ -1,37 +1,30 @@
addProperty('field', $field);
- }
+ protected string $_key = 'variable_width_histogram';
/**
- * @param int $buckets
+ * @param int $value
* @return static
*/
- public function buckets($buckets)
+ public function buckets(int $value): static
{
- return $this->addProperty('buckets', $buckets);
+ return $this->addProperty('buckets', $value);
}
/**
- * @param int $shardBuckets
+ * @param int $value
* @return static
*/
- public function shardBuckets($shardBuckets)
+ public function shardBuckets(int $value): static
{
- return $this->addProperty('shard_buckets', $shardBuckets);
+ return $this->addProperty('shard_buckets', $value);
}
}
diff --git a/src/DSL/Aggs/Metric.php b/src/DSL/Aggs/Metric.php
index d66e88a..30a91ff 100644
--- a/src/DSL/Aggs/Metric.php
+++ b/src/DSL/Aggs/Metric.php
@@ -1,5 +1,7 @@
node(Avg::create(is_string($params) ? ['field' => $params] : $params));
+ return $this->node(Avg::create(is_string($value) ? ['field' => $value] : $value));
}
/**
* Computes the sum of numeric values from a field.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function sum($params)
+ public function sum($value): static
{
- return $this->node(Sum::create(is_string($params) ? ['field' => $params] : $params));
+ return $this->node(Sum::create(is_string($value) ? ['field' => $value] : $value));
}
/**
* Computes the minimum value from a field.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function min($params)
+ public function min($value): static
{
- return $this->node(Min::create(is_string($params) ? ['field' => $params] : $params));
+ return $this->node(Min::create(is_string($value) ? ['field' => $value] : $value));
}
/**
* Computes the maximum value from a field.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function max($params)
+ public function max($value): static
{
- return $this->node(Max::create(is_string($params) ? ['field' => $params] : $params));
+ return $this->node(Max::create(is_string($value) ? ['field' => $value] : $value));
}
/**
* Counts the number of distinct values in a field.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function cardinality($params)
+ public function cardinality($value): static
{
- return $this->node(Cardinality::create(is_string($params) ? ['field' => $params] : $params));
+ return $this->node(Cardinality::create(is_string($value) ? ['field' => $value] : $value));
}
/**
* Counts the number of values in a field, including duplicates.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function valueCount($params)
+ public function valueCount($value): static
{
- return $this->node(ValueCount::create(is_string($params) ? ['field' => $params] : $params));
+ return $this->node(ValueCount::create(is_string($value) ? ['field' => $value] : $value));
}
/**
* Computes count, min, max, avg, and sum stats from a field in one request.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function stats($params)
+ public function stats($value): static
{
- return $this->node(Stats::create(is_string($params) ? ['field' => $params] : $params));
+ return $this->node(Stats::create(is_string($value) ? ['field' => $value] : $value));
}
/**
* Computes extended statistics (stats plus stddev, variance, std error) from a field.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function extendedStats($params)
+ public function extendedStats($value): static
{
- return $this->node(ExtendedStats::create(is_string($params) ? ['field' => $params] : $params));
+ return $this->node(ExtendedStats::create(is_string($value) ? ['field' => $value] : $value));
}
}
diff --git a/src/DSL/Aggs/Metric/Avg.php b/src/DSL/Aggs/Metric/Avg.php
index 51585ea..9e1ae08 100644
--- a/src/DSL/Aggs/Metric/Avg.php
+++ b/src/DSL/Aggs/Metric/Avg.php
@@ -1,5 +1,7 @@
addProperty('field', $field);
- }
+ protected string $_key = 'avg';
/**
- * (Optional) The value to use when the field is missing.
+ * The value to use when the field is missing.
*
- * @param mixed $missing
+ * @param mixed $value
* @return static
*/
- public function missing($missing)
+ public function missing($value): static
{
- return $this->addProperty('missing', $missing);
+ return $this->addProperty('missing', $value);
}
/**
- * (Optional) The script to use for the aggregation.
+ * The script to use for the aggregation.
*
- * @param string|callable $script
+ * @param string|callable $value
* @return static
*/
- public function script($script)
+ public function script($value): static
{
- return $this->addProperty('script', $script);
+ return $this->addProperty('script', $value);
}
}
diff --git a/src/DSL/Aggs/Metric/Cardinality.php b/src/DSL/Aggs/Metric/Cardinality.php
index 9f45379..46d5337 100644
--- a/src/DSL/Aggs/Metric/Cardinality.php
+++ b/src/DSL/Aggs/Metric/Cardinality.php
@@ -1,5 +1,7 @@
addProperty('field', $field);
- }
+ protected string $_key = 'cardinality';
/**
* Controls the precision of the count.
*
- * @param int $threshold
+ * @param int $value
* @return static
*/
- public function precisionThreshold($threshold)
+ public function precisionThreshold(int $value): static
{
- return $this->addProperty('precision_threshold', $threshold);
+ return $this->addProperty('precision_threshold', $value);
}
/**
- * (Optional) The value to use when the field is missing.
+ * The value to use when the field is missing.
*
- * @param mixed $missing
+ * @param mixed $value
* @return static
*/
- public function missing($missing)
+ public function missing($value): static
{
- return $this->addProperty('missing', $missing);
+ return $this->addProperty('missing', $value);
}
/**
- * (Optional) The script to use for the aggregation.
+ * The script to use for the aggregation.
*
- * @param string|callable $script
+ * @param string|callable $value
* @return static
*/
- public function script($script)
+ public function script($value): static
{
- return $this->addProperty('script', $script);
+ return $this->addProperty('script', $value);
}
}
diff --git a/src/DSL/Aggs/Metric/ExtendedStats.php b/src/DSL/Aggs/Metric/ExtendedStats.php
index 616fd76..57fb326 100644
--- a/src/DSL/Aggs/Metric/ExtendedStats.php
+++ b/src/DSL/Aggs/Metric/ExtendedStats.php
@@ -1,5 +1,7 @@
addProperty('field', $field);
- }
+ protected string $_key = 'extended_stats';
/**
- * (Optional) The value to use when the field is missing.
+ * The value to use when the field is missing.
*
- * @param mixed $missing
+ * @param mixed $value
* @return static
*/
- public function missing($missing)
+ public function missing($value): static
{
- return $this->addProperty('missing', $missing);
+ return $this->addProperty('missing', $value);
}
/**
- * (Optional) The script to use for the aggregation.
+ * The script to use for the aggregation.
*
- * @param string|callable $script
+ * @param string|callable $value
* @return static
*/
- public function script($script)
+ public function script($value): static
{
- return $this->addProperty('script', $script);
+ return $this->addProperty('script', $value);
}
/**
* Number of standard deviations above/below the mean to display.
*
- * @param int $sigma
+ * @param int $value
* @return static
*/
- public function sigma($sigma)
+ public function sigma(int $value): static
{
- return $this->addProperty('sigma', $sigma);
+ return $this->addProperty('sigma', $value);
}
}
diff --git a/src/DSL/Aggs/Metric/Max.php b/src/DSL/Aggs/Metric/Max.php
index cb71a35..4393603 100644
--- a/src/DSL/Aggs/Metric/Max.php
+++ b/src/DSL/Aggs/Metric/Max.php
@@ -1,5 +1,7 @@
addProperty('field', $field);
- }
+ protected string $_key = 'max';
/**
- * (Optional) The value to use when the field is missing.
+ * The value to use when the field is missing.
*
- * @param mixed $missing
+ * @param mixed $value
* @return static
*/
- public function missing($missing)
+ public function missing($value): static
{
- return $this->addProperty('missing', $missing);
+ return $this->addProperty('missing', $value);
}
/**
- * (Optional) The script to use for the aggregation.
+ * The script to use for the aggregation.
*
- * @param string|callable $script
+ * @param string|callable $value
* @return static
*/
- public function script($script)
+ public function script($value): static
{
- return $this->addProperty('script', $script);
+ return $this->addProperty('script', $value);
}
}
diff --git a/src/DSL/Aggs/Metric/Min.php b/src/DSL/Aggs/Metric/Min.php
index cb9fd7c..5f3121d 100644
--- a/src/DSL/Aggs/Metric/Min.php
+++ b/src/DSL/Aggs/Metric/Min.php
@@ -1,5 +1,7 @@
addProperty('field', $field);
- }
+ protected string $_key = 'min';
/**
- * (Optional) The value to use when the field is missing.
+ * The value to use when the field is missing.
*
- * @param mixed $missing
+ * @param mixed $value
* @return static
*/
- public function missing($missing)
+ public function missing($value): static
{
- return $this->addProperty('missing', $missing);
+ return $this->addProperty('missing', $value);
}
/**
- * (Optional) The script to use for the aggregation.
+ * The script to use for the aggregation.
*
- * @param string|callable $script
+ * @param string|callable $value
* @return static
*/
- public function script($script)
+ public function script($value): static
{
- return $this->addProperty('script', $script);
+ return $this->addProperty('script', $value);
}
}
diff --git a/src/DSL/Aggs/Metric/Stats.php b/src/DSL/Aggs/Metric/Stats.php
index 9b7ac44..3467257 100644
--- a/src/DSL/Aggs/Metric/Stats.php
+++ b/src/DSL/Aggs/Metric/Stats.php
@@ -1,5 +1,7 @@
addProperty('field', $field);
- }
+ protected string $_key = 'stats';
/**
- * (Optional) The value to use when the field is missing.
+ * The value to use when the field is missing.
*
- * @param mixed $missing
+ * @param mixed $value
* @return static
*/
- public function missing($missing)
+ public function missing($value): static
{
- return $this->addProperty('missing', $missing);
+ return $this->addProperty('missing', $value);
}
/**
- * (Optional) The script to use for the aggregation.
+ * The script to use for the aggregation.
*
- * @param string|callable $script
+ * @param string|callable $value
* @return static
*/
- public function script($script)
+ public function script($value): static
{
- return $this->addProperty('script', $script);
+ return $this->addProperty('script', $value);
}
}
diff --git a/src/DSL/Aggs/Metric/Sum.php b/src/DSL/Aggs/Metric/Sum.php
index f86dfbc..063c905 100644
--- a/src/DSL/Aggs/Metric/Sum.php
+++ b/src/DSL/Aggs/Metric/Sum.php
@@ -1,5 +1,7 @@
addProperty('field', $field);
- }
+ protected string $_key = 'sum';
/**
- * (Optional) The value to use when the field is missing.
+ * The value to use when the field is missing.
*
- * @param mixed $missing
+ * @param mixed $value
* @return static
*/
- public function missing($missing)
+ public function missing($value): static
{
- return $this->addProperty('missing', $missing);
+ return $this->addProperty('missing', $value);
}
/**
- * (Optional) The script to use for the aggregation.
+ * The script to use for the aggregation.
*
- * @param string|callable $script
+ * @param string|callable $value
* @return static
*/
- public function script($script)
+ public function script($value): static
{
- return $this->addProperty('script', $script);
+ return $this->addProperty('script', $value);
}
}
diff --git a/src/DSL/Aggs/Metric/ValueCount.php b/src/DSL/Aggs/Metric/ValueCount.php
index bd7ef87..055fa7e 100644
--- a/src/DSL/Aggs/Metric/ValueCount.php
+++ b/src/DSL/Aggs/Metric/ValueCount.php
@@ -1,5 +1,7 @@
addProperty('field', $field);
- }
+ protected string $_key = 'value_count';
/**
- * (Optional) The script to use for the aggregation.
+ * The script to use for the aggregation.
*
- * @param string|callable $script
+ * @param string|callable $value
* @return static
*/
- public function script($script)
+ public function script($value): static
{
- return $this->addProperty('script', $script);
+ return $this->addProperty('script', $value);
}
}
diff --git a/src/DSL/Aggs/Pipeline.php b/src/DSL/Aggs/Pipeline.php
index 48b1461..09ec673 100644
--- a/src/DSL/Aggs/Pipeline.php
+++ b/src/DSL/Aggs/Pipeline.php
@@ -1,8 +1,11 @@
node(AvgBucket::create(is_string($params) ? ['buckets_path' => $params] : $params));
+ return $this->node(AvgBucket::create(is_string($value) ? ['buckets_path' => $value] : $value));
}
/**
* Computes the sum of a metric across sibling aggregation buckets.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function sumBucket($params)
+ public function sumBucket($value): static
{
- return $this->node(SumBucket::create(is_string($params) ? ['buckets_path' => $params] : $params));
+ return $this->node(SumBucket::create(is_string($value) ? ['buckets_path' => $value] : $value));
}
/**
* Finds the bucket with the maximum value of a metric across sibling buckets.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function maxBucket($params)
+ public function maxBucket($value): static
{
- return $this->node(MaxBucket::create(is_string($params) ? ['buckets_path' => $params] : $params));
+ return $this->node(MaxBucket::create(is_string($value) ? ['buckets_path' => $value] : $value));
}
/**
* Finds the bucket with the minimum value of a metric across sibling buckets.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function minBucket($params)
+ public function minBucket($value): static
{
- return $this->node(MinBucket::create(is_string($params) ? ['buckets_path' => $params] : $params));
+ return $this->node(MinBucket::create(is_string($value) ? ['buckets_path' => $value] : $value));
}
/**
* Computes count, min, max, avg, and sum stats across sibling aggregation buckets.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function statsBucket($params)
+ public function statsBucket($value): static
{
- return $this->node(StatsBucket::create(is_string($params) ? ['buckets_path' => $params] : $params));
+ return $this->node(StatsBucket::create(is_string($value) ? ['buckets_path' => $value] : $value));
}
/**
* Computes a cumulative running sum of a metric across parent histogram buckets.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function cumulativeSum($params)
+ public function cumulativeSum($value): static
{
- return $this->node(CumulativeSum::create(is_string($params) ? ['buckets_path' => $params] : $params));
+ return $this->node(CumulativeSum::create(is_string($value) ? ['buckets_path' => $value] : $value));
}
/**
* Computes the derivative of a metric between consecutive parent histogram buckets.
*
- * @param mixed $params
+ * @param mixed $value
* @return static
*/
- public function derivative($params)
+ public function derivative($value): static
{
- return $this->node(Derivative::create(is_string($params) ? ['buckets_path' => $params] : $params));
+ return $this->node(Derivative::create(is_string($value) ? ['buckets_path' => $value] : $value));
}
/**
* Runs a custom script to compute values from multiple bucket metrics.
*
- * @param mixed $params
+ * Unlike sibling pipeline methods, bucket_script.buckets_path must be a map
+ * (variable => path), so a bare string is rejected.
+ *
+ * @param array|callable|BucketScript $value
* @return static
*/
- public function bucketScript($params)
+ public function bucketScript($value): static
{
- return $this->node(BucketScript::create(is_string($params) ? ['buckets_path' => $params] : $params));
+ if (is_string($value)) {
+ throw new InvalidArgumentException('bucketScript() requires an array or closure; bucket_script.buckets_path must be a map, so a bare string is not valid.');
+ }
+
+ return $this->node(BucketScript::create($value));
}
}
diff --git a/src/DSL/Aggs/Pipeline/AvgBucket.php b/src/DSL/Aggs/Pipeline/AvgBucket.php
index ec494cb..dabce8a 100644
--- a/src/DSL/Aggs/Pipeline/AvgBucket.php
+++ b/src/DSL/Aggs/Pipeline/AvgBucket.php
@@ -1,5 +1,7 @@
addProperty('buckets_path', $path);
+ return $this->addProperty('buckets_path', $value);
}
/**
* Policy to apply when gaps are found in the data.
*
- * @param string $policy
+ * @param string $value
* @return static
*/
- public function gapPolicy($policy)
+ public function gapPolicy(string $value): static
{
- return $this->addProperty('gap_policy', $policy);
+ return $this->addProperty('gap_policy', $value);
}
/**
* Format for the output value.
*
- * @param string $format
+ * @param string $value
* @return static
*/
- public function format($format)
+ public function format(string $value): static
{
- return $this->addProperty('format', $format);
+ return $this->addProperty('format', $value);
}
/**
- * (Optional) The value to use when the aggregation is missing a value.
+ * The value to use when the aggregation is missing a value.
*
- * @param mixed $missing
+ * @param mixed $value
* @return static
*/
- public function missing($missing)
+ public function missing($value): static
{
- return $this->addProperty('missing', $missing);
+ return $this->addProperty('missing', $value);
}
}
diff --git a/src/DSL/Aggs/Pipeline/BucketScript.php b/src/DSL/Aggs/Pipeline/BucketScript.php
index 7df06b0..98eef0b 100644
--- a/src/DSL/Aggs/Pipeline/BucketScript.php
+++ b/src/DSL/Aggs/Pipeline/BucketScript.php
@@ -1,5 +1,7 @@
addProperty('buckets_path', $path);
+ return $this->addProperty('buckets_path', $value);
}
/**
* The script to execute for each bucket.
*
- * @param string|callable $script
+ * @param string|callable $value
* @return static
*/
- public function script($script)
+ public function script($value): static
{
- return $this->addProperty('script', $script);
+ return $this->addProperty('script', $value);
}
/**
* Policy to apply when gaps are found in the data.
*
- * @param string $policy
+ * @param string $value
* @return static
*/
- public function gapPolicy($policy)
+ public function gapPolicy(string $value): static
{
- return $this->addProperty('gap_policy', $policy);
+ return $this->addProperty('gap_policy', $value);
}
/**
* Format for the output value.
*
- * @param string $format
+ * @param string $value
* @return static
*/
- public function format($format)
+ public function format(string $value): static
{
- return $this->addProperty('format', $format);
+ return $this->addProperty('format', $value);
}
}
diff --git a/src/DSL/Aggs/Pipeline/CumulativeSum.php b/src/DSL/Aggs/Pipeline/CumulativeSum.php
index a484122..5eac62b 100644
--- a/src/DSL/Aggs/Pipeline/CumulativeSum.php
+++ b/src/DSL/Aggs/Pipeline/CumulativeSum.php
@@ -1,5 +1,7 @@
addProperty('buckets_path', $value);
+ }
+
+ /**
+ * Policy to apply when gaps are found in the data.
+ *
+ * @param string $value
* @return static
*/
- public function bucketsPath($path)
+ public function gapPolicy(string $value): static
{
- return $this->addProperty('buckets_path', $path);
+ return $this->addProperty('gap_policy', $value);
}
/**
* Format for the output value.
*
- * @param string $format
+ * @param string $value
* @return static
*/
- public function format($format)
+ public function format(string $value): static
{
- return $this->addProperty('format', $format);
+ return $this->addProperty('format', $value);
}
}
diff --git a/src/DSL/Aggs/Pipeline/Derivative.php b/src/DSL/Aggs/Pipeline/Derivative.php
index d087cc7..1f32506 100644
--- a/src/DSL/Aggs/Pipeline/Derivative.php
+++ b/src/DSL/Aggs/Pipeline/Derivative.php
@@ -1,5 +1,7 @@
addProperty('buckets_path', $path);
+ return $this->addProperty('buckets_path', $value);
}
/**
* Policy to apply when gaps are found in the data.
*
- * @param string $policy
+ * @param string $value
* @return static
*/
- public function gapPolicy($policy)
+ public function gapPolicy(string $value): static
{
- return $this->addProperty('gap_policy', $policy);
+ return $this->addProperty('gap_policy', $value);
}
/**
* Format for the output value.
*
- * @param string $format
+ * @param string $value
* @return static
*/
- public function format($format)
+ public function format(string $value): static
{
- return $this->addProperty('format', $format);
+ return $this->addProperty('format', $value);
}
/**
* The unit for the derivative when the histogram uses time units.
*
- * @param string $unit
+ * @param string $value
* @return static
*/
- public function unit($unit)
+ public function unit(string $value): static
{
- return $this->addProperty('unit', $unit);
+ return $this->addProperty('unit', $value);
}
}
diff --git a/src/DSL/Aggs/Pipeline/MaxBucket.php b/src/DSL/Aggs/Pipeline/MaxBucket.php
index ea5942c..e762188 100644
--- a/src/DSL/Aggs/Pipeline/MaxBucket.php
+++ b/src/DSL/Aggs/Pipeline/MaxBucket.php
@@ -1,5 +1,7 @@
addProperty('buckets_path', $path);
+ return $this->addProperty('buckets_path', $value);
}
/**
* Policy to apply when gaps are found in the data.
*
- * @param string $policy
+ * @param string $value
* @return static
*/
- public function gapPolicy($policy)
+ public function gapPolicy(string $value): static
{
- return $this->addProperty('gap_policy', $policy);
+ return $this->addProperty('gap_policy', $value);
}
/**
* Format for the output value.
*
- * @param string $format
+ * @param string $value
* @return static
*/
- public function format($format)
+ public function format(string $value): static
{
- return $this->addProperty('format', $format);
+ return $this->addProperty('format', $value);
}
/**
- * (Optional) The value to use when the aggregation is missing a value.
+ * The value to use when the aggregation is missing a value.
*
- * @param mixed $missing
+ * @param mixed $value
* @return static
*/
- public function missing($missing)
+ public function missing($value): static
{
- return $this->addProperty('missing', $missing);
+ return $this->addProperty('missing', $value);
}
}
diff --git a/src/DSL/Aggs/Pipeline/MinBucket.php b/src/DSL/Aggs/Pipeline/MinBucket.php
index 7cf0cbe..dcb4634 100644
--- a/src/DSL/Aggs/Pipeline/MinBucket.php
+++ b/src/DSL/Aggs/Pipeline/MinBucket.php
@@ -1,5 +1,7 @@
addProperty('buckets_path', $path);
+ return $this->addProperty('buckets_path', $value);
}
/**
* Policy to apply when gaps are found in the data.
*
- * @param string $policy
+ * @param string $value
* @return static
*/
- public function gapPolicy($policy)
+ public function gapPolicy(string $value): static
{
- return $this->addProperty('gap_policy', $policy);
+ return $this->addProperty('gap_policy', $value);
}
/**
* Format for the output value.
*
- * @param string $format
+ * @param string $value
* @return static
*/
- public function format($format)
+ public function format(string $value): static
{
- return $this->addProperty('format', $format);
+ return $this->addProperty('format', $value);
}
/**
- * (Optional) The value to use when the aggregation is missing a value.
+ * The value to use when the aggregation is missing a value.
*
- * @param mixed $missing
+ * @param mixed $value
* @return static
*/
- public function missing($missing)
+ public function missing($value): static
{
- return $this->addProperty('missing', $missing);
+ return $this->addProperty('missing', $value);
}
}
diff --git a/src/DSL/Aggs/Pipeline/StatsBucket.php b/src/DSL/Aggs/Pipeline/StatsBucket.php
index 8eb4520..93649b1 100644
--- a/src/DSL/Aggs/Pipeline/StatsBucket.php
+++ b/src/DSL/Aggs/Pipeline/StatsBucket.php
@@ -1,5 +1,7 @@
addProperty('buckets_path', $path);
+ return $this->addProperty('buckets_path', $value);
}
/**
* Policy to apply when gaps are found in the data.
*
- * @param string $policy
+ * @param string $value
* @return static
*/
- public function gapPolicy($policy)
+ public function gapPolicy(string $value): static
{
- return $this->addProperty('gap_policy', $policy);
+ return $this->addProperty('gap_policy', $value);
}
/**
* Format for the output value.
*
- * @param string $format
+ * @param string $value
* @return static
*/
- public function format($format)
+ public function format(string $value): static
{
- return $this->addProperty('format', $format);
+ return $this->addProperty('format', $value);
}
/**
- * (Optional) The value to use when the aggregation is missing a value.
+ * The value to use when the aggregation is missing a value.
*
- * @param mixed $missing
+ * @param mixed $value
* @return static
*/
- public function missing($missing)
+ public function missing($value): static
{
- return $this->addProperty('missing', $missing);
+ return $this->addProperty('missing', $value);
}
}
diff --git a/src/DSL/Aggs/Pipeline/SumBucket.php b/src/DSL/Aggs/Pipeline/SumBucket.php
index 07a68d0..74dc3c2 100644
--- a/src/DSL/Aggs/Pipeline/SumBucket.php
+++ b/src/DSL/Aggs/Pipeline/SumBucket.php
@@ -1,5 +1,7 @@
addProperty('buckets_path', $path);
+ return $this->addProperty('buckets_path', $value);
}
/**
* Policy to apply when gaps are found in the data.
*
- * @param string $policy
+ * @param string $value
* @return static
*/
- public function gapPolicy($policy)
+ public function gapPolicy(string $value): static
{
- return $this->addProperty('gap_policy', $policy);
+ return $this->addProperty('gap_policy', $value);
}
/**
* Format for the output value.
*
- * @param string $format
+ * @param string $value
* @return static
*/
- public function format($format)
+ public function format(string $value): static
{
- return $this->addProperty('format', $format);
+ return $this->addProperty('format', $value);
}
/**
- * (Optional) The value to use when the aggregation is missing a value.
+ * The value to use when the aggregation is missing a value.
*
- * @param mixed $missing
+ * @param mixed $value
* @return static
*/
- public function missing($missing)
+ public function missing($value): static
{
- return $this->addProperty('missing', $missing);
+ return $this->addProperty('missing', $value);
}
}
diff --git a/src/DSL/DeepClone.php b/src/DSL/DeepClone.php
new file mode 100644
index 0000000..7500d52
--- /dev/null
+++ b/src/DSL/DeepClone.php
@@ -0,0 +1,66 @@
+> */
+ private static array $cloneProperties = [];
+
+ public function __clone(): void
+ {
+ $class = static::class;
+ if (!isset(self::$cloneProperties[$class])) {
+ self::$cloneProperties[$class] = array_filter(
+ (new ReflectionClass($class))->getProperties(),
+ fn ($p) => !$p->isStatic()
+ );
+ }
+
+ foreach (self::$cloneProperties[$class] as $property) {
+ if (!$property->isInitialized($this)) {
+ continue;
+ }
+
+ $value = $property->getValue($this);
+
+ if (is_array($value)) {
+ $property->setValue($this, self::cloneArray($value));
+ } elseif (is_object($value) && !($value instanceof Closure)) {
+ $property->setValue($this, clone $value);
+ }
+ }
+ }
+
+ /**
+ * Recursively clone object entries in an array.
+ *
+ * @param array $array
+ * @return array
+ */
+ private static function cloneArray(array $array): array
+ {
+ foreach ($array as $key => $value) {
+ if (is_array($value)) {
+ $array[$key] = self::cloneArray($value);
+ } elseif (is_object($value) && !($value instanceof Closure)) {
+ $array[$key] = clone $value;
+ }
+ }
+
+ return $array;
+ }
+}
diff --git a/src/DSL/Node.php b/src/DSL/Node.php
index 7967e4d..5e663cb 100644
--- a/src/DSL/Node.php
+++ b/src/DSL/Node.php
@@ -1,67 +1,79 @@
|null
*/
- protected $_properties;
+ protected ?array $_properties = null;
/**
- * Raw scalar value stored separately from properties.
+ * Scalar value stored separately from properties.
* When set, toArray() outputs shorthand (field => value) if no extra
* properties exist, or promotes it under $_valueKey when properties are present.
*
* @var scalar|null
*/
- protected $_rawValue;
+ protected int|float|string|bool|null $_value = null;
/**
- * The key used when promoting $_rawValue into the properties array.
+ * The key used when promoting $_value into the properties array.
* Override in subclasses that use a different key (e.g. 'query' for match queries).
*
* @var string
*/
- protected $_valueKey = 'value';
+ protected string $_valueKey = 'value';
/**
* Whether to use a field name as the top-level attribute of a node.
*
* @var bool
*/
- protected $_isPropertyField = false;
+ protected bool $_fieldKeyed = false;
/**
* The field name used as the top-level attribute of a node.
*
* @var string
*/
- protected $_field;
+ protected string $_field;
/**
* Whether the node supports multiple clauses.
*
* @var bool
*/
- protected $_multi = false;
+ protected bool $_multi = false;
/**
* The Elasticsearch query or aggregation type identifier.
*
* @var string
*/
- protected $_key;
+ protected string $_key;
/**
* Initialize the node.
@@ -72,101 +84,139 @@ abstract class Node
* - new Term('status', fn($t) => ...) — K,V closure
* - new Term([...]) — array properties
* - new Term(fn($t) => ...) — closure
+ * - new Script('_score * ...') — bare scalar (whole node value)
* - new Term() — empty
*
- * @param mixed $field Properties, field name, closure, or null
+ * @param mixed $field Properties, field name, closure, scalar value, or null
* @param mixed $value Value/properties/closure when using two-arg mode
*/
public function __construct($field = null, $value = null)
{
if ($value !== null) {
- // Two-arg mode: field + value
- if ($value instanceof Closure) {
- $value($this);
- } elseif (is_scalar($value)) {
- $this->_rawValue = $value;
- $this->_properties = [];
- } else {
- $this->_properties = $value;
- }
- if ($this->_isPropertyField) {
- $this->field($field);
- }
+ $this->fromKeyValue($field, $value);
} elseif ($field instanceof Closure) {
- // Single-arg closure
- $field($this);
- } elseif ($this->_isPropertyField && is_array($field)) {
- // Array shorthand: ['field_name' => value]
- foreach ($field as $key => $val) {
- $this->field($key);
- if (is_scalar($val)) {
- $this->_rawValue = $val;
- $this->_properties = [];
- } else {
- $this->_properties = $val;
- }
- break;
- }
- } elseif ($this->_isPropertyField && is_scalar($field)) {
- $this->_rawValue = $field;
+ $this->fromClosure($field);
+ } elseif ($this->_fieldKeyed && is_array($field)) {
+ $this->fromArrayField($field);
+ } elseif (is_scalar($field)) {
+ $this->fromScalar($field);
+ } elseif (is_array($field)) {
+ $this->fromArrayProperties($field);
+ }
+ }
+
+ /**
+ * Initialize from a field-value pair.
+ *
+ * @param mixed $field
+ * @param mixed $value
+ */
+ protected function fromKeyValue($field, $value): void
+ {
+ if ($value instanceof Closure) {
+ $value($this);
+ } elseif (is_scalar($value)) {
+ $this->_value = $value;
$this->_properties = [];
+ } elseif (is_array($value)) {
+ $this->_properties = $value;
} else {
- $this->_properties = $field;
+ throw new InvalidArgumentException(sprintf(
+ '%s does not accept %s as a field value; use a clause key, closure, scalar, or array.',
+ static::class,
+ get_debug_type($value)
+ ));
+ }
+ if ($this->_fieldKeyed) {
+ $this->field($field);
}
}
/**
- * Set whether this node uses a field name as the top-level attribute.
+ * Initialize from a closure.
*
- * @param bool $isPropertyField
- * @return static
+ * @param Closure $closure
*/
- protected function isPropertyField($isPropertyField)
+ protected function fromClosure(Closure $closure): void
{
- $this->_isPropertyField = $isPropertyField;
- return $this;
+ $closure($this);
}
/**
- * Whether the node supports multiple clauses.
+ * Initialize from a single-element array where key is field name.
*
- * @param bool $multi
- * @return static
+ * @param array $field
*/
- protected function multi($multi)
+ protected function fromArrayField(array $field): void
{
- $this->_multi = $multi;
- return $this;
+ foreach ($field as $key => $val) {
+ $this->field($key);
+ if (is_scalar($val)) {
+ $this->_value = $val;
+ $this->_properties = [];
+ } elseif (is_array($val)) {
+ $this->_properties = $val;
+ }
+ break;
+ }
}
/**
- * Set whether the node supports multiple clauses.
+ * Initialize from an array of properties.
*
- * @param bool $multi
+ * Default: store as-is. Override to route specific keys through
+ * clause accumulators (addClause) instead of raw addProperty.
+ *
+ * @param array $field
+ */
+ protected function fromArrayProperties(array $field): void
+ {
+ $this->_properties = $field;
+ }
+
+ /**
+ * Initialize from a scalar value.
+ *
+ * @param mixed $value
+ */
+ protected function fromScalar($value): void
+ {
+ $this->_value = $value;
+ $this->_properties = [];
+ }
+
+ /**
+ * Set whether this node uses a field name as the top-level attribute.
+ *
+ * @param bool $fieldKeyed
* @return static
*/
- protected function setMulti($multi)
+ protected function fieldKeyed(bool $fieldKeyed): static
{
- $this->_multi = $multi;
+ $this->_fieldKeyed = $fieldKeyed;
return $this;
}
/**
* Whether the node supports multiple clauses.
*
- * @return bool
+ * @param bool $multi
+ * @return static
*/
- protected function isMulti()
+ protected function multi(bool $multi): static
{
- return $this->_multi;
+ $this->_multi = $multi;
+ return $this;
}
/**
* Get the Elasticsearch type identifier.
*
+ * @internal
+ *
* @return string
*/
- public function key()
+ public function key(): string
{
return $this->_key;
}
@@ -177,9 +227,9 @@ public function key()
* @param string $field
* @return static
*/
- public function field($field)
+ public function field($field): static
{
- if ($this->_isPropertyField) {
+ if ($this->_fieldKeyed) {
$this->_field = $field;
} else {
$this->_properties['field'] = $field;
@@ -195,7 +245,7 @@ public function field($field)
* @param bool $append
* @return static
*/
- public function addProperty($attribute, $value, $append = false)
+ protected function addProperty($attribute, $value, $append = false): static
{
if ($append) {
$this->_properties[$attribute][] = $value;
@@ -215,7 +265,7 @@ public function addProperty($attribute, $value, $append = false)
* @param mixed $value
* @return static
*/
- public static function create($field = null, $value = null)
+ public static function create($field = null, $value = null): static
{
if ($value === null && $field instanceof static) {
return $field;
@@ -229,51 +279,74 @@ public static function create($field = null, $value = null)
* @param array $properties
* @return array
*/
- protected function resolveProperties(array $properties)
+ protected function resolveProperties(array $properties): array
{
foreach ($properties as $key => $property) {
if ($property instanceof Query) {
- $properties[$key] = $property->toArray()['query'];
+ $properties[$key] = $property->toArray()['query'] ?? null;
+ } elseif ($property instanceof Agg) {
+ $properties[$key] = $property->toArray();
} elseif ($property instanceof Node) {
$properties[$key] = $property->toArray();
} elseif ($property instanceof Closure) {
- $properties[$key] = Query::create($property)->toArray()['query'];
+ $properties[$key] = Query::create($property)->toArray()['query'] ?? null;
} elseif (is_array($property)) {
$properties[$key] = $this->resolveProperties($property);
}
}
- return $properties;
+ return array_filter($properties, fn ($v) => $v !== null);
+ }
+
+ /**
+ * Wrap properties under the field name for field-keyed nodes.
+ *
+ * @param mixed $properties
+ * @return mixed
+ * @throws LogicException when the node is field-keyed but no field was set
+ */
+ protected function wrapFieldKeyed($properties): mixed
+ {
+ if (!$this->_fieldKeyed) {
+ return $properties;
+ }
+ if (!isset($this->_field)) {
+ throw new LogicException(sprintf(
+ '%s is field-keyed but no field was set; call field() before serializing.',
+ static::class
+ ));
+ }
+ return [$this->_field => $properties];
}
/**
* Serialize to an Elasticsearch DSL array.
*
* Recursively resolves nested Query and Node instances.
- * When _isPropertyField is true, wraps properties under the field name.
+ * When _fieldKeyed is true, wraps properties under the field name.
*
* @return array|mixed
*/
public function toArray()
{
- if ($this->_rawValue !== null) {
- if (empty($this->_properties)) {
- $properties = $this->_rawValue;
+ if ($this->_value !== null) {
+ $props = $this->_properties ?? [];
+ if ($props === []) {
+ $properties = $this->_value;
} else {
- $properties = $this->resolveProperties($this->_properties);
+ $properties = $this->resolveProperties($props);
if (!isset($properties[$this->_valueKey])) {
- $properties = array_merge([$this->_valueKey => $this->_rawValue], $properties);
+ $properties = array_merge([$this->_valueKey => $this->_value], $properties);
}
}
} else {
- $properties = is_array($this->_properties)
- ? $this->resolveProperties($this->_properties)
- : $this->_properties;
+ if (empty($this->_properties)) {
+ $properties = $this->_fieldKeyed ? null : new stdClass();
+ } else {
+ $properties = $this->resolveProperties($this->_properties);
+ }
}
- if ($this->_isPropertyField) {
- return [$this->_field => $properties];
- }
- return $properties;
+ return $this->wrapFieldKeyed($properties);
}
/**
@@ -283,9 +356,11 @@ public function toArray()
* @param int $depth
* @return string
*/
- public function toJson($flags = JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT, $depth = 512)
+ public function toJson(int $flags = JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_PRESERVE_ZERO_FRACTION, int $depth = 512): string
{
- return json_encode($this->toArray(), $flags, $depth);
+ $json = json_encode($this->toArray(), $flags, $depth);
+
+ return $json === false ? '' : $json;
}
/**
@@ -293,7 +368,7 @@ public function toJson($flags = JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT, $dep
*
* @return string
*/
- public function __toString()
+ public function __toString(): string
{
return $this->toJson();
}
@@ -302,11 +377,29 @@ public function __toString()
* Floating point number used to decrease or increase
* the relevance scores of a query. Defaults to 1.0.
*
- * @param float $boost
+ * @param float $value
* @return static
*/
- public function boost($boost)
+ public function boost($value): static
{
- return $this->addProperty('boost', $boost);
+ return $this->addProperty('boost', $value);
+ }
+
+ /**
+ * Low-frequency, universally-applicable ES fields that need not be declared
+ * on every node: _name (named query, returned in matched_queries).
+ *
+ * @param array $args
+ */
+ public function __call(string $name, array $args): static
+ {
+ if ($name === '_name') {
+ if (!isset($args[0])) {
+ throw new ArgumentCountError(sprintf('%s::_name() expects exactly 1 argument', static::class));
+ }
+ return $this->addProperty('_name', $args[0]);
+ }
+
+ throw new BadMethodCallException(sprintf('Method %s::%s does not exist', static::class, $name));
}
}
diff --git a/src/DSL/Param.php b/src/DSL/Param.php
index 49c0dd8..4469276 100644
--- a/src/DSL/Param.php
+++ b/src/DSL/Param.php
@@ -1,5 +1,7 @@
*/
- protected $_params = [];
+ protected array $_params = [];
/**
* Check if a search request parameter has been set.
@@ -20,7 +22,7 @@ trait Param
* @param string $key
* @return bool
*/
- public function hasParam($key)
+ public function hasParam(string $key): bool
{
return array_key_exists($key, $this->_params);
}
@@ -29,12 +31,12 @@ public function hasParam($key)
* Defines the maximum number of documents to return.
* Defaults to 10.
*
- * @param int $size
+ * @param int $value
* @return $this
*/
- public function size($size)
+ public function size($value): static
{
- $this->_params['size'] = $size;
+ $this->_params['size'] = $value;
return $this;
}
@@ -42,12 +44,12 @@ public function size($size)
* The starting document offset.
* Defaults to 0.
*
- * @param int $from
+ * @param int $value
* @return $this
*/
- public function from($from)
+ public function from($value): static
{
- $this->_params['from'] = $from;
+ $this->_params['from'] = $value;
return $this;
}
@@ -55,12 +57,12 @@ public function from($from)
* Specifies the period of time to wait for
* a response from each shard.
*
- * @param string $timeout
+ * @param string $value
* @return $this
*/
- public function timeout($timeout)
+ public function timeout($value): static
{
- $this->_params['timeout'] = $timeout;
+ $this->_params['timeout'] = $value;
return $this;
}
@@ -68,12 +70,12 @@ public function timeout($timeout)
* Minimum relevance score required for a document
* to be included in the result set.
*
- * @param float $minScore
+ * @param float $value
* @return $this
*/
- public function minScore($minScore)
+ public function minScore($value): static
{
- $this->_params['min_score'] = $minScore;
+ $this->_params['min_score'] = $value;
return $this;
}
@@ -81,12 +83,12 @@ public function minScore($minScore)
* Maximum number of documents to collect for
* each shard, upon reaching which the query execution will terminate early.
*
- * @param int $terminateAfter
+ * @param int $value
* @return $this
*/
- public function terminateAfter($terminateAfter)
+ public function terminateAfter($value): static
{
- $this->_params['terminate_after'] = $terminateAfter;
+ $this->_params['terminate_after'] = $value;
return $this;
}
@@ -94,12 +96,12 @@ public function terminateAfter($terminateAfter)
* If true, returns detailed information about
* score computation as part of a hit.
*
- * @param bool $explain
+ * @param bool $value
* @return $this
*/
- public function explain($explain)
+ public function explain($value): static
{
- $this->_params['explain'] = $explain;
+ $this->_params['explain'] = $value;
return $this;
}
@@ -107,24 +109,24 @@ public function explain($explain)
* If true, returns document version as part
* of a hit.
*
- * @param bool $version
+ * @param bool $value
* @return $this
*/
- public function version($version)
+ public function version($value): static
{
- $this->_params['version'] = $version;
+ $this->_params['version'] = $value;
return $this;
}
/**
* If true, the query is profiled.
*
- * @param bool $profile
+ * @param bool $value
* @return $this
*/
- public function profile($profile)
+ public function profile($value): static
{
- $this->_params['profile'] = $profile;
+ $this->_params['profile'] = $value;
return $this;
}
@@ -132,12 +134,12 @@ public function profile($profile)
* Number of hits matching the query to count
* accurately. Defaults to 10,000.
*
- * @param bool|int $trackTotalHits
+ * @param bool|int $value
* @return $this
*/
- public function trackTotalHits($trackTotalHits)
+ public function trackTotalHits($value): static
{
- $this->_params['track_total_hits'] = $trackTotalHits;
+ $this->_params['track_total_hits'] = $value;
return $this;
}
@@ -145,32 +147,36 @@ public function trackTotalHits($trackTotalHits)
* If true, returns sequence number and primary
* term of the last modification of each hit.
*
- * @param bool $seqNoPrimaryTerm
+ * @param bool $value
* @return $this
*/
- public function seqNoPrimaryTerm($seqNoPrimaryTerm)
+ public function seqNoPrimaryTerm($value): static
{
- $this->_params['seq_no_primary_term'] = $seqNoPrimaryTerm;
+ $this->_params['seq_no_primary_term'] = $value;
return $this;
}
/**
- * Sorts the response by the given criteria.
+ * Sorts the response by the given criteria. Appends to the sort list;
+ * multiple calls chain together.
*
- * - sort('price', 'asc') — field + order, supports chaining
- * - sort([['price' => 'asc']]) — raw ES array format
+ * - sort('price', 'asc') — field + order
+ * - sort('price', ['order' => 'asc', 'mode' => 'avg']) — field + options
* - sort('_score') — field without direction
+ * - sort([['price' => 'asc'], ['age' => 'desc']]) — raw ES list, each spec appended
*
* @param string|array $field
- * @param string|null $order
+ * @param string|array|null $order
* @return $this
*/
- public function sort($field, $order = null)
+ public function sort($field, $order = null): static
{
if ($order !== null) {
$this->_params['sort'][] = [$field => $order];
} elseif (is_array($field)) {
- $this->_params['sort'] = $field;
+ foreach ($field as $spec) {
+ $this->_params['sort'][] = $spec;
+ }
} else {
$this->_params['sort'][] = $field;
}
@@ -181,24 +187,24 @@ public function sort($field, $order = null)
* Indicates which source fields are returned
* for the search hits.
*
- * @param array|string $source
+ * @param array|string $value
* @return $this
*/
- public function source($source)
+ public function source($value): static
{
- $this->_params['_source'] = $source;
+ $this->_params['_source'] = $value;
return $this;
}
/**
* Sort values used to paginate results.
*
- * @param array $searchAfter
+ * @param array $value
* @return $this
*/
- public function searchAfter($searchAfter)
+ public function searchAfter($value): static
{
- $this->_params['search_after'] = $searchAfter;
+ $this->_params['search_after'] = $value;
return $this;
}
@@ -206,37 +212,37 @@ public function searchAfter($searchAfter)
* Controls which stored fields are returned
* as part of a hit.
*
- * @param array $storedFields
+ * @param array $value
* @return $this
*/
- public function storedFields($storedFields)
+ public function storedFields($value): static
{
- $this->_params['stored_fields'] = $storedFields;
+ $this->_params['stored_fields'] = $value;
return $this;
}
/**
* Returns docvalue fields as part of a hit.
*
- * @param array $docvalueFields
+ * @param array $value
* @return $this
*/
- public function docvalueFields($docvalueFields)
+ public function docvalueFields($value): static
{
- $this->_params['docvalue_fields'] = $docvalueFields;
+ $this->_params['docvalue_fields'] = $value;
return $this;
}
/**
- * Boosts the _score of documents from
- * specified indices.
+ * Boosts the _score of documents from specified indices.
+ * Appends to the indices_boost list; multiple calls chain together.
*
- * @param array $indicesBoost
+ * @param array $value
* @return $this
*/
- public function indicesBoost($indicesBoost)
+ public function indicesBoost($value): static
{
- $this->_params['indices_boost'] = [$indicesBoost];
+ $this->_params['indices_boost'][] = $value;
return $this;
}
@@ -244,12 +250,12 @@ public function indicesBoost($indicesBoost)
* If true, compute and return _score even when
* sorting on a field. Defaults to false.
*
- * @param bool $trackScores
+ * @param bool $value
* @return $this
*/
- public function trackScores($trackScores)
+ public function trackScores($value): static
{
- $this->_params['track_scores'] = $trackScores;
+ $this->_params['track_scores'] = $value;
return $this;
}
@@ -257,74 +263,81 @@ public function trackScores($trackScores)
* Returns values from fields in the search response.
* Supports field alias fields and array fields.
*
- * @param array $fields
+ * @param array $value
* @return $this
*/
- public function fields($fields)
+ public function fields($value): static
{
- $this->_params['fields'] = $fields;
+ $this->_params['fields'] = $value;
return $this;
}
/**
* Limits the search to a point in time (PIT).
*
- * @param array $pit
+ * @param array $value
* @return $this
*/
- public function pit($pit)
+ public function pit($value): static
{
- $this->_params['pit'] = $pit;
+ $this->_params['pit'] = $value;
return $this;
}
/**
- * (Optional) Filter applied after query and aggregation execution.
+ * Filter applied after query and aggregation execution.
* Accepts a closure, array, or Query object.
*
- * @param mixed $filter
+ * @param mixed $value
* @return $this
*/
- public function postFilter($filter)
+ public function postFilter($value): static
{
- $this->_params['post_filter'] = Query::create($filter);
+ $this->_params['post_filter'] = Query::create($value);
return $this;
}
/**
- * (Optional) Collapse search results by field value.
+ * Collapse search results by field value.
*
- * @param mixed $collapse
+ * @param mixed $value
* @return $this
*/
- public function collapse($collapse)
+ public function collapse($value): static
{
- $this->_params['collapse'] = Params\Collapse::create($collapse);
+ $this->_params['collapse'] = Params\Collapse::create($value);
return $this;
}
/**
- * (Optional) Rescore the top documents with a secondary query.
+ * Rescore the top documents with a secondary query.
*
- * @param mixed $rescore
+ * @param mixed $value
* @return $this
*/
- public function rescore($rescore)
+ public function rescore($value): static
{
- $this->_params['rescore'] = Params\Rescore::create($rescore);
+ $item = Params\Rescore::create($value);
+ if (!array_key_exists('rescore', $this->_params)) {
+ $this->_params['rescore'] = $item;
+ } elseif (!is_array($this->_params['rescore'])) {
+ $this->_params['rescore'] = [$this->_params['rescore'], $item];
+ } else {
+ $this->_params['rescore'][] = $item;
+ }
return $this;
}
/**
- * (Optional) Highlight search matches in field values.
+ * Highlight search matches in field values.
* Supports chaining — fields are merged across calls.
*
- * @param mixed $highlight
+ * @param mixed $value
* @return $this
*/
- public function highlight($highlight)
+ public function highlight($value): static
{
- $new = Params\Highlight::create($highlight);
+ $new = Params\Highlight::create($value);
if (isset($this->_params['highlight']) && $this->_params['highlight'] instanceof Params\Highlight) {
// Merge new fields into existing highlight
@@ -336,9 +349,9 @@ public function highlight($highlight)
}
// Merge other properties (pre_tags, post_tags, etc) — last wins
if (is_array($new->_properties)) {
- foreach ($new->_properties as $key => $value) {
+ foreach ($new->_properties as $key => $val) {
if ($key !== 'fields') {
- $existing->addProperty($key, $value);
+ $existing->addProperty($key, $val);
}
}
}
@@ -350,43 +363,43 @@ public function highlight($highlight)
}
/**
- * (Optional) Search suggestions based on term, completion, or phrase.
+ * Search suggestions based on term, completion, or phrase.
*
- * @param mixed $suggest
+ * @param mixed $value
* @return $this
*/
- public function suggest($suggest)
+ public function suggest($value): static
{
- $this->_params['suggest'] = Params\Suggest::create($suggest);
+ $this->_params['suggest'] = Params\Suggest::create($value);
return $this;
}
/**
* Returns script evaluation values for each hit.
*
- * @param array $scriptFields
+ * @param array $value
* @return $this
*/
- public function scriptFields($scriptFields)
+ public function scriptFields($value): static
{
- $this->_params['script_fields'] = $scriptFields;
+ $this->_params['script_fields'] = $value;
return $this;
}
/**
* Runtime field definitions used in the search request.
*
- * @param array $runtimeMappings
+ * @param array $value
* @return $this
*/
- public function runtimeMappings($runtimeMappings)
+ public function runtimeMappings($value): static
{
- $this->_params['runtime_mappings'] = $runtimeMappings;
+ $this->_params['runtime_mappings'] = $value;
return $this;
}
/**
- * (Optional) Performs a k-nearest neighbor (kNN) search on a dense_vector field.
+ * Performs a k-nearest neighbor (kNN) search on a dense_vector field.
* Supports chaining — multiple calls append knn clauses as an array.
*
* - knn(array) — raw ES structure
@@ -397,7 +410,7 @@ public function runtimeMappings($runtimeMappings)
* @param array|null $queryVector
* @return $this
*/
- public function knn($knn, $queryVector = null)
+ public function knn($knn, $queryVector = null): static
{
if (is_string($knn) && $queryVector !== null) {
$node = (new Params\Knn())->field($knn)->queryVector($queryVector);
diff --git a/src/DSL/Params/Collapse.php b/src/DSL/Params/Collapse.php
index 1b01b73..8f242da 100644
--- a/src/DSL/Params/Collapse.php
+++ b/src/DSL/Params/Collapse.php
@@ -1,5 +1,7 @@
field($field);
@@ -32,24 +34,13 @@ public static function create($field = null, $value = null)
}
/**
- * The field to collapse the result set on.
- *
- * @param string $field
- * @return static
- */
- public function field($field)
- {
- return $this->addProperty('field', $field);
- }
-
- /**
- * (Optional) Expand each collapsed top hit with the inner_hits option.
+ * Expand each collapsed top hit with the inner_hits option.
*
* @param string $name
* @param mixed $hits
* @return static
*/
- public function innerHits($name, $hits = null)
+ public function innerHits(string $name, $hits = null): static
{
return $this->addProperty('inner_hits', ['name' => $name] + (array)$hits);
}
@@ -57,11 +48,11 @@ public function innerHits($name, $hits = null)
/**
* Maximum number of concurrent group searches.
*
- * @param int $max
+ * @param int $value
* @return static
*/
- public function maxConcurrentGroupSearches($max)
+ public function maxConcurrentGroupSearches(int $value): static
{
- return $this->addProperty('max_concurrent_group_searches', $max);
+ return $this->addProperty('max_concurrent_group_searches', $value);
}
}
diff --git a/src/DSL/Params/Highlight.php b/src/DSL/Params/Highlight.php
index 9f8347b..37e3d6e 100644
--- a/src/DSL/Params/Highlight.php
+++ b/src/DSL/Params/Highlight.php
@@ -1,5 +1,7 @@
field($field);
@@ -34,13 +36,13 @@ public static function create($field = null, $value = null)
}
/**
- * (Required) Add a field to highlight. Empty settings produces `{}`.
+ * Add a field to highlight. Empty settings produces `{}`.
*
* @param string $field
* @param array $settings
* @return static
*/
- public function field($field, $settings = [])
+ public function field($field, array $settings = []): static
{
$value = empty($settings) ? new stdClass() : $settings;
$this->_properties['fields'][$field] = $value;
@@ -50,152 +52,143 @@ public function field($field, $settings = [])
/**
* Opening HTML tags for highlighted snippets.
*
- * @param array $tags
+ * @param array $value
* @return static
*/
- public function preTags($tags)
+ public function preTags(array $value): static
{
- return $this->addProperty('pre_tags', $tags);
+ return $this->addProperty('pre_tags', $value);
}
/**
* Closing HTML tags for highlighted snippets.
*
- * @param array $tags
+ * @param array $value
* @return static
*/
- public function postTags($tags)
+ public function postTags(array $value): static
{
- return $this->addProperty('post_tags', $tags);
+ return $this->addProperty('post_tags', $value);
}
/**
* Size of a highlighted fragment. Defaults to 100.
*
- * @param int $size
+ * @param int $value
* @return static
*/
- public function fragmentSize($size)
+ public function fragmentSize(int $value): static
{
- return $this->addProperty('fragment_size', $size);
+ return $this->addProperty('fragment_size', $value);
}
/**
* Maximum number of fragments to return.
*
- * @param int $num
+ * @param int $value
* @return static
*/
- public function numberOfFragments($num)
+ public function numberOfFragments(int $value): static
{
- return $this->addProperty('number_of_fragments', $num);
+ return $this->addProperty('number_of_fragments', $value);
}
/**
* Highlighter encoder: html or default.
*
- * @param string $encoder
+ * @param string $value
* @return static
*/
- public function encoder($encoder)
+ public function encoder(string $value): static
{
- return $this->addProperty('encoder', $encoder);
+ return $this->addProperty('encoder', $value);
}
/**
* Sort order for highlighted fragments: score or none.
*
- * @param string $order
+ * @param string $value
* @return static
*/
- public function order($order)
+ public function order(string $value): static
{
- return $this->addProperty('order', $order);
+ return $this->addProperty('order', $value);
}
/**
- * (Optional) Highlight against a query other than the search query.
+ * Highlight against a query other than the search query.
*
- * @param mixed $query
+ * @param mixed $value
* @return static
*/
- public function highlightQuery($query)
+ public function highlightQuery($value): static
{
- return $this->addProperty('highlight_query', Query::create($query));
+ return $this->addProperty('highlight_query', Query::create($value));
}
/**
* Highlighter type: unified, plain, or fvh.
*
- * @param string $type
+ * @param string $value
* @return static
*/
- public function type($type)
+ public function type(string $value): static
{
- return $this->addProperty('type', $type);
+ return $this->addProperty('type', $value);
}
/**
* Boundary scanner: chars, sentence, or word.
*
- * @param string $scanner
+ * @param string $value
* @return static
*/
- public function boundaryScanner($scanner)
+ public function boundaryScanner(string $value): static
{
- return $this->addProperty('boundary_scanner', $scanner);
+ return $this->addProperty('boundary_scanner', $value);
}
/**
* Locale for the boundary scanner.
*
- * @param string $locale
+ * @param string $value
* @return static
*/
- public function boundaryScannerLocale($locale)
+ public function boundaryScannerLocale(string $value): static
{
- return $this->addProperty('boundary_scanner_locale', $locale);
+ return $this->addProperty('boundary_scanner_locale', $value);
}
/**
* Maximum distance for the boundary scanner.
*
- * @param int $max
+ * @param int $value
* @return static
*/
- public function boundaryMaxScan($max)
+ public function boundaryMaxScan(int $value): static
{
- return $this->addProperty('boundary_max_scan', $max);
+ return $this->addProperty('boundary_max_scan', $value);
}
/**
* Size of snippet when no matching fragment is found.
*
- * @param int $size
+ * @param int $value
* @return static
*/
- public function noMatchSize($size)
+ public function noMatchSize(int $value): static
{
- return $this->addProperty('no_match_size', $size);
+ return $this->addProperty('no_match_size', $value);
}
/**
* Fragmenter: simple or span (plain highlighter only).
*
- * @param string $fragmenter
+ * @param string $value
* @return static
*/
- public function fragmenter($fragmenter)
- {
- return $this->addProperty('fragmenter', $fragmenter);
- }
-
- public function toArray()
+ public function fragmenter(string $value): static
{
- $result = parent::toArray();
- if (isset($result['highlight_query']) && $result['highlight_query'] instanceof Query) {
- $result['highlight_query'] = $result['highlight_query']->toArray()['query'] ?? new stdClass();
- }
- return $result;
+ return $this->addProperty('fragmenter', $value);
}
}
diff --git a/src/DSL/Params/Knn.php b/src/DSL/Params/Knn.php
index 01f80c2..d3e7140 100644
--- a/src/DSL/Params/Knn.php
+++ b/src/DSL/Params/Knn.php
@@ -1,10 +1,11 @@
addProperty('field', $field);
- }
+ protected string $_key = 'knn';
/**
* The query vector to search for.
*
- * @param array $vector
+ * @param array $value
* @return static
*/
- public function queryVector($vector)
+ public function queryVector(array $value): static
{
- return $this->addProperty('query_vector', $vector);
+ return $this->addProperty('query_vector', $value);
}
/**
* Number of nearest neighbors to return as top hits.
* Defaults to the search request's size.
*
- * @param int $k
+ * @param int $value
* @return static
*/
- public function k($k)
+ public function k(int $value): static
{
- return $this->addProperty('k', $k);
+ return $this->addProperty('k', $value);
}
/**
* Number of candidates to evaluate per shard.
* Defaults to max(k * 4, 50).
*
- * @param int $num
+ * @param int $value
* @return static
*/
- public function numCandidates($num)
+ public function numCandidates(int $value): static
{
- return $this->addProperty('num_candidates', $num);
+ return $this->addProperty('num_candidates', $value);
}
/**
* Minimum similarity threshold for a vector to be
* considered a match.
*
- * @param float $similarity
- * @return static
- */
- public function similarity($similarity)
- {
- return $this->addProperty('similarity', $similarity);
- }
-
- /**
- * Boost value for the kNN score.
- *
- * @param float $boost
+ * @param float $value
* @return static
*/
- public function boost($boost)
+ public function similarity(float $value): static
{
- return $this->addProperty('boost', $boost);
+ return $this->addProperty('similarity', $value);
}
/**
- * (Optional) Pre-filter applied during kNN search. Accepts a closure,
+ * Pre-filter applied during kNN search. Accepts a closure,
* array, or Query object.
*
- * @param mixed $filter
+ * @param mixed $value
* @return static
*/
- public function filter($filter)
+ public function filter($value): static
{
- return $this->addProperty('filter', Query::create($filter));
+ return $this->addProperty('filter', Query::create($value));
}
/**
- * (Optional) Inner hits configuration for nested kNN search.
+ * Inner hits configuration for nested kNN search.
*
- * @param mixed $innerHits
+ * @param mixed $value
* @return static
*/
- public function innerHits($innerHits)
+ public function innerHits($value): static
{
- return $this->addProperty('inner_hits', $innerHits);
+ return $this->addProperty('inner_hits', $value);
}
/**
- * (Optional) Rescore vector configuration for quantized vector rescoring.
+ * Rescore vector configuration for quantized vector rescoring.
*
- * @param array $rescoreVector
+ * @param array $value
* @return static
*/
- public function rescoreVector($rescoreVector)
- {
- return $this->addProperty('rescore_vector', $rescoreVector);
- }
-
- public function toArray()
+ public function rescoreVector(array $value): static
{
- $result = parent::toArray();
- if (isset($result['filter']) && $result['filter'] instanceof Query) {
- $result['filter'] = $result['filter']->toArray()['query'] ?? new stdClass();
- }
- return $result;
+ return $this->addProperty('rescore_vector', $value);
}
}
diff --git a/src/DSL/Params/Rescore.php b/src/DSL/Params/Rescore.php
index cef3e20..a470616 100644
--- a/src/DSL/Params/Rescore.php
+++ b/src/DSL/Params/Rescore.php
@@ -1,10 +1,11 @@
addProperty('window_size', $size);
+ return $this->addProperty('window_size', $value);
}
/**
- * (Required) The query to use for rescoring.
+ * The query to use for rescoring.
*
- * @param mixed $query
+ * @param mixed $value
* @return static
*/
- public function query($query)
+ public function query($value): static
{
- $this->_properties['query']['rescore_query'] = Query::create($query);
+ $this->_properties['query']['rescore_query'] = Query::create($value);
return $this;
}
/**
* Weight of the rescore query. Defaults to 1.0.
*
- * @param float $weight
+ * @param float $value
* @return static
*/
- public function rescoreQueryWeight($weight)
+ public function rescoreQueryWeight(float $value): static
{
- $this->_properties['query']['rescore_query_weight'] = $weight;
+ $this->_properties['query']['rescore_query_weight'] = $value;
return $this;
}
/**
* Weight of the original query. Defaults to 1.0.
*
- * @param float $weight
+ * @param float $value
* @return static
*/
- public function queryWeight($weight)
+ public function queryWeight(float $value): static
{
- $this->_properties['query']['query_weight'] = $weight;
+ $this->_properties['query']['query_weight'] = $value;
return $this;
}
@@ -66,21 +67,12 @@ public function queryWeight($weight)
* How scores are combined. Valid values: total,
* multiply, max, avg. Defaults to total.
*
- * @param string $mode
+ * @param string $value
* @return static
*/
- public function scoreMode($mode)
+ public function scoreMode(string $value): static
{
- $this->_properties['query']['score_mode'] = $mode;
+ $this->_properties['query']['score_mode'] = $value;
return $this;
}
-
- public function toArray()
- {
- $result = parent::toArray();
- if (isset($result['query']['rescore_query']) && $result['query']['rescore_query'] instanceof Query) {
- $result['query']['rescore_query'] = $result['query']['rescore_query']->toArray()['query'] ?? new stdClass();
- }
- return $result;
- }
}
diff --git a/src/DSL/Params/Suggest.php b/src/DSL/Params/Suggest.php
index 79eef40..1b10c6e 100644
--- a/src/DSL/Params/Suggest.php
+++ b/src/DSL/Params/Suggest.php
@@ -1,5 +1,7 @@
['field' => $field]];
if ($text !== null) {
@@ -35,10 +37,10 @@ public function term($alias, $field, $text = null)
*
* @param string $alias
* @param string $field
- * @param string|null $prefix
+ * @param ?string $prefix
* @return static
*/
- public function completion($alias, $field, $prefix = null)
+ public function completion(string $alias, string $field, ?string $prefix = null): static
{
$suggest = ['completion' => ['field' => $field]];
if ($prefix !== null) {
@@ -52,10 +54,10 @@ public function completion($alias, $field, $prefix = null)
*
* @param string $alias
* @param string $field
- * @param string|null $text
+ * @param ?string $text
* @return static
*/
- public function phrase($alias, $field, $text = null)
+ public function phrase(string $alias, string $field, ?string $text = null): static
{
$suggest = ['phrase' => ['field' => $field]];
if ($text !== null) {
diff --git a/src/DSL/Queries/Compound.php b/src/DSL/Queries/Compound.php
index a6a2836..ebc338d 100644
--- a/src/DSL/Queries/Compound.php
+++ b/src/DSL/Queries/Compound.php
@@ -1,8 +1,9 @@
value, ...]) — array of bool clauses
+ * Supports:
+ * bool(closure|Boolean) — full control over the bool query
+ * bool(['must' => value, ...]) — array of bool clauses
+ * bool('must', $query) — set a single clause (two-arg form)
+ * bool('minimum_should_match', 1) — set a single property (two-arg form)
*
* @example $query->bool(function (Boolean $b) { $b->must(function (Query $q) { $q->match('title', 'test') }) })
*
- * @param callable|Boolean|array $bool
+ * @param mixed $field Boolean instance, closure, array, or a clause/property key (two-arg form)
+ * @param mixed $value value for the two-arg form
* @return $this
*/
- public function bool($bool)
+ public function bool($field = null, $value = null): static
{
- if (is_array($bool)) {
- $boolean = new Boolean();
- foreach ($bool as $clause => $val) {
- $method = $clause === 'must_not' ? 'mustNot' : $clause;
- if ($val instanceof \Closure || $val instanceof Query) {
- $boolean->$method($val);
- } else {
- $boolean->addProperty($clause, $val);
- }
- }
- return $this->addQuery($boolean);
- }
- return $this->addQuery(Boolean::create($bool));
+ return $this->addQuery(Boolean::create($field, $value));
}
/**
* Add a boosting query.
*
- * @param callable|Boosting|array $boosting
+ * @param mixed $field
+ * @param mixed $value
* @return $this
*/
- public function boosting($boosting)
+ public function boosting($field = null, $value = null): static
{
- if (is_array($boosting)) {
- $b = new Boosting();
- foreach ($boosting as $key => $val) {
- if (($key === 'positive' || $key === 'negative')
- && ($val instanceof \Closure || $val instanceof Query)) {
- $b->$key($val);
- } else {
- $b->addProperty($key, $val);
- }
- }
- return $this->addQuery($b);
- }
- return $this->addQuery(Boosting::create($boosting));
+ return $this->addQuery(Boosting::create($field, $value));
}
/**
* Add a constant_score query.
*
- * @param callable|ConstantScore|array $constantScore
+ * @param mixed $field
+ * @param mixed $value
* @return $this
*/
- public function constantScore($constantScore)
+ public function constantScore($field = null, $value = null): static
{
- if (is_array($constantScore)) {
- $cs = new ConstantScore();
- foreach ($constantScore as $key => $val) {
- if ($key === 'filter' && ($val instanceof \Closure || $val instanceof Query)) {
- $cs->filter($val);
- } else {
- $cs->addProperty($key, $val);
- }
- }
- return $this->addQuery($cs);
- }
- return $this->addQuery(ConstantScore::create($constantScore));
+ return $this->addQuery(ConstantScore::create($field, $value));
}
/**
* Add a dis_max query.
*
- * @param callable|DisjunctionMax|array $disMax
+ * @param mixed $field
+ * @param mixed $value
* @return $this
*/
- public function disMax($disMax)
+ public function disMax($field = null, $value = null): static
{
- if (is_array($disMax)) {
- $dm = new DisjunctionMax();
- foreach ($disMax as $key => $val) {
- if ($key === 'queries' && ($val instanceof \Closure || $val instanceof Query)) {
- $dm->queries($val);
- } else {
- $dm->addProperty($key, $val);
- }
- }
- return $this->addQuery($dm);
- }
- return $this->addQuery(DisjunctionMax::create($disMax));
+ return $this->addQuery(DisjunctionMax::create($field, $value));
}
/**
* Add a function_score query.
*
- * @param mixed $functionScore
+ * @param mixed $value
* @return $this
*/
- public function functionScore($functionScore)
+ public function functionScore($value): static
{
- return $this->addQuery(FunctionScore::create($functionScore));
+ return $this->addQuery(FunctionScore::create($value));
}
}
diff --git a/src/DSL/Queries/Compound/Boolean.php b/src/DSL/Queries/Compound/Boolean.php
index 658fe51..e073c0f 100644
--- a/src/DSL/Queries/Compound/Boolean.php
+++ b/src/DSL/Queries/Compound/Boolean.php
@@ -1,10 +1,12 @@
addProperty('must', Query::create($must)->multi(true));
- }
-
- /**
- * Append a must clause. Supports multiple calls to incrementally build the bool query.
- * Clauses are held in a temporary buffer and merged into the must property during serialization.
- *
- * @param mixed $must
+ * @param Query|\Closure|array $value
* @return static
*/
- public function addMust($must)
+ public function must($value): static
{
- return $this->pushClause('must', $must);
+ return $this->addClause('must', $value);
}
/**
* The clause (query) should appear in the matching document.
+ * Supports multiple calls to incrementally build the bool query.
*
- * @param mixed $should
- * @return static
- */
- public function should($should)
- {
- return $this->addProperty('should', Query::create($should)->multi(true));
- }
-
- /**
- * Append a should clause. Supports multiple calls to incrementally build the bool query.
- *
- * @param mixed $should
+ * @param Query|\Closure|array $value
* @return static
*/
- public function addShould($should)
+ public function should($value): static
{
- return $this->pushClause('should', $should);
+ return $this->addClause('should', $value);
}
/**
* The clause (query) must appear in matching documents. However unlike must the score of the query will be ignored. Filter clauses are executed in filter context, meaning that scoring is ignored and clauses are considered for caching.
+ * Supports multiple calls to incrementally build the bool query.
*
- * @param mixed $filter
+ * @param Query|\Closure|array $value
* @return static
*/
- public function filter($filter)
+ public function filter($value): static
{
- return $this->addProperty('filter', Query::create($filter)->multi(true));
- }
-
- /**
- * Append a filter clause. Supports multiple calls to incrementally build the bool query.
- *
- * @param mixed $filter
- * @return static
- */
- public function addFilter($filter)
- {
- return $this->pushClause('filter', $filter);
+ return $this->addClause('filter', $value);
}
/**
* The clause (query) must not appear in the matching documents. Clauses are executed in filter context meaning that scoring is ignored and clauses are considered for caching. Because scoring is ignored, a score of 0 for all documents is returned.
+ * Supports multiple calls to incrementally build the bool query.
*
- * @param mixed $mustNot
- * @return static
- */
- public function mustNot($mustNot)
- {
- return $this->addProperty('must_not', Query::create($mustNot)->multi(true));
- }
-
- /**
- * Append a must_not clause. Supports multiple calls to incrementally build the bool query.
- *
- * @param mixed $mustNot
+ * @param Query|\Closure|array $value
* @return static
*/
- public function addMustNot($mustNot)
+ public function mustNot($value): static
{
- return $this->pushClause('must_not', $mustNot);
+ return $this->addClause('must_not', $value);
}
/**
@@ -111,11 +72,11 @@ public function addMustNot($mustNot)
*
* For other valid values, see the minimum_should_match parameter.
*
- * @param int $minimumShouldMatch
+ * @param int|string $value
* @return static
*/
- public function minimumShouldMatch($minimumShouldMatch)
+ public function minimumShouldMatch(int|string $value): static
{
- return $this->addProperty('minimum_should_match', $minimumShouldMatch);
+ return $this->addProperty('minimum_should_match', $value);
}
}
diff --git a/src/DSL/Queries/Compound/Boosting.php b/src/DSL/Queries/Compound/Boosting.php
index 4279c18..679c041 100644
--- a/src/DSL/Queries/Compound/Boosting.php
+++ b/src/DSL/Queries/Compound/Boosting.php
@@ -1,5 +1,7 @@
addProperty('positive', Query::create($positive));
+ return $this->addProperty('positive', Query::create($value));
}
/**
* Query used to decrease the relevance score of matching documents.
*
- * @param mixed $negative
+ * @param mixed $value
* @return static
*/
- public function negative($negative)
+ public function negative($value): static
{
- return $this->addProperty('negative', Query::create($negative));
+ return $this->addProperty('negative', Query::create($value));
}
/**
* Floating point number between 0 and 1.0 used to decrease the relevance scores of documents matching the negative query.
*
- * @param float $negativeBoost
+ * @param float $value
* @return static
*/
- public function negativeBoost($negativeBoost)
+ public function negativeBoost(float $value): static
{
- return $this->addProperty('negative_boost', $negativeBoost);
+ return $this->addProperty('negative_boost', $value);
}
}
diff --git a/src/DSL/Queries/Compound/ConstantScore.php b/src/DSL/Queries/Compound/ConstantScore.php
index 4101f4c..1a80442 100644
--- a/src/DSL/Queries/Compound/ConstantScore.php
+++ b/src/DSL/Queries/Compound/ConstantScore.php
@@ -1,5 +1,7 @@
addProperty('filter', Query::create($query));
+ return $this->addProperty('filter', Query::create($value));
}
}
diff --git a/src/DSL/Queries/Compound/DisjunctionMax.php b/src/DSL/Queries/Compound/DisjunctionMax.php
index 520cc02..ce6a5a7 100644
--- a/src/DSL/Queries/Compound/DisjunctionMax.php
+++ b/src/DSL/Queries/Compound/DisjunctionMax.php
@@ -1,10 +1,11 @@
addProperty('queries', Query::create($queries)->multi(true));
- }
-
- /**
- * Append a query clause. Supports multiple calls to incrementally build.
- *
- * @param mixed $query
+ * @param mixed $value
* @return static
*/
- public function addQuery($query)
+ public function queries($value): static
{
- return $this->pushClause('queries', $query);
+ return $this->addClause('queries', $value);
}
/**
* Floating point number between 0 and 1.0 used to increase the relevance scores of documents matching multiple query clauses. Defaults to 0.0.
*
- * @param float $tieBreaker
+ * @param float $value
* @return static
*/
- public function tieBreaker($tieBreaker)
+ public function tieBreaker(float $value): static
{
- return $this->addProperty('tie_breaker', $tieBreaker);
+ return $this->addProperty('tie_breaker', $value);
}
}
diff --git a/src/DSL/Queries/Compound/FunctionScore.php b/src/DSL/Queries/Compound/FunctionScore.php
index d92fff5..23b7ea8 100644
--- a/src/DSL/Queries/Compound/FunctionScore.php
+++ b/src/DSL/Queries/Compound/FunctionScore.php
@@ -1,5 +1,7 @@
addProperty('score_mode', $scoreMode);
+ return $this->addProperty('score_mode', $value);
}
/**
- * (Optional) Defines how the newly computed function score is combined with the query score.
+ * Defines how the newly computed function score is combined with the query score.
* Options: multiply (default), replace, sum, avg, max, min.
*
- * @param string $boostMode
+ * @param string $value
* @return static
*/
- public function boostMode($boostMode)
+ public function boostMode(string $value): static
{
- return $this->addProperty('boost_mode', $boostMode);
+ return $this->addProperty('boost_mode', $value);
}
/**
- * (Optional) Excludes documents that do not meet the specified score threshold.
+ * Excludes documents that do not meet the specified score threshold.
*
- * @param float $minScore
+ * @param float $value
* @return static
*/
- public function minScore($minScore)
+ public function minScore(float $value): static
{
- return $this->addProperty('min_score', $minScore);
+ return $this->addProperty('min_score', $value);
}
/**
- * (Optional) Restricts the new score to not exceed the specified limit. Defaults to FLT_MAX.
+ * Restricts the new score to not exceed the specified limit. Defaults to FLT_MAX.
*
- * @param float $maxBoost
+ * @param float $value
* @return static
*/
- public function maxBoost($maxBoost)
+ public function maxBoost(float $value): static
{
- return $this->addProperty('max_boost', $maxBoost);
+ return $this->addProperty('max_boost', $value);
}
/**
- * (Required) The query to be scored.
+ * The query to be scored.
*
- * @param mixed $query
+ * @param mixed $value
* @return static
*/
- public function query($query)
+ public function query($value): static
{
- return $this->addProperty('query', Query::create($query));
+ return $this->addProperty('query', Query::create($value));
}
/**
- * (Optional) Array of score functions to apply.
+ * Array of score functions to apply.
*
- * @param array $functions
+ * @param array $value
* @return static
*/
- public function functions($functions)
+ public function functions(array $value): static
{
- return $this->addProperty('functions', $functions);
+ return $this->addProperty('functions', $value);
}
/**
- * (Optional) Appends a score function to the functions array.
+ * Appends a score function to the functions array.
*
- * @param mixed $function
+ * @param mixed $value
* @return static
*/
- public function addFunction($function)
+ public function addFunction($value): static
{
- return $this->addProperty('functions', Function_::create($function), true);
+ return $this->addProperty('functions', Function_::create($value), true);
}
/**
@@ -114,9 +116,6 @@ public function toArray()
$properties = $this->resolveProperties($properties);
- if ($this->_isPropertyField) {
- return [$this->_field => $properties];
- }
- return $properties;
+ return $this->wrapFieldKeyed($properties);
}
}
diff --git a/src/DSL/Queries/Compound/Functions/Exp.php b/src/DSL/Queries/Compound/Functions/Exp.php
index 97bd842..adf6cc2 100644
--- a/src/DSL/Queries/Compound/Functions/Exp.php
+++ b/src/DSL/Queries/Compound/Functions/Exp.php
@@ -1,5 +1,7 @@
addProperty('origin', $origin);
+ return $this->addProperty('origin', $value);
}
/**
- * (Required) Defines the distance from origin + offset at which the computed score will equal the decay parameter.
+ * Defines the distance from origin + offset at which the computed score will equal the decay parameter.
*
- * @param mixed $scale
+ * @param mixed $value
* @return static
*/
- public function scale($scale)
+ public function scale($value): static
{
- return $this->addProperty('scale', $scale);
+ return $this->addProperty('scale', $value);
}
/**
- * (Optional) If defined, the decay function will only compute for documents with a distance greater than this offset. Defaults to 0.
+ * If defined, the decay function will only compute for documents with a distance greater than this offset. Defaults to 0.
*
- * @param mixed $offset
+ * @param mixed $value
* @return static
*/
- public function offset($offset)
+ public function offset($value): static
{
- return $this->addProperty('offset', $offset);
+ return $this->addProperty('offset', $value);
}
/**
- * (Optional) Defines how documents are scored at the distance given at scale. Defaults to 0.5.
+ * Defines how documents are scored at the distance given at scale. Defaults to 0.5.
*
- * @param float $decay
+ * @param float $value
* @return static
*/
- public function decay($decay)
+ public function decay(float $value): static
{
- return $this->addProperty('decay', $decay);
+ return $this->addProperty('decay', $value);
}
}
diff --git a/src/DSL/Queries/Compound/Functions/FieldValueFactor.php b/src/DSL/Queries/Compound/Functions/FieldValueFactor.php
index fdb4f41..8e3b945 100644
--- a/src/DSL/Queries/Compound/Functions/FieldValueFactor.php
+++ b/src/DSL/Queries/Compound/Functions/FieldValueFactor.php
@@ -1,54 +1,45 @@
addProperty('field', $field);
- }
+ protected string $_key = 'field_value_factor';
/**
* Optional factor to multiply the field value with, defaults to 1.
*
- * @param float $factor
+ * @param float $value
* @return static
*/
- public function factor($factor)
+ public function factor(float $value): static
{
- return $this->addProperty('factor', $factor);
+ return $this->addProperty('factor', $value);
}
/**
* Modifier to apply to the field value, can be one of: none, log, log1p, log2p, ln, ln1p, ln2p, square, sqrt, or reciprocal. Defaults to none.
*
- * @param string $modifier
+ * @param string $value
* @return static
*/
- public function modifier($modifier)
+ public function modifier(string $value): static
{
- return $this->addProperty('modifier', $modifier);
+ return $this->addProperty('modifier', $value);
}
/**
* Value used if the document doesn’t have that field. The modifier and factor are still applied to it as though it were read from the document.
*
- * @param mixed $missing
+ * @param string|int|float|bool $value
* @return static
*/
- public function missing($missing)
+ public function missing(string|int|float|bool $value): static
{
- return $this->addProperty('missing', $missing);
+ return $this->addProperty('missing', $value);
}
}
diff --git a/src/DSL/Queries/Compound/Functions/Function_.php b/src/DSL/Queries/Compound/Functions/Function_.php
index cd09444..dfbdbdb 100644
--- a/src/DSL/Queries/Compound/Functions/Function_.php
+++ b/src/DSL/Queries/Compound/Functions/Function_.php
@@ -1,5 +1,7 @@
addProperty('filter', Query::create($filter));
+ return $this->addProperty('filter', Query::create($value));
}
/**
- * (Optional) Multiplies the score by the provided weight value.
+ * Multiplies the score by the provided weight value.
*
- * @param float $weight
+ * @param float $value
* @return static
*/
- public function weight($weight)
+ public function weight(float $value): static
{
- return $this->addProperty('weight', $weight);
+ return $this->addProperty('weight', $value);
}
/**
- * (Optional) Generates uniformly distributed random scores from 0 up to but not including 1.
+ * Generates uniformly distributed random scores from 0 up to but not including 1.
*
- * @param mixed $randomScore
+ * @param mixed $value
* @return static
*/
- public function randomScore($randomScore = null)
+ public function randomScore($value = null): static
{
- return $this->addProperty('random_score', RandomScore::create($randomScore));
+ return $this->addProperty('random_score', RandomScore::create($value));
}
/**
- * (Optional) Wraps another query and customizes the scoring using a script.
+ * Wraps another query and customizes the scoring using a script.
*
- * @param mixed $scriptScore
+ * @param mixed $value
* @return static
*/
- public function scriptScore($scriptScore)
+ public function scriptScore($value): static
{
- return $this->addProperty('script_score', ScriptScore::create($scriptScore));
+ return $this->addProperty('script_score', ScriptScore::create($value));
}
/**
- * (Optional) Sets the script for script_score using a Script object or closure.
+ * Sets the script for script_score using a Script object or closure.
*
- * @param mixed $script
+ * @param mixed $value
* @return static
*/
- public function script($script)
+ public function script($value): static
{
- $scriptScore = (new ScriptScore())->script($script);
- return $this->addProperty('script_score', $scriptScore);
+ $valueScore = (new ScriptScore())->script($value);
+ return $this->addProperty('script_score', $valueScore);
}
/**
- * (Optional) Uses a numeric field value to influence the score.
+ * Uses a numeric field value to influence the score.
*
* @param mixed $field
- * @param mixed $fieldValueFactor
+ * @param mixed $value
* @return static
*/
- public function fieldValueFactor($field, $fieldValueFactor = null)
+ public function fieldValueFactor($field, $value = null): static
{
- return $this->addProperty('field_value_factor', FieldValueFactor::create($field, $fieldValueFactor));
+ return $this->addProperty('field_value_factor', FieldValueFactor::create($field, $value));
}
/**
- * (Optional) Scores documents using normal (Gaussian) decay based on distance from an origin point.
+ * Scores documents using normal (Gaussian) decay based on distance from an origin point.
*
* @param mixed $field
- * @param mixed $gauss
+ * @param mixed $value
* @return static
*/
- public function gauss($field, $gauss = null)
+ public function gauss($field, $value = null): static
{
- return $this->addProperty('gauss', Gauss::create($field, $gauss));
+ return $this->addProperty('gauss', Gauss::create($field, $value));
}
/**
- * (Optional) Scores documents using linear decay based on distance from an origin point.
+ * Scores documents using linear decay based on distance from an origin point.
*
* @param mixed $field
- * @param mixed $linear
+ * @param mixed $value
* @return static
*/
- public function linear($field, $linear = null)
+ public function linear($field, $value = null): static
{
- return $this->addProperty('linear', Linear::create($field, $linear));
+ return $this->addProperty('linear', Linear::create($field, $value));
}
/**
- * (Optional) Scores documents using exponential decay based on distance from an origin point.
+ * Scores documents using exponential decay based on distance from an origin point.
*
* @param mixed $field
- * @param mixed $exp
+ * @param mixed $value
* @return static
*/
- public function exp($field, $exp = null)
+ public function exp($field, $value = null): static
{
- return $this->addProperty('exp', Exp::create($field, $exp));
+ return $this->addProperty('exp', Exp::create($field, $value));
}
/**
diff --git a/src/DSL/Queries/Compound/Functions/Gauss.php b/src/DSL/Queries/Compound/Functions/Gauss.php
index 5bd3fa4..d88dae3 100644
--- a/src/DSL/Queries/Compound/Functions/Gauss.php
+++ b/src/DSL/Queries/Compound/Functions/Gauss.php
@@ -1,5 +1,7 @@
addProperty('origin', $origin);
+ return $this->addProperty('origin', $value);
}
/**
- * (Required) Defines the distance from origin + offset at which the computed score will equal the decay parameter.
+ * Defines the distance from origin + offset at which the computed score will equal the decay parameter.
*
- * @param mixed $scale
+ * @param mixed $value
* @return static
*/
- public function scale($scale)
+ public function scale($value): static
{
- return $this->addProperty('scale', $scale);
+ return $this->addProperty('scale', $value);
}
/**
- * (Optional) If defined, the decay function will only compute for documents with a distance greater than this offset. Defaults to 0.
+ * If defined, the decay function will only compute for documents with a distance greater than this offset. Defaults to 0.
*
- * @param mixed $offset
+ * @param mixed $value
* @return static
*/
- public function offset($offset)
+ public function offset($value): static
{
- return $this->addProperty('offset', $offset);
+ return $this->addProperty('offset', $value);
}
/**
- * (Optional) Defines how documents are scored at the distance given at scale. Defaults to 0.5.
+ * Defines how documents are scored at the distance given at scale. Defaults to 0.5.
*
- * @param float $decay
+ * @param float $value
* @return static
*/
- public function decay($decay)
+ public function decay(float $value): static
{
- return $this->addProperty('decay', $decay);
+ return $this->addProperty('decay', $value);
}
}
diff --git a/src/DSL/Queries/Compound/Functions/Linear.php b/src/DSL/Queries/Compound/Functions/Linear.php
index 32fe187..8d64265 100644
--- a/src/DSL/Queries/Compound/Functions/Linear.php
+++ b/src/DSL/Queries/Compound/Functions/Linear.php
@@ -1,5 +1,7 @@
addProperty('origin', $origin);
+ return $this->addProperty('origin', $value);
}
/**
- * (Required) Defines the distance from origin + offset at which the computed score will equal the decay parameter.
+ * Defines the distance from origin + offset at which the computed score will equal the decay parameter.
*
- * @param mixed $scale
+ * @param mixed $value
* @return static
*/
- public function scale($scale)
+ public function scale($value): static
{
- return $this->addProperty('scale', $scale);
+ return $this->addProperty('scale', $value);
}
/**
- * (Optional) If defined, the decay function will only compute for documents with a distance greater than this offset. Defaults to 0.
+ * If defined, the decay function will only compute for documents with a distance greater than this offset. Defaults to 0.
*
- * @param mixed $offset
+ * @param mixed $value
* @return static
*/
- public function offset($offset)
+ public function offset($value): static
{
- return $this->addProperty('offset', $offset);
+ return $this->addProperty('offset', $value);
}
/**
- * (Optional) Defines how documents are scored at the distance given at scale. Defaults to 0.5.
+ * Defines how documents are scored at the distance given at scale. Defaults to 0.5.
*
- * @param float $decay
+ * @param float $value
* @return static
*/
- public function decay($decay)
+ public function decay(float $value): static
{
- return $this->addProperty('decay', $decay);
+ return $this->addProperty('decay', $value);
}
}
diff --git a/src/DSL/Queries/Compound/Functions/RandomScore.php b/src/DSL/Queries/Compound/Functions/RandomScore.php
index 4a98aa9..a7ae078 100644
--- a/src/DSL/Queries/Compound/Functions/RandomScore.php
+++ b/src/DSL/Queries/Compound/Functions/RandomScore.php
@@ -1,5 +1,7 @@
addProperty('seed', $seed);
- }
+ protected string $_key = 'random_score';
/**
- * (Optional) Field used in combination with the seed to compute reproducible random scores.
+ * Seed value for reproducible random scores.
*
- * @param string $field
+ * @param mixed $value
* @return static
*/
- public function field($field)
+ public function seed($value): static
{
- return $this->addProperty('field', $field);
+ return $this->addProperty('seed', $value);
}
/**
diff --git a/src/DSL/Queries/Compound/Functions/ScriptScore.php b/src/DSL/Queries/Compound/Functions/ScriptScore.php
index 8175b3b..e2c9f7f 100644
--- a/src/DSL/Queries/Compound/Functions/ScriptScore.php
+++ b/src/DSL/Queries/Compound/Functions/ScriptScore.php
@@ -1,5 +1,7 @@
addProperty('script', Script::create($script));
+ return $this->addProperty('script', Script::create($value));
}
}
diff --git a/src/DSL/Queries/FullText.php b/src/DSL/Queries/FullText.php
index 666c3fb..7056c9b 100644
--- a/src/DSL/Queries/FullText.php
+++ b/src/DSL/Queries/FullText.php
@@ -1,5 +1,7 @@
addQuery(Intervals::create($field, $value));
}
@@ -38,7 +40,7 @@ public function intervals($field, $value = null)
* @param callable|string|array $value
* @return $this
*/
- public function match($field, $value = null)
+ public function match($field, $value = null): static
{
return $this->addQuery(Match_::create($field, $value));
}
@@ -50,7 +52,7 @@ public function match($field, $value = null)
* @param mixed $value
* @return $this
*/
- public function matchPhrase($field, $value = null)
+ public function matchPhrase($field, $value = null): static
{
return $this->addQuery(MatchPhrase::create($field, $value));
}
@@ -62,7 +64,7 @@ public function matchPhrase($field, $value = null)
* @param mixed $value
* @return $this
*/
- public function matchPhrasePrefix($field, $value = null)
+ public function matchPhrasePrefix($field, $value = null): static
{
return $this->addQuery(MatchPhrasePrefix::create($field, $value));
}
@@ -74,7 +76,7 @@ public function matchPhrasePrefix($field, $value = null)
* @param mixed $value
* @return $this
*/
- public function matchBoolPrefix($field, $value = null)
+ public function matchBoolPrefix($field, $value = null): static
{
return $this->addQuery(MatchBoolPrefix::create($field, $value));
}
@@ -87,7 +89,7 @@ public function matchBoolPrefix($field, $value = null)
* @param callable|MultiMatch|array $value
* @return $this
*/
- public function multiMatch($value)
+ public function multiMatch($value): static
{
return $this->addQuery(MultiMatch::create($value));
}
@@ -98,7 +100,7 @@ public function multiMatch($value)
* @param mixed $value
* @return $this
*/
- public function combinedFields($value)
+ public function combinedFields($value): static
{
return $this->addQuery(CombinedFields::create($value));
}
@@ -106,22 +108,22 @@ public function combinedFields($value)
/**
* Add a query_string query.
*
- * @param mixed $queryString
+ * @param mixed $value
* @return $this
*/
- public function queryString($queryString)
+ public function queryString($value): static
{
- return $this->addQuery(QueryString::create($queryString));
+ return $this->addQuery(QueryString::create($value));
}
/**
* Add a simple_query_string query.
*
- * @param mixed $simpleQueryString
+ * @param mixed $value
* @return $this
*/
- public function simpleQueryString($simpleQueryString)
+ public function simpleQueryString($value): static
{
- return $this->addQuery(SimpleQueryString::create($simpleQueryString));
+ return $this->addQuery(SimpleQueryString::create($value));
}
}
diff --git a/src/DSL/Queries/FullText/CombinedFields.php b/src/DSL/Queries/FullText/CombinedFields.php
index 4ea5799..5b29314 100644
--- a/src/DSL/Queries/FullText/CombinedFields.php
+++ b/src/DSL/Queries/FullText/CombinedFields.php
@@ -1,5 +1,7 @@
addProperty('query', $query);
+ return $this->addProperty('query', $value);
}
/**
@@ -32,48 +34,48 @@ public function query($query)
* patterns are allowed. Only text fields are supported, and they must all
* have the same search analyzer.
*
- * @param array $fields
+ * @param array $value
* @return static
*/
- public function fields($fields)
+ public function fields(array $value): static
{
- return $this->addProperty('fields', $fields);
+ return $this->addProperty('fields', $value);
}
/**
* If true, match phrase queries are automatically
* created for multi-term synonyms. Defaults to true.
*
- * @param bool $autoGenerateSynonymsPhraseQuery
+ * @param bool $value
* @return static
*/
- public function autoGenerateSynonymsPhraseQuery($autoGenerateSynonymsPhraseQuery)
+ public function autoGenerateSynonymsPhraseQuery(bool $value): static
{
- return $this->addProperty('auto_generate_synonyms_phrase_query', $autoGenerateSynonymsPhraseQuery);
+ return $this->addProperty('auto_generate_synonyms_phrase_query', $value);
}
/**
* Boolean logic used to interpret text in the query
* value. Valid values are: or (Default), and.
*
- * @param string $operator
+ * @param string $value
* @return static
*/
- public function operator($operator)
+ public function operator(string $value): static
{
- return $this->addProperty('operator', $operator);
+ return $this->addProperty('operator', $value);
}
/**
* Minimum number of clauses that must match for a
* document to be returned.
*
- * @param string $minimumShouldMatch
+ * @param int|string $value
* @return static
*/
- public function minimumShouldMatch($minimumShouldMatch)
+ public function minimumShouldMatch(int|string $value): static
{
- return $this->addProperty('minimum_should_match', $minimumShouldMatch);
+ return $this->addProperty('minimum_should_match', $value);
}
/**
@@ -81,11 +83,11 @@ public function minimumShouldMatch($minimumShouldMatch)
* analyzer removes all tokens, such as when using a stop filter.
* Valid values are: none (Default), all.
*
- * @param string $zeroTermsQuery
+ * @param string $value
* @return static
*/
- public function zeroTermsQuery($zeroTermsQuery)
+ public function zeroTermsQuery(string $value): static
{
- return $this->addProperty('zero_terms_query', $zeroTermsQuery);
+ return $this->addProperty('zero_terms_query', $value);
}
}
diff --git a/src/DSL/Queries/FullText/Intervals.php b/src/DSL/Queries/FullText/Intervals.php
index 077167f..6d9fa6a 100644
--- a/src/DSL/Queries/FullText/Intervals.php
+++ b/src/DSL/Queries/FullText/Intervals.php
@@ -1,8 +1,11 @@
*/
- protected $_intervals = [];
+ protected array $_intervals = [];
/**
* Add a match rule that matches analyzed text.
*
- * @param mixed $match
+ * @param mixed $value
* @return static
*/
- public function match($match)
+ public function match($value): static
{
- $this->_intervals[] = Intervals\Match_::create($match);
+ $this->_intervals[] = Intervals\Match_::create($value);
return $this;
}
@@ -34,24 +37,37 @@ public function match($match)
* Add a prefix rule that matches terms that start with a specified set
* of characters.
*
- * @param mixed $prefix
+ * @param mixed $value
* @return static
*/
- public function prefix($prefix)
+ public function prefix($value): static
{
- $this->_intervals[] = Intervals\Prefix::create($prefix);
+ $this->_intervals[] = Intervals\Prefix::create($value);
return $this;
}
/**
* Add a wildcard rule that matches terms using a wildcard pattern.
*
- * @param mixed $wildcard
+ * @param mixed $value
+ * @return static
+ */
+ public function wildcard($value): static
+ {
+ $this->_intervals[] = Intervals\Wildcard::create($value);
+ return $this;
+ }
+
+ /**
+ * Add a regexp rule that matches terms using a regular expression
+ * pattern.
+ *
+ * @param mixed $value
* @return static
*/
- public function wildcard($wildcard)
+ public function regexp($value): static
{
- $this->_intervals[] = Intervals\Wildcard::create($wildcard);
+ $this->_intervals[] = Intervals\Regexp::create($value);
return $this;
}
@@ -59,24 +75,24 @@ public function wildcard($wildcard)
* Add a fuzzy rule that matches terms that are similar to the provided
* term, within a defined edit distance.
*
- * @param mixed $fuzzy
+ * @param mixed $value
* @return static
*/
- public function fuzzy($fuzzy)
+ public function fuzzy($value): static
{
- $this->_intervals[] = Intervals\Fuzzy::create($fuzzy);
+ $this->_intervals[] = Intervals\Fuzzy::create($value);
return $this;
}
/**
* Add a range rule that matches terms that fall within a specified range.
*
- * @param mixed $range
+ * @param mixed $value
* @return static
*/
- public function range($range)
+ public function range($value): static
{
- $this->_intervals[] = Intervals\Range::create($range);
+ $this->_intervals[] = Intervals\Range::create($value);
return $this;
}
@@ -84,12 +100,12 @@ public function range($range)
* Add an all_of rule that returns matches that span a combination of
* other rules.
*
- * @param mixed $allOf
+ * @param mixed $value
* @return static
*/
- public function allOf($allOf)
+ public function allOf($value): static
{
- $this->_intervals[] = Intervals\AllOf::create($allOf);
+ $this->_intervals[] = Intervals\AllOf::create($value);
return $this;
}
@@ -97,12 +113,12 @@ public function allOf($allOf)
* Add an any_of rule that returns intervals produced by any of its
* sub-rules.
*
- * @param mixed $anyOf
+ * @param mixed $value
* @return static
*/
- public function anyOf($anyOf)
+ public function anyOf($value): static
{
- $this->_intervals[] = Intervals\AnyOf::create($anyOf);
+ $this->_intervals[] = Intervals\AnyOf::create($value);
return $this;
}
@@ -120,16 +136,34 @@ public function toArray()
}
}
if (!$this->_multi) {
- $properties = array_reduce($resolved, function ($carry, $item) {
- return array_merge($carry, $item);
- }, []);
+ $properties = $this->mergeUnique($resolved);
+ if (!empty($this->_properties)) {
+ $properties = $this->mergeUnique([$properties, $this->resolveProperties($this->_properties)]);
+ }
} else {
$properties = $resolved;
}
- if ($this->_isPropertyField) {
- return [$this->_field => $properties];
+ return $this->wrapFieldKeyed($properties);
+ }
+
+ /**
+ * Merge clause arrays, throwing on duplicate keys instead of silently overwriting.
+ *
+ * @param array> $clauses
+ * @return array
+ * @throws RuntimeException when two clauses share a key
+ */
+ private function mergeUnique(array $clauses): array
+ {
+ $merged = [];
+ foreach ($clauses as $clause) {
+ $clash = array_intersect_key($merged, $clause);
+ if ($clash) {
+ throw new RuntimeException(sprintf('Duplicate interval clause key "%s".', implode('", "', array_keys($clash))));
+ }
+ $merged += $clause;
}
- return $properties;
+ return $merged;
}
}
diff --git a/src/DSL/Queries/FullText/Intervals/AllOf.php b/src/DSL/Queries/FullText/Intervals/AllOf.php
index 817a3ec..61d3fcb 100644
--- a/src/DSL/Queries/FullText/Intervals/AllOf.php
+++ b/src/DSL/Queries/FullText/Intervals/AllOf.php
@@ -1,5 +1,7 @@
isPropertyField(false)
+ $value = Intervals::create($value)
+ ->fieldKeyed(false)
->multi(true);
- return $this->addProperty('intervals', $intervals);
+ return $this->addProperty('intervals', $value);
}
/**
* Append an interval rule. Supports multiple calls to incrementally build.
*
- * @param mixed $interval
+ * @param mixed $value
* @return static
*/
- public function addInterval($interval)
+ public function addInterval($value): static
{
if (!isset($this->_properties['intervals'])) {
- $this->_properties['intervals'] = (new Intervals())->isPropertyField(false)->multi(true);
+ $this->_properties['intervals'] = (new Intervals())->fieldKeyed(false)->multi(true);
}
$target = $this->_properties['intervals'];
- if ($interval instanceof \Closure) {
- $interval($target);
- } elseif ($interval instanceof Node) {
- $target->addQuery($interval);
+ if ($value instanceof \Closure) {
+ $value($target);
}
return $this;
}
@@ -52,35 +52,35 @@ public function addInterval($interval)
* terms. Intervals produced by the rules further apart than this are not
* considered matches. Defaults to -1 (no restriction).
*
- * @param int $maxGaps
+ * @param int $value
* @return static
*/
- public function maxGaps($maxGaps)
+ public function maxGaps(int $value): static
{
- return $this->addProperty('max_gaps', $maxGaps);
+ return $this->addProperty('max_gaps', $value);
}
/**
* If true, intervals produced by the rules should
* appear in the order in which they are specified. Defaults to false.
*
- * @param bool $ordered
+ * @param bool $value
* @return static
*/
- public function ordered($ordered)
+ public function ordered(bool $value): static
{
- return $this->addProperty('ordered', $ordered);
+ return $this->addProperty('ordered', $value);
}
/**
* Rule used to filter returned
* intervals.
*
- * @param mixed $filter
+ * @param mixed $value
* @return static
*/
- public function filter($filter)
+ public function filter($value): static
{
- return $this->addProperty('filter', $filter);
+ return $this->addProperty('filter', Filter::create($value));
}
}
diff --git a/src/DSL/Queries/FullText/Intervals/AnyOf.php b/src/DSL/Queries/FullText/Intervals/AnyOf.php
index b1da1e6..b5fd8e9 100644
--- a/src/DSL/Queries/FullText/Intervals/AnyOf.php
+++ b/src/DSL/Queries/FullText/Intervals/AnyOf.php
@@ -1,5 +1,7 @@
isPropertyField(false)
+ $value = Intervals::create($value)
+ ->fieldKeyed(false)
->multi(true);
- return $this->addProperty('intervals', $intervals);
+ return $this->addProperty('intervals', $value);
}
/**
* Append an interval rule. Supports multiple calls to incrementally build.
*
- * @param mixed $interval
+ * @param mixed $value
* @return static
*/
- public function addInterval($interval)
+ public function addInterval($value): static
{
if (!isset($this->_properties['intervals'])) {
- $this->_properties['intervals'] = (new Intervals())->isPropertyField(false)->multi(true);
+ $this->_properties['intervals'] = (new Intervals())->fieldKeyed(false)->multi(true);
}
$target = $this->_properties['intervals'];
- if ($interval instanceof \Closure) {
- $interval($target);
- } elseif ($interval instanceof Node) {
- $target->addQuery($interval);
+ if ($value instanceof \Closure) {
+ $value($target);
}
return $this;
}
@@ -50,11 +50,11 @@ public function addInterval($interval)
* Rule used to filter returned
* intervals.
*
- * @param mixed $filter
+ * @param mixed $value
* @return static
*/
- public function filter($filter)
+ public function filter($value): static
{
- return $this->addProperty('filter', Filter::create($filter));
+ return $this->addProperty('filter', Filter::create($value));
}
}
diff --git a/src/DSL/Queries/FullText/Intervals/Filter.php b/src/DSL/Queries/FullText/Intervals/Filter.php
index 940b5a2..5957a91 100644
--- a/src/DSL/Queries/FullText/Intervals/Filter.php
+++ b/src/DSL/Queries/FullText/Intervals/Filter.php
@@ -1,5 +1,7 @@
addProperty('after', Query::create($after));
+ return $this->addProperty('after', Query::create($value));
}
/**
* Query used to return intervals that occur before
* an interval from the filter rule.
*
- * @param mixed $before
+ * @param mixed $value
* @return static
*/
- public function before($before)
+ public function before($value): static
{
- return $this->addProperty('before', Query::create($before));
+ return $this->addProperty('before', Query::create($value));
}
/**
* Query used to return intervals contained by an
* interval from the filter rule.
*
- * @param mixed $containedBy
+ * @param mixed $value
* @return static
*/
- public function containedBy($containedBy)
+ public function containedBy($value): static
{
- return $this->addProperty('contained_by', Query::create($containedBy));
+ return $this->addProperty('contained_by', Query::create($value));
}
/**
* Query used to return intervals that contain an
* interval from the filter rule.
*
- * @param mixed $containing
+ * @param mixed $value
* @return static
*/
- public function containing($containing)
+ public function containing($value): static
{
- return $this->addProperty('containing', Query::create($containing));
+ return $this->addProperty('containing', Query::create($value));
}
/**
* Query used to return intervals that do not
* contain an interval from the filter rule.
*
- * @param mixed $notContaining
+ * @param mixed $value
* @return static
*/
- public function notContaining($notContaining)
+ public function notContaining($value): static
{
- return $this->addProperty('not_containing', Query::create($notContaining));
+ return $this->addProperty('not_containing', Query::create($value));
}
/**
* Query used to return intervals that overlap
* with an interval from the filter rule.
*
- * @param mixed $overlapping
+ * @param mixed $value
* @return static
*/
- public function overlapping($overlapping)
+ public function overlapping($value): static
{
- return $this->addProperty('overlapping', Query::create($overlapping));
+ return $this->addProperty('overlapping', Query::create($value));
}
/**
@@ -89,35 +93,35 @@ public function overlapping($overlapping)
* This script must return a boolean value, true or false. The script can
* use the interval variable with start, end, and gaps methods.
*
- * @param mixed $script
+ * @param mixed $value
* @return static
*/
- public function script($script)
+ public function script($value): static
{
- return $this->addProperty('script', Script::create($script));
+ return $this->addProperty('script', Script::create($value));
}
/**
* Query used to return intervals that are not
* contained by an interval from the filter rule.
*
- * @param mixed $notContainedBy
+ * @param mixed $value
* @return static
*/
- public function notContainedBy($notContainedBy)
+ public function notContainedBy($value): static
{
- return $this->addProperty('not_contained_by', Query::create($notContainedBy));
+ return $this->addProperty('not_contained_by', Query::create($value));
}
/**
* Query used to return intervals that do not
* overlap with an interval from the filter rule.
*
- * @param mixed $notOverlapping
+ * @param mixed $value
* @return static
*/
- public function notOverlapping($notOverlapping)
+ public function notOverlapping($value): static
{
- return $this->addProperty('not_overlapping', Query::create($notOverlapping));
+ return $this->addProperty('not_overlapping', Query::create($value));
}
}
diff --git a/src/DSL/Queries/FullText/Intervals/Fuzzy.php b/src/DSL/Queries/FullText/Intervals/Fuzzy.php
index 1a62ed3..419849b 100644
--- a/src/DSL/Queries/FullText/Intervals/Fuzzy.php
+++ b/src/DSL/Queries/FullText/Intervals/Fuzzy.php
@@ -1,5 +1,7 @@
addProperty('term', $term);
+ return $this->addProperty('term', $value);
}
/**
* Number of beginning characters left unchanged when
* creating expansions. Defaults to 0.
*
- * @param int $prefixLength
+ * @param int $value
* @return static
*/
- public function prefixLength($prefixLength)
+ public function prefixLength(int $value): static
{
- return $this->addProperty('prefix_length', $prefixLength);
+ return $this->addProperty('prefix_length', $value);
}
/**
* Indicates whether edits include transpositions of
* two adjacent characters (ab -> ba). Defaults to true.
*
- * @param bool $transpositions
+ * @param bool $value
* @return static
*/
- public function transpositions($transpositions)
+ public function transpositions(bool $value): static
{
- return $this->addProperty('transpositions', $transpositions);
+ return $this->addProperty('transpositions', $value);
}
/**
* Maximum edit distance allowed for matching.
* Defaults to auto.
*
- * @param string $fuzziness
+ * @param int|string $value
* @return static
*/
- public function fuzziness($fuzziness)
+ public function fuzziness(int|string $value): static
{
- return $this->addProperty('fuzziness', $fuzziness);
+ return $this->addProperty('fuzziness', $value);
}
/**
* Analyzer used to normalize the term. Defaults to the
* top-level field's analyzer.
*
- * @param string $analyzer
+ * @param string $value
* @return static
*/
- public function analyzer($analyzer)
+ public function analyzer(string $value): static
{
- return $this->addProperty('analyzer', $analyzer);
+ return $this->addProperty('analyzer', $value);
}
/**
@@ -77,11 +79,11 @@ public function analyzer($analyzer)
* than the top-level field. The term is normalized using the search
* analyzer from this field.
*
- * @param string $useField
+ * @param string $value
* @return static
*/
- public function useField($useField)
+ public function useField(string $value): static
{
- return $this->addProperty('use_field', $useField);
+ return $this->addProperty('use_field', $value);
}
}
diff --git a/src/DSL/Queries/FullText/Intervals/Match_.php b/src/DSL/Queries/FullText/Intervals/Match_.php
index 6c7e8f9..89657c4 100644
--- a/src/DSL/Queries/FullText/Intervals/Match_.php
+++ b/src/DSL/Queries/FullText/Intervals/Match_.php
@@ -1,5 +1,7 @@
addProperty('query', $query);
+ return $this->addProperty('query', $value);
}
/**
@@ -30,47 +32,47 @@ public function query($query)
* Defaults to -1 (no restriction). If set to 0, the terms must appear
* next to each other.
*
- * @param int $maxGaps
+ * @param int $value
* @return static
*/
- public function maxGaps($maxGaps)
+ public function maxGaps(int $value): static
{
- return $this->addProperty('max_gaps', $maxGaps);
+ return $this->addProperty('max_gaps', $value);
}
/**
* If true, matching terms must appear in their
* specified order. Defaults to false.
*
- * @param bool $ordered
+ * @param bool $value
* @return static
*/
- public function ordered($ordered = false)
+ public function ordered(bool $value = false): static
{
- return $this->addProperty('ordered', $ordered);
+ return $this->addProperty('ordered', $value);
}
/**
* Analyzer used to analyze terms in the query.
* Defaults to the top-level field's analyzer.
*
- * @param string $analyzer
+ * @param string $value
* @return static
*/
- public function analyzer($analyzer)
+ public function analyzer(string $value): static
{
- return $this->addProperty('analyzer', $analyzer);
+ return $this->addProperty('analyzer', $value);
}
/**
* An optional interval filter.
*
- * @param mixed $filter
+ * @param mixed $value
* @return static
*/
- public function filter($filter)
+ public function filter($value): static
{
- return $this->addProperty('filter', Filter::create($filter));
+ return $this->addProperty('filter', Filter::create($value));
}
/**
@@ -78,11 +80,11 @@ public function filter($filter)
* than the top-level field. Terms are analyzed using the search analyzer
* from this field.
*
- * @param string $useField
+ * @param string $value
* @return static
*/
- public function useField($useField)
+ public function useField(string $value): static
{
- return $this->addProperty('use_field', $useField);
+ return $this->addProperty('use_field', $value);
}
}
diff --git a/src/DSL/Queries/FullText/Intervals/Prefix.php b/src/DSL/Queries/FullText/Intervals/Prefix.php
index 2ff087e..38e7d60 100644
--- a/src/DSL/Queries/FullText/Intervals/Prefix.php
+++ b/src/DSL/Queries/FullText/Intervals/Prefix.php
@@ -1,5 +1,7 @@
addProperty('prefix', $prefix);
+ return $this->addProperty('prefix', $value);
}
/**
* Analyzer used to normalize the prefix. Defaults to
* the top-level field's analyzer.
*
- * @param string $analyzer
+ * @param string $value
* @return static
*/
- public function analyzer($analyzer)
+ public function analyzer(string $value): static
{
- return $this->addProperty('analyzer', $analyzer);
+ return $this->addProperty('analyzer', $value);
}
/**
@@ -42,11 +44,11 @@ public function analyzer($analyzer)
* than the top-level field. The prefix is normalized using the search
* analyzer from this field.
*
- * @param string $userField
+ * @param string $value
* @return static
*/
- public function useField($userField)
+ public function useField(string $value): static
{
- return $this->addProperty('use_field', $userField);
+ return $this->addProperty('use_field', $value);
}
}
diff --git a/src/DSL/Queries/FullText/Intervals/Range.php b/src/DSL/Queries/FullText/Intervals/Range.php
index f40a01b..b931ef9 100644
--- a/src/DSL/Queries/FullText/Intervals/Range.php
+++ b/src/DSL/Queries/FullText/Intervals/Range.php
@@ -1,9 +1,11 @@
addProperty('gte', $gte);
+ return $this->addProperty('gte', $value);
}
/**
- * (Optional) Greater than the specified value.
+ * Greater than the specified value.
*
- * @param mixed $gt
+ * @param string|int|float|bool $value
* @return static
*/
- public function gt($gt)
+ public function gt(string|int|float|bool $value): static
{
- return $this->addProperty('gt', $gt);
+ return $this->addProperty('gt', $value);
}
/**
- * (Optional) Less than or equal to the specified value.
+ * Less than or equal to the specified value.
*
- * @param mixed $lte
+ * @param string|int|float|bool $value
* @return static
*/
- public function lte($lte)
+ public function lte(string|int|float|bool $value): static
{
- return $this->addProperty('lte', $lte);
+ return $this->addProperty('lte', $value);
}
/**
- * (Optional) Less than the specified value.
+ * Less than the specified value.
*
- * @param mixed $lt
+ * @param string|int|float|bool $value
* @return static
*/
- public function lt($lt)
+ public function lt(string|int|float|bool $value): static
{
- return $this->addProperty('lt', $lt);
+ return $this->addProperty('lt', $value);
}
/**
* Analyzer used to normalize the range values.
* Defaults to the top-level field's analyzer.
*
- * @param string $analyzer
+ * @param string $value
* @return static
*/
- public function analyzer($analyzer)
+ public function analyzer(string $value): static
{
- return $this->addProperty('analyzer', $analyzer);
+ return $this->addProperty('analyzer', $value);
}
/**
* If specified, match intervals from this field rather
* than the top-level field.
*
- * @param string $useField
+ * @param string $value
* @return static
*/
- public function useField($useField)
+ public function useField(string $value): static
{
- return $this->addProperty('use_field', $useField);
+ return $this->addProperty('use_field', $value);
}
}
diff --git a/src/DSL/Queries/FullText/Intervals/Regexp.php b/src/DSL/Queries/FullText/Intervals/Regexp.php
new file mode 100644
index 0000000..306cbbe
--- /dev/null
+++ b/src/DSL/Queries/FullText/Intervals/Regexp.php
@@ -0,0 +1,55 @@
+addProperty('pattern', $value);
+ }
+
+ /**
+ * Analyzer used to normalize the pattern. Defaults to
+ * the top-level field's analyzer.
+ *
+ * @param string $value
+ * @return static
+ */
+ public function analyzer(string $value): static
+ {
+ return $this->addProperty('analyzer', $value);
+ }
+
+ /**
+ * If specified, match intervals from this field rather
+ * than the top-level field. The pattern is normalized using the search
+ * analyzer from this field.
+ *
+ * @param string $value
+ * @return static
+ */
+ public function useField(string $value): static
+ {
+ return $this->addProperty('use_field', $value);
+ }
+}
diff --git a/src/DSL/Queries/FullText/Intervals/Wildcard.php b/src/DSL/Queries/FullText/Intervals/Wildcard.php
index 0a3174c..92892ab 100644
--- a/src/DSL/Queries/FullText/Intervals/Wildcard.php
+++ b/src/DSL/Queries/FullText/Intervals/Wildcard.php
@@ -1,5 +1,7 @@
addProperty('pattern', $pattern);
+ return $this->addProperty('pattern', $value);
}
/**
* Analyzer used to normalize the pattern. Defaults to
* the top-level field's analyzer.
*
- * @param string $analyzer
+ * @param string $value
* @return static
*/
- public function analyzer($analyzer)
+ public function analyzer(string $value): static
{
- return $this->addProperty('analyzer', $analyzer);
+ return $this->addProperty('analyzer', $value);
}
/**
@@ -42,11 +44,11 @@ public function analyzer($analyzer)
* than the top-level field. The pattern is normalized using the search
* analyzer from this field.
*
- * @param string $useField
+ * @param string $value
* @return static
*/
- public function useField($useField)
+ public function useField(string $value): static
{
- return $this->addProperty('use_field', $useField);
+ return $this->addProperty('use_field', $value);
}
}
diff --git a/src/DSL/Queries/FullText/MatchBoolPrefix.php b/src/DSL/Queries/FullText/MatchBoolPrefix.php
index 9d41172..1c4e311 100644
--- a/src/DSL/Queries/FullText/MatchBoolPrefix.php
+++ b/src/DSL/Queries/FullText/MatchBoolPrefix.php
@@ -1,5 +1,7 @@
addProperty('query', $query);
+ return $this->addProperty('query', $value);
}
/**
* Maximum number of terms to which the last provided
* term of the query value will expand. Defaults to 50.
*
- * @param int $maxExpansions
+ * @param int $value
* @return static
*/
- public function maxExpansions($maxExpansions)
+ public function maxExpansions(int $value): static
{
- return $this->addProperty('max_expansions', $maxExpansions);
+ return $this->addProperty('max_expansions', $value);
}
/**
* If true, format-based errors, such as providing a
* text query value for a numeric field, are ignored. Defaults to false.
*
- * @param bool $lenient
+ * @param bool $value
* @return static
*/
- public function lenient($lenient)
+ public function lenient(bool $value): static
{
- return $this->addProperty('lenient', $lenient);
+ return $this->addProperty('lenient', $value);
}
/**
* Analyzer used to convert text in the query value
* into tokens. Defaults to the index-time analyzer mapped for the field.
*
- * @param string $analyzer
+ * @param string $value
* @return static
*/
- public function analyzer($analyzer)
+ public function analyzer(string $value): static
{
- return $this->addProperty('analyzer', $analyzer);
+ return $this->addProperty('analyzer', $value);
}
/**
* Minimum number of clauses that must match for a
* document to be returned.
*
- * @param string $minimumShouldMatch
+ * @param int|string $value
* @return static
*/
- public function minimumShouldMatch($minimumShouldMatch)
+ public function minimumShouldMatch(int|string $value): static
{
- return $this->addProperty('minimum_should_match', $minimumShouldMatch);
+ return $this->addProperty('minimum_should_match', $value);
}
/**
* Maximum edit distance allowed for fuzzy matching.
*
- * @param string $fuzziness
+ * @param int|string $value
* @return static
*/
- public function fuzziness($fuzziness)
+ public function fuzziness(int|string $value): static
{
- return $this->addProperty('fuzziness', $fuzziness);
+ return $this->addProperty('fuzziness', $value);
}
/**
* Number of beginning characters left unchanged for
* fuzzy matching. Defaults to 0.
*
- * @param int $prefixLength
+ * @param int $value
* @return static
*/
- public function prefixLength($prefixLength)
+ public function prefixLength(int $value): static
{
- return $this->addProperty('prefix_length', $prefixLength);
+ return $this->addProperty('prefix_length', $value);
}
/**
* If true, edits for fuzzy matching include
* transpositions of two adjacent characters (ab -> ba). Defaults to true.
*
- * @param bool $fuzzyTranspositions
+ * @param bool $value
* @return static
*/
- public function fuzzyTranspositions($fuzzyTranspositions)
+ public function fuzzyTranspositions(bool $value): static
{
- return $this->addProperty('fuzzy_transpositions', $fuzzyTranspositions);
+ return $this->addProperty('fuzzy_transpositions', $value);
}
/**
* Method used to rewrite the query for fuzzy matching.
*
- * @param string $fuzzyRewrite
+ * @param string $value
* @return static
*/
- public function fuzzyRewrite($fuzzyRewrite)
+ public function fuzzyRewrite(string $value): static
{
- return $this->addProperty('fuzzy_rewrite', $fuzzyRewrite);
+ return $this->addProperty('fuzzy_rewrite', $value);
}
/**
* Boolean logic used to interpret text in the query
* value. Valid values are: OR (Default), AND.
*
- * @param string $operator
+ * @param string $value
* @return static
*/
- public function operator($operator)
+ public function operator(string $value): static
{
- return $this->addProperty('operator', $operator);
+ return $this->addProperty('operator', $value);
}
}
diff --git a/src/DSL/Queries/FullText/MatchPhrase.php b/src/DSL/Queries/FullText/MatchPhrase.php
index 2ca8a03..921ab50 100644
--- a/src/DSL/Queries/FullText/MatchPhrase.php
+++ b/src/DSL/Queries/FullText/MatchPhrase.php
@@ -1,5 +1,7 @@
addProperty('query', $query);
+ return $this->addProperty('query', $value);
}
/**
* Analyzer used to convert text in the query value
* into tokens. Defaults to the index-time analyzer mapped for the field.
*
- * @param string $analyzer
+ * @param string $value
* @return static
*/
- public function analyzer($analyzer)
+ public function analyzer(string $value): static
{
- return $this->addProperty('analyzer', $analyzer);
+ return $this->addProperty('analyzer', $value);
}
/**
* Maximum number of positions allowed between matching
* tokens. Defaults to 0. Transposed terms have a slop of 2.
*
- * @param int $slop
+ * @param int $value
* @return static
*/
- public function slop($slop)
+ public function slop(int $value): static
{
- return $this->addProperty('slop', $slop);
+ return $this->addProperty('slop', $value);
}
/**
@@ -59,11 +61,11 @@ public function slop($slop)
* analyzer removes all tokens, such as when using a stop filter.
* Valid values are: none (Default), all.
*
- * @param string $zeroTermsQuery
+ * @param string $value
* @return static
*/
- public function zeroTermsQuery($zeroTermsQuery)
+ public function zeroTermsQuery(string $value): static
{
- return $this->addProperty('zero_terms_query', $zeroTermsQuery);
+ return $this->addProperty('zero_terms_query', $value);
}
}
diff --git a/src/DSL/Queries/FullText/MatchPhrasePrefix.php b/src/DSL/Queries/FullText/MatchPhrasePrefix.php
index ee99983..9b2fcc6 100644
--- a/src/DSL/Queries/FullText/MatchPhrasePrefix.php
+++ b/src/DSL/Queries/FullText/MatchPhrasePrefix.php
@@ -1,5 +1,7 @@
addProperty('query', $query);
+ return $this->addProperty('query', $value);
}
/**
* Analyzer used to convert text in the query value
* into tokens. Defaults to the index-time analyzer mapped for the field.
*
- * @param string $analyzer
+ * @param string $value
* @return static
*/
- public function analyzer($analyzer)
+ public function analyzer(string $value): static
{
- return $this->addProperty('analyzer', $analyzer);
+ return $this->addProperty('analyzer', $value);
}
/**
* Maximum number of terms to which the last provided
* term of the query value will expand. Defaults to 50.
*
- * @param int $maxExpansions
+ * @param int $value
* @return static
*/
- public function maxExpansions($maxExpansions)
+ public function maxExpansions(int $value): static
{
- return $this->addProperty('max_expansions', $maxExpansions);
+ return $this->addProperty('max_expansions', $value);
}
/**
* Maximum number of positions allowed between matching
* tokens. Defaults to 0. Transposed terms have a slop of 2.
*
- * @param int $slop
+ * @param int $value
* @return static
*/
- public function slop($slop)
+ public function slop(int $value): static
{
- return $this->addProperty('slop', $slop);
+ return $this->addProperty('slop', $value);
}
/**
@@ -72,11 +74,11 @@ public function slop($slop)
* analyzer removes all tokens, such as when using a stop filter.
* Valid values are: none (Default), all.
*
- * @param string $zeroTermsQuery
+ * @param string $value
* @return static
*/
- public function zeroTermsQuery($zeroTermsQuery)
+ public function zeroTermsQuery(string $value): static
{
- return $this->addProperty('zero_terms_query', $zeroTermsQuery);
+ return $this->addProperty('zero_terms_query', $value);
}
}
diff --git a/src/DSL/Queries/FullText/Match_.php b/src/DSL/Queries/FullText/Match_.php
index f57ca8b..70a0466 100644
--- a/src/DSL/Queries/FullText/Match_.php
+++ b/src/DSL/Queries/FullText/Match_.php
@@ -1,5 +1,7 @@
addProperty('query', $query);
+ return $this->addProperty('query', $value);
}
/**
* Analyzer used to convert the text in the query value
* into tokens. Defaults to the index-time analyzer mapped for the field.
*
- * @param string $analyzer
+ * @param string $value
* @return static
*/
- public function analyzer($analyzer)
+ public function analyzer(string $value): static
{
- return $this->addProperty('analyzer', $analyzer);
+ return $this->addProperty('analyzer', $value);
}
/**
* If true, match phrase queries are automatically
* created for multi-term synonyms. Defaults to true.
*
- * @param bool $autoGenerateSynonymsPhraseQuery
+ * @param bool $value
* @return static
*/
- public function autoGenerateSynonymsPhraseQuery($autoGenerateSynonymsPhraseQuery)
+ public function autoGenerateSynonymsPhraseQuery(bool $value): static
{
- return $this->addProperty('auto_generate_synonyms_phrase_query', $autoGenerateSynonymsPhraseQuery);
+ return $this->addProperty('auto_generate_synonyms_phrase_query', $value);
}
/**
* Maximum edit distance allowed for matching.
* See Fuzziness for valid values and more information.
*
- * @param string $fuzziness
+ * @param int|string $value
* @return static
*/
- public function fuzziness($fuzziness)
+ public function fuzziness(int|string $value): static
{
- return $this->addProperty('fuzziness', $fuzziness);
+ return $this->addProperty('fuzziness', $value);
}
/**
* Maximum number of terms to which the query will
* expand. Defaults to 50.
*
- * @param int $maxExpansions
+ * @param int $value
* @return static
*/
- public function maxExpansions($maxExpansions)
+ public function maxExpansions(int $value): static
{
- return $this->addProperty('max_expansions', $maxExpansions);
+ return $this->addProperty('max_expansions', $value);
}
/**
* Number of beginning characters left unchanged for
* fuzzy matching. Defaults to 0.
*
- * @param int $prefixLength
+ * @param int $value
* @return static
*/
- public function prefixLength($prefixLength)
+ public function prefixLength(int $value): static
{
- return $this->addProperty('prefix_length', $prefixLength);
+ return $this->addProperty('prefix_length', $value);
}
/**
* If true, edits for fuzzy matching include
* transpositions of two adjacent characters (ab -> ba). Defaults to true.
*
- * @param bool $fuzzyTranspositions
+ * @param bool $value
* @return static
*/
- public function fuzzyTranspositions($fuzzyTranspositions)
+ public function fuzzyTranspositions(bool $value): static
{
- return $this->addProperty('fuzzy_transpositions', $fuzzyTranspositions);
+ return $this->addProperty('fuzzy_transpositions', $value);
}
/**
@@ -109,48 +111,48 @@ public function fuzzyTranspositions($fuzzyTranspositions)
* parameter is not 0, the match query uses a fuzzy_rewrite method of
* top_terms_blended_freqs_${max_expansions} by default.
*
- * @param string $fuzzyRewrite
+ * @param string $value
* @return static
*/
- public function fuzzyRewrite($fuzzyRewrite)
+ public function fuzzyRewrite(string $value): static
{
- return $this->addProperty('fuzzy_rewrite', $fuzzyRewrite);
+ return $this->addProperty('fuzzy_rewrite', $value);
}
/**
* If true, format-based errors, such as providing a
* text query value for a numeric field, are ignored. Defaults to false.
*
- * @param bool $lenient
+ * @param bool $value
* @return static
*/
- public function lenient($lenient)
+ public function lenient(bool $value): static
{
- return $this->addProperty('lenient', $lenient);
+ return $this->addProperty('lenient', $value);
}
/**
* Boolean logic used to interpret text in the query
* value. Valid values are: OR (Default), AND.
*
- * @param string $operator
+ * @param string $value
* @return static
*/
- public function operator($operator)
+ public function operator(string $value): static
{
- return $this->addProperty('operator', $operator);
+ return $this->addProperty('operator', $value);
}
/**
* Minimum number of clauses that must match for a
* document to be returned.
*
- * @param string $minimumShouldMatch
+ * @param int|string $value
* @return static
*/
- public function minimumShouldMatch($minimumShouldMatch)
+ public function minimumShouldMatch(int|string $value): static
{
- return $this->addProperty('minimum_should_match', $minimumShouldMatch);
+ return $this->addProperty('minimum_should_match', $value);
}
/**
@@ -158,11 +160,11 @@ public function minimumShouldMatch($minimumShouldMatch)
* analyzer removes all tokens, such as when using a stop filter.
* Valid values are: none (Default), all.
*
- * @param string $zeroTermsQuery
+ * @param string $value
* @return static
*/
- public function zeroTermsQuery($zeroTermsQuery)
+ public function zeroTermsQuery(string $value): static
{
- return $this->addProperty('zero_terms_query', $zeroTermsQuery);
+ return $this->addProperty('zero_terms_query', $value);
}
}
diff --git a/src/DSL/Queries/FullText/MultiMatch.php b/src/DSL/Queries/FullText/MultiMatch.php
index 768bec5..38f35e3 100644
--- a/src/DSL/Queries/FullText/MultiMatch.php
+++ b/src/DSL/Queries/FullText/MultiMatch.php
@@ -1,5 +1,7 @@
addProperty('query', $query);
+ return $this->addProperty('query', $value);
}
/**
@@ -30,12 +32,12 @@ public function query($query)
* wildcards (*). Individual fields can be boosted with the caret (^)
* notation.
*
- * @param array $fields
+ * @param array $value
* @return static
*/
- public function fields($fields)
+ public function fields(array $value): static
{
- return $this->addProperty('fields', $fields);
+ return $this->addProperty('fields', $value);
}
/**
@@ -43,130 +45,130 @@ public function fields($fields)
* Valid values are: best_fields (Default), most_fields, cross_fields,
* phrase, phrase_prefix, bool_prefix.
*
- * @param string $type
+ * @param string $value
* @return static
*/
- public function type($type)
+ public function type(string $value): static
{
- return $this->addProperty('type', $type);
+ return $this->addProperty('type', $value);
}
/**
* Boolean logic used to interpret text in the query
* value. Valid values are: OR (Default), AND.
*
- * @param string $operator
+ * @param string $value
* @return static
*/
- public function operator($operator)
+ public function operator(string $value): static
{
- return $this->addProperty('operator', $operator);
+ return $this->addProperty('operator', $value);
}
/**
* Analyzer used to convert the text in the query value
* into tokens. Defaults to the index-time analyzer mapped for the field.
*
- * @param string $analyzer
+ * @param string $value
* @return static
*/
- public function analyzer($analyzer)
+ public function analyzer(string $value): static
{
- return $this->addProperty('analyzer', $analyzer);
+ return $this->addProperty('analyzer', $value);
}
/**
* Minimum number of clauses that must match for a
* document to be returned.
*
- * @param string $minimumShouldMatch
+ * @param int|string $value
* @return static
*/
- public function minimumShouldMatch($minimumShouldMatch)
+ public function minimumShouldMatch(int|string $value): static
{
- return $this->addProperty('minimum_should_match', $minimumShouldMatch);
+ return $this->addProperty('minimum_should_match', $value);
}
/**
* Floating point number used to decrease or increase
* the relevance scores of a query. Defaults to 1.0.
*
- * @param float $tieBreaker
+ * @param float $value
* @return static
*/
- public function tieBreaker($tieBreaker)
+ public function tieBreaker(float $value): static
{
- return $this->addProperty('tie_breaker', $tieBreaker);
+ return $this->addProperty('tie_breaker', $value);
}
/**
* Maximum edit distance allowed for fuzzy matching.
*
- * @param string $fuzziness
+ * @param int|string $value
* @return static
*/
- public function fuzziness($fuzziness)
+ public function fuzziness(int|string $value): static
{
- return $this->addProperty('fuzziness', $fuzziness);
+ return $this->addProperty('fuzziness', $value);
}
/**
* Number of beginning characters left unchanged for
* fuzzy matching. Defaults to 0.
*
- * @param int $prefixLength
+ * @param int $value
* @return static
*/
- public function prefixLength($prefixLength)
+ public function prefixLength(int $value): static
{
- return $this->addProperty('prefix_length', $prefixLength);
+ return $this->addProperty('prefix_length', $value);
}
/**
* Maximum number of terms to which the query will
* expand. Defaults to 50.
*
- * @param int $maxExpansions
+ * @param int $value
* @return static
*/
- public function maxExpansions($maxExpansions)
+ public function maxExpansions(int $value): static
{
- return $this->addProperty('max_expansions', $maxExpansions);
+ return $this->addProperty('max_expansions', $value);
}
/**
* If true, edits for fuzzy matching include
* transpositions of two adjacent characters (ab -> ba). Defaults to true.
*
- * @param bool $fuzzyTranspositions
+ * @param bool $value
* @return static
*/
- public function fuzzyTranspositions($fuzzyTranspositions)
+ public function fuzzyTranspositions(bool $value): static
{
- return $this->addProperty('fuzzy_transpositions', $fuzzyTranspositions);
+ return $this->addProperty('fuzzy_transpositions', $value);
}
/**
* Method used to rewrite the query for fuzzy matching.
*
- * @param string $fuzzyRewrite
+ * @param string $value
* @return static
*/
- public function fuzzyRewrite($fuzzyRewrite)
+ public function fuzzyRewrite(string $value): static
{
- return $this->addProperty('fuzzy_rewrite', $fuzzyRewrite);
+ return $this->addProperty('fuzzy_rewrite', $value);
}
/**
* If true, format-based errors, such as providing a
* text query value for a numeric field, are ignored. Defaults to false.
*
- * @param bool $lenient
+ * @param bool $value
* @return static
*/
- public function lenient($lenient)
+ public function lenient(bool $value): static
{
- return $this->addProperty('lenient', $lenient);
+ return $this->addProperty('lenient', $value);
}
/**
@@ -174,35 +176,35 @@ public function lenient($lenient)
* analyzer removes all tokens, such as when using a stop filter.
* Valid values are: none (Default), all.
*
- * @param string $zeroTermsQuery
+ * @param string $value
* @return static
*/
- public function zeroTermsQuery($zeroTermsQuery)
+ public function zeroTermsQuery(string $value): static
{
- return $this->addProperty('zero_terms_query', $zeroTermsQuery);
+ return $this->addProperty('zero_terms_query', $value);
}
/**
* Maximum number of positions allowed between matching
* tokens. Defaults to 0. Transposed terms have a slop of 2.
*
- * @param int $slop
+ * @param int $value
* @return static
*/
- public function slop($slop)
+ public function slop(int $value): static
{
- return $this->addProperty('slop', $slop);
+ return $this->addProperty('slop', $value);
}
/**
* If true, match phrase queries are automatically
* created for multi-term synonyms. Defaults to true.
*
- * @param bool $autoGenerateSynonymsPhraseQuery
+ * @param bool $value
* @return static
*/
- public function autoGenerateSynonymsPhraseQuery($autoGenerateSynonymsPhraseQuery)
+ public function autoGenerateSynonymsPhraseQuery(bool $value): static
{
- return $this->addProperty('auto_generate_synonyms_phrase_query', $autoGenerateSynonymsPhraseQuery);
+ return $this->addProperty('auto_generate_synonyms_phrase_query', $value);
}
}
diff --git a/src/DSL/Queries/FullText/QueryString.php b/src/DSL/Queries/FullText/QueryString.php
index 8446b3e..b4145fb 100644
--- a/src/DSL/Queries/FullText/QueryString.php
+++ b/src/DSL/Queries/FullText/QueryString.php
@@ -1,5 +1,7 @@
addProperty('query', $query);
+ return $this->addProperty('query', $value);
}
/**
* Default field to search if no field is provided in
* the query string. Supports wildcards (*). Defaults to *.
*
- * @param string $defaultField
+ * @param string $value
* @return static
*/
- public function defaultField($defaultField)
+ public function defaultField(string $value): static
{
- return $this->addProperty('default_field', $defaultField);
+ return $this->addProperty('default_field', $value);
}
/**
* If true, the wildcard characters * and ? are allowed
* as the first character of the query string. Defaults to true.
*
- * @param bool $allowLeadingWildcard
+ * @param bool $value
* @return static
*/
- public function allowLeadingWildcard($allowLeadingWildcard)
+ public function allowLeadingWildcard(bool $value): static
{
- return $this->addProperty('allow_leading_wildcard', $allowLeadingWildcard);
+ return $this->addProperty('allow_leading_wildcard', $value);
}
/**
@@ -54,24 +56,24 @@ public function allowLeadingWildcard($allowLeadingWildcard)
* into tokens. Defaults to the index-time analyzer mapped for the
* default_field.
*
- * @param string $analyzer
+ * @param string $value
* @return static
*/
- public function analyzer($analyzer)
+ public function analyzer(string $value): static
{
- return $this->addProperty('analyzer', $analyzer);
+ return $this->addProperty('analyzer', $value);
}
/**
* If true, match phrase queries are automatically
* created for multi-term synonyms. Defaults to true.
*
- * @param bool $autoGenerateSynonymsPhraseQuery
+ * @param bool $value
* @return static
*/
- public function autoGenerateSynonymsPhraseQuery($autoGenerateSynonymsPhraseQuery)
+ public function autoGenerateSynonymsPhraseQuery(bool $value): static
{
- return $this->addProperty('auto_generate_synonyms_phrase_query', $autoGenerateSynonymsPhraseQuery);
+ return $this->addProperty('auto_generate_synonyms_phrase_query', $value);
}
/**
@@ -79,119 +81,119 @@ public function autoGenerateSynonymsPhraseQuery($autoGenerateSynonymsPhraseQuery
* query string if no operators are specified. Valid values are:
* OR (Default), AND.
*
- * @param string $defaultOperator
+ * @param string $value
* @return static
*/
- public function defaultOperator($defaultOperator)
+ public function defaultOperator(string $value): static
{
- return $this->addProperty('default_operator', $defaultOperator);
+ return $this->addProperty('default_operator', $value);
}
/**
* If true, enable position increments in queries
* constructed from a query_string search. Defaults to true.
*
- * @param bool $enablePositionIncrements
+ * @param bool $value
* @return static
*/
- public function enablePositionIncrements($enablePositionIncrements)
+ public function enablePositionIncrements(bool $value): static
{
- return $this->addProperty('enable_position_increments', $enablePositionIncrements);
+ return $this->addProperty('enable_position_increments', $value);
}
/**
* Array of fields to search. Supports
* wildcards (*).
*
- * @param array $fields
+ * @param array $value
* @return static
*/
- public function fields($fields)
+ public function fields(array $value): static
{
- return $this->addProperty('fields', $fields);
+ return $this->addProperty('fields', $value);
}
/**
* Maximum edit distance allowed for fuzzy matching.
*
- * @param string $fuzziness
+ * @param int|string $value
* @return static
*/
- public function fuzziness($fuzziness)
+ public function fuzziness(int|string $value): static
{
- return $this->addProperty('fuzziness', $fuzziness);
+ return $this->addProperty('fuzziness', $value);
}
/**
* Maximum number of terms to which the query expands
* for fuzzy matching. Defaults to 50.
*
- * @param int $fuzzyMaxExpansions
+ * @param int $value
* @return static
*/
- public function fuzzyMaxExpansions($fuzzyMaxExpansions)
+ public function fuzzyMaxExpansions(int $value): static
{
- return $this->addProperty('fuzzy_max_expansions', $fuzzyMaxExpansions);
+ return $this->addProperty('fuzzy_max_expansions', $value);
}
/**
* Number of beginning characters left unchanged for
* fuzzy matching. Defaults to 0.
*
- * @param int $fuzzyPrefixLength
+ * @param int $value
* @return static
*/
- public function fuzzyPrefixLength($fuzzyPrefixLength)
+ public function fuzzyPrefixLength(int $value): static
{
- return $this->addProperty('fuzzy_prefix_length', $fuzzyPrefixLength);
+ return $this->addProperty('fuzzy_prefix_length', $value);
}
/**
* If true, edits for fuzzy matching include
* transpositions of two adjacent characters (ab -> ba). Defaults to true.
*
- * @param bool $fuzzyTranspositions
+ * @param bool $value
* @return static
*/
- public function fuzzyTranspositions($fuzzyTranspositions)
+ public function fuzzyTranspositions(bool $value): static
{
- return $this->addProperty('fuzzy_transpositions', $fuzzyTranspositions);
+ return $this->addProperty('fuzzy_transpositions', $value);
}
/**
* If true, format-based errors, such as providing a
* text value for a numeric field, are ignored. Defaults to false.
*
- * @param bool $lenient
+ * @param bool $value
* @return static
*/
- public function lenient($lenient)
+ public function lenient(bool $value): static
{
- return $this->addProperty('lenient', $lenient);
+ return $this->addProperty('lenient', $value);
}
/**
* Maximum number of automaton states required for
* the query. Default is 10000.
*
- * @param int $maxDeterminizedStates
+ * @param int $value
* @return static
*/
- public function maxDeterminizedStates($maxDeterminizedStates)
+ public function maxDeterminizedStates(int $value): static
{
- return $this->addProperty('max_determinized_states', $maxDeterminizedStates);
+ return $this->addProperty('max_determinized_states', $value);
}
/**
* Minimum number of clauses that must match for a
* document to be returned.
*
- * @param string $minimumShouldMatch
+ * @param int|string $value
* @return static
*/
- public function minimumShouldMatch($minimumShouldMatch)
+ public function minimumShouldMatch(int|string $value): static
{
- return $this->addProperty('minimum_should_match', $minimumShouldMatch);
+ return $this->addProperty('minimum_should_match', $value);
}
/**
@@ -199,12 +201,12 @@ public function minimumShouldMatch($minimumShouldMatch)
* string into tokens. Defaults to the search_quote_analyzer mapped for
* the default_field.
*
- * @param string $quoteAnalyzer
+ * @param string $value
* @return static
*/
- public function quoteAnalyzer($quoteAnalyzer)
+ public function quoteAnalyzer(string $value): static
{
- return $this->addProperty('quote_analyzer', $quoteAnalyzer);
+ return $this->addProperty('quote_analyzer', $value);
}
/**
@@ -212,12 +214,12 @@ public function quoteAnalyzer($quoteAnalyzer)
* tokens for phrases. Defaults to 0. If 0, exact phrase matches are
* required. Transposed terms have a slop of 2.
*
- * @param int $phraseSlop
+ * @param int $value
* @return static
*/
- public function phraseSlop($phraseSlop)
+ public function phraseSlop(int $value): static
{
- return $this->addProperty('phrase_slop', $phraseSlop);
+ return $this->addProperty('phrase_slop', $value);
}
/**
@@ -225,35 +227,35 @@ public function phraseSlop($phraseSlop)
* You can use this suffix to use a different analysis method for exact
* matches.
*
- * @param string $quoteFieldSuffix
+ * @param string $value
* @return static
*/
- public function quoteFieldSuffix($quoteFieldSuffix)
+ public function quoteFieldSuffix(string $value): static
{
- return $this->addProperty('quote_field_suffix', $quoteFieldSuffix);
+ return $this->addProperty('quote_field_suffix', $value);
}
/**
* Method used to rewrite the query.
*
- * @param string $rewrite
+ * @param string $value
* @return static
*/
- public function rewrite($rewrite)
+ public function rewrite(string $value): static
{
- return $this->addProperty('rewrite', $rewrite);
+ return $this->addProperty('rewrite', $value);
}
/**
* Coordinated Universal Time (UTC) offset or IANA time
* zone used to convert date values in the query string to UTC.
*
- * @param string $timeZone
+ * @param string $value
* @return static
*/
- public function timeZone($timeZone)
+ public function timeZone(string $value): static
{
- return $this->addProperty('time_zone', $timeZone);
+ return $this->addProperty('time_zone', $value);
}
/**
@@ -261,35 +263,35 @@ public function timeZone($timeZone)
* when searching multiple fields. Valid values are: best_fields (Default),
* most_fields, cross_fields, phrase, phrase_prefix, bool_prefix.
*
- * @param string $type
+ * @param string $value
* @return static
*/
- public function type($type)
+ public function type(string $value): static
{
- return $this->addProperty('type', $type);
+ return $this->addProperty('type', $value);
}
/**
* If true, the query attempts to analyze wildcard terms
* in the query string. Defaults to false.
*
- * @param bool $analyzeWildcard
+ * @param bool $value
* @return static
*/
- public function analyzeWildcard($analyzeWildcard)
+ public function analyzeWildcard(bool $value): static
{
- return $this->addProperty('analyze_wildcard', $analyzeWildcard);
+ return $this->addProperty('analyze_wildcard', $value);
}
/**
* Floating point number used to control the scoring of
* results when searching multiple fields. Defaults to 0.
*
- * @param float $tieBreaker
+ * @param float $value
* @return static
*/
- public function tieBreaker($tieBreaker)
+ public function tieBreaker(float $value): static
{
- return $this->addProperty('tie_breaker', $tieBreaker);
+ return $this->addProperty('tie_breaker', $value);
}
}
diff --git a/src/DSL/Queries/FullText/SimpleQueryString.php b/src/DSL/Queries/FullText/SimpleQueryString.php
index 5b475c9..700a93e 100644
--- a/src/DSL/Queries/FullText/SimpleQueryString.php
+++ b/src/DSL/Queries/FullText/SimpleQueryString.php
@@ -1,5 +1,7 @@
addProperty('query', $query);
+ return $this->addProperty('query', $value);
}
/**
@@ -30,12 +32,12 @@ public function query($query)
* Supports wildcard expressions and per-field boosting with caret (^)
* notation.
*
- * @param array $fields
+ * @param array $value
* @return static
*/
- public function fields($fields)
+ public function fields(array $value): static
{
- return $this->addProperty('fields', $fields);
+ return $this->addProperty('fields', $value);
}
/**
@@ -43,24 +45,24 @@ public function fields($fields)
* query string if no operators are specified. Valid values are:
* OR (Default), AND.
*
- * @param string $defaultOperator
+ * @param string $value
* @return static
*/
- public function defaultOperator($defaultOperator)
+ public function defaultOperator(string $value): static
{
- return $this->addProperty('default_operator', $defaultOperator);
+ return $this->addProperty('default_operator', $value);
}
/**
* If true, the query attempts to analyze wildcard terms
* in the query string. Defaults to false.
*
- * @param bool $analyzeWildcard
+ * @param bool $value
* @return static
*/
- public function analyzeWildcard($analyzeWildcard)
+ public function analyzeWildcard(bool $value): static
{
- return $this->addProperty('analyze_wildcard', $analyzeWildcard);
+ return $this->addProperty('analyze_wildcard', $value);
}
/**
@@ -68,96 +70,96 @@ public function analyzeWildcard($analyzeWildcard)
* into tokens. Defaults to the index-time analyzer mapped for the
* default_field.
*
- * @param string $analyzer
+ * @param string $value
* @return static
*/
- public function analyzer($analyzer)
+ public function analyzer(string $value): static
{
- return $this->addProperty('analyzer', $analyzer);
+ return $this->addProperty('analyzer', $value);
}
/**
* If true, the parser creates a match_phrase query
* for each multi-position token. Defaults to true.
*
- * @param bool $autoGenerateSynonymsPhraseQuery
+ * @param bool $value
* @return static
*/
- public function autoGenerateSynonymsPhraseQuery($autoGenerateSynonymsPhraseQuery)
+ public function autoGenerateSynonymsPhraseQuery(bool $value): static
{
- return $this->addProperty('auto_generate_synonyms_phrase_query', $autoGenerateSynonymsPhraseQuery);
+ return $this->addProperty('auto_generate_synonyms_phrase_query', $value);
}
/**
* List of enabled operators for the simple query string
* syntax. Defaults to ALL (all operators).
*
- * @param string $flags
+ * @param string $value
* @return static
*/
- public function flags($flags)
+ public function flags(string $value): static
{
- return $this->addProperty('flags', $flags);
+ return $this->addProperty('flags', $value);
}
/**
* Maximum number of terms to which the query expands
* for fuzzy matching. Defaults to 50.
*
- * @param int $fuzzyMaxExpansions
+ * @param int $value
* @return static
*/
- public function fuzzyMaxExpansions($fuzzyMaxExpansions)
+ public function fuzzyMaxExpansions(int $value): static
{
- return $this->addProperty('fuzzy_max_expansions', $fuzzyMaxExpansions);
+ return $this->addProperty('fuzzy_max_expansions', $value);
}
/**
* Number of beginning characters left unchanged for
* fuzzy matching. Defaults to 0.
*
- * @param int $fuzzyPrefixLength
+ * @param int $value
* @return static
*/
- public function fuzzyPrefixLength($fuzzyPrefixLength)
+ public function fuzzyPrefixLength(int $value): static
{
- return $this->addProperty('fuzzy_prefix_length', $fuzzyPrefixLength);
+ return $this->addProperty('fuzzy_prefix_length', $value);
}
/**
* If true, edits for fuzzy matching include
* transpositions of two adjacent characters (ab -> ba). Defaults to true.
*
- * @param bool $fuzzyTranspositions
+ * @param bool $value
* @return static
*/
- public function fuzzyTranspositions($fuzzyTranspositions)
+ public function fuzzyTranspositions(bool $value): static
{
- return $this->addProperty('fuzzy_transpositions', $fuzzyTranspositions);
+ return $this->addProperty('fuzzy_transpositions', $value);
}
/**
* If true, format-based errors, such as providing a
* text value for a numeric field, are ignored. Defaults to false.
*
- * @param bool $lenient
+ * @param bool $value
* @return static
*/
- public function lenient($lenient)
+ public function lenient(bool $value): static
{
- return $this->addProperty('lenient', $lenient);
+ return $this->addProperty('lenient', $value);
}
/**
* Minimum number of clauses that must match for a
* document to be returned.
*
- * @param string $minimumShouldMatch
+ * @param int|string $value
* @return static
*/
- public function minimumShouldMatch($minimumShouldMatch)
+ public function minimumShouldMatch(int|string $value): static
{
- return $this->addProperty('minimum_should_match', $minimumShouldMatch);
+ return $this->addProperty('minimum_should_match', $value);
}
/**
@@ -165,11 +167,11 @@ public function minimumShouldMatch($minimumShouldMatch)
* You can use this suffix to use a different analysis method for exact
* matches.
*
- * @param string $quoteFieldSuffix
+ * @param string $value
* @return static
*/
- public function quoteFieldSuffix($quoteFieldSuffix)
+ public function quoteFieldSuffix(string $value): static
{
- return $this->addProperty('quote_field_suffix', $quoteFieldSuffix);
+ return $this->addProperty('quote_field_suffix', $value);
}
}
diff --git a/src/DSL/Queries/Geo.php b/src/DSL/Queries/Geo.php
index ddddf22..51f567d 100644
--- a/src/DSL/Queries/Geo.php
+++ b/src/DSL/Queries/Geo.php
@@ -1,5 +1,7 @@
addQuery(GeoBoundingBox::create($field, $value));
}
@@ -33,7 +35,7 @@ public function geoBoundingBox($field, $value = null)
* @param callable|GeoDistance|array $value
* @return $this
*/
- public function geoDistance($value = null)
+ public function geoDistance($value = null): static
{
return $this->addQuery(GeoDistance::create($value));
}
@@ -45,7 +47,7 @@ public function geoDistance($value = null)
* @param mixed $value
* @return $this
*/
- public function geoGrid($field, $value = null)
+ public function geoGrid($field, $value = null): static
{
return $this->addQuery(GeoGrid::create($field, $value));
}
@@ -57,7 +59,7 @@ public function geoGrid($field, $value = null)
* @param mixed $value
* @return $this
*/
- public function geoPolygon($field, $value = null)
+ public function geoPolygon($field, $value = null): static
{
return $this->addQuery(GeoPolygon::create($field, $value));
}
@@ -69,7 +71,7 @@ public function geoPolygon($field, $value = null)
* @param mixed $value
* @return $this
*/
- public function geoShape($field, $value = null)
+ public function geoShape($field, $value = null): static
{
return $this->addQuery(GeoShape::create($field, $value));
}
diff --git a/src/DSL/Queries/Geo/GeoBoundingBox.php b/src/DSL/Queries/Geo/GeoBoundingBox.php
index 728ae91..2502bda 100644
--- a/src/DSL/Queries/Geo/GeoBoundingBox.php
+++ b/src/DSL/Queries/Geo/GeoBoundingBox.php
@@ -1,5 +1,7 @@
addProperty('top_left', $topLeft);
+ return $this->addProperty('top_left', $value);
}
/**
* Bottom-right corner of the bounding box.
*
- * @param mixed $bottomRight
+ * @param mixed $value
* @return static
*/
- public function bottomRight($bottomRight)
+ public function bottomRight($value): static
{
- return $this->addProperty('bottom_right', $bottomRight);
+ return $this->addProperty('bottom_right', $value);
}
/**
* Top latitude of the bounding box. Can be used instead of topLeft/bottomRight pairs to set values separately.
*
- * @param float $top
+ * @param float $value
* @return static
*/
- public function top($top)
+ public function top(float $value): static
{
- return $this->addProperty('top', $top);
+ return $this->addProperty('top', $value);
}
/**
* Left longitude of the bounding box. Can be used instead of topLeft/bottomRight pairs to set values separately.
*
- * @param float $left
+ * @param float $value
* @return static
*/
- public function left($left)
+ public function left(float $value): static
{
- return $this->addProperty('left', $left);
+ return $this->addProperty('left', $value);
}
/**
* Bottom latitude of the bounding box. Can be used instead of topLeft/bottomRight pairs to set values separately.
*
- * @param float $bottom
+ * @param float $value
* @return static
*/
- public function bottom($bottom)
+ public function bottom(float $value): static
{
- return $this->addProperty('bottom', $bottom);
+ return $this->addProperty('bottom', $value);
}
/**
* Right longitude of the bounding box. Can be used instead of topLeft/bottomRight pairs to set values separately.
*
- * @param float $right
+ * @param float $value
* @return static
*/
- public function right($right)
+ public function right(float $value): static
{
- return $this->addProperty('right', $right);
+ return $this->addProperty('right', $value);
}
/**
* Bounding box defined as Well-Known Text (WKT) BBOX format.
*
- * @param string $wkt
+ * @param string $value
* @return static
*/
- public function wkt($wkt)
+ public function wkt(string $value): static
{
- return $this->addProperty('wkt', $wkt);
+ return $this->addProperty('wkt', $value);
}
/**
* Top-right corner of the bounding box. Can be used with bottomLeft instead of topLeft/bottomRight.
*
- * @param mixed $topRight
+ * @param mixed $value
* @return static
*/
- public function topRight($topRight)
+ public function topRight($value): static
{
- return $this->addProperty('top_right', $topRight);
+ return $this->addProperty('top_right', $value);
}
/**
* Bottom-left corner of the bounding box. Can be used with topRight instead of topLeft/bottomRight.
*
- * @param mixed $bottomLeft
+ * @param mixed $value
* @return static
*/
- public function bottomLeft($bottomLeft)
+ public function bottomLeft($value): static
{
- return $this->addProperty('bottom_left', $bottomLeft);
+ return $this->addProperty('bottom_left', $value);
}
/**
* Set to IGNORE_MALFORMED to accept geo points with invalid latitude or longitude,
* set to COERCE to also try to infer correct latitude or longitude. Defaults to STRICT.
*
- * @param string $validationMethod
+ * @param string $value
* @return static
*/
- public function validationMethod($validationMethod)
+ public function validationMethod(string $value): static
{
- return $this->addProperty('validation_method', $validationMethod);
+ return $this->addProperty('validation_method', $value);
}
/**
* When set to true, the ignore_unmapped option will ignore an unmapped field
* and will not match any documents for this query. Defaults to false.
*
- * @param bool $ignoreUnmapped
+ * @param bool $value
* @return static
*/
- public function ignoreUnmapped($ignoreUnmapped)
+ public function ignoreUnmapped(bool $value): static
{
- return $this->addProperty('ignore_unmapped', $ignoreUnmapped);
+ return $this->addProperty('ignore_unmapped', $value);
}
}
diff --git a/src/DSL/Queries/Geo/GeoDistance.php b/src/DSL/Queries/Geo/GeoDistance.php
index fca2402..4308765 100644
--- a/src/DSL/Queries/Geo/GeoDistance.php
+++ b/src/DSL/Queries/Geo/GeoDistance.php
@@ -1,5 +1,7 @@
addProperty('distance', $distance);
+ return $this->addProperty('distance', $value);
}
/**
@@ -39,7 +41,7 @@ public function distance($distance)
* @param mixed $location
* @return static
*/
- public function location($field, $location)
+ public function location(string $field, $location): static
{
return $this->addProperty($field, $location);
}
@@ -48,36 +50,23 @@ public function location($field, $location)
* How to compute the distance. Can either be arc (default) or
* plane (faster, but inaccurate on long distances and close to the poles).
*
- * @param string $distanceType
- * @return static
- */
- public function distanceType($distanceType)
- {
- return $this->addProperty('distance_type', $distanceType);
- }
-
- /**
- * Optional name field to identify the query.
- *
- * @param string $_name
+ * @param string $value
* @return static
- * @SuppressWarnings(PHPMD.CamelCaseParameterName)
- * @SuppressWarnings(PHPMD.CamelCaseVariableName)
*/
- public function _name($_name)
+ public function distanceType(string $value): static
{
- return $this->addProperty('_name', $_name);
+ return $this->addProperty('distance_type', $value);
}
/**
* Set to IGNORE_MALFORMED to accept geo points with invalid latitude or longitude,
* set to COERCE to additionally try and infer correct coordinates. Defaults to STRICT.
*
- * @param string $validationMethod
+ * @param string $value
* @return static
*/
- public function validationMethod($validationMethod)
+ public function validationMethod(string $value): static
{
- return $this->addProperty('validation_method', $validationMethod);
+ return $this->addProperty('validation_method', $value);
}
}
diff --git a/src/DSL/Queries/Geo/GeoGrid.php b/src/DSL/Queries/Geo/GeoGrid.php
index d6fdb98..66337a8 100644
--- a/src/DSL/Queries/Geo/GeoGrid.php
+++ b/src/DSL/Queries/Geo/GeoGrid.php
@@ -1,5 +1,7 @@
addProperty('geohex', $geohex);
+ return $this->addProperty('geohex', $value);
}
/**
* The geotile grid key to match (e.g. "6/32/21").
* Usable with geo_point and geo_shape fields.
*
- * @param string $geotile
+ * @param string $value
* @return static
*/
- public function geotile($geotile)
+ public function geotile(string $value): static
{
- return $this->addProperty('geotile', $geotile);
+ return $this->addProperty('geotile', $value);
}
/**
* The geohash grid key to match (e.g. "u1").
* Usable with geo_point and geo_shape fields.
*
- * @param string $geohash
+ * @param string $value
* @return static
*/
- public function geohash($geohash)
+ public function geohash(string $value): static
{
- return $this->addProperty('geohash', $geohash);
+ return $this->addProperty('geohash', $value);
}
}
diff --git a/src/DSL/Queries/Geo/GeoPolygon.php b/src/DSL/Queries/Geo/GeoPolygon.php
index d456e23..cd07c11 100644
--- a/src/DSL/Queries/Geo/GeoPolygon.php
+++ b/src/DSL/Queries/Geo/GeoPolygon.php
@@ -1,5 +1,7 @@
> $points
+ * @param array> $value
* @return static
*/
- public function points($points)
+ public function points(array $value): static
{
- return $this->addProperty('points', $points);
+ return $this->addProperty('points', $value);
}
/**
* Set to IGNORE_MALFORMED to accept geo points with invalid latitude or longitude,
* set to COERCE to try and infer correct latitude or longitude, or STRICT (default).
*
- * @param string $validationMethod
+ * @param string $value
* @return static
*/
- public function validationMethod($validationMethod)
+ public function validationMethod(string $value): static
{
- return $this->addProperty('validation_method', $validationMethod);
+ return $this->addProperty('validation_method', $value);
}
/**
* When set to true, the ignore_unmapped option will ignore an unmapped field
* and will not match any documents for this query. Defaults to false.
*
- * @param bool $ignoreUnmapped
+ * @param bool $value
* @return static
*/
- public function ignoreUnmapped($ignoreUnmapped)
+ public function ignoreUnmapped(bool $value): static
{
- return $this->addProperty('ignore_unmapped', $ignoreUnmapped);
+ return $this->addProperty('ignore_unmapped', $value);
}
}
diff --git a/src/DSL/Queries/Geo/GeoShape.php b/src/DSL/Queries/Geo/GeoShape.php
index 638ed51..e600074 100644
--- a/src/DSL/Queries/Geo/GeoShape.php
+++ b/src/DSL/Queries/Geo/GeoShape.php
@@ -1,5 +1,7 @@
addProperty('shape', $shape);
+ return $this->addProperty('shape', $value);
}
/**
* Spatial relation operator to use at search time.
* Valid values: INTERSECTS (default), DISJOINT, WITHIN, CONTAINS.
*
- * @param string $relation
+ * @param string $value
* @return static
*/
- public function relation($relation)
+ public function relation(string $value): static
{
- return $this->addProperty('relation', $relation);
+ return $this->addProperty('relation', $value);
}
/**
* Reference to a pre-indexed shape. Contains id, index, path, and routing fields
* to identify the shape document in another index.
*
- * @param mixed $indexedShape
+ * @param mixed $value
* @return static
*/
- public function indexedShape($indexedShape)
+ public function indexedShape($value): static
{
- return $this->addProperty('indexed_shape', $indexedShape);
+ return $this->addProperty('indexed_shape', $value);
}
/**
* When set to true, the ignore_unmapped option will ignore an unmapped field
* and will not match any documents for this query. Defaults to false.
*
- * @param bool $ignoreUnmapped
+ * @param bool $value
* @return static
*/
- public function ignoreUnmapped($ignoreUnmapped)
+ public function ignoreUnmapped(bool $value): static
{
- return $this->addProperty('ignore_unmapped', $ignoreUnmapped);
+ return $this->addProperty('ignore_unmapped', $value);
}
}
diff --git a/src/DSL/Queries/Joining.php b/src/DSL/Queries/Joining.php
index d814469..caf8527 100644
--- a/src/DSL/Queries/Joining.php
+++ b/src/DSL/Queries/Joining.php
@@ -1,5 +1,7 @@
|null $query optional query when first arg is a path string
* @return $this
*/
- public function nested($path, $query = null)
+ public function nested($path, $query = null): static
{
if (is_string($path) && $query !== null) {
$nested = Nested::create($path);
@@ -39,7 +41,7 @@ public function nested($path, $query = null)
* @param callable|Query|array|null $query optional query when first arg is a type string
* @return $this
*/
- public function hasChild($type, $query = null)
+ public function hasChild($type, $query = null): static
{
if (is_string($type) && $query !== null) {
$hasChild = HasChild::create();
@@ -57,7 +59,7 @@ public function hasChild($type, $query = null)
* @param callable|Query|array|null $query optional query when first arg is a parent_type string
* @return $this
*/
- public function hasParent($type, $query = null)
+ public function hasParent($type, $query = null): static
{
if (is_string($type) && $query !== null) {
$hasParent = HasParent::create();
@@ -71,11 +73,11 @@ public function hasParent($type, $query = null)
/**
* Add a parent_id query.
*
- * @param mixed $parentId
+ * @param mixed $value
* @return $this
*/
- public function parentId($parentId)
+ public function parentId($value): static
{
- return $this->addQuery(ParentId::create($parentId));
+ return $this->addQuery(ParentId::create($value));
}
}
diff --git a/src/DSL/Queries/Joining/HasChild.php b/src/DSL/Queries/Joining/HasChild.php
index 303cd39..5f6ac72 100644
--- a/src/DSL/Queries/Joining/HasChild.php
+++ b/src/DSL/Queries/Joining/HasChild.php
@@ -1,5 +1,7 @@
addProperty('type', $type);
+ return $this->addProperty('type', $value);
}
/**
* Query you wish to run on child documents of the type field.
* If a child document matches the search, the query returns the parent document.
*
- * @param mixed $query
+ * @param mixed $value
* @return static
*/
- public function query($query)
+ public function query($value): static
{
- return $this->addProperty('query', Query::create($query));
+ return $this->addProperty('query', Query::create($value));
}
/**
* Indicates whether to ignore an unmapped type and not return any
* documents instead of an error. Defaults to false.
*
- * @param bool $ignoreUnmapped
+ * @param bool $value
* @return static
*/
- public function ignoreUnmapped($ignoreUnmapped)
+ public function ignoreUnmapped(bool $value): static
{
- return $this->addProperty('ignore_unmapped', $ignoreUnmapped);
+ return $this->addProperty('ignore_unmapped', $value);
}
/**
* Maximum number of child documents that match the query allowed for a
* returned parent document. If the parent document exceeds this limit, it is excluded from the search results.
*
- * @param int $maxChildren
+ * @param int $value
* @return static
*/
- public function maxChildren($maxChildren)
+ public function maxChildren(int $value): static
{
- return $this->addProperty('max_children', $maxChildren);
+ return $this->addProperty('max_children', $value);
}
/**
@@ -66,23 +68,23 @@ public function maxChildren($maxChildren)
* the query for a returned parent document. If the parent document does not meet this limit,
* it is excluded from the search results.
*
- * @param int $minChildren
+ * @param int $value
* @return static
*/
- public function minChildren($minChildren)
+ public function minChildren(int $value): static
{
- return $this->addProperty('min_children', $minChildren);
+ return $this->addProperty('min_children', $value);
}
/**
* Indicates how scores for matching child documents affect the root parent
* document's relevance score. Valid values: none (default), avg, max, min, sum.
*
- * @param string $scoreMode
+ * @param string $value
* @return static
*/
- public function scoreMode($scoreMode)
+ public function scoreMode(string $value): static
{
- return $this->addProperty('score_mode', $scoreMode);
+ return $this->addProperty('score_mode', $value);
}
}
diff --git a/src/DSL/Queries/Joining/HasParent.php b/src/DSL/Queries/Joining/HasParent.php
index 17c0c16..c01178b 100644
--- a/src/DSL/Queries/Joining/HasParent.php
+++ b/src/DSL/Queries/Joining/HasParent.php
@@ -1,5 +1,7 @@
addProperty('parent_type', $parentType);
+ return $this->addProperty('parent_type', $value);
}
/**
* Query you wish to run on parent documents of the parent_type field.
* If a parent document matches the search, the query returns its child documents.
*
- * @param mixed $query
+ * @param mixed $value
* @return static
*/
- public function query($query)
+ public function query($value): static
{
- return $this->addProperty('query', Query::create($query));
+ return $this->addProperty('query', Query::create($value));
}
/**
* Indicates whether the relevance score of a matching parent document is
* aggregated into its child documents. Defaults to false.
*
- * @param bool $score
+ * @param bool $value
* @return static
*/
- public function score($score)
+ public function score(bool $value): static
{
- return $this->addProperty('score', $score);
+ return $this->addProperty('score', $value);
}
/**
* Indicates whether to ignore an unmapped parent_type and not return any
* documents instead of an error. Defaults to false.
*
- * @param bool $ignoreUnmapped
+ * @param bool $value
* @return static
*/
- public function ignoreUnmapped($ignoreUnmapped)
+ public function ignoreUnmapped(bool $value): static
{
- return $this->addProperty('ignore_unmapped', $ignoreUnmapped);
+ return $this->addProperty('ignore_unmapped', $value);
}
}
diff --git a/src/DSL/Queries/Joining/Nested.php b/src/DSL/Queries/Joining/Nested.php
index 4849437..5493c0d 100644
--- a/src/DSL/Queries/Joining/Nested.php
+++ b/src/DSL/Queries/Joining/Nested.php
@@ -1,5 +1,7 @@
path($field);
@@ -35,47 +37,47 @@ public static function create($field = null, $value = null)
/**
* Path to the nested object you wish to search.
*
- * @param string $path
+ * @param string $value
* @return static
*/
- public function path($path)
+ public function path(string $value): static
{
- return $this->addProperty('path', $path);
+ return $this->addProperty('path', $value);
}
/**
* Query you wish to run on nested objects in the path.
* If an object matches the search, the nested query returns the root parent document.
*
- * @param mixed $query
+ * @param mixed $value
* @return static
*/
- public function query($query)
+ public function query($value): static
{
- return $this->addProperty('query', Query::create($query));
+ return $this->addProperty('query', Query::create($value));
}
/**
* Indicates how scores for matching child objects affect the root parent
* document's relevance score. Valid values: avg (default), max, min, none, sum.
*
- * @param string $scoreMode
+ * @param string $value
* @return static
*/
- public function scoreMode($scoreMode)
+ public function scoreMode(string $value): static
{
- return $this->addProperty('score_mode', $scoreMode);
+ return $this->addProperty('score_mode', $value);
}
/**
* Indicates whether to ignore an unmapped path and not return any
* documents instead of an error. Defaults to false.
*
- * @param bool $ignoreUnmapped
+ * @param bool $value
* @return static
*/
- public function ignoreUnmapped($ignoreUnmapped)
+ public function ignoreUnmapped(bool $value): static
{
- return $this->addProperty('ignore_unmapped', $ignoreUnmapped);
+ return $this->addProperty('ignore_unmapped', $value);
}
}
diff --git a/src/DSL/Queries/Joining/ParentId.php b/src/DSL/Queries/Joining/ParentId.php
index 6be7e8a..9914e16 100644
--- a/src/DSL/Queries/Joining/ParentId.php
+++ b/src/DSL/Queries/Joining/ParentId.php
@@ -1,5 +1,7 @@
addProperty('type', $type);
+ return $this->addProperty('type', $value);
}
/**
* ID of the parent document. The query will return child documents of this parent document.
*
- * @param string $id
+ * @param string $value
* @return static
*/
- public function id($id)
+ public function id(string $value): static
{
- return $this->addProperty('id', $id);
+ return $this->addProperty('id', $value);
}
/**
* Indicates whether to ignore an unmapped type and not return any
* documents instead of an error. Defaults to false.
*
- * @param bool $ignoreUnmapped
+ * @param bool $value
* @return static
*/
- public function ignoreUnmapped($ignoreUnmapped)
+ public function ignoreUnmapped(bool $value): static
{
- return $this->addProperty('ignore_unmapped', $ignoreUnmapped);
+ return $this->addProperty('ignore_unmapped', $value);
}
}
diff --git a/src/DSL/Queries/MatchAll.php b/src/DSL/Queries/MatchAll.php
index efb892a..4f0af23 100644
--- a/src/DSL/Queries/MatchAll.php
+++ b/src/DSL/Queries/MatchAll.php
@@ -1,5 +1,7 @@
addQuery(QMatchAll::create($matchAll));
+ return $this->addQuery(QMatchAll::create($value));
}
/**
@@ -26,7 +28,7 @@ public function matchAll($matchAll = null)
*
* @return $this
*/
- public function matchNone()
+ public function matchNone(): static
{
return $this->addQuery(MatchNone::create());
}
diff --git a/src/DSL/Queries/MatchAll/MatchAll.php b/src/DSL/Queries/MatchAll/MatchAll.php
index f461f26..1044cbf 100644
--- a/src/DSL/Queries/MatchAll/MatchAll.php
+++ b/src/DSL/Queries/MatchAll/MatchAll.php
@@ -1,5 +1,7 @@
addProperty('id', $id);
+ return $this->addProperty('id', $value);
}
/**
- * (Optional) The script language. Defaults to painless.
+ * The script language. Defaults to painless.
*
- * @param string $lang
+ * @param string $value
* @return static
*/
- public function lang($lang)
+ public function lang(string $value): static
{
- return $this->addProperty('lang', $lang);
+ return $this->addProperty('lang', $value);
}
/**
- * (Required) The inline script source to execute.
+ * The inline script source to execute.
*
- * @param string $source
+ * @param string $value
* @return static
*/
- public function source($source)
+ public function source(string $value): static
{
- return $this->addProperty('source', $source);
+ return $this->addProperty('source', $value);
}
/**
- * (Optional) Named parameters passed into the script.
+ * Named parameters passed into the script.
*
- * @param array $params
+ * @param array $value
* @return static
*/
- public function params($params)
+ public function params(array $value): static
{
- return $this->addProperty('params', $params);
+ return $this->addProperty('params', $value);
}
}
diff --git a/src/DSL/Queries/Shape.php b/src/DSL/Queries/Shape.php
index 1980745..ee708b8 100644
--- a/src/DSL/Queries/Shape.php
+++ b/src/DSL/Queries/Shape.php
@@ -1,5 +1,7 @@
addQuery(QShape::create($field, $value));
}
diff --git a/src/DSL/Queries/Shape/Shape.php b/src/DSL/Queries/Shape/Shape.php
index c92f0ee..b1de24e 100644
--- a/src/DSL/Queries/Shape/Shape.php
+++ b/src/DSL/Queries/Shape/Shape.php
@@ -1,5 +1,7 @@
addProperty('shape', $shape);
+ return $this->addProperty('shape', $value);
}
/**
* Spatial relation operator to use at search time.
* Valid values: INTERSECTS (default), DISJOINT, WITHIN, CONTAINS.
*
- * @param string $relation
+ * @param string $value
* @return static
*/
- public function relation($relation)
+ public function relation(string $value): static
{
- return $this->addProperty('relation', $relation);
+ return $this->addProperty('relation', $value);
}
/**
* Reference to a pre-indexed shape. Contains id, index, path, and routing fields
* to identify the shape document in another index.
*
- * @param mixed $indexedShape
+ * @param mixed $value
* @return static
*/
- public function indexedShape($indexedShape)
+ public function indexedShape($value): static
{
- return $this->addProperty('indexed_shape', $indexedShape);
+ return $this->addProperty('indexed_shape', $value);
}
}
diff --git a/src/DSL/Queries/Span.php b/src/DSL/Queries/Span.php
index 22bffbc..8a14d5f 100644
--- a/src/DSL/Queries/Span.php
+++ b/src/DSL/Queries/Span.php
@@ -1,5 +1,7 @@
addQuery(SpanContaining::create($spanContaining));
+ return $this->addQuery(SpanContaining::create($value));
}
/**
* Add a span_field_masking query.
*
- * @param mixed $spanFieldMasking
+ * @param mixed $value
* @return $this
*/
- public function spanFieldMasking($spanFieldMasking)
+ public function spanFieldMasking($value): static
{
- return $this->addQuery(SpanFieldMasking::create($spanFieldMasking));
+ return $this->addQuery(SpanFieldMasking::create($value));
}
/**
* Add a span_first query.
*
- * @param mixed $spanFirst
+ * @param mixed $value
* @return $this
*/
- public function spanFirst($spanFirst)
+ public function spanFirst($value): static
{
- return $this->addQuery(SpanFirst::create($spanFirst));
+ return $this->addQuery(SpanFirst::create($value));
}
/**
* Add a span_multi query.
*
- * @param mixed $spanMulti
+ * @param mixed $value
* @return $this
*/
- public function spanMulti($spanMulti)
+ public function spanMulti($value): static
{
- return $this->addQuery(SpanMulti::create($spanMulti));
+ return $this->addQuery(SpanMulti::create($value));
}
/**
* Add a span_near query.
*
- * @param mixed $spanNear
+ * @param mixed $field
+ * @param mixed $value
* @return $this
*/
- public function spanNear($spanNear)
+ public function spanNear($field = null, $value = null): static
{
- return $this->addQuery(SpanNear::create($spanNear));
+ return $this->addQuery(SpanNear::create($field, $value));
}
/**
* Add a span_not query.
*
- * @param mixed $spanNot
+ * @param mixed $value
* @return $this
*/
- public function spanNot($spanNot)
+ public function spanNot($value): static
{
- return $this->addQuery(SpanNot::create($spanNot));
+ return $this->addQuery(SpanNot::create($value));
}
/**
* Add a span_or query.
*
- * @param mixed $spanOr
+ * @param mixed $field
+ * @param mixed $value
* @return $this
*/
- public function spanOr($spanOr)
+ public function spanOr($field = null, $value = null): static
{
- return $this->addQuery(SpanOr::create($spanOr));
+ return $this->addQuery(SpanOr::create($field, $value));
}
/**
@@ -102,7 +106,7 @@ public function spanOr($spanOr)
* @param mixed $value
* @return $this
*/
- public function spanTerm($field, $value = null)
+ public function spanTerm($field, $value = null): static
{
return $this->addQuery(SpanTerm::create($field, $value));
}
@@ -110,11 +114,11 @@ public function spanTerm($field, $value = null)
/**
* Add a span_within query.
*
- * @param mixed $spanWithin
+ * @param mixed $value
* @return $this
*/
- public function spanWithin($spanWithin)
+ public function spanWithin($value): static
{
- return $this->addQuery(SpanWithin::create($spanWithin));
+ return $this->addQuery(SpanWithin::create($value));
}
}
diff --git a/src/DSL/Queries/Span/SpanContaining.php b/src/DSL/Queries/Span/SpanContaining.php
index b44f222..d2593fa 100644
--- a/src/DSL/Queries/Span/SpanContaining.php
+++ b/src/DSL/Queries/Span/SpanContaining.php
@@ -1,5 +1,7 @@
addProperty('little', Query::create($little));
+ return $this->addProperty('little', Query::create($value));
}
/**
* The big span query that must contain matches from the little span.
*
- * @param mixed $big
+ * @param mixed $value
* @return static
*/
- public function big($big)
+ public function big($value): static
{
- return $this->addProperty('big', Query::create($big));
+ return $this->addProperty('big', Query::create($value));
}
}
diff --git a/src/DSL/Queries/Span/SpanFieldMasking.php b/src/DSL/Queries/Span/SpanFieldMasking.php
index ea62edf..458344d 100644
--- a/src/DSL/Queries/Span/SpanFieldMasking.php
+++ b/src/DSL/Queries/Span/SpanFieldMasking.php
@@ -1,5 +1,7 @@
addProperty('query', Query::create($query));
- }
-
- /**
- * The masked field to use for the span query.
- *
- * @param string $field
+ * @param mixed $value
* @return static
*/
- public function field($field)
+ public function query($value): static
{
- return $this->addProperty('field', $field);
+ return $this->addProperty('query', Query::create($value));
}
}
diff --git a/src/DSL/Queries/Span/SpanFirst.php b/src/DSL/Queries/Span/SpanFirst.php
index 2881f43..660b048 100644
--- a/src/DSL/Queries/Span/SpanFirst.php
+++ b/src/DSL/Queries/Span/SpanFirst.php
@@ -1,5 +1,7 @@
addProperty('match', Query::create($match));
+ return $this->addProperty('match', Query::create($value));
}
/**
* The maximum end position permitted for the span match.
*
- * @param int $end
+ * @param int $value
* @return static
*/
- public function end($end)
+ public function end(int $value): static
{
- return $this->addProperty('end', $end);
+ return $this->addProperty('end', $value);
}
}
diff --git a/src/DSL/Queries/Span/SpanMulti.php b/src/DSL/Queries/Span/SpanMulti.php
index ecdad4a..61094f9 100644
--- a/src/DSL/Queries/Span/SpanMulti.php
+++ b/src/DSL/Queries/Span/SpanMulti.php
@@ -1,5 +1,7 @@
addProperty('match', Query::create($match));
+ return $this->addProperty('match', Query::create($value));
}
}
diff --git a/src/DSL/Queries/Span/SpanNear.php b/src/DSL/Queries/Span/SpanNear.php
index c5a5ce6..3cf7404 100644
--- a/src/DSL/Queries/Span/SpanNear.php
+++ b/src/DSL/Queries/Span/SpanNear.php
@@ -1,10 +1,11 @@
addProperty('clauses', Query::create($clauses)->multi(true));
- }
-
- /**
- * Append a span query clause. Supports multiple calls to incrementally build.
- *
- * @param mixed $clause
+ * @param mixed $value
* @return static
*/
- public function addClause($clause)
+ public function clauses($value): static
{
- return $this->pushClause('clauses', $clause);
+ return $this->addClause('clauses', $value);
}
/**
* The maximum number of positions allowed between matching spans.
*
- * @param int $slop
+ * @param int $value
* @return static
*/
- public function slop($slop)
+ public function slop(int $value): static
{
- return $this->addProperty('slop', $slop);
+ return $this->addProperty('slop', $value);
}
/**
* Whether the span clauses must appear in their specified order.
*
- * @param bool $inOrder
+ * @param bool $value
* @return static
*/
- public function inOrder($inOrder)
+ public function inOrder(bool $value): static
{
- return $this->addProperty('in_order', $inOrder);
+ return $this->addProperty('in_order', $value);
}
}
diff --git a/src/DSL/Queries/Span/SpanNot.php b/src/DSL/Queries/Span/SpanNot.php
index 31b77c3..9a7d34f 100644
--- a/src/DSL/Queries/Span/SpanNot.php
+++ b/src/DSL/Queries/Span/SpanNot.php
@@ -1,5 +1,7 @@
addProperty('include', Query::create($include));
+ return $this->addProperty('include', Query::create($value));
}
/**
* The span query whose overlapping matches are excluded.
*
- * @param mixed $exclude
+ * @param mixed $value
* @return static
*/
- public function exclude($exclude)
+ public function exclude($value): static
{
- return $this->addProperty('exclude', Query::create($exclude));
+ return $this->addProperty('exclude', Query::create($value));
}
/**
* The number of positions before the include span that must not overlap with the exclude span.
*
- * @param int $pre
+ * @param int $value
* @return static
*/
- public function pre($pre)
+ public function pre(int $value): static
{
- return $this->addProperty('pre', $pre);
+ return $this->addProperty('pre', $value);
}
/**
* The number of positions after the include span that must not overlap with the exclude span.
*
- * @param int $post
+ * @param int $value
* @return static
*/
- public function post($post)
+ public function post(int $value): static
{
- return $this->addProperty('post', $post);
+ return $this->addProperty('post', $value);
}
/**
* The number of positions both before and after the include span that must not overlap with the exclude span.
*
- * @param int $dist
+ * @param int $value
* @return static
*/
- public function dist($dist)
+ public function dist(int $value): static
{
- return $this->addProperty('dist', $dist);
+ return $this->addProperty('dist', $value);
}
}
diff --git a/src/DSL/Queries/Span/SpanOr.php b/src/DSL/Queries/Span/SpanOr.php
index df9a3c9..1b7152d 100644
--- a/src/DSL/Queries/Span/SpanOr.php
+++ b/src/DSL/Queries/Span/SpanOr.php
@@ -1,10 +1,11 @@
addProperty('clauses', Query::create($clauses)->multi(true));
- }
-
- /**
- * Append a span query clause. Supports multiple calls to incrementally build.
- *
- * @param mixed $clause
+ * @param mixed $value
* @return static
*/
- public function addClause($clause)
+ public function clauses($value): static
{
- return $this->pushClause('clauses', $clause);
+ return $this->addClause('clauses', $value);
}
}
diff --git a/src/DSL/Queries/Span/SpanTerm.php b/src/DSL/Queries/Span/SpanTerm.php
index ad34f62..71958a9 100644
--- a/src/DSL/Queries/Span/SpanTerm.php
+++ b/src/DSL/Queries/Span/SpanTerm.php
@@ -1,5 +1,7 @@
addProperty('term', $term);
+ return $this->value($value);
}
/**
* The value of the term to match (alias for the field value).
*
- * @param string $value
+ * @param string|int|float|bool $value
* @return static
*/
- public function value($value)
+ public function value(string|int|float|bool $value): static
{
return $this->addProperty('value', $value);
}
diff --git a/src/DSL/Queries/Span/SpanWithin.php b/src/DSL/Queries/Span/SpanWithin.php
index d60ca32..2e63664 100644
--- a/src/DSL/Queries/Span/SpanWithin.php
+++ b/src/DSL/Queries/Span/SpanWithin.php
@@ -1,5 +1,7 @@
addProperty('little', Query::create($little));
+ return $this->addProperty('little', Query::create($value));
}
/**
* The big span query that must contain matches from the little span.
*
- * @param mixed $big
+ * @param mixed $value
* @return static
*/
- public function big($big)
+ public function big($value): static
{
- return $this->addProperty('big', Query::create($big));
+ return $this->addProperty('big', Query::create($value));
}
}
diff --git a/src/DSL/Queries/Specialized.php b/src/DSL/Queries/Specialized.php
index 445d49c..0bdcb10 100644
--- a/src/DSL/Queries/Specialized.php
+++ b/src/DSL/Queries/Specialized.php
@@ -1,5 +1,7 @@
addQuery(DistanceFeature::create($distanceFeature));
+ return $this->addQuery(DistanceFeature::create($value));
}
/**
* Add a more_like_this query.
*
- * @param mixed $moreLikeThis
+ * @param mixed $value
* @return $this
*/
- public function moreLikeThis($moreLikeThis)
+ public function moreLikeThis($value): static
{
- return $this->addQuery(MoreLikeThis::create($moreLikeThis));
+ return $this->addQuery(MoreLikeThis::create($value));
}
/**
* Add a percolate query.
*
- * @param mixed $percolate
+ * @param mixed $value
* @return $this
*/
- public function percolate($percolate)
+ public function percolate($value): static
{
- return $this->addQuery(Percolate::create($percolate));
+ return $this->addQuery(Percolate::create($value));
}
/**
* Add a rank_feature query.
*
- * @param mixed $rankFeature
+ * @param mixed $value
* @return $this
*/
- public function rankFeature($rankFeature)
+ public function rankFeature($value): static
{
- return $this->addQuery(RankFeature::create($rankFeature));
+ return $this->addQuery(RankFeature::create($value));
}
/**
* Add a script query.
*
- * @param mixed $script
+ * @param mixed $value
* @return $this
*/
- public function script($script)
+ public function script($value): static
{
- return $this->addQuery(Script::create($script));
+ return $this->addQuery(Script::create($value));
}
/**
* Add a script_score query.
*
- * @param mixed $scriptScore
+ * @param mixed $value
* @return $this
*/
- public function scriptScore($scriptScore)
+ public function scriptScore($value): static
{
- return $this->addQuery(ScriptScore::create($scriptScore));
+ return $this->addQuery(ScriptScore::create($value));
}
/**
* Add a wrapper query.
*
- * @param mixed $wrapper
+ * @param mixed $value
* @return $this
*/
- public function wrapper($wrapper)
+ public function wrapper($value): static
{
- return $this->addQuery(Wrapper::create($wrapper));
+ return $this->addQuery(Wrapper::create($value));
}
/**
* Add a pinned query.
*
- * @param mixed $pinned
+ * @param mixed $value
* @return $this
*/
- public function pinned($pinned)
+ public function pinned($value): static
{
- return $this->addQuery(Pinned::create($pinned));
+ return $this->addQuery(Pinned::create($value));
}
}
diff --git a/src/DSL/Queries/Specialized/DistanceFeature.php b/src/DSL/Queries/Specialized/DistanceFeature.php
index f1d2bc5..7bd8715 100644
--- a/src/DSL/Queries/Specialized/DistanceFeature.php
+++ b/src/DSL/Queries/Specialized/DistanceFeature.php
@@ -1,5 +1,7 @@
addProperty('origin', $origin);
+ return $this->addProperty('origin', $value);
}
/**
* Distance from the origin at which relevance scores receive half of the boost value.
*
- * @param mixed $pivot
+ * @param string $value
* @return static
*/
- public function pivot($pivot)
+ public function pivot(string $value): static
{
- return $this->addProperty('pivot', $pivot);
+ return $this->addProperty('pivot', $value);
}
}
diff --git a/src/DSL/Queries/Specialized/MoreLikeThis.php b/src/DSL/Queries/Specialized/MoreLikeThis.php
index d7ab65b..f12b310 100644
--- a/src/DSL/Queries/Specialized/MoreLikeThis.php
+++ b/src/DSL/Queries/Specialized/MoreLikeThis.php
@@ -1,5 +1,7 @@
$array
+ * @param array $value
* @return static
*/
- public function fields($array)
+ public function fields(array $value): static
{
- return $this->addProperty('fields', $array);
+ return $this->addProperty('fields', $value);
}
/**
* Text or documents to find similar documents for.
*
- * @param mixed $string
+ * @param mixed $value
* @return static
*/
- public function like($string)
+ public function like($value): static
{
- return $this->addProperty('like', $string);
+ return $this->addProperty('like', $value);
}
/**
* Minimum term frequency below which terms are ignored. Defaults to 2.
*
- * @param int $int
+ * @param int $value
* @return static
*/
- public function minTermFreq($int)
+ public function minTermFreq(int $value): static
{
- return $this->addProperty('min_term_freq', $int);
+ return $this->addProperty('min_term_freq', $value);
}
/**
* Maximum number of query terms to be selected per result document. Defaults to 25.
*
- * @param int $int
+ * @param int $value
* @return static
*/
- public function maxQueryTerms($int)
+ public function maxQueryTerms(int $value): static
{
- return $this->addProperty('max_query_terms', $int);
+ return $this->addProperty('max_query_terms', $value);
}
}
diff --git a/src/DSL/Queries/Specialized/Percolate.php b/src/DSL/Queries/Specialized/Percolate.php
index 74d882e..635d058 100644
--- a/src/DSL/Queries/Specialized/Percolate.php
+++ b/src/DSL/Queries/Specialized/Percolate.php
@@ -1,5 +1,7 @@
addProperty('document', $document);
+ return $this->addProperty('document', $value);
}
}
diff --git a/src/DSL/Queries/Specialized/Pinned.php b/src/DSL/Queries/Specialized/Pinned.php
index 0ed91d5..81c11a9 100644
--- a/src/DSL/Queries/Specialized/Pinned.php
+++ b/src/DSL/Queries/Specialized/Pinned.php
@@ -1,5 +1,7 @@
$ids
+ * @param array $value
* @return static
*/
- public function ids($ids)
+ public function ids(array $value): static
{
- return $this->addProperty('ids', $ids);
+ return $this->addProperty('ids', $value);
}
/**
* The organic query used to rank non-pinned documents.
*
- * @param mixed $organic
+ * @param mixed $value
* @return static
*/
- public function organic($organic)
+ public function organic($value): static
{
- return $this->addProperty('organic', Query::create($organic));
+ return $this->addProperty('organic', Query::create($value));
}
/**
* A document to pin instead of using an ID.
*
- * @param mixed $doc
+ * @param mixed $value
* @return static
*/
- public function doc($doc)
+ public function doc($value): static
{
- return $this->addProperty('doc', $doc);
+ return $this->addProperty('doc', $value);
}
}
diff --git a/src/DSL/Queries/Specialized/RankFeature.php b/src/DSL/Queries/Specialized/RankFeature.php
index 0cd8746..ed77d5b 100644
--- a/src/DSL/Queries/Specialized/RankFeature.php
+++ b/src/DSL/Queries/Specialized/RankFeature.php
@@ -1,5 +1,7 @@
addProperty('saturation', $saturation);
+ return $this->addProperty('saturation', $value);
}
/**
* Logarithmic function to compute the score. Supports a scaling_factor parameter.
*
- * @param mixed $log
+ * @param mixed $value
* @return static
*/
- public function log($log)
+ public function log($value): static
{
- return $this->addProperty('log', $log);
+ return $this->addProperty('log', $value);
}
/**
* Sigmoid function to compute the score. Requires exponent and pivot parameters.
*
- * @param mixed $sigmoid
+ * @param mixed $value
* @return static
*/
- public function sigmoid($sigmoid)
+ public function sigmoid($value): static
{
- return $this->addProperty('sigmoid', $sigmoid);
+ return $this->addProperty('sigmoid', $value);
}
/**
* Linear function to compute the score, producing a linear relation between the feature value and the score.
*
- * @param mixed $linear
+ * @param mixed $value
* @return static
*/
- public function linear($linear)
+ public function linear($value): static
{
- return $this->addProperty('linear', $linear);
+ return $this->addProperty('linear', $value);
}
}
diff --git a/src/DSL/Queries/Specialized/Script.php b/src/DSL/Queries/Specialized/Script.php
index a51cd1e..2c5cb2e 100644
--- a/src/DSL/Queries/Specialized/Script.php
+++ b/src/DSL/Queries/Specialized/Script.php
@@ -1,5 +1,7 @@
addProperty('script', \ElasticKit\DSL\Queries\Script::create($script));
+ return $this->addProperty('script', \ElasticKit\DSL\Queries\Script::create($value));
}
}
diff --git a/src/DSL/Queries/Specialized/ScriptScore.php b/src/DSL/Queries/Specialized/ScriptScore.php
index d58091b..290f8f5 100644
--- a/src/DSL/Queries/Specialized/ScriptScore.php
+++ b/src/DSL/Queries/Specialized/ScriptScore.php
@@ -1,5 +1,7 @@
addProperty('query', Query::create($query));
+ return $this->addProperty('query', Query::create($value));
}
/**
* The script used to compute the new relevance score.
*
- * @param mixed $script
+ * @param mixed $value
* @return static
*/
- public function script($script)
+ public function script($value): static
{
- return $this->addProperty('script', \ElasticKit\DSL\Queries\Script::create($script));
+ return $this->addProperty('script', \ElasticKit\DSL\Queries\Script::create($value));
}
/**
* Minimum relevance score threshold. Documents with a lower score are excluded.
*
- * @param float $minScore
+ * @param float $value
* @return static
*/
- public function minScore($minScore)
+ public function minScore(float $value): static
{
- return $this->addProperty('min_score', Query::create($minScore));
+ return $this->addProperty('min_score', $value);
}
}
diff --git a/src/DSL/Queries/Specialized/Wrapper.php b/src/DSL/Queries/Specialized/Wrapper.php
index 53f8b4d..48c83c7 100644
--- a/src/DSL/Queries/Specialized/Wrapper.php
+++ b/src/DSL/Queries/Specialized/Wrapper.php
@@ -1,5 +1,7 @@
query($field);
@@ -32,11 +34,11 @@ public static function create($field = null, $value = null)
/**
* A query in base64 encoded format.
*
- * @param string $query
+ * @param string $value
* @return static
*/
- public function query($query)
+ public function query(string $value): static
{
- return $this->addProperty('query', $query);
+ return $this->addProperty('query', $value);
}
}
diff --git a/src/DSL/Queries/TermLevel.php b/src/DSL/Queries/TermLevel.php
index 7e06f7a..6f1e34c 100644
--- a/src/DSL/Queries/TermLevel.php
+++ b/src/DSL/Queries/TermLevel.php
@@ -1,5 +1,7 @@
addQuery(Fuzzy::create($field, $value));
}
@@ -50,7 +52,7 @@ public function fuzzy($field, $value = null)
* @param mixed $field
* @return $this
*/
- public function exists($field)
+ public function exists($field): static
{
return $this->addQuery(Exists::create($field));
}
@@ -58,15 +60,15 @@ public function exists($field)
/**
* Returns documents based on their IDs. This query uses document IDs stored in the _id field.
*
- * @param mixed $ids
+ * @param mixed $value
* @return $this
*/
- public function ids($ids)
+ public function ids($value): static
{
- if (is_array($ids) && !isset($ids['values'])) {
- $ids = ['values' => $ids];
+ if (is_array($value) && !isset($value['values'])) {
+ $value = ['values' => $value];
}
- return $this->addQuery(IDs::create($ids));
+ return $this->addQuery(IDs::create($value));
}
/**
@@ -76,7 +78,7 @@ public function ids($ids)
* @param mixed $value
* @return $this
*/
- public function prefix($field, $value = null)
+ public function prefix($field, $value = null): static
{
return $this->addQuery(Prefix::create($field, $value));
}
@@ -91,7 +93,7 @@ public function prefix($field, $value = null)
* @param callable|array $value
* @return $this
*/
- public function range($field, $value = null)
+ public function range($field, $value = null): static
{
return $this->addQuery(Range::create($field, $value));
}
@@ -103,7 +105,7 @@ public function range($field, $value = null)
* @param mixed $value
* @return $this
*/
- public function regexp($field, $value = null)
+ public function regexp($field, $value = null): static
{
return $this->addQuery(Regexp::create($field, $value));
}
@@ -117,7 +119,7 @@ public function regexp($field, $value = null)
* @param callable|string|array $value
* @return $this
*/
- public function term($field, $value = null)
+ public function term($field, $value = null): static
{
return $this->addQuery(Term::create($field, $value));
}
@@ -132,7 +134,7 @@ public function term($field, $value = null)
* @param mixed $value
* @return $this
*/
- public function terms($field, $value = null)
+ public function terms($field, $value = null): static
{
return $this->addQuery(Terms::create($field, $value));
}
@@ -149,7 +151,7 @@ public function terms($field, $value = null)
* @param mixed $value
* @return $this
*/
- public function termsSet($field, $value = null)
+ public function termsSet($field, $value = null): static
{
return $this->addQuery(TermsSet::create($field, $value));
}
@@ -163,7 +165,7 @@ public function termsSet($field, $value = null)
* @param mixed $value
* @return $this
*/
- public function wildcard($field, $value = null)
+ public function wildcard($field, $value = null): static
{
return $this->addQuery(Wildcard::create($field, $value));
}
diff --git a/src/DSL/Queries/TermLevel/Exists.php b/src/DSL/Queries/TermLevel/Exists.php
index 7d82f6d..850d5c7 100644
--- a/src/DSL/Queries/TermLevel/Exists.php
+++ b/src/DSL/Queries/TermLevel/Exists.php
@@ -1,5 +1,7 @@
.
@@ -16,7 +18,7 @@ class Fuzzy extends Node
* @param string $value
* @return static
*/
- public function value($value)
+ public function value(string $value): static
{
return $this->addProperty('value', $value);
}
@@ -24,55 +26,55 @@ public function value($value)
/**
* Maximum edit distance allowed for matching. See Fuzziness for valid values and more information.
*
- * @param string $fuzziness
+ * @param int|string $value
* @return static
*/
- public function fuzziness($fuzziness)
+ public function fuzziness(int|string $value): static
{
- return $this->addProperty('fuzziness', $fuzziness);
+ return $this->addProperty('fuzziness', $value);
}
/**
* Maximum number of variations created. Defaults to 50.
*
- * @param int $maxExpansions
+ * @param int $value
* @return static
*/
- public function maxExpansions($maxExpansions)
+ public function maxExpansions(int $value): static
{
- return $this->addProperty('max_expansions', $maxExpansions);
+ return $this->addProperty('max_expansions', $value);
}
/**
* Number of beginning characters left unchanged when creating expansions. Defaults to 0.
*
- * @param int $prefixLength
+ * @param int $value
* @return static
*/
- public function prefixLength($prefixLength)
+ public function prefixLength(int $value): static
{
- return $this->addProperty('prefix_length', $prefixLength);
+ return $this->addProperty('prefix_length', $value);
}
/**
* Indicates whether edits include transpositions of two adjacent characters (ab → ba). Defaults to true.
*
- * @param bool $transpositions
+ * @param bool $value
* @return static
*/
- public function transpositions($transpositions)
+ public function transpositions(bool $value): static
{
- return $this->addProperty('transpositions', $transpositions);
+ return $this->addProperty('transpositions', $value);
}
/**
* Method used to rewrite the query. For valid values and more information, see the rewrite parameter.
*
- * @param string $rewrite
+ * @param string $value
* @return static
*/
- public function rewrite($rewrite)
+ public function rewrite(string $value): static
{
- return $this->addProperty('rewrite', $rewrite);
+ return $this->addProperty('rewrite', $value);
}
}
diff --git a/src/DSL/Queries/TermLevel/IDs.php b/src/DSL/Queries/TermLevel/IDs.php
index 2f36123..fcab3a4 100644
--- a/src/DSL/Queries/TermLevel/IDs.php
+++ b/src/DSL/Queries/TermLevel/IDs.php
@@ -1,5 +1,7 @@
$values
+ * @param array $value
* @return static
*/
- public function values($values)
+ public function values(array $value): static
{
- return $this->addProperty('values', $values);
+ return $this->addProperty('values', $value);
}
}
diff --git a/src/DSL/Queries/TermLevel/Prefix.php b/src/DSL/Queries/TermLevel/Prefix.php
index 609dc7b..eec7485 100644
--- a/src/DSL/Queries/TermLevel/Prefix.php
+++ b/src/DSL/Queries/TermLevel/Prefix.php
@@ -1,14 +1,16 @@
.
@@ -16,7 +18,7 @@ class Prefix extends Node
* @param string $value
* @return static
*/
- public function value($value)
+ public function value(string $value): static
{
return $this->addProperty('value', $value);
}
@@ -24,22 +26,22 @@ public function value($value)
/**
* Method used to rewrite the query. For valid values and more information, see the rewrite parameter.
*
- * @param string $rewrite
+ * @param string $value
* @return static
*/
- public function rewrite($rewrite)
+ public function rewrite(string $value): static
{
- return $this->addProperty('rewrite', $rewrite);
+ return $this->addProperty('rewrite', $value);
}
/**
* Allows ASCII case insensitive matching of the value with the indexed field values when set to true. Default is false which means the case sensitivity of matching depends on the underlying field’s mapping.
*
- * @param bool $caseInsensitive
+ * @param bool $value
* @return static
*/
- public function caseInsensitive($caseInsensitive)
+ public function caseInsensitive(bool $value): static
{
- return $this->addProperty('case_insensitive', $caseInsensitive);
+ return $this->addProperty('case_insensitive', $value);
}
}
diff --git a/src/DSL/Queries/TermLevel/Range.php b/src/DSL/Queries/TermLevel/Range.php
index 892e331..dc2192f 100644
--- a/src/DSL/Queries/TermLevel/Range.php
+++ b/src/DSL/Queries/TermLevel/Range.php
@@ -1,60 +1,62 @@
addProperty('gte', $gte);
+ return $this->addProperty('gte', $value);
}
/**
- * (Optional) Greater than.
+ * Greater than.
*
- * @param mixed $gt
+ * @param string|int|float|bool $value
* @return static
*/
- public function gt($gt)
+ public function gt(string|int|float|bool $value): static
{
- return $this->addProperty('gt', $gt);
+ return $this->addProperty('gt', $value);
}
/**
- * (Optional) Less than or equal to.
+ * Less than or equal to.
*
- * @param mixed $lte
+ * @param string|int|float|bool $value
* @return static
*/
- public function lte($lte)
+ public function lte(string|int|float|bool $value): static
{
- return $this->addProperty('lte', $lte);
+ return $this->addProperty('lte', $value);
}
/**
- * (Optional) Less than.
+ * Less than.
*
- * @param mixed $lt
+ * @param string|int|float|bool $value
* @return static
*/
- public function lt($lt)
+ public function lt(string|int|float|bool $value): static
{
- return $this->addProperty('lt', $lt);
+ return $this->addProperty('lt', $value);
}
/**
@@ -66,12 +68,12 @@ public function lt($lt)
*
* If a format or date value is incomplete, the range query replaces any missing components with default values. See Missing date components.
*
- * @param string $format
+ * @param string $value
* @return static
*/
- public function format($format)
+ public function format(string $value): static
{
- return $this->addProperty('format', $format);
+ return $this->addProperty('format', $value);
}
/**
@@ -84,22 +86,22 @@ public function format($format)
* WITHIN
* Matches documents with a range field value entirely within the query’s range.
*
- * @param string $relation
+ * @param string $value
* @return static
*/
- public function relation($relation)
+ public function relation(string $value): static
{
- return $this->addProperty('relation', $relation);
+ return $this->addProperty('relation', $value);
}
/**
* Coordinated Universal Time (UTC) offset or IANA time zone used to convert date values in the query to UTC.
*
- * @param string $timeZone
+ * @param string $value
* @return static
*/
- public function timeZone($timeZone)
+ public function timeZone(string $value): static
{
- return $this->addProperty('time_zone', $timeZone);
+ return $this->addProperty('time_zone', $value);
}
}
diff --git a/src/DSL/Queries/TermLevel/Regexp.php b/src/DSL/Queries/TermLevel/Regexp.php
index 80ed83c..f7d9a83 100644
--- a/src/DSL/Queries/TermLevel/Regexp.php
+++ b/src/DSL/Queries/TermLevel/Regexp.php
@@ -1,14 +1,16 @@
. For a list of supported operators, see Regular expression syntax.
@@ -20,7 +22,7 @@ class Regexp extends Node
* @param string $value
* @return static
*/
- public function value($value)
+ public function value(string $value): static
{
return $this->addProperty('value', $value);
}
@@ -28,23 +30,23 @@ public function value($value)
/**
* Enables optional operators for the regular expression. For valid values and more information, see Regular expression syntax.
*
- * @param string $flags
+ * @param string $value
* @return static
*/
- public function flags($flags)
+ public function flags(string $value): static
{
- return $this->addProperty('flags', $flags);
+ return $this->addProperty('flags', $value);
}
/**
* Allows case insensitive matching of the regular expression value with the indexed field values when set to true. Default is false which means the case sensitivity of matching depends on the underlying field’s mapping.
*
- * @param bool $caseInsensitive
+ * @param bool $value
* @return static
*/
- public function caseInsensitive($caseInsensitive)
+ public function caseInsensitive(bool $value): static
{
- return $this->addProperty('case_insensitive', $caseInsensitive);
+ return $this->addProperty('case_insensitive', $value);
}
/**
@@ -54,22 +56,22 @@ public function caseInsensitive($caseInsensitive)
*
* You can use this parameter to prevent that conversion from unintentionally consuming too many resources. You may need to increase this limit to run complex regular expressions.
*
- * @param int $maxDeterminizedStates
+ * @param int $value
* @return static
*/
- public function maxDeterminizedStates($maxDeterminizedStates)
+ public function maxDeterminizedStates(int $value): static
{
- return $this->addProperty('max_determinized_states', $maxDeterminizedStates);
+ return $this->addProperty('max_determinized_states', $value);
}
/**
* Method used to rewrite the query. For valid values and more information, see the rewrite parameter.
*
- * @param string $rewrite
+ * @param string $value
* @return static
*/
- public function rewrite($rewrite)
+ public function rewrite(string $value): static
{
- return $this->addProperty('rewrite', $rewrite);
+ return $this->addProperty('rewrite', $value);
}
}
diff --git a/src/DSL/Queries/TermLevel/Term.php b/src/DSL/Queries/TermLevel/Term.php
index 699d9cf..ad4f0e8 100644
--- a/src/DSL/Queries/TermLevel/Term.php
+++ b/src/DSL/Queries/TermLevel/Term.php
@@ -1,5 +1,7 @@
. To return a document, the term must exactly match the field value, including whitespace and capitalization.
*
- * @param string $value
+ * @param string|int|float|bool $value
* @return static
*/
- public function value($value)
+ public function value(string|int|float|bool $value): static
{
return $this->addProperty('value', $value);
}
@@ -36,12 +38,12 @@ public function value($value)
*
* Allows ASCII case insensitive matching of the value with the indexed field values when set to true. Default is false which means the case sensitivity of matching depends on the underlying field’s mapping.
*
- * @param bool $caseInsensitive
+ * @param bool $value
* @return static
* @version 7.10.0
*/
- public function caseInsensitive($caseInsensitive)
+ public function caseInsensitive(bool $value): static
{
- return $this->addProperty('case_insensitive', $caseInsensitive);
+ return $this->addProperty('case_insensitive', $value);
}
}
diff --git a/src/DSL/Queries/TermLevel/Terms.php b/src/DSL/Queries/TermLevel/Terms.php
index e980715..ab8cb59 100644
--- a/src/DSL/Queries/TermLevel/Terms.php
+++ b/src/DSL/Queries/TermLevel/Terms.php
@@ -1,14 +1,16 @@
$values
+ * @param array $values
* @return static
*/
- public function values($field, $values)
+ public function values(string $field, array $values): static
{
return $this->addProperty($field, $values);
}
diff --git a/src/DSL/Queries/TermLevel/TermsSet.php b/src/DSL/Queries/TermLevel/TermsSet.php
index e3a5669..1c60488 100644
--- a/src/DSL/Queries/TermLevel/TermsSet.php
+++ b/src/DSL/Queries/TermLevel/TermsSet.php
@@ -1,5 +1,7 @@
. To return a document, a required number of terms must exactly match the field values, including whitespace and capitalization.
*
* The required number of matching terms is defined in the minimum_should_match, minimum_should_match_field or minimum_should_match_script parameters. Exactly one of these parameters must be provided.
*
- * @param array $terms
+ * @param array $value
* @return static
*/
- public function terms($terms)
+ public function terms(array $value): static
{
- return $this->addProperty('terms', $terms);
+ return $this->addProperty('terms', $value);
}
/**
- * (Optional) Specification for the number of matching terms required to return a document.
+ * Specification for the number of matching terms required to return a document.
*
* For valid values, see minimum_should_match parameter.
*
- * @param mixed $minimumShouldMatch
+ * @param int|string $value
* @return static
*/
- public function minimumShouldMatch($minimumShouldMatch)
+ public function minimumShouldMatch(int|string $value): static
{
- return $this->addProperty('minimum_should_match', $minimumShouldMatch);
+ return $this->addProperty('minimum_should_match', $value);
}
/**
- * @param string $field
+ * @param string $value
* @return static
*/
- public function minimumShouldMatchField($field)
+ public function minimumShouldMatchField(string $value): static
{
- return $this->addProperty('minimum_should_match_field', $field);
+ return $this->addProperty('minimum_should_match_field', $value);
}
/**
@@ -56,11 +58,11 @@ public function minimumShouldMatchField($field)
*
* For an example query using the minimum_should_match_script parameter, see How to use the minimum_should_match_script parameter.
*
- * @param mixed $minimumShouldMatchScript
+ * @param mixed $value
* @return static
*/
- public function minimumShouldMatchScript($minimumShouldMatchScript)
+ public function minimumShouldMatchScript($value): static
{
- return $this->addProperty('minimum_should_match_script', Script::create($minimumShouldMatchScript));
+ return $this->addProperty('minimum_should_match_script', Script::create($value));
}
}
diff --git a/src/DSL/Queries/TermLevel/Wildcard.php b/src/DSL/Queries/TermLevel/Wildcard.php
index aaaff69..88c6d69 100644
--- a/src/DSL/Queries/TermLevel/Wildcard.php
+++ b/src/DSL/Queries/TermLevel/Wildcard.php
@@ -1,36 +1,38 @@
addProperty('case_insensitive', $caseInsensitive);
+ return $this->addProperty('case_insensitive', $value);
}
/**
* Method used to rewrite the query. For valid values and more information, see the rewrite parameter.
*
- * @param string $rewrite
+ * @param string $value
* @return static
*/
- public function rewrite($rewrite)
+ public function rewrite(string $value): static
{
- return $this->addProperty('rewrite', $rewrite);
+ return $this->addProperty('rewrite', $value);
}
/**
@@ -45,7 +47,7 @@ public function rewrite($rewrite)
* @param string $value
* @return static
*/
- public function value($value)
+ public function value(string $value): static
{
return $this->addProperty('value', $value);
}
@@ -53,11 +55,11 @@ public function value($value)
/**
* An alias for the value parameter. If you specify both value and wildcard, the query uses the last one in the request body.
*
- * @param string $wildcard
+ * @param string $value
* @return static
*/
- public function wildcard($wildcard)
+ public function wildcard(string $value): static
{
- return $this->addProperty('wildcard', $wildcard);
+ return $this->addProperty('wildcard', $value);
}
}
diff --git a/src/DSL/Query.php b/src/DSL/Query.php
index 30acc37..664c0c8 100644
--- a/src/DSL/Query.php
+++ b/src/DSL/Query.php
@@ -1,9 +1,11 @@
*/
- protected $_queryClauses = [];
+ protected array $_queries = [];
/**
* Aggregation nodes stored independently from type properties.
*
* @var array
*/
- protected $_aggregations = [];
-
- /**
- * Set whether the query supports multiple clauses.
- *
- * @param bool $multi
- * @return static
- */
- protected function setMulti($multi)
- {
- $this->_multi = $multi;
- return $this;
- }
-
- /**
- * Whether the query supports multiple clauses.
- *
- * @return bool
- */
- protected function isMulti()
- {
- return $this->_multi;
- }
+ protected array $_aggregations = [];
/**
* Initialize the query container.
@@ -99,43 +82,66 @@ public function __construct($field = null, $value = null)
return;
}
if ($field instanceof Closure) {
- $field($this);
+ $this->fromClosure($field);
} elseif ($field instanceof Node) {
- $this->_queryClauses[] = $field;
+ $this->_queries[] = $field;
} elseif (is_array($field)) {
- if (array_key_exists('query', $field)) {
- $this->_properties = $field;
- } else {
- $this->_queryClauses[] = $field;
- }
- } elseif ($field !== null) {
+ $this->fromArray($field);
+ }
+ }
+
+ /**
+ * Initialize from a raw ES body array.
+ *
+ * Arrays with a 'query' key are stored as-is (raw DSL).
+ * Other arrays are added as query clauses.
+ *
+ * @param array $field
+ */
+ protected function fromArray(array $field): void
+ {
+ if (array_key_exists('query', $field)) {
$this->_properties = $field;
+ } else {
+ $this->_queries[] = $field;
}
}
+ /**
+ * Get all query clauses.
+ *
+ * @return array
+ */
+ public function getQueries(): array
+ {
+ return $this->_queries;
+ }
+
/**
* Add a query clause to the query container.
*
- * @param mixed $query
+ * @param Query|\Closure|array|Node $clause
* @return $this
*/
- public function addQuery($query)
+ public function addQuery($clause): static
{
- $this->_queryClauses[] = $query;
+ $this->_queries[] = $clause;
return $this;
}
/**
* Conditionally add a query clause.
*
- * @param bool|callable $condition
- * @param mixed $query
- * @param mixed $default
+ * $condition is a bool, or a Closure returning a bool.
+ *
+ * @param bool|\Closure $condition
+ * @param Query|\Closure|array $query
+ * @param Query|\Closure|array|null $default
* @return $this
*/
- public function when($condition, $query, $default = null)
+ public function when(bool|\Closure $condition, $query, $default = null): static
{
- $truthy = is_callable($condition) ? $condition() : $condition;
+ $truthy = $condition instanceof \Closure ? $condition() : $condition;
if ($truthy) {
$this->addQuery(static::create($query));
@@ -156,50 +162,11 @@ public function when($condition, $query, $default = null)
*
* @param string|Agg|array $alias
* @param callable|Agg|array|null $aggs
- * @return $this
- * @throws \BadMethodCallException if called with a string alias and no definition
+ * @return static
*/
- public function aggs($alias, $aggs = null)
+ public function aggs($alias, $aggs = null): static
{
- if ($aggs === null && !is_string($alias)) {
- $aggs = $alias;
- $alias = null;
- }
-
- if ($aggs instanceof Agg) {
- if ($alias !== null) {
- $aggs->alias($alias);
- }
- $this->_aggregations[$alias ?? $aggs->getAlias()] = $aggs;
- return $this;
- }
-
- if (is_array($aggs)) {
- $childAgg = Agg::create($aggs);
- if ($alias !== null) {
- $childAgg->alias($alias);
- }
- $this->_aggregations[$alias] = $childAgg;
- return $this;
- }
-
- if ($alias !== null && !isset($this->_aggregations[$alias])) {
- $this->_aggregations[$alias] = new Agg();
- $this->_aggregations[$alias]->alias($alias);
- }
-
- if ($aggs instanceof \Closure) {
- $aggs($this->_aggregations[$alias]);
- return $this;
- }
-
- if ($alias !== null) {
- throw new BadMethodCallException(
- sprintf('aggs() requires a second argument. Use aggs("%s", $definition) where $definition is a closure, array, or Agg instance.', $alias)
- );
- }
-
- return $this;
+ return $this->registerAgg($alias, $aggs, $this->_aggregations);
}
/**
@@ -210,85 +177,137 @@ public function aggs($alias, $aggs = null)
*
* @return array
*/
- public function toArray()
+ public function toArray(): array
{
- $dsl = is_array($this->_properties) ? $this->resolveProperties($this->_properties) : [];
+ $dsl = $this->_properties !== null ? $this->resolveProperties($this->_properties) : [];
$query = $this->buildQuery();
- if (!empty($query)) {
+ if ($this->_multi || !empty($query)) {
$dsl['query'] = $query;
}
- $this->buildAggs($dsl);
- $this->buildParams($dsl);
+ $dsl = $this->buildAggs($dsl);
+ $dsl = $this->buildParams($dsl);
- return array_filter($dsl, function ($v) {
- return $v !== [];
- });
+ return array_filter($dsl, fn ($v) => $v !== null);
}
/**
* Build the query clause array from stored query clauses.
*
- * @return array|object
+ * @return array
*/
- private function buildQuery()
+ private function buildQuery(): array
{
- if (empty($this->_queryClauses)) {
- return $this->_multi ? (object)[] : [];
+ if (empty($this->_queries)) {
+ return [];
}
+ // Flatten nested Query instances
+ $flat = [];
+ foreach ($this->_queries as $item) {
+ if ($item instanceof self) {
+ foreach ($item->getQueries() as $clause) {
+ $flat[] = $clause;
+ }
+ } else {
+ $flat[] = $item;
+ }
+ }
+
+ $clauses = $this->buildClauses($flat);
+
+ if ($this->_multi) {
+ return $clauses;
+ }
+
+ return $this->mergeClauses($clauses);
+ }
+
+ /**
+ * Build clause entries from flattened queries, skipping nodes that
+ * serialize to null/[] (e.g. an empty bool built dynamically).
+ *
+ * @param array $flat
+ * @return array>
+ */
+ private function buildClauses(array $flat): array
+ {
$clauses = [];
- foreach ($this->_queryClauses as $query) {
- if ($query instanceof Query) {
- foreach ($query->toArray()['query'] as $field => $item) {
- $clauses[] = [$field => $item];
+ foreach ($flat as $query) {
+ if ($query instanceof Node) {
+ $body = $query->toArray();
+ if ($body === null) {
+ continue;
}
- } elseif ($query instanceof Node) {
- $clauses[] = [$query->key() => $query->toArray()];
+ $clauses[] = [$query->key() => $body];
} elseif (is_array($query)) {
foreach ($query as $field => $item) {
if ($item instanceof Node) {
$item = $item->toArray();
}
+ if ($item === null) {
+ continue;
+ }
$clauses[] = [$field => $item];
}
+ } else {
+ throw new RuntimeException(sprintf(
+ 'Unsupported clause type %s; use a Node, array, or closure.',
+ get_debug_type($query)
+ ));
}
}
- if ($this->_multi) {
- return $clauses; // @phpstan-ignore return.type
- }
- if (empty($clauses)) {
- return [];
+ return $clauses;
+ }
+
+ /**
+ * Merge per-clause arrays, throwing on duplicate keys instead of silently overwriting.
+ *
+ * @param array> $clauses
+ * @return array
+ * @throws RuntimeException when two clauses share a key
+ */
+ private function mergeClauses(array $clauses): array
+ {
+ $merged = [];
+ foreach ($clauses as $clause) {
+ $clash = array_intersect_key($merged, $clause);
+ if ($clash) {
+ throw new RuntimeException(sprintf('Duplicate query clause key "%s".', implode('", "', array_keys($clash))));
+ }
+ $merged += $clause;
}
- return array_merge(...$clauses);
+
+ return $merged;
}
/**
* Build aggregation entries into the DSL array.
*
* @param array $dsl
- * @return void
+ * @return array
*/
- private function buildAggs(array &$dsl)
+ private function buildAggs(array $dsl): array
{
if (empty($this->_aggregations)) {
- return;
+ return $dsl;
}
$dsl['aggs'] = [];
foreach ($this->_aggregations as $agg) {
$dsl['aggs'] += $agg->toArray();
}
+ return $dsl;
}
/**
* Build search request parameters into the DSL array.
*
* @param array $dsl
- * @return void
+ * @return array
*/
- private function buildParams(array &$dsl)
+ private function buildParams(array $dsl): array
{
foreach ($this->_params as $key => $value) {
if ($value instanceof Query) {
@@ -300,5 +319,6 @@ private function buildParams(array &$dsl)
}
$dsl[$key] = $value;
}
+ return $dsl;
}
}
diff --git a/src/DSL/Shared/ClausesSupport.php b/src/DSL/Shared/ClausesSupport.php
deleted file mode 100644
index d543853..0000000
--- a/src/DSL/Shared/ClausesSupport.php
+++ /dev/null
@@ -1,69 +0,0 @@
->
- */
- protected $_clauses = [];
-
- /**
- * Push a clause for the given property.
- *
- * @param string $key Property name (e.g. 'must', 'clauses', 'queries')
- * @param mixed $clause
- * @return static
- */
- protected function pushClause(string $key, $clause)
- {
- $this->_clauses[$key][] = $clause;
- return $this;
- }
-
- /**
- * Serialize to array, merging clauses into the final output.
- *
- * @return array
- */
- public function toArray()
- {
- $this->mergeClauses();
- return parent::toArray();
- }
-
- /**
- * Merge clauses into _properties, adding each clause
- * directly to the target container's clause list.
- *
- * @return void
- */
- private function mergeClauses()
- {
- foreach ($this->_clauses as $key => $clauses) {
- if (!isset($this->_properties[$key])) {
- $this->_properties[$key] = (new Query())->multi(true);
- }
- $target = $this->_properties[$key];
- if ($target instanceof Query) {
- foreach ($clauses as $clause) {
- $resolved = ($clause instanceof Closure) ? Query::create($clause) : $clause;
- $target->addQuery($resolved);
- }
- }
- }
- $this->_clauses = [];
- }
-}
diff --git a/src/DSL/Support/ClausesSupport.php b/src/DSL/Support/ClausesSupport.php
new file mode 100644
index 0000000..303c0e5
--- /dev/null
+++ b/src/DSL/Support/ClausesSupport.php
@@ -0,0 +1,95 @@
+_properties[$key])) {
+ $this->_properties[$key] = (new Query())->multi(true);
+ }
+ $target = $this->_properties[$key];
+ if ($clause instanceof Closure) {
+ $clause($target);
+ } elseif (is_array($clause) && array_is_list($clause)) {
+ foreach ($clause as $item) {
+ $target->addQuery($item);
+ }
+ } else {
+ $target->addQuery($clause);
+ }
+ return $this;
+ }
+
+ /**
+ * Handle two-argument construction: route through routeKeyValueClause,
+ * otherwise fall back to the default field-value handling.
+ *
+ * @param mixed $field
+ * @param mixed $value
+ */
+ protected function fromKeyValue($field, $value): void
+ {
+ if ($this->routeKeyValueClause($field, $value)) {
+ return;
+ }
+ parent::fromKeyValue($field, $value);
+ }
+
+ /**
+ * Route a field-value pair to its setter when the DSL key maps to a method
+ * (snake_case → camelCase). Powers both array input (fromArrayProperties)
+ * and two-arg construction (new Boolean('must', $query)).
+ *
+ * Whether a clause accumulates (addClause) or overwrites (addProperty) is
+ * decided inside each setter, so no clause-key declaration is needed.
+ *
+ * @param mixed $field
+ * @param mixed $value
+ */
+ protected function routeKeyValueClause($field, $value): bool
+ {
+ if (!is_string($field)) {
+ return false;
+ }
+ $method = lcfirst(str_replace('_', '', ucwords($field, '_')));
+ if (method_exists($this, $method)) {
+ $this->$method($value);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Route keys with a setter through it; everything else is a raw property.
+ *
+ * @param array $field
+ */
+ protected function fromArrayProperties(array $field): void
+ {
+ foreach ($field as $key => $val) {
+ if (!$this->routeKeyValueClause($key, $val)) {
+ $this->addProperty($key, $val);
+ }
+ }
+ }
+}
diff --git a/src/DSL/Shared/RangeSupport.php b/src/DSL/Support/RangeSupport.php
similarity index 59%
rename from src/DSL/Shared/RangeSupport.php
rename to src/DSL/Support/RangeSupport.php
index f53cc05..bfc23e6 100644
--- a/src/DSL/Shared/RangeSupport.php
+++ b/src/DSL/Support/RangeSupport.php
@@ -1,6 +1,10 @@
=, >, <=, <) and [start, end] to ES range keys.
@@ -24,10 +28,10 @@ public function __construct($field = null, $value = null)
}
/**
- * @param array $props
+ * @param array $props
* @return array
*/
- private static function normalizeKeys(array $props)
+ private static function normalizeKeys(array $props): array
{
$operators = [
'>=' => 'gte', '>' => 'gt', '<=' => 'lte', '<' => 'lt',
@@ -37,6 +41,14 @@ private static function normalizeKeys(array $props)
if (isset($operators[$operator])) {
unset($props[$operator]);
$props[$operators[$operator]] = $val;
+ } elseif (is_int($operator)) {
+ // A positional element beyond the [start, end] shorthand is invalid;
+ // without this guard it leaks into DSL as a numeric string key.
+ throw new InvalidArgumentException(sprintf(
+ 'Range shorthand only supports two positional elements [start, end]; '
+ . 'unexpected element at index %d. Use [\'gte\' => ..., \'lte\' => ...] instead.',
+ $operator
+ ));
}
}
return $props;
diff --git a/src/DSL/Support/RegistersAgg.php b/src/DSL/Support/RegistersAgg.php
new file mode 100644
index 0000000..ecb95aa
--- /dev/null
+++ b/src/DSL/Support/RegistersAgg.php
@@ -0,0 +1,84 @@
+ $store the target aggregation store (by reference)
+ * @return static
+ *
+ * @throws BadMethodCallException if the alias is empty or the definition is invalid
+ * @throws RuntimeException if the alias already exists in the store
+ */
+ protected function registerAgg($alias, $aggs, array &$store): static
+ {
+ if ($aggs === null && !is_string($alias)) {
+ $aggs = $alias;
+ $alias = null;
+ }
+
+ if ($aggs instanceof Agg) {
+ $key = $alias ?? $aggs->getAlias();
+ if ($key === null || $key === '') {
+ throw new BadMethodCallException('aggs() requires a non-empty alias.');
+ }
+ $aggs->alias($key);
+ if (isset($store[$key])) {
+ throw new RuntimeException(sprintf('Duplicate aggregation alias "%s".', $key));
+ }
+ $store[$key] = $aggs;
+ return $this;
+ }
+
+ if ($alias === null || $alias === '') {
+ throw new BadMethodCallException(
+ 'aggs() requires a non-empty alias. Use aggs("name", $definition).'
+ );
+ }
+
+ if (is_array($aggs)) {
+ $childAgg = Agg::create($aggs);
+ $childAgg->alias($alias);
+ if (isset($store[$alias])) {
+ throw new RuntimeException(sprintf('Duplicate aggregation alias "%s".', $alias));
+ }
+ $store[$alias] = $childAgg;
+ return $this;
+ }
+
+ if (isset($store[$alias])) {
+ throw new RuntimeException(sprintf('Duplicate aggregation alias "%s".', $alias));
+ }
+
+ $store[$alias] = new Agg();
+ $store[$alias]->alias($alias);
+
+ if ($aggs instanceof \Closure) {
+ $aggs($store[$alias]);
+ return $this;
+ }
+
+ throw new BadMethodCallException(
+ sprintf('aggs("%s", ...) requires a closure, array, or Agg instance as the definition.', $alias)
+ );
+ }
+}
diff --git a/src/Index/AggregationShortcut.php b/src/Index/AggregationShortcut.php
deleted file mode 100644
index 87581f2..0000000
--- a/src/Index/AggregationShortcut.php
+++ /dev/null
@@ -1,74 +0,0 @@
-aggregateScalar('max', $field);
- }
-
- /**
- * Return the minimum value of a field.
- *
- * @param string $field
- * @return float|null
- */
- public function min($field)
- {
- return $this->aggregateScalar('min', $field);
- }
-
- /**
- * Return the average value of a field.
- *
- * @param string $field
- * @return float|null
- */
- public function avg($field)
- {
- return $this->aggregateScalar('avg', $field);
- }
-
- /**
- * Return the sum of a field.
- *
- * @param string $field
- * @return float|null
- */
- public function sum($field)
- {
- return $this->aggregateScalar('sum', $field);
- }
-
- /**
- * Execute a metric aggregation and return the scalar value.
- *
- * @param string $type
- * @param string $field
- * @return float|null
- */
- private function aggregateScalar($type, $field)
- {
- $saved = $this->query;
- $this->query = clone $this->query;
- $this->query->size(0);
- $this->query->aggs('__scalar', [$type => ['field' => $field]]);
-
- $response = $this->doSearch($type);
-
- $this->query = $saved;
-
- return $response['aggregations']['__scalar']['value'] ?? null;
- }
-}
diff --git a/src/Index/Bulk.php b/src/Index/Bulk.php
index 2ab5c3b..b373647 100644
--- a/src/Index/Bulk.php
+++ b/src/Index/Bulk.php
@@ -1,50 +1,55 @@
*/
- private $body = [];
+ private array $body = [];
/**
* @var int
*/
- private $retryOnConflict = 0;
+ private int $retryOnConflict = 0;
/**
* @var string|null
*/
- private $targetIndex = null;
+ private ?string $targetIndex = null;
/**
+ * Auto-flush threshold (0 = disabled). When set, the buffer is flushed
+ * automatically inside the enqueue methods once docCount reaches it.
+ *
* @var int
*/
- private $batchSize = 0;
+ private int $batchSize = 0;
/**
* @var int
*/
- private $docCount = 0;
+ private int $docCount = 0;
/**
- * @param Index $index
+ * @var callable|null
*/
- public function __construct(Index $index)
- {
- $this->index = $index;
+ private $errorHandler = null;
+
+ public function __construct(
+ private readonly Index $index
+ ) {
}
/**
@@ -52,11 +57,15 @@ public function __construct(Index $index)
*
* @param string $indexName
* @return $this
- * @throws \InvalidArgumentException if indexName starts with a dot (system index)
+ * @throws \InvalidArgumentException if indexName is empty or starts with a dot (system index)
*/
- public function target($indexName)
+ public function target(string $indexName): static
{
- if (strpos($indexName, '.') === 0) {
+ if ($indexName === '') {
+ throw new InvalidArgumentException('Target index name must not be empty.');
+ }
+
+ if (str_starts_with($indexName, '.')) {
throw new InvalidArgumentException("System index names (starting with '.') are not allowed: {$indexName}");
}
@@ -66,12 +75,13 @@ public function target($indexName)
}
/**
- * Auto-execute when doc count reaches this batch size.
+ * Auto-flush threshold: flush automatically once docCount reaches $size.
+ * Off by default (0). When off, the buffer only sends on an explicit flush().
*
* @param int $size
* @return $this
*/
- public function batchSize($size)
+ public function batchSize(int $size): static
{
$this->batchSize = $size;
@@ -79,12 +89,34 @@ public function batchSize($size)
}
/**
- * Set retry_on_conflict for all update actions in this batch.
+ * Set a callback to handle bulk errors.
+ *
+ * On error the callback receives three tools and decides what to do:
+ * - $response: the raw ES response (items[] carry per-item status/error);
+ * - $body: the full original batch in native ES format (successes included);
+ * - $newbulk: a fresh Bulk bound to the same index and target, for re-send.
+ *
+ * Extract the failures from $body using $response (items[k] matches the k-th
+ * action), re-enqueue them on $newbulk, and call $newbulk->flush() to retry.
+ * Return to consume this batch (cleared), or throw to abort and leave it.
+ *
+ * @param callable $handler function (array $response, array $body, Bulk $newbulk): void
+ * @return $this
+ */
+ public function onError(callable $handler): static
+ {
+ $this->errorHandler = $handler;
+ return $this;
+ }
+
+ /**
+ * Set retry_on_conflict for all subsequent update actions. Persists across
+ * flush() calls (it's a setting, not per-batch).
*
* @param int $count
* @return $this
*/
- public function retryOnConflict($count)
+ public function retryOnConflict(int $count): static
{
$this->retryOnConflict = $count;
@@ -95,17 +127,17 @@ public function retryOnConflict($count)
* Queue an index (create/overwrite) action.
*
* @param string|int|null $id document ID, or null to let ES auto-generate
- * @param array $document
+ * @param array $data
* @return $this
*/
- public function index($id, $document)
+ public function index(string|int|null $id, array $data): static
{
$action = ['index' => ['_index' => $this->resolveIndex()]];
if ($id !== null && $id !== '') {
$action['index']['_id'] = $id;
}
$this->body[] = $action;
- $this->body[] = $document;
+ $this->body[] = $data;
$this->afterPush();
return $this;
@@ -115,25 +147,29 @@ public function index($id, $document)
* Alias for index(). Queue a save (create/overwrite) action.
*
* @param string|int|null $id
- * @param array $document
+ * @param array $data
* @return $this
*/
- public function save($id, $document)
+ public function save(string|int|null $id, array $data): static
{
- return $this->index($id, $document);
+ return $this->index($id, $data);
}
/**
* Queue a create action (fail if document already exists).
*
- * @param string|int $id
- * @param array $document
+ * @param string|int|null $id document ID, or null/'' to let ES auto-generate
+ * @param array $data
* @return $this
*/
- public function create($id, $document)
+ public function create(string|int|null $id, array $data): static
{
- $this->body[] = ['create' => ['_index' => $this->resolveIndex(), '_id' => $id]];
- $this->body[] = $document;
+ $action = ['create' => ['_index' => $this->resolveIndex()]];
+ if ($id !== null && $id !== '') {
+ $action['create']['_id'] = $id;
+ }
+ $this->body[] = $action;
+ $this->body[] = $data;
$this->afterPush();
return $this;
@@ -149,7 +185,7 @@ public function create($id, $document)
* @param bool $upsert
* @return $this
*/
- public function update($id, $data, $upsert = false)
+ public function update(string|int $id, array $data, bool $upsert = false): static
{
$action = ['update' => ['_index' => $this->resolveIndex(), '_id' => $id]];
@@ -170,7 +206,7 @@ public function update($id, $data, $upsert = false)
* @param string|int $id
* @return $this
*/
- public function delete($id)
+ public function delete(string|int $id): static
{
$this->body[] = ['delete' => ['_index' => $this->resolveIndex(), '_id' => $id]];
$this->afterPush();
@@ -179,12 +215,19 @@ public function delete($id)
}
/**
- * Execute all queued actions and return the raw ES response.
+ * Flush all queued actions to ES and return the raw response.
+ *
+ * On success the queue is cleared. On error: with an onError handler the
+ * batch is handed off (response, the full body, and a fresh Bulk) and cleared
+ * on return; without a handler a RuntimeException is thrown and the batch is
+ * preserved for the caller to retry. Call this at the end of a batch to flush
+ * the remainder — batchSize() auto-flushes full batches during enqueue.
*
* @param array $options top-level bulk API params (refresh, timeout, etc)
* @return array
+ * @throws \RuntimeException when the response has errors and no handler swallowed them
*/
- public function execute(array $options = [])
+ public function flush(array $options = []): array
{
if (empty($this->body)) {
return [];
@@ -193,9 +236,9 @@ public function execute(array $options = [])
$indexName = $this->resolveIndex();
$actions = $this->body;
- $e = new Event('bulk.execute.before', $indexName);
+ $e = new Event('bulk.flush.before', $indexName);
$e->actions = $actions;
- Index::dispatch($e);
+ EventDispatcher::dispatch($e);
$start = microtime(true);
$response = $this->index->getClient()->bulk(
@@ -203,15 +246,38 @@ public function execute(array $options = [])
)->asArray();
$duration = microtime(true) - $start;
- $this->body = [];
- $this->docCount = 0;
- $this->retryOnConflict = 0;
-
- $e = new Event('bulk.execute.after', $indexName);
+ $e = new Event('bulk.flush.after', $indexName);
$e->actions = $actions;
$e->response = $response;
$e->duration = $duration;
- Index::dispatch($e);
+ EventDispatcher::dispatch($e);
+
+ if (!empty($response['errors'])) {
+ if ($this->errorHandler) {
+ // Hand the caller the raw materials: the response, the full body,
+ // and a fresh Bulk on the same index/target. The caller extracts
+ // the failures and re-sends them however it likes.
+ $newbulk = new Bulk($this->index);
+ $newbulk->targetIndex = $this->targetIndex;
+ ($this->errorHandler)($response, $actions, $newbulk);
+ } else {
+ $json = json_encode($response, JSON_UNESCAPED_UNICODE);
+ // json_encode() can return false on malformed payloads; guard required
+ // under strict_types to avoid passing false to strlen().
+ if ($json === false) {
+ $json = '(unable to encode bulk response)';
+ }
+ if (strlen($json) > 4096) {
+ $json = mb_strcut($json, 0, 4096) . '... [truncated]';
+ }
+ throw new RuntimeException("Bulk request has errors: {$json}");
+ }
+ }
+
+ // Success, or the handler consumed the batch. Only the buffer is reset;
+ // retryOnConflict persists across flushes (it's a setting, like target()).
+ $this->body = [];
+ $this->docCount = 0;
return $response;
}
@@ -221,7 +287,7 @@ public function execute(array $options = [])
*
* @return string
*/
- private function resolveIndex()
+ private function resolveIndex(): string
{
return $this->targetIndex ?? $this->index->name();
}
@@ -234,7 +300,7 @@ private function afterPush(): void
$this->docCount++;
if ($this->batchSize > 0 && $this->docCount >= $this->batchSize) {
- $this->execute();
+ $this->flush();
}
}
}
diff --git a/src/Index/Doc.php b/src/Index/Doc.php
index c5a194f..421e5de 100644
--- a/src/Index/Doc.php
+++ b/src/Index/Doc.php
@@ -1,59 +1,55 @@
index = $index;
- $this->id = $id;
+ public function __construct(
+ private readonly Index $index,
+ private readonly string|int|null $id
+ ) {
}
/**
* Return the document ID.
*
- * @return string|int
+ * @return string|int|null
*/
- public function id()
+ public function id(): string|int|null
{
return $this->id;
}
/**
- * Set retry_on_conflict for the next write operation.
+ * Set retry_on_conflict for subsequent write operations.
*
* @param int $count
* @return $this
*/
- public function retryOnConflict($count)
+ public function retryOnConflict(int $count): static
{
$this->retryOnConflict = $count;
@@ -61,12 +57,12 @@ public function retryOnConflict($count)
}
/**
- * Set refresh for the next write operation (true/false/wait_for).
+ * Set refresh for subsequent write operations (true/false/wait_for).
*
* @param string $value
* @return $this
*/
- public function refresh($value)
+ public function refresh(string $value): static
{
$this->refresh = $value;
@@ -78,11 +74,13 @@ public function refresh($value)
*
* @return array
*/
- public function get()
+ public function get(): array
{
+ $id = $this->requireId('get');
+
return $this->index->getClient()->get([
'index' => $this->index->name(),
- 'id' => $this->id,
+ 'id' => $id,
])->asArray();
}
@@ -91,11 +89,13 @@ public function get()
*
* @return array
*/
- public function source()
+ public function source(): array
{
+ $id = $this->requireId('source');
+
return $this->index->getClient()->getSource([
'index' => $this->index->name(),
- 'id' => $this->id,
+ 'id' => $id,
])->asArray();
}
@@ -104,11 +104,13 @@ public function source()
*
* @return bool
*/
- public function exists()
+ public function exists(): bool
{
+ $id = $this->requireId('exists');
+
return $this->index->getClient()->exists([
'index' => $this->index->name(),
- 'id' => $this->id,
+ 'id' => $id,
])->asBool();
}
@@ -121,11 +123,13 @@ public function exists()
* @param bool $upsert
* @return array
*/
- public function update($data, $upsert = false)
+ public function update(array $data, bool $upsert = false): array
{
+ $id = $this->requireId('update');
+
$params = [
'index' => $this->index->name(),
- 'id' => $this->id,
+ 'id' => $id,
'body' => [
'doc' => $data,
'doc_as_upsert' => $upsert,
@@ -140,7 +144,6 @@ public function update($data, $upsert = false)
$params['refresh'] = $this->refresh;
}
- $this->resetOptions();
return $this->index->getClient()->update($params)->asArray();
}
@@ -148,22 +151,27 @@ public function update($data, $upsert = false)
/**
* Index (create or overwrite) the document.
*
- * @param array $document
+ * If $id is null or empty, ES auto-generates an id.
+ *
+ * @param array $data
* @return array
*/
- public function index($document)
+ public function index(array $data): array
{
$params = [
'index' => $this->index->name(),
- 'id' => $this->id,
- 'body' => $document,
];
+ if ($this->id !== null && $this->id !== '') {
+ $params['id'] = $this->id;
+ }
+
+ $params['body'] = $data;
+
if ($this->refresh !== null) {
$params['refresh'] = $this->refresh;
}
- $this->resetOptions();
return $this->index->getClient()->index($params)->asArray();
}
@@ -171,34 +179,40 @@ public function index($document)
/**
* Alias for index(). Create or overwrite the document.
*
- * @param array $document
+ * @param array $data
* @return array
*/
- public function save($document)
+ public function save(array $data): array
{
- return $this->index($document);
+ return $this->index($data);
}
/**
* Create the document (fail if already exists).
*
- * @param array $document
+ * If $id is null or empty, ES auto-generates an id (always a create, since
+ * auto-generated ids are unique).
+ *
+ * @param array $data
* @return array
*/
- public function create($document)
+ public function create(array $data): array
{
$params = [
'index' => $this->index->name(),
- 'id' => $this->id,
- 'body' => $document,
- 'op_type' => 'create',
];
+ if ($this->id !== null && $this->id !== '') {
+ $params['id'] = $this->id;
+ }
+
+ $params['body'] = $data;
+ $params['op_type'] = 'create';
+
if ($this->refresh !== null) {
$params['refresh'] = $this->refresh;
}
- $this->resetOptions();
return $this->index->getClient()->index($params)->asArray();
}
@@ -208,28 +222,42 @@ public function create($document)
*
* @return array
*/
- public function delete()
+ public function delete(): array
{
+ $id = $this->requireId('delete');
+
$params = [
'index' => $this->index->name(),
- 'id' => $this->id,
+ 'id' => $id,
];
if ($this->refresh !== null) {
$params['refresh'] = $this->refresh;
}
- $this->resetOptions();
return $this->index->getClient()->delete($params)->asArray();
}
/**
- * Reset pending options after a write operation.
+ * Resolve the document id, throwing for operations that cannot auto-generate.
+ *
+ * get/source/exists/update/delete address an existing document and require
+ * an explicit id; only index/create let ES auto-generate.
+ *
+ * @return string|int
+ * @throws RuntimeException if the id is null or empty
*/
- private function resetOptions(): void
+ private function requireId(string $operation): string|int
{
- $this->retryOnConflict = 0;
- $this->refresh = null;
+ if ($this->id === null || $this->id === '') {
+ throw new RuntimeException(sprintf(
+ '%s() requires an explicit document id; got null/empty. '
+ . 'Use index() or create() to let Elasticsearch auto-generate one.',
+ $operation
+ ));
+ }
+
+ return $this->id;
}
}
diff --git a/src/Index/Event.php b/src/Index/Event.php
deleted file mode 100644
index 9c250d0..0000000
--- a/src/Index/Event.php
+++ /dev/null
@@ -1,72 +0,0 @@
-|null $dsl Request body (search.query.before/after)
- * @property string|null $action Calling method name: get, first, count, scroll, paginate (search.query/scroll events)
- * @property array|null $response ES API response (all after events)
- * @property float|null $duration Execution time in seconds (all after events)
- * @property string|null $scrollId Scroll context ID (search.scroll events)
- * @property array|null $actions Bulk action lines (bulk.execute events)
- * @property string|null $newIndex New backing index name (rebuild.run.after)
- * @property string|null $oldIndex Previous backing index name (rebuild.run.after)
- */
-class Event
-{
- /**
- * @var string
- */
- public $name;
-
- /**
- * @var string
- */
- public $index;
-
- /**
- * @var array
- */
- private $data = [];
-
- /**
- * @param string $name
- * @param string $index
- */
- public function __construct($name, $index)
- {
- $this->name = $name;
- $this->index = $index;
- }
-
- /**
- * @param string $key
- * @return mixed
- */
- public function __get($key)
- {
- return $this->data[$key] ?? null;
- }
-
- /**
- * @param string $key
- * @param mixed $value
- */
- public function __set($key, $value)
- {
- $this->data[$key] = $value;
- }
-
- /**
- * @param string $key
- * @return bool
- */
- public function __isset($key)
- {
- return isset($this->data[$key]);
- }
-}
diff --git a/src/Index/Exception/PaginationTotalUnavailableException.php b/src/Index/Exception/PaginationTotalUnavailableException.php
new file mode 100644
index 0000000..6de645c
--- /dev/null
+++ b/src/Index/Exception/PaginationTotalUnavailableException.php
@@ -0,0 +1,19 @@
+
- */
- protected static $clients = [];
-
/**
* @var string
*/
- protected $connection = 'default';
+ protected string $connection = 'default';
/**
* @var string
*/
- protected $name;
+ protected string $name;
/**
* @var array
*/
- protected $mappings = [];
+ protected array $mappings = [];
/**
* @var array
*/
- protected $settings = [];
+ protected array $settings = [];
/**
* @var int
*/
- protected $perPage = 15;
+ protected int $perPage = 15;
/**
* @var int
*/
- protected $maxPerPage = 100;
+ protected int $maxPerPage = 100;
/**
- * @var callable|null
+ * Whether searches on this index track the total hit count.
+ *
+ * false (default) leaves the total unset (Elasticsearch omits hits.total);
+ * true counts every hit; an int caps the count at that many hits.
+ *
+ * @var int|bool
*/
- protected static $pageResolver;
+ protected int|bool $trackTotalHits = false;
/**
- * @var callable|null
+ * Register an Elasticsearch client. Optionally name the connection.
+ *
+ * @param ClientInterface $client
+ * @param string $connection connection name, defaults to 'default'
+ * @return void
*/
- protected static $paginatorResolver;
+ public static function setClient(ClientInterface $client, string $connection = 'default'): void
+ {
+ ClientManager::set($client, $connection);
+ }
/**
- * @var array>
+ * Return the Elasticsearch client for this index's connection.
+ *
+ * @return ClientInterface
*/
- protected static $listeners = [];
+ public function getClient(): ClientInterface
+ {
+ return ClientManager::get($this->connection);
+ }
/**
- * Register an Elasticsearch client. Optionally name the connection.
+ * Set the connection name for this index instance.
*
- * @param ClientInterface $client
- * @param string|null $name connection name, null for default
- * @return void
+ * @param string $connection
+ * @return $this
*/
- public static function setClient(ClientInterface $client, $name = null)
+ public function setConnection(string $connection): static
{
- self::$clients[$name ?? 'default'] = $client;
+ $this->connection = $connection;
+
+ return $this;
}
/**
- * Return the Elasticsearch client for this index's connection.
+ * Return the connection name for this index.
*
- * @return ClientInterface
+ * @return string
*/
- public function getClient()
+ public function getConnection(): string
{
- if (isset(self::$clients[$this->connection])) {
- return self::$clients[$this->connection];
- }
- if (isset(self::$clients['default'])) {
- return self::$clients['default'];
- }
- throw new RuntimeException(
- 'Elasticsearch client not registered for connection \'' . $this->connection . '\'. '
- . 'Call ' . static::class . '::setClient($client) first.'
- );
+ return $this->connection;
+ }
+
+ /**
+ * Create a new index instance with the given connection.
+ *
+ * @param string $connection
+ * @return static
+ */
+ public static function on(string $connection): static
+ {
+ return (new static())->setConnection($connection);
}
/**
@@ -100,49 +118,59 @@ public function getClient()
*
* @return string
*/
- public function name()
+ public function name(): string
{
+ if (!isset($this->name) || $this->name === '') {
+ throw new RuntimeException(
+ sprintf('Index $name is not set in %s', static::class)
+ );
+ }
+
return $this->name;
}
/**
- * Create a new Search instance. Supports both static and instance call.
+ * Create a new Search instance from this index instance.
*
+ * @param Query|null $query
* @return Search
*/
- public static function query(Query $query = null)
+ public function newQuery(?Query $query = null): Search
{
- return new Search(new static(), $query);
+ return new Search($this, $query);
}
/**
- * Create a DocReference for a single document. Supports both static and instance call.
+ * Create a Search instance. Delegates to newQuery() with a fresh instance.
*
- * @param string|int $id
- * @return Doc
+ * @param Query|null $query
+ * @return Search
*/
- public static function doc($id)
+ public static function query(?Query $query = null): Search
{
- return new Doc(new static(), $id);
+ return (new static())->newQuery($query);
}
/**
- * Insert (create or overwrite) a single document.
+ * Create a Doc reference from this index instance.
*
- * @param string|int|null $id document ID, null or empty string to let ES auto-generate
- * @param array $document document body
- * @return array
+ * @param string|int|null $id document id, or null/'' to let ES auto-generate
+ * @return Doc
*/
- public static function insert($id, array $document)
+ public function newDoc(string|int|null $id): Doc
{
- $index = new static();
- $params = ['index' => $index->name(), 'body' => $document];
-
- if ($id !== null && $id !== '') {
- $params['id'] = $id;
- }
+ return new Doc($this, $id);
+ }
- return $index->getClient()->index($params)->asArray();
+ /**
+ * Create a Doc reference. Delegates to newDoc() with a fresh instance.
+ *
+ * @param string|int|null $id document id, or null/'' to let ES auto-generate
+ * @return Doc
+ */
+ public static function doc(string|int|null $id): Doc
+ {
+ return (new static())->newDoc($id);
}
/**
@@ -150,7 +178,7 @@ public static function insert($id, array $document)
*
* @return array
*/
- public function mappings()
+ public function mappings(): array
{
return $this->mappings;
}
@@ -160,7 +188,7 @@ public function mappings()
*
* @return array
*/
- public function settings()
+ public function settings(): array
{
return $this->settings;
}
@@ -180,7 +208,7 @@ public function rebuildName(): string
*
* @return int
*/
- public function perPage()
+ public function perPage(): int
{
return $this->perPage;
}
@@ -190,51 +218,19 @@ public function perPage()
*
* @return int
*/
- public function maxPerPage()
+ public function maxPerPage(): int
{
return $this->maxPerPage;
}
/**
- * Register a resolver that extracts page and perPage from the request.
- *
- * @param callable $resolver returns [$page, $perPage]
- * @return void
- */
- public static function setPageResolver(callable $resolver)
- {
- self::$pageResolver = $resolver;
- }
-
- /**
- * Register a resolver that converts Results into a framework paginator.
- *
- * @param callable $resolver receives (Results $results, int $page, int $perPage)
- * @return void
- */
- public static function setPaginatorResolver(callable $resolver)
- {
- self::$paginatorResolver = $resolver;
- }
-
- /**
- * Return the registered page resolver, or null.
- *
- * @return callable|null
- */
- public static function getPageResolver()
- {
- return self::$pageResolver;
- }
-
- /**
- * Return the registered paginator resolver, or null.
+ * Return the track_total_hits setting: true, false, or a count cap.
*
- * @return callable|null
+ * @return int|bool
*/
- public static function getPaginatorResolver()
+ public function trackTotalHits(): int|bool
{
- return self::$paginatorResolver;
+ return $this->trackTotalHits;
}
/**
@@ -251,49 +247,4 @@ public function source(array $context = []): iterable
);
}
- /**
- * Register an event listener. Supports exact event name, category wildcard (e.g. 'search.*'), or global '*'.
- *
- * @param string $event
- * @param callable $listener receives (Event $event)
- * @return void
- */
- public static function listen($event, callable $listener)
- {
- self::$listeners[$event][] = $listener;
- }
-
- /**
- * Dispatch an event to all matching listeners.
- *
- * @param Event $event
- * @return void
- */
- public static function dispatch(Event $event)
- {
- foreach (self::$listeners as $pattern => $listeners) {
- if ($pattern === $event->name || $pattern === '*' || static::matchesCategory($pattern, $event->name)) {
- foreach ($listeners as $listener) {
- $listener($event);
- }
- }
- }
- }
-
- /**
- * Check if a wildcard pattern matches an event by category.
- *
- * @param string $pattern
- * @param string $event
- * @return bool
- */
- protected static function matchesCategory($pattern, $event)
- {
- if (substr($pattern, -2) !== '.*') {
- return false;
- }
-
- $prefix = substr($pattern, 0, -1);
- return strpos($event, $prefix) === 0;
- }
}
diff --git a/src/Index/Manager.php b/src/Index/Manager.php
index f166ac3..d09d369 100644
--- a/src/Index/Manager.php
+++ b/src/Index/Manager.php
@@ -1,7 +1,12 @@
index = $index;
+ public function __construct(
+ private readonly Index $index
+ ) {
}
/**
@@ -31,12 +28,12 @@ public function __construct(Index $index)
*
* @return array
*/
- public function create()
+ public function create(): array
{
$indexName = $this->index->name();
$e = new Event('manager.create.before', $indexName);
- Index::dispatch($e);
+ EventDispatcher::dispatch($e);
$mappings = $this->index->mappings();
$settings = $this->index->settings();
@@ -51,30 +48,50 @@ public function create()
$e = new Event('manager.create.after', $indexName);
$e->response = $response;
- Index::dispatch($e);
+ EventDispatcher::dispatch($e);
return $response;
}
/**
- * Delete the index. If the name is an alias, resolves to the backing index first.
+ * Delete the index.
+ *
+ * Refuses by default when name() is an alias: deleting a backing index is
+ * destructive and irreversible. Pass resolveAlias=true to delete the backing
+ * index(es) the alias points to; use removeAlias() to drop the alias itself.
*
+ * @param bool $resolveAlias delete the backing index(es) when name() is an alias
* @return array
+ * @throws RuntimeException when name() is an alias and resolveAlias is false
*/
- public function delete()
+ public function delete(bool $resolveAlias = false): array
{
- $indexName = $this->resolveIndexName();
+ $name = $this->index->name();
+ $indices = $this->index->getClient()->indices();
- $e = new Event('manager.delete.before', $indexName);
- Index::dispatch($e);
+ $isAlias = $indices->existsAlias(['name' => $name])->asBool();
- $response = $this->index->getClient()->indices()->delete([
- 'index' => $indexName,
- ])->asArray();
+ if ($isAlias && !$resolveAlias) {
+ throw new RuntimeException(sprintf(
+ 'Index [%s] is an alias; pass resolveAlias=true to delete its backing index(es), or removeAlias() to drop the alias.',
+ $name
+ ));
+ }
+
+ $target = $name;
+ if ($isAlias) {
+ $aliases = $indices->getAlias(['name' => $name])->asArray();
+ $target = implode(',', array_keys($aliases));
+ }
- $e = new Event('manager.delete.after', $indexName);
+ $e = new Event('manager.delete.before', $target);
+ EventDispatcher::dispatch($e);
+
+ $response = $indices->delete(['index' => $target])->asArray();
+
+ $e = new Event('manager.delete.after', $target);
$e->response = $response;
- Index::dispatch($e);
+ EventDispatcher::dispatch($e);
return $response;
}
@@ -84,7 +101,7 @@ public function delete()
*
* @return bool
*/
- public function exists()
+ public function exists(): bool
{
return $this->index->getClient()->indices()->exists([
'index' => $this->index->name(),
@@ -96,7 +113,7 @@ public function exists()
*
* @return array
*/
- public function get()
+ public function get(): array
{
return $this->index->getClient()->indices()->get([
'index' => $this->index->name(),
@@ -108,11 +125,13 @@ public function get()
*
* @return array
*/
- public function putMapping()
+ public function putMapping(): array
{
+ $mappings = $this->index->mappings();
+
return $this->index->getClient()->indices()->putMapping([
'index' => $this->index->name(),
- 'body' => $this->index->mappings(),
+ 'body' => empty($mappings) ? new stdClass() : $mappings,
])->asArray();
}
@@ -121,7 +140,7 @@ public function putMapping()
*
* @return array
*/
- public function getMapping()
+ public function getMapping(): array
{
return $this->index->getClient()->indices()->getMapping([
'index' => $this->index->name(),
@@ -134,7 +153,7 @@ public function getMapping()
* @param array $settings
* @return array
*/
- public function putSettings(array $settings)
+ public function putSettings(array $settings): array
{
return $this->index->getClient()->indices()->putSettings([
'index' => $this->index->name(),
@@ -147,7 +166,7 @@ public function putSettings(array $settings)
*
* @return array
*/
- public function getSettings()
+ public function getSettings(): array
{
return $this->index->getClient()->indices()->getSettings([
'index' => $this->index->name(),
@@ -159,7 +178,7 @@ public function getSettings()
*
* @return array
*/
- public function refresh()
+ public function refresh(): array
{
return $this->index->getClient()->indices()->refresh([
'index' => $this->index->name(),
@@ -172,7 +191,7 @@ public function refresh()
* @param array $options ES params: max_num_segments, only_expunge_deletes, flush.
* @return array
*/
- public function forceMerge(array $options = [])
+ public function forceMerge(array $options = []): array
{
return $this->index->getClient()->indices()->forcemerge(
array_merge(['index' => $this->index->name()], $options)
@@ -184,7 +203,7 @@ public function forceMerge(array $options = [])
*
* @return array
*/
- public function close()
+ public function close(): array
{
return $this->index->getClient()->indices()->close([
'index' => $this->index->name(),
@@ -196,7 +215,7 @@ public function close()
*
* @return array
*/
- public function open()
+ public function open(): array
{
return $this->index->getClient()->indices()->open([
'index' => $this->index->name(),
@@ -211,7 +230,7 @@ public function open()
* @param array $options additional alias options: routing, filter, is_write_index.
* @return array
*/
- public function addAlias($alias, array $options = [])
+ public function addAlias(string $alias, array $options = []): array
{
$indexName = $this->resolveIndexName();
@@ -234,7 +253,7 @@ public function addAlias($alias, array $options = [])
* @param string $alias
* @return array
*/
- public function removeAlias($alias)
+ public function removeAlias(string $alias): array
{
$indexName = $this->resolveIndexName();
@@ -251,12 +270,12 @@ public function removeAlias($alias)
* @param string $fromIndex
* @return array
*/
- public function swapAlias($alias, $fromIndex)
+ public function swapAlias(string $alias, string $fromIndex): array
{
$indexName = $this->index->name();
$e = new Event('manager.swap_alias.before', $indexName);
- Index::dispatch($e);
+ EventDispatcher::dispatch($e);
$response = $this->index->getClient()->indices()->updateAliases([
'body' => [
@@ -269,7 +288,7 @@ public function swapAlias($alias, $fromIndex)
$e = new Event('manager.swap_alias.after', $indexName);
$e->response = $response;
- Index::dispatch($e);
+ EventDispatcher::dispatch($e);
return $response;
}
@@ -280,7 +299,7 @@ public function swapAlias($alias, $fromIndex)
*
* @return array
*/
- public function getAliases()
+ public function getAliases(): array
{
$indexName = $this->resolveIndexName();
@@ -294,14 +313,14 @@ public function getAliases()
*
* @return string
*/
- private function resolveIndexName()
+ private function resolveIndexName(): string
{
$name = $this->index->name();
$client = $this->index->getClient()->indices();
if ($client->existsAlias(['name' => $name])->asBool()) {
$aliases = $client->getAlias(['name' => $name])->asArray();
- return array_key_first($aliases);
+ return array_key_first($aliases) ?? $name;
}
return $name;
diff --git a/src/Index/Rebuild.php b/src/Index/Rebuild.php
index 534064e..4256b4a 100644
--- a/src/Index/Rebuild.php
+++ b/src/Index/Rebuild.php
@@ -1,7 +1,12 @@
>|null
+ * @var callable|iterable>|null
*/
private $dataSource;
- /**
- * @param Index $index
- */
- public function __construct(Index $index)
- {
- $this->index = $index;
+ public function __construct(
+ private readonly Index $index
+ ) {
}
/**
@@ -52,21 +56,27 @@ public function __construct(Index $index)
* @param int $size
* @return $this
*/
- public function batchSize($size)
+ public function batchSize(int $size): static
{
$this->batchSize = $size;
return $this;
}
/**
- * Skip bulk import errors instead of throwing. Defaults to false.
+ * Set a callback to handle bulk import errors.
*
- * @param bool $skip
+ * Forwards to Bulk::onError(): the callback receives the raw ES response, the
+ * full batch body, and a fresh Bulk bound to the new backing index. Extract
+ * the failures from $body via $response and re-import them on the Bulk, or
+ * return to drop them and continue, or throw to abort (the new index is then
+ * deleted). Without a handler, any import error aborts the rebuild.
+ *
+ * @param callable $handler function (array $response, array $body, Bulk $newbulk): void
* @return $this
*/
- public function skipErrors($skip = true)
+ public function onError(callable $handler): static
{
- $this->skipErrors = $skip;
+ $this->errorHandler = $handler;
return $this;
}
@@ -76,7 +86,7 @@ public function skipErrors($skip = true)
* @param bool $allow
* @return $this
*/
- public function allowEmpty($allow = true)
+ public function allowEmpty(bool $allow = true): static
{
$this->allowEmpty = $allow;
return $this;
@@ -86,10 +96,10 @@ public function allowEmpty($allow = true)
* Set a custom data source. Accepts a callable or iterable.
* Defaults to Index::source() when not set.
*
- * @param callable|\Iterator> $source
+ * @param callable|iterable> $source
* @return $this
*/
- public function source($source)
+ public function source(callable|iterable $source): static
{
$this->dataSource = $source;
return $this;
@@ -103,55 +113,86 @@ public function source($source)
* - $name is a real index: throws RuntimeException (convert to alias mode first)
* - $name does not exist: create alias pointing to new backing index
*
+ * The lock is always released: on failure doRun()'s exception is rethrown
+ * (never masked); on success a release failure surfaces as a RuntimeException
+ * noting the rebuild completed but the lock is stale.
+ *
* @param array $context user-defined context passed to source()
* @return array{newIndex: string, oldIndex: string|null}
*/
- public function run(array $context = [])
+ public function run(array $context = []): array
{
- $name = $this->index->name();
- $client = $this->index->getClient()->indices();
-
- Index::dispatch(new Event('rebuild.run.before', $name));
-
- $newName = $this->createIndex();
+ $this->acquireLock();
try {
- $this->import($newName, $context);
+ $result = $this->doRun($context);
} catch (\Throwable $e) {
- $client->delete(['index' => $newName]);
+ try {
+ $this->releaseLock();
+ } catch (\Throwable) {
+ // Ignore release failure; the original exception propagates below.
+ }
throw $e;
}
- $client->refresh(['index' => $newName]);
-
- $oldIndex = null;
-
- if ($client->existsAlias(['name' => $name])->asBool()) {
- $oldIndices = array_keys($client->getAlias(['name' => $name])->asArray());
- $oldIndex = $oldIndices[0] ?? null;
- $actions = [];
- foreach ($oldIndices as $idx) {
- $actions[] = ['remove' => ['index' => $idx, 'alias' => $name]];
- }
- $actions[] = ['add' => ['index' => $newName, 'alias' => $name]];
- $client->updateAliases(['body' => ['actions' => $actions]]);
- } elseif ($client->exists(['index' => $name])->asBool()) {
- $client->delete(['index' => $newName]);
+ // Success: data is swapped, so a release failure means only the lock is stale.
+ try {
+ $this->releaseLock();
+ } catch (\Throwable $e) {
throw new RuntimeException(
- "Index [{$name}] is a real index, not an alias. "
- . "Rebuild requires an alias to swap atomically. "
- . "Delete the index manually or convert it to alias mode before running rebuild."
+ "Rebuild for [{$this->index->name()}] succeeded but the lock could not be released. "
+ . "Data is already swapped to the new index. Call forceUnlock() to clear the stale lock.",
+ 0,
+ $e
);
- } else {
- $client->putAlias(['index' => $newName, 'name' => $name]);
}
- $e = new Event('rebuild.run.after', $name);
- $e->newIndex = $newName;
- $e->oldIndex = $oldIndex;
- Index::dispatch($e);
+ return $result;
+ }
- return ['newIndex' => $newName, 'oldIndex' => $oldIndex];
+ /**
+ * Forcibly remove a stale lock left by a crashed rebuild.
+ *
+ * Under normal circumstances the lock is released automatically by run().
+ * Call this only when a previous rebuild crashed and the lock persists.
+ *
+ * Idempotent: a 404 (lock index or document missing) is treated as success.
+ */
+ public function forceUnlock(): void
+ {
+ try {
+ $this->index->getClient()->delete([
+ 'index' => self::LOCK_INDEX,
+ 'id' => $this->index->name(),
+ ]);
+ } catch (ClientResponseException $e) {
+ if ($e->getResponse()->getStatusCode() !== 404) {
+ throw $e;
+ }
+ }
+ }
+
+ /**
+ * Check whether a rebuild lock is currently held for this index.
+ *
+ * Returns false only on a real 404; other ES errors propagate so an
+ * unreachable cluster is never mistaken for an unlocked index.
+ *
+ * @return bool
+ */
+ public function isLocked(): bool
+ {
+ try {
+ return $this->index->getClient()->exists([
+ 'index' => self::LOCK_INDEX,
+ 'id' => $this->index->name(),
+ ])->asBool();
+ } catch (ClientResponseException $e) {
+ if ($e->getResponse()->getStatusCode() === 404) {
+ return false;
+ }
+ throw $e;
+ }
}
/**
@@ -187,47 +228,190 @@ public function rollback(string $targetIndex): string
throw new RuntimeException("Target index [{$targetIndex}] does not exist");
}
+ $actions = [];
+ foreach ($currentList as $idx) {
+ $actions[] = ['remove' => ['index' => $idx, 'alias' => $name]];
+ }
+ $actions[] = ['add' => ['index' => $targetIndex, 'alias' => $name]];
+
$client->updateAliases([
'body' => [
- 'actions' => [
- ['remove' => ['index' => $currentList[0], 'alias' => $name]],
- ['add' => ['index' => $targetIndex, 'alias' => $name]],
- ],
+ 'actions' => $actions,
],
]);
return $currentList[0];
}
+ /**
+ * Execute the rebuild logic (called after lock is acquired).
+ *
+ * @param array $context
+ * @return array{newIndex: string, oldIndex: string|null}
+ */
+ private function doRun(array $context): array
+ {
+ $name = $this->index->name();
+ $client = $this->index->getClient()->indices();
+
+ EventDispatcher::dispatch(new Event('rebuild.run.before', $name));
+
+ $newIndex = $this->createIndex();
+
+ try {
+ $this->import($newIndex, $context);
+ } catch (\Throwable $e) {
+ $client->delete(['index' => $newIndex]);
+ throw $e;
+ }
+
+ $oldIndex = null;
+
+ try {
+ $client->refresh(['index' => $newIndex]);
+
+ if ($client->existsAlias(['name' => $name])->asBool()) {
+ $oldIndices = array_keys($client->getAlias(['name' => $name])->asArray());
+ $oldIndex = $oldIndices[0] ?? null;
+ $actions = [];
+ foreach ($oldIndices as $idx) {
+ $actions[] = ['remove' => ['index' => $idx, 'alias' => $name]];
+ }
+ $actions[] = ['add' => ['index' => $newIndex, 'alias' => $name]];
+ $client->updateAliases(['body' => ['actions' => $actions]]);
+ } elseif ($client->exists(['index' => $name])->asBool()) {
+ throw new RuntimeException(
+ "Index [{$name}] is a real index, not an alias. "
+ . "Rebuild requires an alias to swap atomically. "
+ . "Delete the index manually or convert it to alias mode before running rebuild."
+ );
+ } else {
+ $client->putAlias(['index' => $newIndex, 'name' => $name]);
+ }
+ } catch (\Throwable $e) {
+ // Swap failed (or precondition unmet) — remove the orphaned new index.
+ $client->delete(['index' => $newIndex]);
+ throw $e;
+ }
+
+ $e = new Event('rebuild.run.after', $name);
+ $e->newIndex = $newIndex;
+ $e->oldIndex = $oldIndex;
+ EventDispatcher::dispatch($e);
+
+ return ['newIndex' => $newIndex, 'oldIndex' => $oldIndex];
+ }
+
+ /**
+ * Acquire the distributed lock using ES document create (op_type=create).
+ *
+ * @throws RuntimeException if the lock is already held
+ */
+ private function acquireLock(): void
+ {
+ $this->ensureLockIndex();
+
+ $name = $this->index->name();
+
+ try {
+ $this->index->getClient()->index([
+ 'index' => self::LOCK_INDEX,
+ 'id' => $name,
+ 'body' => ['locked_at' => date(\DateTimeInterface::ATOM)],
+ 'op_type' => 'create',
+ ]);
+ } catch (ClientResponseException $e) {
+ if ($e->getResponse()->getStatusCode() === 409) {
+ throw new RuntimeException(
+ "Rebuild for [{$name}] is already running. "
+ . "Call forceUnlock() if the previous rebuild crashed."
+ );
+ }
+ throw $e;
+ }
+ }
+
+ /**
+ * Release the lock. Idempotent: only 404 is swallowed; other failures
+ * (transport, 5xx) propagate so a stale lock stays detectable.
+ *
+ * @throws ClientResponseException for any non-404 failure
+ */
+ private function releaseLock(): void
+ {
+ try {
+ $this->index->getClient()->delete([
+ 'index' => self::LOCK_INDEX,
+ 'id' => $this->index->name(),
+ ]);
+ } catch (ClientResponseException $e) {
+ if ($e->getResponse()->getStatusCode() === 404) {
+ return; // Lock already gone — idempotent
+ }
+ throw $e;
+ }
+ }
+
+ /**
+ * Ensure the lock index exists. Handles concurrent creation races.
+ */
+ private function ensureLockIndex(): void
+ {
+ $indices = $this->index->getClient()->indices();
+
+ if ($indices->exists(['index' => self::LOCK_INDEX])->asBool()) {
+ return;
+ }
+
+ try {
+ $indices->create([
+ 'index' => self::LOCK_INDEX,
+ 'body' => [
+ 'settings' => [
+ 'number_of_shards' => 1,
+ 'number_of_replicas' => 0,
+ 'index.hidden' => true,
+ ],
+ ],
+ ]);
+ } catch (ClientResponseException $e) {
+ // Race: another process may have created it concurrently
+ if ($indices->exists(['index' => self::LOCK_INDEX])->asBool()) {
+ return;
+ }
+ throw $e;
+ }
+ }
+
/**
* Create a new backing index using mappings/settings from the Index subclass.
*
* @return string the new backing index name
*/
- protected function createIndex()
+ protected function createIndex(): string
{
- $newName = $this->index->rebuildName();
+ $newIndex = $this->index->rebuildName();
$mappings = $this->index->mappings();
$settings = $this->index->settings();
$this->index->getClient()->indices()->create([
- 'index' => $newName,
+ 'index' => $newIndex,
'body' => [
'mappings' => empty($mappings) ? new stdClass() : $mappings,
'settings' => empty($settings) ? new stdClass() : $settings,
],
]);
- return $newName;
+ return $newIndex;
}
/**
* Import data into the target index.
*
- * @param string $newName
+ * @param string $newIndex
* @param array $context
*/
- protected function import($newName, array $context): void
+ protected function import(string $newIndex, array $context): void
{
if ($this->dataSource !== null) {
$items = is_callable($this->dataSource) ? ($this->dataSource)($context) : $this->dataSource;
@@ -235,7 +419,11 @@ protected function import($newName, array $context): void
$items = $this->index->source($context);
}
- $bulk = (new Bulk($this->index))->target($newName)->batchSize($this->batchSize);
+ $bulk = (new Bulk($this->index))->target($newIndex)->batchSize($this->batchSize);
+ if ($this->errorHandler) {
+ $bulk->onError($this->errorHandler);
+ }
+
$count = 0;
foreach ($items as $id => $doc) {
if (!is_array($doc)) {
@@ -250,25 +438,11 @@ protected function import($newName, array $context): void
if ($count === 0 && !$this->allowEmpty) {
throw new RuntimeException(
- "Rebuild imported 0 documents for index [{$newName}]. "
+ "Rebuild imported 0 documents for index [{$newIndex}]. "
. "Call allowEmpty() if this is intentional."
);
}
- $response = $bulk->execute();
-
- if (!empty($response['errors'])) {
- $e = new Event('rebuild.import.failed', $this->index->name());
- $e->response = $response;
- Index::dispatch($e);
-
- if (!$this->skipErrors) {
- $json = json_encode($response, JSON_UNESCAPED_UNICODE);
- if (strlen($json) > 4096) {
- $json = substr($json, 0, 4096) . '... [truncated]';
- }
- throw new RuntimeException("Bulk import failed for index [{$newName}]: {$json}");
- }
- }
+ $bulk->flush();
}
}
diff --git a/src/Index/Results.php b/src/Index/Results.php
index 878389f..b17ab3a 100644
--- a/src/Index/Results.php
+++ b/src/Index/Results.php
@@ -1,8 +1,12 @@
*/
- protected $response;
+ protected array $response;
/**
* @var int
*/
- protected $page = 1;
+ protected int $page = 1;
/**
* @var int
*/
- protected $perPage = 15;
+ protected int $perPage = 15;
/**
* @var bool
*/
- protected $paginated = false;
+ protected bool $paginated = false;
/**
* @param array $response
@@ -44,7 +48,7 @@ public function __construct(array $response)
* @param int $perPage
* @return $this
*/
- public function paginate($page, $perPage)
+ public function paginate(int $page, int $perPage): static
{
$this->page = $page;
$this->perPage = $perPage;
@@ -53,13 +57,15 @@ public function paginate($page, $perPage)
}
/**
- * Return the total number of matching documents.
+ * Return the total number of matching documents, or null when unavailable.
*
- * @return int
+ * Null when track_total_hits is false (Elasticsearch omits hits.total).
+ *
+ * @return int|null
*/
- public function total()
+ public function total(): ?int
{
- return $this->response['hits']['total']['value'] ?? 0;
+ return $this->response['hits']['total']['value'] ?? null;
}
/**
@@ -67,7 +73,7 @@ public function total()
*
* @return array>
*/
- public function hits()
+ public function hits(): array
{
return $this->response['hits']['hits'] ?? [];
}
@@ -77,7 +83,7 @@ public function hits()
*
* @return array|null>
*/
- public function docs()
+ public function docs(): array
{
return array_column($this->hits(), '_source');
}
@@ -87,7 +93,7 @@ public function docs()
*
* @return array
*/
- public function ids()
+ public function ids(): array
{
return array_column($this->hits(), '_id');
}
@@ -97,20 +103,20 @@ public function ids()
*
* @return array|null
*/
- public function first()
+ public function first(): ?array
{
$docs = $this->docs();
return $docs[0] ?? null;
}
/**
- * Return the aggregations from the response.
+ * Return the aggregations from the response, or null if none were requested.
*
- * @return array
+ * @return array|null
*/
- public function aggregations()
+ public function aggregations(): ?array
{
- return $this->response['aggregations'] ?? [];
+ return $this->response['aggregations'] ?? null;
}
/**
@@ -118,19 +124,34 @@ public function aggregations()
*
* @return string|null
*/
- public function scrollId()
+ public function scrollId(): ?string
{
return $this->response['_scroll_id'] ?? null;
}
/**
- * Return whether there are more hits available.
+ * Return the hits.total.relation value from the Elasticsearch response.
+ *
+ * "eq" = total is exact, "gte" = total is a lower bound.
+ *
+ * @return string|null "eq" or "gte"
+ */
+ public function totalRelation(): ?string
+ {
+ return $this->response['hits']['total']['relation'] ?? null;
+ }
+
+ /**
+ * Whether the current result set has no hits.
+ *
+ * For scroll loops: `while (! $results->isEmpty())`. For pagination
+ * "has a next page", use hasMorePages() (works with or without a total).
*
* @return bool
*/
- public function hasMore()
+ public function isEmpty(): bool
{
- return !empty($this->response['hits']['hits']);
+ return empty($this->response['hits']['hits']);
}
/**
@@ -138,7 +159,7 @@ public function hasMore()
*
* @return int
*/
- public function took()
+ public function took(): int
{
return $this->response['took'] ?? 0;
}
@@ -148,7 +169,7 @@ public function took()
*
* @return bool
*/
- public function timedOut()
+ public function timedOut(): bool
{
return $this->response['timed_out'] ?? false;
}
@@ -158,7 +179,7 @@ public function timedOut()
*
* @return array
*/
- public function raw()
+ public function raw(): array
{
return $this->response;
}
@@ -168,7 +189,7 @@ public function raw()
*
* @return int
*/
- public function page()
+ public function page(): int
{
return $this->page;
}
@@ -178,39 +199,56 @@ public function page()
*
* @return int
*/
- public function perPage()
+ public function perPage(): int
{
return $this->perPage;
}
/**
- * Return the last page number.
+ * Return the last page number, or null when the total is unavailable.
*
- * @return int
+ * @return int|null
*/
- public function lastPage()
+ public function lastPage(): ?int
{
+ if ($this->total() === null) {
+ return null;
+ }
+
+ if ($this->perPage < 1) {
+ return 1;
+ }
+
return (int) ceil($this->total() / $this->perPage) ?: 1;
}
/**
- * Alias for docs(), aligned with paginator semantics.
+ * Whether there is a page after the current one.
*
- * @return array|null>
+ * With a known total: page() < lastPage(). Without one (track_total_hits
+ * is false): a full page implies more, a partial page is the last.
+ *
+ * @return bool
*/
- public function items()
+ public function hasMorePages(): bool
{
- return $this->docs();
+ $lastPage = $this->lastPage();
+
+ if ($lastPage !== null) {
+ return $this->page < $lastPage;
+ }
+
+ return $this->perPage > 0 && count($this->hits()) === $this->perPage;
}
/**
- * Return whether the result set is empty.
+ * Alias for docs(), aligned with paginator semantics.
*
- * @return bool
+ * @return array|null>
*/
- public function isEmpty()
+ public function items(): array
{
- return empty($this->response['hits']['hits']);
+ return $this->docs();
}
/**
@@ -227,10 +265,17 @@ public function toPaginator()
);
}
- $resolver = Index::getPaginatorResolver();
+ if ($this->total() === null) {
+ throw new PaginationTotalUnavailableException(
+ 'Cannot build a length-aware paginator: total is unavailable (track_total_hits is false). '
+ . 'Enable track_total_hits on the index, or use hasMorePages()/chunk() for total-less pagination.'
+ );
+ }
+
+ $resolver = Pagination::getPaginatorResolver();
if ($resolver === null) {
throw new RuntimeException(
- 'Paginator resolver not registered. Call Index::setPaginatorResolver() first.'
+ 'Paginator resolver not registered. Call Pagination::setPaginatorResolver() first.'
);
}
diff --git a/src/Index/Search.php b/src/Index/Search.php
index ea2335a..6cfe677 100644
--- a/src/Index/Search.php
+++ b/src/Index/Search.php
@@ -1,9 +1,16 @@
*/
- private $urlParams = [];
+ private array $urlParams = [];
- /**
- * @param Index $index
- */
- public function __construct(Index $index, Query $query = null)
- {
+ public function __construct(
+ private readonly Index $index,
+ ?Query $query = null
+ ) {
$this->query = $query ?? new Query();
- $this->index = $index;
}
/**
@@ -45,7 +45,7 @@ public function __construct(Index $index, Query $query = null)
* @param string $routing
* @return $this
*/
- public function routing($routing)
+ public function routing(string $routing): static
{
$this->urlParams['routing'] = $routing;
return $this;
@@ -56,14 +56,14 @@ public function routing($routing)
*
* @param string $method
* @param array $args
- * @return $this|mixed
+ * @return mixed
* @throws BadMethodCallException
*/
- public function __call($method, $args)
+ public function __call(string $method, array $args): mixed
{
if (!method_exists($this->query, $method)) {
throw new BadMethodCallException(
- sprintf('Method %s does not exist on %s', $method, get_class($this->query))
+ sprintf('Method %s does not exist on %s (index: %s)', $method, get_class($this->query), $this->index->name())
);
}
@@ -81,7 +81,7 @@ public function __call($method, $args)
*
* @return Results
*/
- public function get()
+ public function get(): Results
{
return new Results($this->doSearch('get'));
}
@@ -91,15 +91,9 @@ public function get()
*
* @return array|null
*/
- public function first()
+ public function first(): ?array
{
- $saved = $this->query;
- $this->query = clone $this->query;
- $this->query->size(1);
-
- $response = $this->doSearch('first');
-
- $this->query = $saved;
+ $response = $this->doSearch('first', ['body' => ['size' => 1, 'from' => 0]]);
$docs = (new Results($response))->docs();
return $docs[0] ?? null;
@@ -110,9 +104,17 @@ public function first()
*
* @return int
*/
- public function count()
+ public function count(): int
{
- return $this->doCount()['count'];
+ $response = $this->doCount();
+
+ if (!isset($response['count'])) {
+ throw new RuntimeException(
+ sprintf('Missing "count" in Elasticsearch response for index [%s].', $this->index->name())
+ );
+ }
+
+ return $response['count'];
}
/**
@@ -122,22 +124,18 @@ public function count()
* @param string $duration
* @return Results
*/
- public function scroll($scrollId = null, $duration = '5m')
+ public function scroll(?string $scrollId = null, string $duration = '5m'): Results
{
if ($scrollId !== null) {
return $this->doScroll($scrollId, $duration);
}
- $saved = $this->query;
- $this->query = clone $this->query;
-
+ $extra = ['scroll' => $duration];
if (!$this->query->hasParam('size')) {
- $this->query->size(1000);
+ $extra['body'] = ['size' => 1000];
}
- $response = $this->doSearch('scroll', ['scroll' => $duration]);
-
- $this->query = $saved;
+ $response = $this->doSearch('scroll', $extra);
return new Results($response);
}
@@ -149,7 +147,7 @@ public function scroll($scrollId = null, $duration = '5m')
* @param string $duration
* @return Results
*/
- public function next(Results $results, $duration = '5m')
+ public function next(Results $results, string $duration = '5m'): Results
{
return $this->doScroll($results->scrollId(), $duration);
}
@@ -160,7 +158,7 @@ public function next(Results $results, $duration = '5m')
* @param Results $results
* @return void
*/
- public function clear(Results $results)
+ public function clear(Results $results): void
{
$scrollId = $results->scrollId();
if ($scrollId !== null) {
@@ -177,14 +175,14 @@ public function clear(Results $results)
* @param string $duration
* @return Results
*/
- protected function doScroll($scrollId, $duration)
+ protected function doScroll(string $scrollId, string $duration): Results
{
$indexName = $this->index->name();
$e = new Event('search.scroll.before', $indexName);
$e->action = 'scroll';
$e->scrollId = $scrollId;
- Index::dispatch($e);
+ EventDispatcher::dispatch($e);
$start = microtime(true);
$response = $this->index->getClient()->scroll([
@@ -199,23 +197,24 @@ protected function doScroll($scrollId, $duration)
$e->scrollId = $scrollId;
$e->response = $response;
$e->duration = $durationTime;
- Index::dispatch($e);
+ EventDispatcher::dispatch($e);
return new Results($response);
}
/**
- * Return a generator that yields Results batches via scroll.
+ * Lazily yield Results batches via scroll. Each yielded Results is one
+ * scroll batch; the scroll context is cleared when iteration ends.
*
- * @param string $duration
- * @return \Generator
+ * @param string $duration scroll keep-alive
+ * @return \Generator
*/
- public function cursor($duration = '5m')
+ public function chunk(string $duration = '5m'): \Generator
{
$results = $this->scroll(null, $duration);
try {
- while ($results->hasMore()) {
+ while (! $results->isEmpty()) {
yield $results;
$results = $this->next($results, $duration);
}
@@ -224,6 +223,22 @@ public function cursor($duration = '5m')
}
}
+ /**
+ * Lazily yield individual search hits, flattened across scroll batches.
+ *
+ * Each value is a raw ES hit (_id, _score, _source, ...) — the same shape
+ * as Results::hits() entries. Reuses chunk()'s scroll cleanup.
+ *
+ * @param string $duration scroll keep-alive
+ * @return \Generator, mixed, void>
+ */
+ public function cursor(string $duration = '5m'): \Generator
+ {
+ foreach ($this->chunk($duration) as $results) {
+ yield from $results->hits();
+ }
+ }
+
/**
* Execute a paginated search. Uses pageResolver if no arguments are given.
*
@@ -231,10 +246,10 @@ public function cursor($duration = '5m')
* @param int|null $perPage
* @return Results
*/
- public function paginate($page = null, $perPage = null)
+ public function paginate(?int $page = null, ?int $perPage = null): Results
{
if ($page === null && $perPage === null) {
- $resolver = Index::getPageResolver();
+ $resolver = Pagination::getPageResolver();
if ($resolver !== null) {
[$page, $perPage] = $resolver();
}
@@ -248,14 +263,10 @@ public function paginate($page = null, $perPage = null)
$perPage = $maxPerPage;
}
- $saved = $this->query;
- $this->query = clone $this->query;
- $this->query->from(($page - 1) * $perPage);
- $this->query->size($perPage);
-
- $response = $this->doSearch('paginate');
-
- $this->query = $saved;
+ $response = $this->doSearch('paginate', ['body' => [
+ 'from' => ($page - 1) * $perPage,
+ 'size' => $perPage,
+ ]]);
return (new Results($response))->paginate($page, $perPage);
}
@@ -265,7 +276,7 @@ public function paginate($page = null, $perPage = null)
*
* @return array
*/
- protected function doCount()
+ protected function doCount(): array
{
$indexName = $this->index->name();
$body = $this->query->toArray() ?: new stdClass();
@@ -273,7 +284,7 @@ protected function doCount()
$e = new Event('search.query.before', $indexName);
$e->dsl = $body;
$e->action = 'count';
- Index::dispatch($e);
+ EventDispatcher::dispatch($e);
$start = microtime(true);
$response = $this->index->getClient()->count([
@@ -288,7 +299,7 @@ protected function doCount()
$e->response = $response;
$e->duration = $duration;
$e->action = 'count';
- Index::dispatch($e);
+ EventDispatcher::dispatch($e);
return $response;
}
@@ -297,18 +308,30 @@ protected function doCount()
* Execute an ES search call with before/after events.
*
* @param string $action calling method name (get, first, scroll, paginate)
- * @param array $extra extra request params (e.g. scroll)
+ * @param array $extra extra request params (e.g. scroll); a 'body' key shallow-merges top-level scalar overrides (size, from) into the query body — nested keys (aggs, query) would replace, not merge
* @return array
*/
- protected function doSearch($action, array $extra = [])
+ protected function doSearch(string $action, array $extra = []): array
{
$indexName = $this->index->name();
- $body = $this->query->toArray() ?: new stdClass();
+
+ // Apply the index's track_total_hits default unless explicitly set.
+ // Skipped for scroll: ES forbids disabling track_total_hits in a scroll context.
+ if ($action !== 'scroll' && !$this->query->hasParam('track_total_hits')) {
+ $this->query->trackTotalHits($this->index->trackTotalHits());
+ }
+
+ $body = $this->query->toArray();
+ if (isset($extra['body'])) {
+ $body = array_merge($body, $extra['body']);
+ unset($extra['body']);
+ }
+ $body = $body ?: new stdClass();
$e = new Event('search.query.before', $indexName);
$e->dsl = $body;
$e->action = $action;
- Index::dispatch($e);
+ EventDispatcher::dispatch($e);
$params = array_merge(['index' => $indexName, 'body' => $body], $this->urlParams, $extra);
@@ -322,7 +345,7 @@ protected function doSearch($action, array $extra = [])
$e->response = $response;
$e->duration = $duration;
$e->action = $action;
- Index::dispatch($e);
+ EventDispatcher::dispatch($e);
return $response;
}
diff --git a/src/Index/Support/ClientManager.php b/src/Index/Support/ClientManager.php
new file mode 100644
index 0000000..5893af8
--- /dev/null
+++ b/src/Index/Support/ClientManager.php
@@ -0,0 +1,59 @@
+
+ */
+ private static array $clients = [];
+
+ /**
+ * Register an Elasticsearch client. Optionally name the connection.
+ *
+ * @param ClientInterface $client
+ * @param string $connection connection name, defaults to 'default'
+ * @return void
+ */
+ public static function set(ClientInterface $client, string $connection = 'default'): void
+ {
+ self::$clients[$connection] = $client;
+ }
+
+ /**
+ * Return the Elasticsearch client for the given connection name.
+ *
+ * @param string $connection
+ * @return ClientInterface
+ * @throws RuntimeException if the connection is not registered
+ */
+ public static function get(string $connection = 'default'): ClientInterface
+ {
+ if (isset(self::$clients[$connection])) {
+ return self::$clients[$connection];
+ }
+ throw new RuntimeException(
+ "Elasticsearch client not registered for connection '{$connection}'. "
+ . 'Call ClientManager::set($client) first.'
+ );
+ }
+
+ /**
+ * Reset all registered clients. Mainly for testing.
+ *
+ * @return void
+ */
+ public static function reset(): void
+ {
+ self::$clients = [];
+ }
+}
diff --git a/src/Index/Support/Event.php b/src/Index/Support/Event.php
new file mode 100644
index 0000000..8fe28c4
--- /dev/null
+++ b/src/Index/Support/Event.php
@@ -0,0 +1,48 @@
+|\stdClass|null Request body (search.query.before/after) */
+ public array|\stdClass|null $dsl = null;
+
+ /** @var string|null Calling method: get/first/count/scroll/paginate (search events) */
+ public ?string $action = null;
+
+ /** @var array|null ES API response (all after events) */
+ public ?array $response = null;
+
+ /** @var float|null Execution time in seconds (all after events) */
+ public ?float $duration = null;
+
+ /** @var string|null Scroll context ID (search.scroll events) */
+ public ?string $scrollId = null;
+
+ /** @var array|null Bulk action lines (bulk.flush events) */
+ public ?array $actions = null;
+
+ /** @var string|null New backing index name (rebuild.run.after) */
+ public ?string $newIndex = null;
+
+ /** @var string|null Previous backing index name (rebuild.run.after) */
+ public ?string $oldIndex = null;
+
+ public function __construct(string $name, string $index)
+ {
+ $this->name = $name;
+ $this->index = $index;
+ }
+}
diff --git a/src/Index/Support/EventDispatcher.php b/src/Index/Support/EventDispatcher.php
new file mode 100644
index 0000000..d1ea382
--- /dev/null
+++ b/src/Index/Support/EventDispatcher.php
@@ -0,0 +1,74 @@
+>
+ */
+ private static array $listeners = [];
+
+ /**
+ * Register an event listener.
+ *
+ * Supports exact event name, category wildcard (e.g. 'search.*'), or global '*'.
+ *
+ * @param string $event
+ * @param callable $listener receives (Event $event)
+ * @return void
+ */
+ public static function listen(string $event, callable $listener): void
+ {
+ self::$listeners[$event][] = $listener;
+ }
+
+ /**
+ * Dispatch an event to all matching listeners.
+ *
+ * @param Event $event
+ * @return void
+ */
+ public static function dispatch(Event $event): void
+ {
+ foreach (self::$listeners as $pattern => $listeners) {
+ if ($pattern === $event->name || $pattern === '*' || self::matchesCategory($pattern, $event->name)) {
+ foreach ($listeners as $listener) {
+ $listener($event);
+ }
+ }
+ }
+ }
+
+ /**
+ * Reset all registered listeners. Mainly for testing.
+ *
+ * @return void
+ */
+ public static function reset(): void
+ {
+ self::$listeners = [];
+ }
+
+ /**
+ * Check if a wildcard pattern matches an event by category.
+ *
+ * @param string $pattern
+ * @param string $event
+ * @return bool
+ */
+ private static function matchesCategory(string $pattern, string $event): bool
+ {
+ if (substr($pattern, -2) !== '.*') {
+ return false;
+ }
+
+ $prefix = substr($pattern, 0, -1);
+ return strpos($event, $prefix) === 0;
+ }
+}
diff --git a/src/Index/Support/Pagination.php b/src/Index/Support/Pagination.php
new file mode 100644
index 0000000..01d470b
--- /dev/null
+++ b/src/Index/Support/Pagination.php
@@ -0,0 +1,74 @@
+aggregateScalar('max', $field);
+ }
+
+ /**
+ * Return the minimum value of a field.
+ *
+ * @param string $field
+ * @return float|null
+ */
+ public function min(string $field): ?float
+ {
+ return $this->aggregateScalar('min', $field);
+ }
+
+ /**
+ * Return the average value of a field.
+ *
+ * @param string $field
+ * @return float|null
+ */
+ public function avg(string $field): ?float
+ {
+ return $this->aggregateScalar('avg', $field);
+ }
+
+ /**
+ * Return the sum of a field.
+ *
+ * @param string $field
+ * @return float|null
+ */
+ public function sum(string $field): ?float
+ {
+ return $this->aggregateScalar('sum', $field);
+ }
+
+ /**
+ * Return stats (count, min, max, avg, sum) for a field in a single request.
+ *
+ * @param string $field
+ * @return array{count: int, min: float|null, max: float|null, avg: float|null, sum: float|null}|null
+ */
+ public function stats(string $field): ?array
+ {
+ $saved = $this->query;
+ $this->query = clone $this->query;
+ $this->query->size(0);
+ $this->query->aggs('__stats', ['stats' => ['field' => $field]]);
+ try {
+ $response = $this->doSearch('stats');
+ } finally {
+ $this->query = $saved;
+ }
+
+ $raw = $response['aggregations']['__stats'] ?? null;
+ if ($raw === null) {
+ return null;
+ }
+
+ return [
+ 'count' => $raw['count'] ?? 0,
+ 'min' => $raw['min'] ?? null,
+ 'max' => $raw['max'] ?? null,
+ 'avg' => $raw['avg'] ?? null,
+ 'sum' => $raw['sum'] ?? null,
+ ];
+ }
+
+ /**
+ * Execute a metric aggregation and return the scalar value.
+ *
+ * @param string $type
+ * @param string $field
+ * @return float|null
+ */
+ private function aggregateScalar(string $type, string $field): ?float
+ {
+ $saved = $this->query;
+ $this->query = clone $this->query;
+ $this->query->size(0);
+ $this->query->aggs('__scalar', [$type => ['field' => $field]]);
+ try {
+ $response = $this->doSearch($type);
+ } finally {
+ $this->query = $saved;
+ }
+
+ return $response['aggregations']['__scalar']['value'] ?? null;
+ }
+}
diff --git a/stubs/ClientInterface.stub b/stubs/ClientInterface.stub
index 61229db..71c12ac 100644
--- a/stubs/ClientInterface.stub
+++ b/stubs/ClientInterface.stub
@@ -3,6 +3,12 @@
namespace Elastic\Elasticsearch;
/**
+ * NOTE: phpstan does not merge interface-stub methods over the vendored
+ * ClientInterface, so calls like $client->search() are still reported as
+ * undefined and suppressed via phpstan-baseline.neon (the Indices class stub
+ * works; this one does not). Adding a new endpoint here requires a matching
+ * baseline entry. Kept for intent and the ESResponse return type.
+ *
* @phpstan-type ESResponse array|object
*/
interface ClientInterface
diff --git a/tests/AggsTest.php b/tests/AggsTest.php
index 2b39e8d..3afc5b8 100644
--- a/tests/AggsTest.php
+++ b/tests/AggsTest.php
@@ -2,15 +2,27 @@
use Tests\DslTestCase;
use ElasticKit\DSL\Query;
+use ElasticKit\DSL\Agg;
use ElasticKit\DSL\Aggs\Bucket\Terms;
use ElasticKit\DSL\Aggs\Bucket\Range;
use ElasticKit\DSL\Aggs\Bucket\GeoDistance;
class AggsTest extends DslTestCase
{
- public function testTermsregation()
+ public function testAggsAcceptsAggInstanceAsOnlyArgument()
{
-$expectedJson = <<terms(['field' => 'status']);
+ $agg->alias('by_status');
+
+ $query = new Query();
+ $query->aggs($agg);
+
+ $this->assertQuery('{"aggs":{"by_status":{"terms":{"field":"status"}}}}', $query);
+ }
+
+ public function testTermsAggregation()
+ {
+ $expectedJson = <<matchAll();
$query->aggs('all', function ($a) {
- $a->globalAggregation();
+ $a->global();
});
$this->assertQuery($expectedJson, $query);
}
public function testIpPrefixAggregation()
{
-$expectedJson = <<assertQuery('{"query":{"match_all":{}},"aggs":{"by_status":{"terms":{"field":"status"}}}}', $query);
}
+ public function testEmptyArrayAggSerializesToObject()
+ {
+ $query = new Query();
+ $query->matchAll();
+ $query->aggs('empty', []);
+ $this->assertQuery('{"query":{"match_all":{}},"aggs":{"empty":{}}}', $query);
+ }
+
public function testHistogramStringShorthand()
{
$query = new Query();
@@ -1291,4 +1311,15 @@ public function testSumBucketStringShorthand()
$query->aggs('total', ['sum_bucket' => ['buckets_path' => 'monthly>sales']]);
$this->assertQuery('{"query":{"match_all":{}},"aggs":{"total":{"sum_bucket":{"buckets_path":"monthly>sales"}}}}', $query);
}
+
+ public function testBucketScriptRejectsStringShorthand()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('bucket_script.buckets_path must be a map');
+
+ $query = new Query();
+ $query->aggs('script', function ($a) {
+ $a->bucketScript('bare_string');
+ });
+ }
}
diff --git a/tests/CloneTest.php b/tests/CloneTest.php
new file mode 100644
index 0000000..69ba3e2
--- /dev/null
+++ b/tests/CloneTest.php
@@ -0,0 +1,70 @@
+match('title', 'foo');
+ $clone = clone $original;
+
+ $this->assertNotSame($original->getQueries()[0], $clone->getQueries()[0]);
+
+ // mutating the clone's clause must not leak into the original
+ $before = $original->toArray();
+ $clone->getQueries()[0]->boost(2.0);
+ $this->assertSame($before, $original->toArray());
+ }
+
+ public function testQueryDeepClonesAggregations()
+ {
+ $original = (new Query())->aggs('by_status', Agg::create()->terms('status'));
+ $clone = clone $original;
+
+ $this->assertNotSame(
+ $this->prop($original, '_aggregations')['by_status'],
+ $this->prop($clone, '_aggregations')['by_status']
+ );
+ }
+
+ public function testAggDeepClonesNodeAndSubAggs()
+ {
+ $original = (new Agg())->terms('status');
+ $original->aggs('avg_price', Agg::create()->avg('price'));
+ $clone = clone $original;
+
+ $this->assertNotSame($this->prop($original, '_node'), $this->prop($clone, '_node'));
+ $this->assertNotSame(
+ $this->prop($original, '_subAggs')['avg_price'],
+ $this->prop($clone, '_subAggs')['avg_price']
+ );
+ }
+
+ public function testQueryMutationOnCloneDoesNotLeakToOriginal()
+ {
+ $original = (new Query())->match('title', 'foo');
+ $before = $original->toArray();
+
+ $clone = clone $original;
+ $clone->match('content', 'bar'); // add to clone only
+ $clone->getQueries()[0]->boost(3.0); // mutate clone's first clause
+
+ $this->assertSame($before, $original->toArray());
+ }
+
+ /**
+ * @return mixed
+ */
+ private function prop(object $object, string $name)
+ {
+ return (new \ReflectionProperty($object, $name))->getValue($object);
+ }
+}
diff --git a/tests/ClosureReturnTest.php b/tests/ClosureReturnTest.php
index f6b9a0d..cd1e8f8 100644
--- a/tests/ClosureReturnTest.php
+++ b/tests/ClosureReturnTest.php
@@ -2,7 +2,6 @@
use Tests\DslTestCase;
use ElasticKit\DSL\Query;
-use ElasticKit\DSL\Queries\FullText\Match_;
class ClosureReturnTest extends DslTestCase
{
@@ -58,13 +57,13 @@ public function testTypeEmptyClosureProducesEmptyField()
$this->assertQuery('{"query":{"match":{"title":null}}}', $query);
}
- public function testQueryEmptyClosureProducesEmptyQuery()
+ public function testEmptyMustClosureKeepsEmptyArray()
{
$query = new Query();
$query->bool(['must' => function () {
}]);
- $this->assertQuery('{"query":{"bool":{"must":{}}}}', $query);
+ $this->assertQuery('{"query":{"bool":{"must":[]}}}', $query);
}
public function testBoolClosureReturnsNewQuery()
diff --git a/tests/CompoundQueriesTest.php b/tests/CompoundQueriesTest.php
index d65a4ec..48323cf 100644
--- a/tests/CompoundQueriesTest.php
+++ b/tests/CompoundQueriesTest.php
@@ -141,6 +141,113 @@ public function testBoolArrayWithQueryObject()
$this->assertQuery($expectedJson, $query);
}
+ public function testBoolArrayWithListAppendsMultipleClauses()
+ {
+ $expectedJson = <<bool(['must' => [
+ ['match' => ['title' => 'A']],
+ ['term' => ['status' => 'published']],
+ ]]);
+ $this->assertQuery($expectedJson, $query);
+ }
+
+ public function testBoolDynamicBuildWithNoMatchingConditionsKeepsEmptyBool()
+ {
+ $conditions = [
+ ['active' => false, 'q' => ['match' => ['title' => 'A']]],
+ ];
+ $query = new Query();
+ $query->bool(function ($b) use ($conditions) {
+ foreach ($conditions as $c) {
+ if ($c['active']) {
+ $b->must($c['q']);
+ }
+ }
+ });
+ $this->assertQuery('{"query":{"bool":{}}}', $query);
+ }
+
+ public function testBoolTwoArgWithClosure()
+ {
+ $expectedJson = <<bool(new Boolean('must', function (Query $q) {
+ $q->match('title', 'test');
+ }));
+ $this->assertQuery($expectedJson, $query);
+ }
+
+ public function testBoolTwoArgWithQueryObject()
+ {
+ $expectedJson = <<term('status', 'published');
+ $query = new Query();
+ $query->bool(new Boolean('must', $inner));
+ $this->assertQuery($expectedJson, $query);
+ }
+
+ public function testBoolTraitTwoArgWithClosure()
+ {
+ $expectedJson = <<bool('must', function (Query $q) {
+ $q->match('title', 'test');
+ });
+ $this->assertQuery($expectedJson, $query);
+ }
+
+ public function testBoolTwoArgRejectsUnknownClauseKey()
+ {
+ $inner = new Query();
+ $inner->term('status', 'published');
+
+ $this->expectException(\InvalidArgumentException::class);
+ new Boolean('unknown', $inner);
+ }
+
public function testBoosting()
{
$expectedJson = <<when(true, function (Query $q) {
$q->term('status', 'published');
})
- ->match('content', 'guide');
- $this->assertQuery('{"query":{"match":{"title":"elasticsearch"},"term":{"status":"published"},"match":{"content":"guide"}}}', $query);
+ ->exists('tags');
+ $this->assertQuery('{"query":{"match":{"title":"elasticsearch"},"term":{"status":"published"},"exists":{"field":"tags"}}}', $query);
}
public function testWhenWithArrayQuery()
@@ -585,8 +692,8 @@ public function testBoolAddMustWithNode()
{
$query = new Query();
$query->bool(function (Boolean $b) {
- $b->addMust(new \ElasticKit\DSL\Queries\FullText\Match_('title', 'test'));
- $b->addFilter(new \ElasticKit\DSL\Queries\TermLevel\Term('status', 'published'));
+ $b->must(new \ElasticKit\DSL\Queries\FullText\Match_('title', 'test'));
+ $b->filter(new \ElasticKit\DSL\Queries\TermLevel\Term('status', 'published'));
});
$expectedJson = << 'nike', 'color' => 'red'];
$query = new Query();
$query->bool(function (Boolean $b) use ($filters) {
- $b->addMust(function (Query $q) {
+ $b->must(function (Query $q) {
$q->match('title', 'shoes');
});
foreach ($filters as $field => $value) {
- $b->addFilter(new \ElasticKit\DSL\Queries\TermLevel\Term($field, $value));
+ $b->filter(new \ElasticKit\DSL\Queries\TermLevel\Term($field, $value));
}
});
$expectedJson = <<disMax(function (DisjunctionMax $dm) {
- $dm->addQuery(function (Query $q) {
+ $dm->queries(function (Query $q) {
$q->term('title', 'Quick pets');
});
- $dm->addQuery(function (Query $q) {
+ $dm->queries(function (Query $q) {
$q->term('body', 'Quick pets');
});
$dm->tieBreaker(0.7);
diff --git a/tests/DslTestCase.php b/tests/DslTestCase.php
index 7e01d1b..a7757d1 100644
--- a/tests/DslTestCase.php
+++ b/tests/DslTestCase.php
@@ -6,10 +6,10 @@
use ElasticKit\DSL\Query;
/**
- * Base test case for DSL tests with optional ES validation.
+ * Base test case for DSL tests: asserts the built JSON structure.
*
- * When ELASTICKIT_TEST_HOST env var is set, assertQuery() also sends the query to
- * Elasticsearch and verifies it is accepted without error.
+ * ES integration validation (connecting to ES, sending the query) lives in the
+ * integration test layer, which reuses the index/seed helpers below.
*/
abstract class DslTestCase extends TestCase
{
@@ -19,76 +19,20 @@ abstract class DslTestCase extends TestCase
protected static $esClient;
/**
- * @var string
- */
- protected static $esIndex = 'elastickit_test';
-
- public static function setUpBeforeClass(): void
- {
- $esHost = getenv('ELASTICKIT_TEST_HOST');
- if ($esHost) {
- try {
- static::$esClient = \Elastic\Elasticsearch\ClientBuilder::create()
- ->setHosts([$esHost])
- ->build();
-
- static::ensureIndex();
- } catch (\Exception $e) {
- static::$esClient = null;
- }
- }
- }
-
- /**
- * (Required, string) Assert Query produces expected JSON, and optionally validate against ES.
+ * Assert Query produces the expected JSON structure.
*
- * @param $expectedJson
- * @param $query
+ * @param string $expectedJson
+ * @param Query $query
*/
protected function assertQuery(string $expectedJson, Query $query)
{
$this->assertJsonStringEqualsJsonString($expectedJson, $query->toJson(), 'JSON mismatch');
-
- if (static::$esClient) {
- try {
- $params = [
- 'index' => static::$esIndex,
- 'body' => $query->toArray(),
- ];
- $response = static::$esClient->search($params);
- if (isset($response['error'])) {
- fwrite(STDERR, "\n [ES Warning] " . $this->getName() . ': ' . json_encode($response['error']) . "\n");
- }
- } catch (\Exception $e) {
- fwrite(STDERR, "\n [ES Warning] " . $this->getName() . ': ' . $e->getMessage() . "\n");
- }
- }
- }
-
- /**
- * Ensure the test index exists with proper mapping and seed data.
- */
- private static function ensureIndex(): void
- {
- if (!static::$esClient) {
- return;
- }
-
- $client = static::$esClient;
- $index = static::$esIndex;
-
- if (!$client->indices()->exists(['index' => $index])) {
- static::createIndex($client, $index);
- static::seedData($client, $index);
- }
-
- static::ensureSpecialFields($client, $index);
}
/**
* Create the test index with full mapping.
*/
- private static function createIndex(\Elastic\Elasticsearch\ClientInterface $client, string $index): void
+ protected static function createIndex(\Elastic\Elasticsearch\ClientInterface $client, string $index): void
{
$client->indices()->create([
'index' => $index,
@@ -127,7 +71,7 @@ private static function createIndex(\Elastic\Elasticsearch\ClientInterface $clie
/**
* Seed test documents.
*/
- private static function seedData(\Elastic\Elasticsearch\ClientInterface $client, string $index): void
+ protected static function seedData(\Elastic\Elasticsearch\ClientInterface $client, string $index): void
{
$docs = [
[
@@ -218,43 +162,4 @@ private static function seedData(\Elastic\Elasticsearch\ClientInterface $client,
$client->indices()->refresh(['index' => $index]);
}
-
- /**
- * Ensure special field mappings (percolator, rank_feature, shape).
- */
- private static function ensureSpecialFields(\Elastic\Elasticsearch\ClientInterface $client, string $index): void
- {
- $mapping = $client->indices()->getMapping(['index' => $index]);
- $properties = $mapping[$index]['mappings']['properties'] ?? [];
-
- $newFields = [];
- if (!isset($properties['query'])) {
- $newFields['query'] = ['type' => 'percolator'];
- }
- if (!isset($properties['pagerank'])) {
- $newFields['pagerank'] = ['type' => 'rank_feature'];
- }
- if (!isset($properties['cartesian_shape'])) {
- $newFields['cartesian_shape'] = ['type' => 'shape'];
- }
-
- if (!empty($newFields)) {
- $client->indices()->putMapping([
- 'index' => $index,
- 'body' => ['properties' => $newFields],
- ]);
-
- if (isset($newFields['query'])) {
- $client->index([
- 'index' => $index,
- 'id' => 'percolator_1',
- 'body' => [
- 'query' => ['match' => ['title' => 'elasticsearch']],
- ],
- ]);
- }
-
- $client->indices()->refresh(['index' => $index]);
- }
- }
}
diff --git a/tests/FullTextQueriesTest.php b/tests/FullTextQueriesTest.php
index 054e72e..eb9257d 100644
--- a/tests/FullTextQueriesTest.php
+++ b/tests/FullTextQueriesTest.php
@@ -2,7 +2,6 @@
use Tests\DslTestCase;
use ElasticKit\DSL\Query;
-use ElasticKit\DSL\Queries\Compound\Boolean;
use ElasticKit\DSL\Queries\FullText\CombinedFields;
use ElasticKit\DSL\Queries\FullText\Intervals;
use ElasticKit\DSL\Queries\FullText\MatchBoolPrefix;
@@ -67,6 +66,46 @@ public function testIntervals()
$this->assertQuery($exampleJson, $query);
}
+ public function testIntervalsPreservesInheritedProperties()
+ {
+ // boost() and other Node-inherited methods were previously dropped silently
+ // by Intervals::toArray(); toArray must no longer bypass $_properties.
+ $exampleJson = <<intervals('my_text', function (Intervals $intervals) {
+ $intervals->match(['query' => 'salty']);
+ $intervals->boost(2.0);
+ });
+ $this->assertQuery($exampleJson, $query);
+ }
+
+ public function testIntervalsThrowsOnDuplicateRuleKey()
+ {
+ // A field node accepts a single rule; two match calls must not be silently merged (second overwriting first) — it must throw.
+ $query = new Query();
+ $query->intervals('my_text', function (Intervals $intervals) {
+ $intervals->match(['query' => 'foo']);
+ $intervals->match(['query' => 'bar']);
+ });
+
+ $this->expectException(\RuntimeException::class);
+ $this->expectExceptionMessage('Duplicate interval clause key "match"');
+ $query->toArray();
+ }
+
public function testIntervals2()
{
$exampleJson = <<assertQuery($exampleJson, $query);
}
- public function testA()
+ public function testQueryStringCrossFields()
{
$exampleJson = <<assertQuery($exampleJson, $query);
}
+ public function testIntervalsRegexp()
+ {
+ $exampleJson = <<intervals('my_text', function (Intervals $intervals) {
+ $intervals->regexp(function (Intervals\Regexp $r) {
+ $r->pattern('h[ao]t');
+ });
+ });
+ $this->assertQuery($exampleJson, $query);
+ }
+
public function testIntervalsRange()
{
$exampleJson = <<assertQuery($exampleJson, $query);
}
+
+ public function testAllOfFilter()
+ {
+ // all_of::filter() must wrap in a Filter rule like any_of::filter()
+ $exampleJson = <<intervals('my_text', function (Intervals $intervals) {
+ $intervals->allOf(function (Intervals\AllOf $allOf) {
+ $allOf->addInterval(function (Intervals $i) {
+ $i->match(['query' => 'hot water']);
+ });
+ $allOf->filter(function (Intervals\Filter $filter) {
+ $filter->after(['match' => ['query' => 'cold porridge']]);
+ });
+ });
+ });
+ $this->assertQuery($exampleJson, $query);
+ }
}
diff --git a/tests/GeoQueriesTest.php b/tests/GeoQueriesTest.php
index 78e3aac..d14b32d 100644
--- a/tests/GeoQueriesTest.php
+++ b/tests/GeoQueriesTest.php
@@ -14,7 +14,7 @@ class GeoQueriesTest extends DslTestCase
{
public function testGeoBoundingBox()
{
-$exampleJson = <<createMock(TestClient::class));
- }
-
- protected function tearDown(): void
- {
- $ref = new ReflectionProperty(Index::class, 'clients');
- $ref->setAccessible(true);
- $ref->setValue(null, []);
- }
-
- protected function createIndex($name = 'products')
- {
- return new class($name) extends Index {
- public function __construct($name = 'products')
- {
- $this->name = $name;
- }
- };
- }
-
- protected function mockSearchResponse($aggValue)
- {
- $client = $this->createMock(TestClient::class);
- $client->method('search')->willReturn(new ArrayResponse([
- 'hits' => ['total' => ['value' => 0], 'hits' => []],
- 'aggregations' => ['__scalar' => ['value' => $aggValue]],
- ]));
- Index::setClient($client);
- }
-
- public function testMax()
- {
- $this->mockSearchResponse(199.99);
- $index = $this->createIndex();
-
- $result = $index->query()->term('status', 'published')->max('price');
-
- $this->assertEquals(199.99, $result);
- }
-
- public function testMin()
- {
- $this->mockSearchResponse(9.99);
- $index = $this->createIndex();
-
- $result = $index->query()->term('status', 'published')->min('price');
-
- $this->assertEquals(9.99, $result);
- }
-
- public function testAvg()
- {
- $this->mockSearchResponse(49.5);
- $index = $this->createIndex();
-
- $result = $index->query()->match('title', 'elasticsearch')->avg('price');
-
- $this->assertEquals(49.5, $result);
- }
-
- public function testSum()
- {
- $this->mockSearchResponse(1500.0);
- $index = $this->createIndex();
-
- $result = $index->query()->matchAll()->sum('price');
-
- $this->assertEquals(1500.0, $result);
- }
-
- public function testAggregationShortcutSendsCorrectBody()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())->method('search')->with($this->callback(function ($params) {
- $body = $params['body'];
- return $body['size'] === 0
- && isset($body['aggs']['__scalar']['max']['field'])
- && $body['aggs']['__scalar']['max']['field'] === 'price';
- }))->willReturn(new ArrayResponse([
- 'hits' => ['total' => ['value' => 0], 'hits' => []],
- 'aggregations' => ['__scalar' => ['value' => 100]],
- ]));
- Index::setClient($client);
-
- $index = $this->createIndex();
- $index->query()->max('price');
- }
-
- public function testAggregationShortcutDoesNotMutateQuery()
- {
- $lastBody = null;
- $client = $this->createMock(TestClient::class);
- $client->method('search')->willReturnCallback(function ($params) use (&$lastBody) {
- $lastBody = $params['body'];
- return new ArrayResponse([
- 'hits' => ['total' => ['value' => 0], 'hits' => []],
- 'aggregations' => ['__scalar' => ['value' => 100]],
- ]);
- });
- Index::setClient($client);
-
- $index = $this->createIndex();
- $search = $index->query()->match('title', 'test')->size(20);
-
- $search->max('price');
-
- // Query body sent to ES should have size=0, but the next get() should use size=20
- $this->assertEquals(0, $lastBody['size']);
- }
-
- public function testAggregationShortcutReturnsNullWhenNoValue()
- {
- $client = $this->createMock(TestClient::class);
- $client->method('search')->willReturn(new ArrayResponse([
- 'hits' => ['total' => ['value' => 0], 'hits' => []],
- ]));
- Index::setClient($client);
-
- $index = $this->createIndex();
- $result = $index->query()->max('nonexistent');
-
- $this->assertNull($result);
- }
-}
diff --git a/tests/Index/BulkTest.php b/tests/Index/BulkTest.php
deleted file mode 100644
index ee2f9c6..0000000
--- a/tests/Index/BulkTest.php
+++ /dev/null
@@ -1,272 +0,0 @@
-createMock(TestClient::class));
- }
-
- protected function tearDown(): void
- {
- $ref = new ReflectionProperty(Index::class, 'clients');
- $ref->setAccessible(true);
- $ref->setValue(null, []);
- }
-
- protected function createIndex($name = 'products')
- {
- return new class($name) extends Index {
- public function __construct($name)
- {
- $this->name = $name;
- }
- };
- }
-
- public function testIndexAction()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('bulk')
- ->with([
- 'body' => [
- ['index' => ['_index' => 'products', '_id' => '1']],
- ['title' => 'foo'],
- ],
- ])
- ->willReturn(new ArrayResponse(['errors' => false, 'items' => []]));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $result = (new Bulk($index))->index('1', ['title' => 'foo'])->execute();
-
- $this->assertFalse($result['errors']);
- }
-
- public function testCreateAction()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('bulk')
- ->with([
- 'body' => [
- ['create' => ['_index' => 'products', '_id' => '1']],
- ['title' => 'foo'],
- ],
- ])
- ->willReturn(new ArrayResponse(['errors' => false, 'items' => []]));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- (new Bulk($index))->create('1', ['title' => 'foo'])->execute();
- }
-
- public function testUpdateAction()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('bulk')
- ->with([
- 'body' => [
- ['update' => ['_index' => 'products', '_id' => '1']],
- ['doc' => ['title' => 'updated'], 'doc_as_upsert' => false],
- ],
- ])
- ->willReturn(new ArrayResponse(['errors' => false, 'items' => []]));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- (new Bulk($index))->update('1', ['title' => 'updated'])->execute();
- }
-
- public function testUpdateWithoutUpsert()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('bulk')
- ->with([
- 'body' => [
- ['update' => ['_index' => 'products', '_id' => '1']],
- ['doc' => ['title' => 'updated'], 'doc_as_upsert' => false],
- ],
- ])
- ->willReturn(new ArrayResponse(['errors' => false, 'items' => []]));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- (new Bulk($index))->update('1', ['title' => 'updated'], false)->execute();
- }
-
- public function testUpdateWithRetryOnConflict()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('bulk')
- ->with([
- 'body' => [
- ['update' => ['_index' => 'products', '_id' => '1', 'retry_on_conflict' => 3]],
- ['doc' => ['title' => 'updated'], 'doc_as_upsert' => false],
- ['update' => ['_index' => 'products', '_id' => '2', 'retry_on_conflict' => 3]],
- ['doc' => ['title' => 'bar'], 'doc_as_upsert' => false],
- ],
- ])
- ->willReturn(new ArrayResponse(['errors' => false, 'items' => []]));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- (new Bulk($index))
- ->retryOnConflict(3)
- ->update('1', ['title' => 'updated'])
- ->update('2', ['title' => 'bar'])
- ->execute();
- }
-
- public function testDeleteAction()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('bulk')
- ->with([
- 'body' => [
- ['delete' => ['_index' => 'products', '_id' => '1']],
- ],
- ])
- ->willReturn(new ArrayResponse(['errors' => false, 'items' => []]));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- (new Bulk($index))->delete('1')->execute();
- }
-
- public function testMixedActions()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('bulk')
- ->with([
- 'body' => [
- ['index' => ['_index' => 'products', '_id' => '1']],
- ['title' => 'foo'],
- ['update' => ['_index' => 'products', '_id' => '2']],
- ['doc' => ['title' => 'bar'], 'doc_as_upsert' => false],
- ['delete' => ['_index' => 'products', '_id' => '3']],
- ],
- ])
- ->willReturn(new ArrayResponse(['errors' => false, 'items' => []]));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- (new Bulk($index))
- ->index('1', ['title' => 'foo'])
- ->update('2', ['title' => 'bar'])
- ->delete('3')
- ->execute();
- }
-
- public function testExecuteWithOptions()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('bulk')
- ->with([
- 'body' => [
- ['index' => ['_index' => 'products', '_id' => '1']],
- ['title' => 'foo'],
- ],
- 'refresh' => 'wait_for',
- 'timeout' => '5s',
- ])
- ->willReturn(new ArrayResponse(['errors' => false, 'items' => []]));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- (new Bulk($index))
- ->index('1', ['title' => 'foo'])
- ->execute(['refresh' => 'wait_for', 'timeout' => '5s']);
- }
-
- public function testExecuteClearsBodyAndRetryOnConflict()
- {
- $callCount = 0;
- $client = $this->createMock(TestClient::class);
- $client->method('bulk')->willReturnCallback(function ($params) use (&$callCount) {
- $callCount++;
- if ($callCount === 1) {
- $this->assertEquals(3, $params['body'][0]['update']['retry_on_conflict']);
- $this->assertEquals('wait_for', $params['refresh']);
- } else {
- $this->assertArrayNotHasKey('retry_on_conflict', $params['body'][0]['update']);
- $this->assertArrayNotHasKey('refresh', $params);
- }
- return new ArrayResponse(['errors' => false, 'items' => []]);
- });
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $bulk = new Bulk($index);
-
- $bulk->retryOnConflict(3)->update('1', ['title' => 'first'])->execute(['refresh' => 'wait_for']);
- $bulk->update('1', ['title' => 'second'])->execute();
-
- $this->assertEquals(2, $callCount);
- }
-
- public function testTargetOverridesIndexName()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('bulk')
- ->with([
- 'body' => [
- ['index' => ['_index' => 'products_new', '_id' => '1']],
- ['title' => 'foo'],
- ],
- ])
- ->willReturn(new ArrayResponse(['errors' => false, 'items' => []]));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- (new Bulk($index))->target('products_new')->index('1', ['title' => 'foo'])->execute();
- }
-
- public function testAutoFlushTriggersExecute()
- {
- $callCount = 0;
- $client = $this->createMock(TestClient::class);
- $client->method('bulk')->willReturnCallback(function () use (&$callCount) {
- $callCount++;
- return new ArrayResponse(['errors' => false, 'items' => []]);
- });
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $bulk = (new Bulk($index))->batchSize(2);
-
- $bulk->index('1', ['title' => 'a']);
- $this->assertEquals(0, $callCount);
-
- $bulk->index('2', ['title' => 'b']);
- $this->assertEquals(1, $callCount);
-
- $bulk->index('3', ['title' => 'c']);
- $bulk->execute();
- $this->assertEquals(2, $callCount);
- }
-
- public function testExecuteReturnsEmptyWhenBodyIsEmpty()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->never())->method('bulk');
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $result = (new Bulk($index))->execute();
-
- $this->assertEquals([], $result);
- }
-}
diff --git a/tests/Index/DocTest.php b/tests/Index/DocTest.php
deleted file mode 100644
index 71168f4..0000000
--- a/tests/Index/DocTest.php
+++ /dev/null
@@ -1,335 +0,0 @@
-createMock(TestClient::class));
- }
-
- protected function tearDown(): void
- {
- $ref = new ReflectionProperty(Index::class, 'clients');
- $ref->setAccessible(true);
- $ref->setValue(null, []);
- }
-
- protected function createIndex($name = 'products')
- {
- return new class($name) extends Index {
- public function __construct($name = 'products')
- {
- $this->name = $name;
- }
- };
- }
-
- public function testId()
- {
- $index = $this->createIndex('products');
- $doc = $index->doc('abc123');
- $this->assertEquals('abc123', $doc->id());
- }
-
- public function testGet()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('get')
- ->with(['index' => 'products', 'id' => '1'])
- ->willReturn(new ArrayResponse([
- '_index' => 'products',
- '_id' => '1',
- '_source' => ['title' => 'foo'],
- ]));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $result = $index->doc('1')->get();
-
- $this->assertEquals('1', $result['_id']);
- $this->assertEquals(['title' => 'foo'], $result['_source']);
- }
-
- public function testSource()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('getSource')
- ->with(['index' => 'products', 'id' => '1'])
- ->willReturn(new ArrayResponse(['title' => 'foo']));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $source = $index->doc('1')->source();
-
- $this->assertEquals(['title' => 'foo'], $source);
- }
-
- public function testExistsReturnsTrue()
- {
- $client = $this->createMock(TestClient::class);
- $client->method('exists')->with(['index' => 'products', 'id' => '1'])->willReturn(new BoolResponse(true));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $this->assertTrue($index->doc('1')->exists());
- }
-
- public function testExistsReturnsFalse()
- {
- $client = $this->createMock(TestClient::class);
- $client->method('exists')->with(['index' => 'products', 'id' => '999'])->willReturn(new BoolResponse(false));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $this->assertFalse($index->doc('999')->exists());
- }
-
- public function testUpdate()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('update')
- ->with([
- 'index' => 'products',
- 'id' => '1',
- 'body' => [
- 'doc' => ['title' => 'updated'],
- 'doc_as_upsert' => false,
- ],
- ])
- ->willReturn(new ArrayResponse(['result' => 'updated']));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $result = $index->doc('1')->update(['title' => 'updated']);
-
- $this->assertEquals('updated', $result['result']);
- }
-
- public function testUpdateWithoutUpsert()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('update')
- ->with([
- 'index' => 'products',
- 'id' => '1',
- 'body' => [
- 'doc' => ['title' => 'updated'],
- 'doc_as_upsert' => false,
- ],
- ])
- ->willReturn(new ArrayResponse(['result' => 'updated']));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $index->doc('1')->update(['title' => 'updated'], false);
- }
-
- public function testUpdateWithRetryOnConflict()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('update')
- ->with([
- 'index' => 'products',
- 'id' => '1',
- 'body' => [
- 'doc' => ['title' => 'updated'],
- 'doc_as_upsert' => false,
- ],
- 'retry_on_conflict' => 3,
- ])
- ->willReturn(new ArrayResponse(['result' => 'updated']));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $index->doc('1')->retryOnConflict(3)->update(['title' => 'updated']);
- }
-
- public function testUpdateWithRefresh()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('update')
- ->with([
- 'index' => 'products',
- 'id' => '1',
- 'body' => [
- 'doc' => ['title' => 'updated'],
- 'doc_as_upsert' => false,
- ],
- 'refresh' => 'wait_for',
- ])
- ->willReturn(new ArrayResponse(['result' => 'updated']));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $index->doc('1')->refresh('wait_for')->update(['title' => 'updated']);
- }
-
- public function testUpdateWithAllOptions()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('update')
- ->with([
- 'index' => 'products',
- 'id' => '1',
- 'body' => [
- 'doc' => ['title' => 'updated'],
- 'doc_as_upsert' => false,
- ],
- 'retry_on_conflict' => 5,
- 'refresh' => 'true',
- ])
- ->willReturn(new ArrayResponse(['result' => 'updated']));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $index->doc('1')->retryOnConflict(5)->refresh('true')->update(['title' => 'updated'], false);
- }
-
- public function testUpdateOptionsResetAfterCall()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->exactly(2))
- ->method('update')
- ->willReturnMap([
- [
- [
- 'index' => 'products',
- 'id' => '1',
- 'body' => ['doc' => ['title' => 'first'], 'doc_as_upsert' => false],
- 'retry_on_conflict' => 3,
- 'refresh' => 'wait_for',
- ],
- new ArrayResponse(['result' => 'updated']),
- ],
- [
- [
- 'index' => 'products',
- 'id' => '1',
- 'body' => ['doc' => ['title' => 'second'], 'doc_as_upsert' => false],
- ],
- new ArrayResponse(['result' => 'updated']),
- ],
- ]);
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $doc = $index->doc('1');
- $doc->retryOnConflict(3)->refresh('wait_for')->update(['title' => 'first']);
- $doc->update(['title' => 'second']);
- }
-
- public function testIndex()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('index')
- ->with([
- 'index' => 'products',
- 'id' => '1',
- 'body' => ['title' => 'foo'],
- ])
- ->willReturn(new ArrayResponse(['result' => 'created']));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $result = $index->doc('1')->index(['title' => 'foo']);
-
- $this->assertEquals('created', $result['result']);
- }
-
- public function testIndexWithRefresh()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('index')
- ->with([
- 'index' => 'products',
- 'id' => '1',
- 'body' => ['title' => 'foo'],
- 'refresh' => 'wait_for',
- ])
- ->willReturn(new ArrayResponse(['result' => 'created']));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $index->doc('1')->refresh('wait_for')->index(['title' => 'foo']);
- }
-
- public function testCreate()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('index')
- ->with([
- 'index' => 'products',
- 'id' => '1',
- 'body' => ['title' => 'foo'],
- 'op_type' => 'create',
- ])
- ->willReturn(new ArrayResponse(['result' => 'created']));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $result = $index->doc('1')->create(['title' => 'foo']);
-
- $this->assertEquals('created', $result['result']);
- }
-
- public function testCreateWithRefresh()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('index')
- ->with([
- 'index' => 'products',
- 'id' => '1',
- 'body' => ['title' => 'foo'],
- 'op_type' => 'create',
- 'refresh' => 'true',
- ])
- ->willReturn(new ArrayResponse(['result' => 'created']));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $index->doc('1')->refresh('true')->create(['title' => 'foo']);
- }
-
- public function testDelete()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('delete')
- ->with(['index' => 'products', 'id' => '1'])
- ->willReturn(new ArrayResponse(['result' => 'deleted']));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $result = $index->doc('1')->delete();
-
- $this->assertEquals('deleted', $result['result']);
- }
-
- public function testDeleteWithRefresh()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('delete')
- ->with(['index' => 'products', 'id' => '1', 'refresh' => 'wait_for'])
- ->willReturn(new ArrayResponse(['result' => 'deleted']));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $index->doc('1')->refresh('wait_for')->delete();
- }
-}
diff --git a/tests/Index/EventTest.php b/tests/Index/EventTest.php
deleted file mode 100644
index 09edd1f..0000000
--- a/tests/Index/EventTest.php
+++ /dev/null
@@ -1,322 +0,0 @@
-setAccessible(true);
- $ref->setValue(null, []);
-
- $clientRef = new ReflectionProperty(Index::class, 'clients');
- $clientRef->setAccessible(true);
- $clientRef->setValue(null, []);
- }
-
- protected function createIndex($name = 'products')
- {
- return new class($name) extends Index {
- public function __construct($name = 'products')
- {
- $this->name = $name;
- }
- };
- }
-
- protected function mockClient()
- {
- $client = $this->createMock(TestClient::class);
- $client->method('search')->willReturn(new ArrayResponse([
- 'hits' => ['total' => ['value' => 0], 'hits' => []],
- ]));
- Index::setClient($client);
- return $client;
- }
-
- public function testListenAndDispatch()
- {
- $received = null;
- Index::listen('search.query.before', function (Event $e) use (&$received) {
- $received = $e;
- });
-
- $this->mockClient();
- $index = $this->createIndex();
- $index->query()->get();
-
- $this->assertNotNull($received);
- $this->assertEquals('search.query.before', $received->name);
- $this->assertEquals('products', $received->index);
- }
-
- public function testSearchBeforePassesDsl()
- {
- $dsl = null;
- Index::listen('search.query.before', function (Event $e) use (&$dsl) {
- $dsl = $e->dsl;
- });
-
- $this->mockClient();
- $index = $this->createIndex();
- $index->query()->match('title', 'test')->get();
-
- $this->assertIsArray($dsl);
- $this->assertArrayHasKey('query', $dsl);
- }
-
- public function testSearchAfterPassesResponse()
- {
- $response = null;
- Index::listen('search.query.after', function (Event $e) use (&$response) {
- $response = $e->response;
- });
-
- $this->mockClient();
- $index = $this->createIndex();
- $index->query()->get();
-
- $this->assertIsArray($response);
- $this->assertArrayHasKey('hits', $response);
- }
-
- public function testSearchAfterContainsDuration()
- {
- $duration = null;
- Index::listen('search.query.after', function (Event $e) use (&$duration) {
- $duration = $e->duration;
- });
-
- $this->mockClient();
- $index = $this->createIndex();
- $index->query()->get();
-
- $this->assertIsFloat($duration);
- $this->assertGreaterThanOrEqual(0, $duration);
- }
-
- public function testSearchEventPassesAction()
- {
- $action = null;
- Index::listen('search.query.before', function (Event $e) use (&$action) {
- $action = $e->action;
- });
-
- $this->mockClient();
- $index = $this->createIndex();
- $index->query()->get();
-
- $this->assertEquals('get', $action);
- }
-
- public function testFirstTriggersSearchWithAction()
- {
- $action = null;
- Index::listen('search.query.before', function (Event $e) use (&$action) {
- $action = $e->action;
- });
-
- $this->mockClient();
- $index = $this->createIndex();
- $index->query()->first();
-
- $this->assertEquals('first', $action);
- }
-
- public function testWildcardListener()
- {
- $events = [];
- Index::listen('*', function (Event $e) use (&$events) {
- $events[] = $e->name;
- });
-
- $this->mockClient();
- $index = $this->createIndex();
- $index->query()->get();
-
- $this->assertContains('search.query.before', $events);
- $this->assertContains('search.query.after', $events);
- }
-
- public function testCategoryWildcardListener()
- {
- $events = [];
- Index::listen('search.*', function (Event $e) use (&$events) {
- $events[] = $e->name;
- });
-
- $this->mockClient();
- $index = $this->createIndex();
- $index->query()->get();
-
- $this->assertContains('search.query.before', $events);
- $this->assertContains('search.query.after', $events);
- }
-
- public function testCategoryWildcardMatchesSearchQueryEvents()
- {
- $events = [];
- Index::listen('search.*', function (Event $e) use (&$events) {
- $events[] = $e->name;
- });
-
- $client = $this->createMock(TestClient::class);
- $client->method('search')->willReturn([
- 'hits' => ['total' => ['value' => 0], 'hits' => []],
- ]);
- $client->method('count')->willReturn(new ArrayResponse(['count' => 0]));
- Index::setClient($client);
-
- $index = $this->createIndex();
- $index->query()->count();
-
- $this->assertNotEmpty($events);
- $this->assertContains('search.query.before', $events);
- $this->assertContains('search.query.after', $events);
- }
-
- public function testMultipleListeners()
- {
- $count = 0;
- Index::listen('search.query.before', function (Event $e) use (&$count) {
- $count++;
- });
- Index::listen('search.query.before', function (Event $e) use (&$count) {
- $count++;
- });
-
- $this->mockClient();
- $index = $this->createIndex();
- $index->query()->get();
-
- $this->assertEquals(2, $count);
- }
-
- public function testNoListenersDoesNotError()
- {
- $this->mockClient();
- $index = $this->createIndex();
-
- $index->query()->get();
- $this->assertTrue(true);
- }
-
- public function testBulkExecutePassesActions()
- {
- $actions = null;
- Index::listen('bulk.execute.before', function (Event $e) use (&$actions) {
- $actions = $e->actions;
- });
-
- $client = $this->createMock(TestClient::class);
- $client->method('bulk')->willReturn(new ArrayResponse(['errors' => false]));
- Index::setClient($client);
-
- $index = $this->createIndex();
- $bulk = new \ElasticKit\Index\Bulk($index);
- $bulk->index(1, ['title' => 'test']);
- $bulk->execute();
-
- $this->assertIsArray($actions);
- $this->assertCount(2, $actions);
- }
-
- public function testManagerCreateEvents()
- {
- $events = [];
- Index::listen('manager.create.*', function (Event $e) use (&$events) {
- $events[] = $e->name;
- });
-
- $indices = $this->createMock(TestIndices::class);
- $indices->method('create')->willReturn(new ArrayResponse(['acknowledged' => true]));
- $client = $this->createMock(TestClient::class);
- $client->method('indices')->willReturn($indices);
- Index::setClient($client);
-
- $index = $this->createIndex();
- $manager = new \ElasticKit\Index\Manager($index);
- $manager->create();
-
- $this->assertContains('manager.create.before', $events);
- $this->assertContains('manager.create.after', $events);
- }
-
- public function testManagerDeleteEvents()
- {
- $events = [];
- Index::listen('manager.delete.*', function (Event $e) use (&$events) {
- $events[] = $e->name;
- });
-
- $indices = $this->createMock(TestIndices::class);
- $indices->method('delete')->willReturn(new ArrayResponse(['acknowledged' => true]));
- $indices->method('existsAlias')->willReturn(new BoolResponse(false));
- $client = $this->createMock(TestClient::class);
- $client->method('indices')->willReturn($indices);
- Index::setClient($client);
-
- $index = $this->createIndex();
- $manager = new \ElasticKit\Index\Manager($index);
- $manager->delete();
-
- $this->assertContains('manager.delete.before', $events);
- $this->assertContains('manager.delete.after', $events);
- }
-
- public function testManagerReadOperationsHaveNoEvents()
- {
- $events = [];
- Index::listen('*', function (Event $e) use (&$events) {
- $events[] = $e->name;
- });
-
- $indices = $this->createMock(TestIndices::class);
- $indices->method('exists')->willReturn(new BoolResponse(true));
- $indices->method('get')->willReturn(new ArrayResponse([]));
- $indices->method('getMapping')->willReturn(new ArrayResponse([]));
- $indices->method('getSettings')->willReturn(new ArrayResponse([]));
- $indices->method('existsAlias')->willReturn(new BoolResponse(false));
- $client = $this->createMock(TestClient::class);
- $client->method('indices')->willReturn($indices);
- Index::setClient($client);
-
- $index = $this->createIndex();
- $manager = new \ElasticKit\Index\Manager($index);
- $manager->exists();
- $manager->get();
- $manager->getMapping();
- $manager->getSettings();
-
- $this->assertEmpty($events);
- }
-
- public function testRebuildRunBeforeAndAfterEvents()
- {
- $events = [];
- Index::listen('rebuild.*', function (Event $e) use (&$events) {
- $events[] = $e->name;
- });
-
- $indices = $this->createMock(TestIndices::class);
- $indices->method('create')->willReturn(new ArrayResponse(['acknowledged' => true]));
- $indices->method('existsAlias')->willReturn(new BoolResponse(false));
- $indices->method('exists')->willReturn(new BoolResponse(false));
- $indices->method('putAlias')->willReturn(new ArrayResponse(['acknowledged' => true]));
- $client = $this->createMock(TestClient::class);
- $client->method('indices')->willReturn($indices);
- $client->method('bulk')->willReturn(new ArrayResponse(['errors' => false]));
- Index::setClient($client);
-
- $index = $this->createIndex();
- $rebuild = new \ElasticKit\Index\Rebuild($index);
- $rebuild->source(function () { yield 1 => ['title' => 'test']; });
- $rebuild->run();
-
- $this->assertContains('rebuild.run.before', $events);
- $this->assertContains('rebuild.run.after', $events);
- }
-}
diff --git a/tests/Index/IndexTest.php b/tests/Index/IndexTest.php
deleted file mode 100644
index acc5d73..0000000
--- a/tests/Index/IndexTest.php
+++ /dev/null
@@ -1,724 +0,0 @@
-createMock(TestClient::class));
- }
-
- protected function tearDown(): void
- {
- // Reset static clients between tests
- $ref = new ReflectionProperty(Index::class, 'clients');
- $ref->setAccessible(true);
- $ref->setValue(null, []);
-
- // Reset static resolvers between tests
- $pageRef = new ReflectionProperty(Index::class, 'pageResolver');
- $pageRef->setAccessible(true);
- $pageRef->setValue(null, null);
-
- $paginatorRef = new ReflectionProperty(Index::class, 'paginatorResolver');
- $paginatorRef->setAccessible(true);
- $paginatorRef->setValue(null, null);
- }
-
- protected function createIndex($name = 'products')
- {
- return new class($name) extends Index {
- public function __construct($name = 'products')
- {
- $this->name = $name;
- }
- };
- }
-
- public function testSetClientAndGetClient()
- {
- $client = $this->createMock(TestClient::class);
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $this->assertSame($client, $index->getClient());
- }
-
- public function testQueryReturnsSearch()
- {
- $index = $this->createIndex('products');
- $search = $index->query();
-
- $this->assertInstanceOf(Search::class, $search);
- }
-
- public function testQueryReturnsNewInstance()
- {
- $index = $this->createIndex('products');
- $search1 = $index->query();
- $search2 = $index->query();
-
- $this->assertNotSame($search1, $search2);
- }
-
- public function testSearchDelegatesQueryDSL()
- {
- $index = $this->createIndex('products');
- $search = $index->query();
-
- $search->match('title', 'elasticsearch');
- $search->size(10);
-
- $array = $search->toArray();
- $this->assertArrayHasKey('query', $array);
- $this->assertEquals(10, $array['size']);
- }
-
- public function testGetReturnsResults()
- {
- $client = $this->createMock(TestClient::class);
- $client->method('search')->willReturn(new ArrayResponse([
- 'hits' => [
- 'total' => ['value' => 1],
- 'hits' => [['_source' => ['title' => 'elasticsearch']]],
- ],
- ]));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $results = $index->query()
- ->match('title', 'elasticsearch')
- ->size(10)
- ->get();
-
- $this->assertInstanceOf(Results::class, $results);
- $this->assertEquals(1, $results->total());
- }
-
- public function testGetCallsClientWithCorrectParams()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('search')
- ->with([
- 'index' => 'products',
- 'body' => [
- 'query' => [
- 'match' => ['title' => 'elasticsearch'],
- ],
- 'size' => 10,
- ],
- ])
- ->willReturn(new ArrayResponse([
- 'hits' => [
- 'total' => ['value' => 1],
- 'hits' => [['_source' => ['title' => 'elasticsearch']]],
- ],
- ]));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $results = $index->query()
- ->match('title', 'elasticsearch')
- ->size(10)
- ->get();
-
- $this->assertEquals(1, $results->total());
- }
-
- public function testFirstReturnsSource()
- {
- $client = $this->createMock(TestClient::class);
- $client->method('search')->willReturn(new ArrayResponse([
- 'hits' => [
- 'total' => ['value' => 1],
- 'hits' => [['_source' => ['title' => 'elasticsearch']]],
- ],
- ]));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $doc = $index->query()
- ->match('title', 'elasticsearch')
- ->first();
-
- $this->assertEquals(['title' => 'elasticsearch'], $doc);
- }
-
- public function testFirstReturnsNullWhenEmpty()
- {
- $client = $this->createMock(TestClient::class);
- $client->method('search')->willReturn(new ArrayResponse([
- 'hits' => [
- 'total' => ['value' => 0],
- 'hits' => [],
- ],
- ]));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $doc = $index->query()->matchAll()->first();
-
- $this->assertNull($doc);
- }
-
- public function testFirstSetsSizeOnQuery()
- {
- $client = $this->createMock(TestClient::class);
- $client->method('search')->willReturnCallback(function ($params) {
- $this->assertEquals(1, $params['body']['size']);
- return new ArrayResponse([
- 'hits' => [
- 'total' => ['value' => 1],
- 'hits' => [['_source' => ['title' => 'test']]],
- ],
- ]);
- });
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $result = $index->query()->matchAll()->size(100)->first();
-
- $this->assertEquals(['title' => 'test'], $result);
- }
-
- public function testCountReturnsTotal()
- {
- $client = $this->createMock(TestClient::class);
- $client->method('count')->willReturn(new ArrayResponse(['count' => 42]));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $count = $index->query()
- ->term('status', 'published')
- ->count();
-
- $this->assertEquals(42, $count);
- }
-
- public function testCountDoesNotMutateSearchState()
- {
- $searchBody = null;
- $client = $this->createMock(TestClient::class);
- $client->method('count')->willReturn(new ArrayResponse(['count' => 1]));
- $client->method('search')->willReturnCallback(function ($params) use (&$searchBody) {
- $searchBody = $params['body'];
- return new ArrayResponse([
- 'hits' => [
- 'total' => ['value' => 1],
- 'hits' => [],
- ],
- ]);
- });
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $search = $index->query()->matchAll()->size(100);
-
- $search->count();
- $search->get();
-
- // size should still be 100 in the search body
- $this->assertEquals(100, $searchBody['size']);
- }
-
- public function testBoolShorthandOnSearch()
- {
- $client = $this->createMock(TestClient::class);
- $client->method('search')->willReturn(new ArrayResponse([
- 'hits' => [
- 'total' => ['value' => 2],
- 'hits' => [
- ['_source' => ['mobile' => '13800138000']],
- ['_source' => ['mobile' => '13900139000']],
- ],
- ],
- ]));
- Index::setClient($client);
-
- $index = $this->createIndex('users');
- $results = $index->query()
- ->bool(['should' => function (Query $q) {
- $q->term('mobile', '13800138000');
- $q->term('id_card', '13800138000');
- }])
- ->get();
-
- $this->assertEquals(2, $results->total());
- }
-
- public function testScrollDefaultsTo1000BatchSize()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('search')
- ->with([
- 'index' => 'products',
- 'body' => [
- 'query' => ['match_all' => (object)[]],
- 'size' => 1000,
- ],
- 'scroll' => '1m',
- ])
- ->willReturn(new ArrayResponse([
- '_scroll_id' => 'scroll123',
- 'hits' => ['total' => ['value' => 0], 'hits' => []],
- ]));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $results = $index->query()->matchAll()->scroll(null, '1m');
-
- $this->assertEquals('scroll123', $results->scrollId());
- }
-
- public function testScrollRespectsUserSetSize()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('search')
- ->with([
- 'index' => 'products',
- 'body' => [
- 'query' => ['match_all' => (object)[]],
- 'size' => 500,
- ],
- 'scroll' => '5m',
- ])
- ->willReturn(new ArrayResponse([
- '_scroll_id' => 'scroll456',
- 'hits' => ['total' => ['value' => 0], 'hits' => []],
- ]));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $results = $index->query()->matchAll()->size(500)->scroll(null, '5m');
-
- $this->assertEquals('scroll456', $results->scrollId());
- }
-
- public function testScrollContinuesWithScrollId()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('scroll')
- ->with([
- 'scroll_id' => 'existing_scroll_id',
- 'scroll' => '5m',
- ])
- ->willReturn(new ArrayResponse([
- '_scroll_id' => 'new_scroll_id',
- 'hits' => [
- 'total' => ['value' => 1],
- 'hits' => [['_source' => ['title' => 'continued']]],
- ],
- ]));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $results = $index->query()->scroll('existing_scroll_id');
-
- $this->assertEquals('new_scroll_id', $results->scrollId());
- $this->assertEquals([['title' => 'continued']], $results->docs());
- }
-
- public function testNextCallsScrollApi()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('scroll')
- ->with([
- 'scroll_id' => 'scroll789',
- 'scroll' => '5m',
- ])
- ->willReturn(new ArrayResponse([
- '_scroll_id' => 'scroll789_new',
- 'hits' => [
- 'total' => ['value' => 1],
- 'hits' => [['_source' => ['title' => 'bar']]],
- ],
- ]));
- Index::setClient($client);
-
- $previousResults = new Results([
- '_scroll_id' => 'scroll789',
- 'hits' => ['total' => ['value' => 1], 'hits' => [['_source' => ['title' => 'foo']]]],
- ]);
-
- $index = $this->createIndex('products');
- $nextResults = $index->query()->next($previousResults);
-
- $this->assertEquals('scroll789_new', $nextResults->scrollId());
- $this->assertEquals([['title' => 'bar']], $nextResults->docs());
- }
-
- public function testClearCallsClearScroll()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('clearScroll')
- ->with(['scroll_id' => 'scroll_abc']);
- Index::setClient($client);
-
- $results = new Results([
- '_scroll_id' => 'scroll_abc',
- 'hits' => ['total' => ['value' => 0], 'hits' => []],
- ]);
-
- $index = $this->createIndex('products');
- $index->query()->clear($results);
- }
-
- public function testClearSkipsWhenNoScrollId()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->never())->method('clearScroll');
- Index::setClient($client);
-
- $results = new Results([
- 'hits' => ['total' => ['value' => 0], 'hits' => []],
- ]);
-
- $index = $this->createIndex('products');
- $index->query()->clear($results);
- }
-
- public function testCursorYieldsResultsBatches()
- {
- $client = $this->createMock(TestClient::class);
-
- $client->method('search')->willReturn(new ArrayResponse([
- '_scroll_id' => 'scroll1',
- 'hits' => [
- 'total' => ['value' => 3],
- 'hits' => [
- ['_id' => '1', '_source' => ['id' => 1]],
- ['_id' => '2', '_source' => ['id' => 2]],
- ],
- ],
- ]));
-
- $callCount = 0;
- $client->method('scroll')->willReturnCallback(function () use (&$callCount) {
- $callCount++;
- if ($callCount === 1) {
- return new ArrayResponse([
- '_scroll_id' => 'scroll2',
- 'hits' => [
- 'total' => ['value' => 3],
- 'hits' => [['_id' => '3', '_source' => ['id' => 3]]],
- ],
- ]);
- }
- return new ArrayResponse([
- '_scroll_id' => 'scroll3',
- 'hits' => ['total' => ['value' => 3], 'hits' => []],
- ]);
- });
-
- $client->method('clearScroll');
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $batches = [];
- foreach ($index->query()->matchAll()->cursor('1m') as $results) {
- $this->assertInstanceOf(Results::class, $results);
- $batches[] = $results;
- }
-
- // First batch: 2 docs, second batch: 1 doc, third batch (empty) stops the loop
- $this->assertCount(2, $batches);
- $this->assertEquals(['1', '2'], $batches[0]->ids());
- $this->assertEquals(['3'], $batches[1]->ids());
- }
-
- public function testNameReturnsIndexName()
- {
- $index = $this->createIndex('orders');
- $this->assertEquals('orders', $index->name());
- }
-
- public function testPaginateWithExplicitParams()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('search')
- ->with([
- 'index' => 'products',
- 'body' => [
- 'query' => ['match_all' => (object)[]],
- 'from' => 10,
- 'size' => 5,
- ],
- ])
- ->willReturn(new ArrayResponse([
- 'hits' => [
- 'total' => ['value' => 50],
- 'hits' => [['_source' => ['title' => 'test']]],
- ],
- ]));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $results = $index->query()->matchAll()->paginate(3, 5);
-
- $this->assertInstanceOf(Results::class, $results);
- $this->assertEquals(50, $results->total());
- $this->assertEquals(3, $results->page());
- $this->assertEquals(5, $results->perPage());
- $this->assertEquals(10, $results->lastPage());
- }
-
- public function testPaginateWithPageResolver()
- {
- Index::setPageResolver(function () {
- return [2, 20];
- });
-
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('search')
- ->with([
- 'index' => 'products',
- 'body' => [
- 'query' => ['match_all' => (object)[]],
- 'from' => 20,
- 'size' => 20,
- ],
- ])
- ->willReturn(new ArrayResponse([
- 'hits' => [
- 'total' => ['value' => 100],
- 'hits' => [],
- ],
- ]));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $results = $index->query()->matchAll()->paginate();
-
- $this->assertInstanceOf(Results::class, $results);
- $this->assertEquals(100, $results->total());
- $this->assertEquals(2, $results->page());
- $this->assertEquals(20, $results->perPage());
- $this->assertEquals(5, $results->lastPage());
- }
-
- public function testPaginateReturnsResultsWithMetadata()
- {
- $client = $this->createMock(TestClient::class);
- $client->method('search')->willReturn(new ArrayResponse([
- 'hits' => [
- 'total' => ['value' => 30],
- 'hits' => [
- ['_source' => ['title' => 'a']],
- ['_source' => ['title' => 'b']],
- ],
- ],
- ]));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $results = $index->query()->matchAll()->paginate(1, 10);
-
- $this->assertInstanceOf(Results::class, $results);
- $this->assertEquals(30, $results->total());
- $this->assertEquals(1, $results->page());
- $this->assertEquals(10, $results->perPage());
- $this->assertEquals(3, $results->lastPage());
- $this->assertEquals([['title' => 'a'], ['title' => 'b']], $results->items());
- $this->assertFalse($results->isEmpty());
- }
-
- public function testPaginateWithoutResolversUsesDefaults()
- {
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('search')
- ->with([
- 'index' => 'products',
- 'body' => [
- 'query' => ['match_all' => (object)[]],
- 'from' => 0,
- 'size' => 15,
- ],
- ])
- ->willReturn(new ArrayResponse([
- 'hits' => [
- 'total' => ['value' => 5],
- 'hits' => [],
- ],
- ]));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $results = $index->query()->matchAll()->paginate();
-
- $this->assertInstanceOf(Results::class, $results);
- $this->assertEquals(5, $results->total());
- $this->assertEquals(1, $results->page());
- $this->assertEquals(15, $results->perPage());
- $this->assertEquals(1, $results->lastPage());
- $this->assertTrue($results->isEmpty());
- }
-
- public function testPaginateUsesIndexPerPage()
- {
- $index = new class('products') extends Index {
- public function __construct($name = 'products')
- {
- $this->name = $name;
- $this->perPage = 25;
- }
- };
-
- $client = $this->createMock(TestClient::class);
- $client->expects($this->once())
- ->method('search')
- ->with([
- 'index' => 'products',
- 'body' => [
- 'query' => ['match_all' => (object)[]],
- 'from' => 25,
- 'size' => 25,
- ],
- ])
- ->willReturn(new ArrayResponse([
- 'hits' => [
- 'total' => ['value' => 100],
- 'hits' => [],
- ],
- ]));
- Index::setClient($client);
-
- $results = $index->query()->matchAll()->paginate(2);
-
- $this->assertEquals(100, $results->total());
- $this->assertEquals(2, $results->page());
- $this->assertEquals(25, $results->perPage());
- $this->assertEquals(4, $results->lastPage());
- }
-
- public function testToPaginatorReturnsFrameworkPaginator()
- {
- Index::setPaginatorResolver(function (Results $results) {
- return [
- 'data' => $results->items(),
- 'total' => $results->total(),
- 'page' => $results->page(),
- 'perPage' => $results->perPage(),
- 'lastPage' => $results->lastPage(),
- ];
- });
-
- $client = $this->createMock(TestClient::class);
- $client->method('search')->willReturn(new ArrayResponse([
- 'hits' => [
- 'total' => ['value' => 30],
- 'hits' => [['_source' => ['title' => 'test']]],
- ],
- ]));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $results = $index->query()->matchAll()->paginate(1, 10);
- $paginator = $results->toPaginator();
-
- $this->assertEquals([
- 'data' => [['title' => 'test']],
- 'total' => 30,
- 'page' => 1,
- 'perPage' => 10,
- 'lastPage' => 3,
- ], $paginator);
- }
-
- public function testToPaginatorThrowsWithoutResolver()
- {
- $results = new Results([
- 'hits' => ['total' => ['value' => 0], 'hits' => []],
- ]);
-
- $this->expectException(\RuntimeException::class);
- $results->toPaginator();
- }
-
- public function testSetNamedClient()
- {
- $defaultClient = $this->createMock(TestClient::class);
- $logClient = $this->createMock(TestClient::class);
-
- Index::setClient($defaultClient);
- Index::setClient($logClient, 'log');
-
- $products = $this->createIndex('products');
- $this->assertSame($defaultClient, $products->getClient());
-
- $logs = new class('logs') extends Index {
- public function __construct($name)
- {
- $this->name = $name;
- $this->connection = 'log';
- }
- };
- $this->assertSame($logClient, $logs->getClient());
- }
-
- public function testGetClientResolvesByConnection()
- {
- $logClient = $this->createMock(TestClient::class);
- Index::setClient($logClient, 'log');
-
- $logs = new class('logs') extends Index {
- public function __construct($name)
- {
- $this->name = $name;
- $this->connection = 'log';
- }
- };
-
- $this->assertSame($logClient, $logs->getClient());
- }
-
- public function testGetClientFallsBackToDefault()
- {
- $defaultClient = $this->createMock(TestClient::class);
- Index::setClient($defaultClient);
-
- $index = new class('unknown') extends Index {
- public function __construct($name)
- {
- $this->name = $name;
- $this->connection = 'nonexistent';
- }
- };
-
- $this->assertSame($defaultClient, $index->getClient());
- }
-
- public function testGetClientThrowsWhenNotRegistered()
- {
- // Ensure no clients are registered
- $ref = new ReflectionProperty(Index::class, 'clients');
- $ref->setAccessible(true);
- $ref->setValue(null, []);
-
- $this->expectException(\RuntimeException::class);
- $this->expectExceptionMessage('nonexistent');
-
- $index = new class('test') extends Index {
- public function __construct($name)
- {
- $this->name = $name;
- $this->connection = 'nonexistent';
- }
- };
-
- $index->getClient();
- }
-}
diff --git a/tests/Index/ManagerTest.php b/tests/Index/ManagerTest.php
deleted file mode 100644
index d1bdbac..0000000
--- a/tests/Index/ManagerTest.php
+++ /dev/null
@@ -1,253 +0,0 @@
-createMock(TestClient::class));
- }
-
- protected function tearDown(): void
- {
- $ref = new ReflectionProperty(Index::class, 'clients');
- $ref->setAccessible(true);
- $ref->setValue(null, []);
- }
-
- protected function createIndex($name = 'products', $mappings = [], $settings = [])
- {
- return new class($name, $mappings, $settings) extends Index {
- public function __construct($name, $mappings, $settings)
- {
- $this->name = $name;
- $this->mappings = $mappings;
- $this->settings = $settings;
- }
- };
- }
-
- protected function mockIndices($method, $with, $return)
- {
- $wrapped = is_bool($return) ? new BoolResponse($return) : new ArrayResponse($return);
-
- $indices = $this->createMock(TestIndices::class);
- $indices->method('existsAlias')->willReturn(new BoolResponse(false));
- $indices->expects($this->once())->method($method)->with($with)->willReturn($wrapped);
-
- $client = $this->createMock(TestClient::class);
- $client->method('indices')->willReturn($indices);
- Index::setClient($client);
- }
-
- public function testCreate()
- {
- $this->mockIndices('create', [
- 'index' => 'products',
- 'body' => [
- 'settings' => ['number_of_shards' => 3],
- 'mappings' => ['properties' => ['title' => ['type' => 'text']]],
- ],
- ], ['acknowledged' => true]);
-
- $index = $this->createIndex('products', ['properties' => ['title' => ['type' => 'text']]], ['number_of_shards' => 3]);
- $result = (new Manager($index))->create();
-
- $this->assertTrue($result['acknowledged']);
- }
-
- public function testCreateWithoutBody()
- {
- $this->mockIndices('create', [
- 'index' => 'products',
- 'body' => [
- 'mappings' => new \stdClass(),
- 'settings' => new \stdClass(),
- ],
- ], ['acknowledged' => true]);
-
- $index = $this->createIndex('products');
- $result = (new Manager($index))->create();
-
- $this->assertTrue($result['acknowledged']);
- }
-
- public function testDelete()
- {
- $this->mockIndices('delete', ['index' => 'products'], ['acknowledged' => true]);
-
- $index = $this->createIndex('products');
- $result = (new Manager($index))->delete();
-
- $this->assertTrue($result['acknowledged']);
- }
-
- public function testExistsReturnsTrue()
- {
- $this->mockIndices('exists', ['index' => 'products'], true);
-
- $index = $this->createIndex('products');
- $this->assertTrue((new Manager($index))->exists());
- }
-
- public function testExistsReturnsFalse()
- {
- $this->mockIndices('exists', ['index' => 'products'], false);
-
- $index = $this->createIndex('products');
- $this->assertFalse((new Manager($index))->exists());
- }
-
- public function testGet()
- {
- $return = ['products' => ['aliases' => [], 'mappings' => [], 'settings' => []]];
- $this->mockIndices('get', ['index' => 'products'], $return);
-
- $index = $this->createIndex('products');
- $this->assertEquals($return, (new Manager($index))->get());
- }
-
- public function testPutMapping()
- {
- $this->mockIndices('putMapping', [
- 'index' => 'products',
- 'body' => ['properties' => ['title' => ['type' => 'text']]],
- ], ['acknowledged' => true]);
-
- $index = $this->createIndex('products', ['properties' => ['title' => ['type' => 'text']]]);
- (new Manager($index))->putMapping();
- }
-
- public function testGetMapping()
- {
- $return = ['products' => ['mappings' => ['properties' => []]]];
- $this->mockIndices('getMapping', ['index' => 'products'], $return);
-
- $index = $this->createIndex('products');
- $this->assertEquals($return, (new Manager($index))->getMapping());
- }
-
- public function testPutSettings()
- {
- $this->mockIndices('putSettings', [
- 'index' => 'products',
- 'body' => ['index' => ['number_of_replicas' => 2]],
- ], ['acknowledged' => true]);
-
- $index = $this->createIndex('products');
- (new Manager($index))->putSettings(['index' => ['number_of_replicas' => 2]]);
- }
-
- public function testGetSettings()
- {
- $return = ['products' => ['settings' => ['index' => ['number_of_replicas' => '1']]]];
- $this->mockIndices('getSettings', ['index' => 'products'], $return);
-
- $index = $this->createIndex('products');
- $this->assertEquals($return, (new Manager($index))->getSettings());
- }
-
- public function testRefresh()
- {
- $this->mockIndices('refresh', ['index' => 'products'], ['_shards' => ['total' => 1]]);
-
- $index = $this->createIndex('products');
- (new Manager($index))->refresh();
- }
-
- public function testForceMerge()
- {
- $this->mockIndices('forcemerge', ['index' => 'products'], ['_shards' => ['total' => 1]]);
-
- $index = $this->createIndex('products');
- (new Manager($index))->forceMerge();
- }
-
- public function testForceMergeWithOptions()
- {
- $this->mockIndices('forcemerge', [
- 'index' => 'products',
- 'max_num_segments' => 1,
- ], ['_shards' => ['total' => 1]]);
-
- $index = $this->createIndex('products');
- (new Manager($index))->forceMerge(['max_num_segments' => 1]);
- }
-
- public function testClose()
- {
- $this->mockIndices('close', ['index' => 'products'], ['acknowledged' => true]);
-
- $index = $this->createIndex('products');
- (new Manager($index))->close();
- }
-
- public function testOpen()
- {
- $this->mockIndices('open', ['index' => 'products'], ['acknowledged' => true]);
-
- $index = $this->createIndex('products');
- (new Manager($index))->open();
- }
-
- public function testAddAlias()
- {
- $this->mockIndices('putAlias', [
- 'index' => 'products',
- 'name' => 'products_active',
- ], ['acknowledged' => true]);
-
- $index = $this->createIndex('products');
- (new Manager($index))->addAlias('products_active');
- }
-
- public function testAddAliasWithOptions()
- {
- $this->mockIndices('putAlias', [
- 'index' => 'products',
- 'name' => 'products_active',
- 'body' => ['is_write_index' => true],
- ], ['acknowledged' => true]);
-
- $index = $this->createIndex('products');
- (new Manager($index))->addAlias('products_active', ['is_write_index' => true]);
- }
-
- public function testRemoveAlias()
- {
- $this->mockIndices('deleteAlias', [
- 'index' => 'products',
- 'name' => 'products_active',
- ], ['acknowledged' => true]);
-
- $index = $this->createIndex('products');
- (new Manager($index))->removeAlias('products_active');
- }
-
- public function testSwapAlias()
- {
- $this->mockIndices('updateAliases', [
- 'body' => [
- 'actions' => [
- ['remove' => ['index' => 'products_v1', 'alias' => 'products_active']],
- ['add' => ['index' => 'products', 'alias' => 'products_active']],
- ],
- ],
- ], ['acknowledged' => true]);
-
- $index = $this->createIndex('products');
- (new Manager($index))->swapAlias('products_active', 'products_v1');
- }
-
- public function testGetAliases()
- {
- $return = ['products' => ['aliases' => ['products_active' => []]]];
- $this->mockIndices('getAlias', ['index' => 'products'], $return);
-
- $index = $this->createIndex('products');
- $this->assertEquals($return, (new Manager($index))->getAliases());
- }
-}
diff --git a/tests/Index/RebuildTest.php b/tests/Index/RebuildTest.php
deleted file mode 100644
index 7e8b79c..0000000
--- a/tests/Index/RebuildTest.php
+++ /dev/null
@@ -1,390 +0,0 @@
-createMock(TestClient::class));
- }
-
- protected function tearDown(): void
- {
- $ref = new ReflectionProperty(Index::class, 'clients');
- $ref->setAccessible(true);
- $ref->setValue(null, []);
- }
-
- protected function createIndex($name = 'products', $mappings = [], $settings = [])
- {
- return new class($name, $mappings, $settings) extends Index {
- public function __construct($name, $mappings, $settings)
- {
- $this->name = $name;
- $this->mappings = $mappings;
- $this->settings = $settings;
- }
- };
- }
-
- public function testRunCreatesBackingIndexAndSetsAlias()
- {
- $indices = $this->createMock(TestIndices::class);
- $indices->expects($this->once())->method('create')->with($this->callback(function ($params) {
- return strpos($params['index'], 'products_') === 0
- && $params['body']['mappings'] === ['properties' => ['title' => ['type' => 'text']]]
- && $params['body']['settings'] === ['number_of_shards' => 1];
- }))->willReturn(new ArrayResponse(['acknowledged' => true]));
-
- $indices->method('existsAlias')->willReturn(new BoolResponse(false));
- $indices->method('exists')->willReturn(new BoolResponse(false));
- $indices->expects($this->once())->method('putAlias')->with($this->callback(function ($params) {
- return strpos($params['index'], 'products_') === 0 && $params['name'] === 'products';
- }))->willReturn(new ArrayResponse(['acknowledged' => true]));
-
- $client = $this->createMock(TestClient::class);
- $client->method('indices')->willReturn($indices);
- $client->expects($this->once())->method('bulk')->with($this->callback(function ($params) {
- return count($params['body']) === 4
- && $params['body'][0]['index']['_id'] === 1
- && $params['body'][2]['index']['_id'] === 2;
- }))->willReturn(new ArrayResponse(['items' => []]));
- Index::setClient($client);
-
- $index = new class extends Index {
- public function __construct()
- {
- $this->name = 'products';
- $this->mappings = ['properties' => ['title' => ['type' => 'text']]];
- $this->settings = ['number_of_shards' => 1];
- }
-
- public function source(array $context = []): iterable
- {
- yield 1 => ['title' => 'A'];
- yield 2 => ['title' => 'B'];
- }
- };
-
- $result = (new Rebuild($index))->run();
- $this->assertStringStartsWith('products_', $result['newIndex']);
- $this->assertNull($result['oldIndex']);
- }
-
- public function testRunSwapsAliasAtomically()
- {
- $indices = $this->createMock(TestIndices::class);
- $indices->expects($this->once())->method('create')->willReturn(new ArrayResponse(['acknowledged' => true]));
- $indices->method('existsAlias')->willReturn(new BoolResponse(true));
- $indices->method('getAlias')->willReturn(new ArrayResponse(['products_v1' => ['aliases' => ['products' => []]]]));
- $indices->expects($this->once())->method('updateAliases')->with($this->callback(function ($params) {
- $actions = $params['body']['actions'];
- return $actions[0]['remove']['index'] === 'products_v1'
- && $actions[0]['remove']['alias'] === 'products'
- && strpos($actions[1]['add']['index'], 'products_') === 0
- && $actions[1]['add']['alias'] === 'products';
- }))->willReturn(new ArrayResponse(['acknowledged' => true]));
-
- $client = $this->createMock(TestClient::class);
- $client->method('indices')->willReturn($indices);
- $client->method('bulk')->willReturn(new ArrayResponse(['items' => []]));
- Index::setClient($client);
-
- $index = new class extends Index {
- public function __construct()
- {
- $this->name = 'products';
- }
-
- public function source(array $context = []): iterable
- {
- return [];
- }
- };
-
- $result = (new Rebuild($index))->allowEmpty()->run();
- $this->assertEquals('products_v1', $result['oldIndex']);
- }
-
- public function testRunThrowsWhenNameIsRealIndex()
- {
- $indices = $this->createMock(TestIndices::class);
- $indices->method('create')->willReturn(new ArrayResponse(['acknowledged' => true]));
- $indices->method('existsAlias')->willReturn(new BoolResponse(false));
- $indices->method('exists')->willReturn(new BoolResponse(true));
- $indices->method('delete')->willReturn(new ArrayResponse(['acknowledged' => true]));
-
- $client = $this->createMock(TestClient::class);
- $client->method('indices')->willReturn($indices);
- $client->method('bulk')->willReturn(new ArrayResponse(['items' => []]));
- Index::setClient($client);
-
- $index = new class extends Index {
- public function __construct()
- {
- $this->name = 'products';
- }
-
- public function source(array $context = []): iterable
- {
- return [];
- }
- };
-
- $this->expectException(\RuntimeException::class);
- $this->expectExceptionMessage('is a real index, not an alias');
- (new Rebuild($index))->allowEmpty()->run();
- }
-
- public function testRunWithBatchSize()
- {
- $indices = $this->createMock(TestIndices::class);
- $indices->method('create')->willReturn(new ArrayResponse(['acknowledged' => true]));
- $indices->method('existsAlias')->willReturn(new BoolResponse(false));
- $indices->method('exists')->willReturn(new BoolResponse(false));
- $indices->method('putAlias')->willReturn(new ArrayResponse(['acknowledged' => true]));
-
- $client = $this->createMock(TestClient::class);
- $client->method('indices')->willReturn($indices);
- $client->expects($this->exactly(2))->method('bulk')->willReturn(new ArrayResponse(['items' => []]));
- Index::setClient($client);
-
- $index = new class extends Index {
- public function __construct()
- {
- $this->name = 'products';
- }
-
- public function source(array $context = []): iterable
- {
- yield 1 => ['title' => 'A'];
- yield 2 => ['title' => 'B'];
- yield 3 => ['title' => 'C'];
- }
- };
-
- (new Rebuild($index))->batchSize(2)->run();
- }
-
- public function testRunWithCustomSource()
- {
- $indices = $this->createMock(TestIndices::class);
- $indices->method('create')->willReturn(new ArrayResponse(['acknowledged' => true]));
- $indices->method('existsAlias')->willReturn(new BoolResponse(false));
- $indices->method('exists')->willReturn(new BoolResponse(false));
- $indices->method('putAlias')->willReturn(new ArrayResponse(['acknowledged' => true]));
-
- $client = $this->createMock(TestClient::class);
- $client->method('indices')->willReturn($indices);
- $client->expects($this->once())->method('bulk')->willReturn(new ArrayResponse(['items' => []]));
- Index::setClient($client);
-
- $index = $this->createIndex('products');
-
- $result = (new Rebuild($index))->source([
- 1 => ['title' => 'A'],
- 2 => ['title' => 'B'],
- ])->run();
-
- $this->assertStringStartsWith('products_', $result['newIndex']);
- }
-
- public function testRunWithCustomRealName()
- {
- $indices = $this->createMock(TestIndices::class);
- $indices->expects($this->once())->method('create')->with($this->callback(function ($params) {
- return strpos($params['index'], 'products_v') === 0;
- }))->willReturn(new ArrayResponse(['acknowledged' => true]));
- $indices->method('existsAlias')->willReturn(new BoolResponse(false));
- $indices->method('exists')->willReturn(new BoolResponse(false));
- $indices->method('putAlias')->willReturn(new ArrayResponse(['acknowledged' => true]));
-
- $client = $this->createMock(TestClient::class);
- $client->method('indices')->willReturn($indices);
- $client->method('bulk')->willReturn(new ArrayResponse(['items' => []]));
- Index::setClient($client);
-
- $index = new class extends Index {
- public function __construct()
- {
- $this->name = 'products';
- }
-
- public function rebuildName(): string
- {
- return $this->name . '_v' . time();
- }
- };
-
- $result = (new Rebuild($index))->source(function () { return []; })->allowEmpty()->run();
- }
-
- public function testCleanDeletesSpecificIndex()
- {
- $indices = $this->createMock(TestIndices::class);
- $indices->expects($this->once())->method('delete')->with($this->callback(function ($params) {
- return $params['index'] === 'products_20250522_090000';
- }))->willReturn(new ArrayResponse(['acknowledged' => true]));
-
- $client = $this->createMock(TestClient::class);
- $client->method('indices')->willReturn($indices);
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- (new Rebuild($index))->clean('products_20250522_090000');
- }
-
- public function testRollbackToSpecificIndex()
- {
- $indices = $this->createMock(TestIndices::class);
- $indices->method('exists')->willReturn(new BoolResponse(true));
- $indices->method('getAlias')->willReturnCallback(function ($params) {
- if (($params['name'] ?? null) === 'products') {
- return new ArrayResponse(['products_20250523_143000' => ['aliases' => ['products' => []]]]);
- }
- return new ArrayResponse([]);
- });
- $indices->expects($this->once())->method('updateAliases')->with($this->callback(function ($params) {
- $actions = $params['body']['actions'];
- return $actions[0]['remove']['index'] === 'products_20250523_143000'
- && $actions[0]['remove']['alias'] === 'products'
- && $actions[1]['add']['index'] === 'products_20250520_080000'
- && $actions[1]['add']['alias'] === 'products';
- }))->willReturn(new ArrayResponse(['acknowledged' => true]));
-
- $client = $this->createMock(TestClient::class);
- $client->method('indices')->willReturn($indices);
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- $rolledBack = (new Rebuild($index))->rollback('products_20250520_080000');
-
- $this->assertEquals('products_20250523_143000', $rolledBack);
- }
-
- public function testRollbackThrowsWhenNoAlias()
- {
- $this->expectException(\RuntimeException::class);
-
- $indices = $this->createMock(TestIndices::class);
- $indices->method('getAlias')->willReturn(new ArrayResponse([]));
-
- $client = $this->createMock(TestClient::class);
- $client->method('indices')->willReturn($indices);
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- (new Rebuild($index))->rollback('products_20250520_080000');
- }
-
- public function testRollbackThrowsWhenTargetNotExist()
- {
- $this->expectException(\RuntimeException::class);
-
- $indices = $this->createMock(TestIndices::class);
- $indices->method('exists')->willReturn(new BoolResponse(false));
- $indices->method('getAlias')->willReturnCallback(function ($params) {
- if (($params['name'] ?? null) === 'products') {
- return new ArrayResponse(['products_20250523_143000' => ['aliases' => ['products' => []]]]);
- }
- return new ArrayResponse([]);
- });
-
- $client = $this->createMock(TestClient::class);
- $client->method('indices')->willReturn($indices);
- Index::setClient($client);
-
- $index = $this->createIndex('products');
- (new Rebuild($index))->rollback('products_20250520_080000');
- }
-
- public function testRunThrowsOnEmptyImport()
- {
- $indices = $this->createMock(TestIndices::class);
- $indices->method('create')->willReturn(new ArrayResponse(['acknowledged' => true]));
- $indices->method('delete')->with($this->callback(function ($params) {
- return strpos($params['index'], 'products_') === 0;
- }))->willReturn(new ArrayResponse(['acknowledged' => true]));
-
- $client = $this->createMock(TestClient::class);
- $client->method('indices')->willReturn($indices);
- Index::setClient($client);
-
- $index = new class extends Index {
- public function __construct()
- {
- $this->name = 'products';
- }
-
- public function source(array $context = []): iterable
- {
- return [];
- }
- };
-
- $this->expectException(\RuntimeException::class);
- $this->expectExceptionMessage('Rebuild imported 0 documents');
- (new Rebuild($index))->run();
- }
-
- public function testRunAllowsEmptyImportWithAllowEmpty()
- {
- $indices = $this->createMock(TestIndices::class);
- $indices->method('create')->willReturn(new ArrayResponse(['acknowledged' => true]));
- $indices->method('existsAlias')->willReturn(new BoolResponse(false));
- $indices->method('exists')->willReturn(new BoolResponse(false));
- $indices->method('putAlias')->willReturn(new ArrayResponse(['acknowledged' => true]));
-
- $client = $this->createMock(TestClient::class);
- $client->method('indices')->willReturn($indices);
- $client->method('bulk')->willReturn(new ArrayResponse(['items' => []]));
- Index::setClient($client);
-
- $index = new class extends Index {
- public function __construct()
- {
- $this->name = 'products';
- }
-
- public function source(array $context = []): iterable
- {
- return [];
- }
- };
-
- $result = (new Rebuild($index))->allowEmpty()->run();
- $this->assertStringStartsWith('products_', $result['newIndex']);
- }
-
- public function testRunDeletesNewIndexOnImportFailure()
- {
- $indices = $this->createMock(TestIndices::class);
- $indices->method('create')->willReturn(new ArrayResponse(['acknowledged' => true]));
- $indices->expects($this->once())->method('delete')->with($this->callback(function ($params) {
- return strpos($params['index'], 'products_') === 0;
- }))->willReturn(new ArrayResponse(['acknowledged' => true]));
-
- $client = $this->createMock(TestClient::class);
- $client->method('indices')->willReturn($indices);
- $client->method('bulk')->willReturn(new ArrayResponse(['items' => [], 'errors' => true]));
- Index::setClient($client);
-
- $index = new class extends Index {
- public function __construct()
- {
- $this->name = 'products';
- }
-
- public function source(array $context = []): iterable
- {
- yield 1 => ['title' => 'A'];
- }
- };
-
- $this->expectException(\RuntimeException::class);
- (new Rebuild($index))->run();
- }
-}
diff --git a/tests/Index/ResultsTest.php b/tests/Index/ResultsTest.php
deleted file mode 100644
index 76fb5d5..0000000
--- a/tests/Index/ResultsTest.php
+++ /dev/null
@@ -1,232 +0,0 @@
- [
- 'total' => ['value' => 42],
- 'hits' => [],
- ],
- ]);
- $this->assertEquals(42, $results->total());
- }
-
- public function testTotalDefaultsToZero()
- {
- $results = new Results([]);
- $this->assertEquals(0, $results->total());
- }
-
- public function testHits()
- {
- $hits = [
- ['_id' => '1', '_source' => ['title' => 'foo']],
- ['_id' => '2', '_source' => ['title' => 'bar']],
- ];
- $results = new Results([
- 'hits' => ['total' => ['value' => 2], 'hits' => $hits],
- ]);
- $this->assertEquals($hits, $results->hits());
- }
-
- public function testHitsDefaultsToEmpty()
- {
- $results = new Results([]);
- $this->assertEquals([], $results->hits());
- }
-
- public function testDocs()
- {
- $results = new Results([
- 'hits' => [
- 'total' => ['value' => 2],
- 'hits' => [
- ['_id' => '1', '_source' => ['title' => 'foo']],
- ['_id' => '2', '_source' => ['title' => 'bar']],
- ],
- ],
- ]);
- $this->assertEquals([['title' => 'foo'], ['title' => 'bar']], $results->docs());
- }
-
- public function testDocsDefaultsToEmpty()
- {
- $results = new Results([]);
- $this->assertEquals([], $results->docs());
- }
-
- public function testAggregations()
- {
- $aggs = ['price_avg' => ['value' => 100.5]];
- $results = new Results([
- 'hits' => ['total' => ['value' => 0], 'hits' => []],
- 'aggregations' => $aggs,
- ]);
- $this->assertEquals($aggs, $results->aggregations());
- }
-
- public function testAggregationsDefaultsToEmpty()
- {
- $results = new Results(['hits' => []]);
- $this->assertEquals([], $results->aggregations());
- }
-
- public function testScrollId()
- {
- $results = new Results([
- '_scroll_id' => 'abc123',
- 'hits' => ['total' => ['value' => 0], 'hits' => []],
- ]);
- $this->assertEquals('abc123', $results->scrollId());
- }
-
- public function testScrollIdDefaultsToNull()
- {
- $results = new Results([]);
- $this->assertNull($results->scrollId());
- }
-
- public function testHasMore()
- {
- $results = new Results([
- 'hits' => [
- 'total' => ['value' => 1],
- 'hits' => [['_source' => ['title' => 'foo']]],
- ],
- ]);
- $this->assertTrue($results->hasMore());
- }
-
- public function testHasMoreReturnsFalseWhenEmpty()
- {
- $results = new Results([
- 'hits' => ['total' => ['value' => 0], 'hits' => []],
- ]);
- $this->assertFalse($results->hasMore());
- }
-
- public function testRaw()
- {
- $response = [
- 'took' => 5,
- 'hits' => ['total' => ['value' => 1], 'hits' => []],
- ];
- $results = new Results($response);
- $this->assertEquals($response, $results->raw());
- }
-
- public function testPaginateSetsMetadata()
- {
- $results = new Results([
- 'hits' => ['total' => ['value' => 50], 'hits' => []],
- ]);
- $results->paginate(3, 10);
-
- $this->assertEquals(3, $results->page());
- $this->assertEquals(10, $results->perPage());
- $this->assertEquals(5, $results->lastPage());
- }
-
- public function testLastPageMinimumIsOne()
- {
- $results = new Results([
- 'hits' => ['total' => ['value' => 0], 'hits' => []],
- ]);
- $results->paginate(1, 15);
-
- $this->assertEquals(1, $results->lastPage());
- }
-
- public function testItemsReturnsDocs()
- {
- $results = new Results([
- 'hits' => [
- 'total' => ['value' => 2],
- 'hits' => [
- ['_id' => '1', '_source' => ['title' => 'foo']],
- ['_id' => '2', '_source' => ['title' => 'bar']],
- ],
- ],
- ]);
- $this->assertEquals($results->docs(), $results->items());
- }
-
- public function testIsEmpty()
- {
- $results = new Results([
- 'hits' => ['total' => ['value' => 0], 'hits' => []],
- ]);
- $this->assertTrue($results->isEmpty());
-
- $results = new Results([
- 'hits' => [
- 'total' => ['value' => 1],
- 'hits' => [['_source' => ['title' => 'foo']]],
- ],
- ]);
- $this->assertFalse($results->isEmpty());
- }
-
- public function testToPaginatorCallsResolver()
- {
- Index::setPaginatorResolver(function (Results $results) {
- return ['total' => $results->total(), 'page' => $results->page()];
- });
-
- $results = new Results([
- 'hits' => ['total' => ['value' => 50], 'hits' => []],
- ]);
- $results->paginate(2, 10);
-
- $paginator = $results->toPaginator();
- $this->assertEquals(['total' => 50, 'page' => 2], $paginator);
-
- // Clean up
- $ref = new ReflectionProperty(Index::class, 'paginatorResolver');
- $ref->setAccessible(true);
- $ref->setValue(null, null);
- }
-
- public function testToPaginatorThrowsWithoutResolver()
- {
- $results = new Results(['hits' => []]);
- $this->expectException(\RuntimeException::class);
- $results->toPaginator();
- }
-
- public function testTook()
- {
- $results = new Results([
- 'took' => 5,
- 'hits' => ['total' => ['value' => 1], 'hits' => []],
- ]);
- $this->assertEquals(5, $results->took());
- }
-
- public function testTookDefaultsToZero()
- {
- $results = new Results([]);
- $this->assertEquals(0, $results->took());
- }
-
- public function testTimedOut()
- {
- $results = new Results([
- 'timed_out' => true,
- 'hits' => ['total' => ['value' => 0], 'hits' => []],
- ]);
- $this->assertTrue($results->timedOut());
- }
-
- public function testTimedOutDefaultsToFalse()
- {
- $results = new Results([]);
- $this->assertFalse($results->timedOut());
- }
-}
diff --git a/tests/InputValidationTest.php b/tests/InputValidationTest.php
new file mode 100644
index 0000000..d9194e1
--- /dev/null
+++ b/tests/InputValidationTest.php
@@ -0,0 +1,38 @@
+expectException(\BadMethodCallException::class);
+ (new Query())->aggs(null, ['avg' => ['field' => 'price']]);
+ }
+
+ public function testAggsRejectsEmptyStringAlias()
+ {
+ $this->expectException(\BadMethodCallException::class);
+ (new Query())->aggs('', ['avg' => ['field' => 'price']]);
+ }
+
+ public function testWhenTreatsStringAsTruthyNotInvoked()
+ {
+ // 'count' must NOT be invoked as a function; it is a truthy value.
+ $query = (new Query())->when('count', fn (Query $q) => $q->match('title', 'x'));
+
+ $this->assertNotEmpty($query->getQueries());
+ }
+
+ public function testWhenFalseSkipsTheClause()
+ {
+ $query = (new Query())->when(false, fn (Query $q) => $q->match('title', 'x'));
+
+ $this->assertEmpty($query->getQueries());
+ }
+}
diff --git a/tests/Integration/Dsl/AggregationContractTest.php b/tests/Integration/Dsl/AggregationContractTest.php
new file mode 100644
index 0000000..8ed0469
--- /dev/null
+++ b/tests/Integration/Dsl/AggregationContractTest.php
@@ -0,0 +1,65 @@
+matchAll();
+ $q->aggs('by_status', ['terms' => ['field' => 'status']]);
+ $r = $this->assertQueryEs($q);
+ $buckets = array_column($r['aggregations']['by_status']['buckets'] ?? [], null, 'key');
+ $this->assertSame(2, $buckets['published']['doc_count'] ?? null);
+ $this->assertSame(1, $buckets['draft']['doc_count'] ?? null);
+ }
+
+ public function testSumAgg(): void
+ {
+ $q = (new Query())->matchAll();
+ $q->aggs('total_price', ['sum' => ['field' => 'price']]);
+ $r = $this->assertQueryEs($q);
+ $this->assertEquals(90.0, $r['aggregations']['total_price']['value'] ?? null);
+ }
+
+ public function testAvgAgg(): void
+ {
+ $q = (new Query())->matchAll();
+ $q->aggs('avg_score', ['avg' => ['field' => 'score']]);
+ $r = $this->assertQueryEs($q);
+ // (8.5 + 6.0 + 7.0) / 3 ≈ 7.17
+ $this->assertEqualsWithDelta(7.17, $r['aggregations']['avg_score']['value'] ?? 0, 0.01);
+ }
+
+ public function testStatsAgg(): void
+ {
+ $q = (new Query())->matchAll();
+ $q->aggs('price_stats', ['stats' => ['field' => 'price']]);
+ $r = $this->assertQueryEs($q);
+ $stats = $r['aggregations']['price_stats'] ?? [];
+ $this->assertSame(3, $stats['count'] ?? null);
+ $this->assertEquals(25.0, $stats['min'] ?? null);
+ $this->assertEquals(35.0, $stats['max'] ?? null);
+ }
+
+ public function testCardinalityAgg(): void
+ {
+ $q = (new Query())->matchAll();
+ $q->aggs('authors', ['cardinality' => ['field' => 'author']]);
+ $r = $this->assertQueryEs($q);
+ $this->assertSame(2, $r['aggregations']['authors']['value'] ?? null);
+ }
+
+ public function testDateHistogramAgg(): void
+ {
+ $q = (new Query())->matchAll();
+ $q->aggs('by_month', ['date_histogram' => ['field' => 'created', 'calendar_interval' => 'month']]);
+ $r = $this->assertQueryEs($q);
+ $this->assertCount(3, $r['aggregations']['by_month']['buckets'] ?? []);
+ }
+}
diff --git a/tests/Integration/Dsl/CompoundContractTest.php b/tests/Integration/Dsl/CompoundContractTest.php
new file mode 100644
index 0000000..2649eea
--- /dev/null
+++ b/tests/Integration/Dsl/CompoundContractTest.php
@@ -0,0 +1,76 @@
+ doc 3
+ $q = (new Query())->bool(function (Boolean $b) {
+ $b->must(function (Query $q) {
+ $q->term('status', 'published');
+ })->must(function (Query $q) {
+ $q->term('color', 'green');
+ });
+ });
+ $this->assertQueryEs($q, 1);
+ }
+
+ public function testBoolShould(): void
+ {
+ // alice (1,3) OR bob (2) -> 3
+ $q = (new Query())->bool(function (Boolean $b) {
+ $b->should(function (Query $q) {
+ $q->term('author', 'alice');
+ })->should(function (Query $q) {
+ $q->term('author', 'bob');
+ })->minimumShouldMatch(1);
+ });
+ $this->assertQueryEs($q, 3);
+ }
+
+ public function testBoolFilter(): void
+ {
+ // price >= 30 -> docs 2,3
+ $q = (new Query())->bool(function (Boolean $b) {
+ $b->filter(function (Query $q) {
+ $q->range('price', function (Range $r) {
+ $r->gte(30);
+ });
+ });
+ });
+ $this->assertQueryEs($q, 2);
+ }
+
+ public function testBoolMustNot(): void
+ {
+ // all except published -> doc 2 (draft)
+ $q = (new Query())->bool(function (Boolean $b) {
+ $b->must(function (Query $q) {
+ $q->matchAll();
+ })->mustNot(function (Query $q) {
+ $q->term('status', 'published');
+ });
+ });
+ $this->assertQueryEs($q, 1);
+ }
+
+ public function testConstantScore(): void
+ {
+ $q = (new Query())->constantScore(function (ConstantScore $c) {
+ $c->filter(function (Query $q) {
+ $q->term('status', 'published');
+ });
+ });
+ $this->assertQueryEs($q, 2);
+ }
+}
diff --git a/tests/Integration/Dsl/FullTextContractTest.php b/tests/Integration/Dsl/FullTextContractTest.php
new file mode 100644
index 0000000..7d768e1
--- /dev/null
+++ b/tests/Integration/Dsl/FullTextContractTest.php
@@ -0,0 +1,42 @@
+assertQueryEs((new Query())->match('content', 'elasticsearch'), 2);
+ }
+
+ public function testMatchPhrase(): void
+ {
+ // "database design" appears in docs 1 and 3 content
+ $this->assertQueryEs((new Query())->matchPhrase('content', ['query' => 'database design']), 2);
+ }
+
+ public function testQueryString(): void
+ {
+ $q = (new Query())->queryString(function (QueryString $qs) {
+ $qs->query('status:published AND color:green');
+ });
+ $this->assertQueryEs($q, 1);
+ }
+
+ public function testMatchBoolPrefix(): void
+ {
+ // authors starting with "al" -> alice (docs 1,3)
+ $q = (new Query())->matchBoolPrefix('author', function (MatchBoolPrefix $m) {
+ $m->query('al');
+ });
+ $this->assertQueryEs($q, 2);
+ }
+}
diff --git a/tests/Integration/Dsl/GeoContractTest.php b/tests/Integration/Dsl/GeoContractTest.php
new file mode 100644
index 0000000..545d0f7
--- /dev/null
+++ b/tests/Integration/Dsl/GeoContractTest.php
@@ -0,0 +1,31 @@
+ doc 1 (0km), doc 2 (~115km); doc 3 (~350km) excluded
+ $q = (new Query())->geoDistance(function (GeoDistance $g) {
+ $g->distance('200km')->location('location', ['lat' => 40.7, 'lon' => -74.0]);
+ });
+ $this->assertQueryEs($q, 2);
+ }
+
+ public function testGeoBoundingBox(): void
+ {
+ // box covering lat 38-42, lon -75..-70 -> all 3 docs
+ $q = (new Query())->geoBoundingBox('location', function (GeoBoundingBox $g) {
+ $g->topLeft(['lat' => 42, 'lon' => -75])->bottomRight(['lat' => 38, 'lon' => -70]);
+ });
+ $this->assertQueryEs($q, 3);
+ }
+}
diff --git a/tests/Integration/Dsl/JoiningContractTest.php b/tests/Integration/Dsl/JoiningContractTest.php
new file mode 100644
index 0000000..7afe03e
--- /dev/null
+++ b/tests/Integration/Dsl/JoiningContractTest.php
@@ -0,0 +1,34 @@
+ doc 3 ("Very helpful")
+ $q = (new Query())->nested(function (Nested $n) {
+ $n->path('comments')->query(function (Query $q) {
+ $q->match('comments.content', 'helpful');
+ });
+ });
+ $this->assertQueryEs($q, 1);
+ }
+
+ public function testNestedTerm(): void
+ {
+ // bob in comments.author -> docs 2, 3
+ $q = (new Query())->nested(function (Nested $n) {
+ $n->path('comments')->query(function (Query $q) {
+ $q->term('comments.author', 'bob');
+ });
+ });
+ $this->assertQueryEs($q, 2);
+ }
+}
diff --git a/tests/Integration/Dsl/SpecializedContractTest.php b/tests/Integration/Dsl/SpecializedContractTest.php
new file mode 100644
index 0000000..2a7c210
--- /dev/null
+++ b/tests/Integration/Dsl/SpecializedContractTest.php
@@ -0,0 +1,39 @@
+ 29 -> docs 2 (30), 3 (35)
+ $q = (new Query())->script(function (ScriptQuery $s) {
+ $s->script(function (Script $script) {
+ $script->source("doc['price'].value > 29");
+ });
+ });
+ $this->assertQueryEs($q, 2);
+ }
+
+ public function testScriptScore(): void
+ {
+ // score = doc['score'].value; doc 1 (8.5) ranks first
+ $q = (new Query())->scriptScore(function (ScriptScore $ss) {
+ $ss->query(function (Query $query) {
+ $query->matchAll();
+ })->script(function (Script $script) {
+ $script->source("doc['score'].value");
+ });
+ });
+ $r = $this->assertQueryEs($q, 3);
+ $this->assertSame('1', $r['hits']['hits'][0]['_id']);
+ }
+}
diff --git a/tests/Integration/Dsl/TermLevelContractTest.php b/tests/Integration/Dsl/TermLevelContractTest.php
new file mode 100644
index 0000000..1bb3f7f
--- /dev/null
+++ b/tests/Integration/Dsl/TermLevelContractTest.php
@@ -0,0 +1,67 @@
+assertQueryEs((new Query())->term('status', 'published'), 2);
+ }
+
+ public function testTerms(): void
+ {
+ $this->assertQueryEs((new Query())->terms('color', ['red', 'blue']), 2);
+ }
+
+ public function testRange(): void
+ {
+ $q = (new Query())->range('price', function (Range $r) {
+ $r->gte(25)->lte(30);
+ });
+ $this->assertQueryEs($q, 2);
+ }
+
+ public function testExists(): void
+ {
+ $this->assertQueryEs((new Query())->exists('category'), 3);
+ }
+
+ public function testPrefix(): void
+ {
+ $q = (new Query())->prefix('author', function (Prefix $p) {
+ $p->value('al');
+ });
+ $this->assertQueryEs($q, 2);
+ }
+
+ public function testWildcard(): void
+ {
+ $q = (new Query())->wildcard('author', function (Wildcard $w) {
+ $w->value('b*');
+ });
+ $this->assertQueryEs($q, 1);
+ }
+
+ public function testFuzzy(): void
+ {
+ $q = (new Query())->fuzzy('author', function (Fuzzy $f) {
+ $f->value('alic');
+ });
+ $this->assertQueryEs($q, 2);
+ }
+
+ public function testIds(): void
+ {
+ $this->assertQueryEs((new Query())->ids(['1', '3']), 2);
+ }
+}
diff --git a/tests/Integration/Index/BulkContractTest.php b/tests/Integration/Index/BulkContractTest.php
new file mode 100644
index 0000000..a3540d4
--- /dev/null
+++ b/tests/Integration/Index/BulkContractTest.php
@@ -0,0 +1,116 @@
+makeIndex();
+ (new Bulk($index))->index('10', ['title' => 'bulk doc'])->flush();
+ $this->refreshIndex();
+ $this->assertTrue($index->newDoc('10')->exists());
+ }
+
+ public function testCreate(): void
+ {
+ $index = $this->makeIndex();
+ (new Bulk($index))->create('11', ['title' => 'created'])->flush();
+ $this->refreshIndex();
+ $this->assertTrue($index->newDoc('11')->exists());
+ }
+
+ public function testUpdate(): void
+ {
+ $index = $this->makeIndex();
+ (new Bulk($index))->update('1', ['price' => 88])->flush();
+ $this->refreshIndex();
+ $this->assertEquals(88, $index->newDoc('1')->source()['price']);
+ }
+
+ public function testDelete(): void
+ {
+ $index = $this->makeIndex();
+ (new Bulk($index))->delete('1')->flush();
+ $this->refreshIndex();
+ $this->assertFalse($index->newDoc('1')->exists());
+ }
+
+ public function testBatchSizeAutoFlush(): void
+ {
+ $index = $this->makeIndex();
+ $bulk = (new Bulk($index))->batchSize(2);
+ $bulk->index('20', ['title' => 'a']);
+ $bulk->index('21', ['title' => 'b']); // auto-flush at 2
+ $bulk->index('22', ['title' => 'c']);
+ $bulk->flush(); // tail
+ $this->refreshIndex();
+ $this->assertTrue($index->newDoc('20')->exists());
+ $this->assertTrue($index->newDoc('21')->exists());
+ $this->assertTrue($index->newDoc('22')->exists());
+ }
+
+ public function testOnErrorReceivesFailures(): void
+ {
+ // create on the already-seeded id '1' triggers a bulk error -> onError
+ $index = $this->makeIndex();
+ $received = null;
+ (new Bulk($index))
+ ->onError(function ($response) use (&$received) {
+ $received = $response;
+ })
+ ->create('1', ['title' => 'dup'])
+ ->flush();
+ $this->assertTrue($received['errors'] ?? false);
+ }
+
+ public function testSaveIsAliasForIndex(): void
+ {
+ $index = $this->makeIndex();
+ (new Bulk($index))->save('30', ['title' => 'saved'])->flush();
+ $this->refreshIndex();
+ $this->assertTrue($index->newDoc('30')->exists());
+ }
+
+ public function testTargetWritesToTargetIndex(): void
+ {
+ $index = $this->makeIndex();
+ (new Bulk($index))->target($this->indexName)->index('31', ['title' => 'targeted'])->flush();
+ $this->refreshIndex();
+ $this->assertTrue($index->newDoc('31')->exists());
+ }
+
+ public function testEmptyFlushReturnsEmptyArray(): void
+ {
+ $result = (new Bulk($this->makeIndex()))->flush();
+ $this->assertSame([], $result);
+ }
+
+ public function testOnErrorCanResendFailures(): void
+ {
+ $index = $this->makeIndex();
+ // create on existing id '1' fails; onError re-sends as index() (overwrite)
+ $resendOk = false;
+ (new Bulk($index))
+ ->onError(function ($response, $body, $newbulk) use (&$resendOk) {
+ foreach ($response['items'] as $i => $item) {
+ $meta = $item[array_key_first($item)];
+ if (($meta['status'] ?? 200) >= 400) {
+ $newbulk->index($meta['_id'], $body[$i * 2 + 1]);
+ }
+ }
+ $newbulk->flush();
+ $resendOk = true;
+ })
+ ->create('1', ['title' => 'resend'])
+ ->flush();
+ $this->assertTrue($resendOk);
+ $this->refreshIndex();
+ $this->assertSame('resend', $index->newDoc('1')->source()['title']);
+ }
+}
diff --git a/tests/Integration/Index/DocContractTest.php b/tests/Integration/Index/DocContractTest.php
new file mode 100644
index 0000000..67a5205
--- /dev/null
+++ b/tests/Integration/Index/DocContractTest.php
@@ -0,0 +1,113 @@
+makeIndex();
+ $index->newDoc('7')->index(['title' => 'new doc']);
+ $this->refreshIndex();
+ $doc = $index->newDoc('7')->get();
+ $this->assertSame('7', $doc['_id']);
+ $this->assertSame(['title' => 'new doc'], $doc['_source']);
+ }
+
+ public function testSource(): void
+ {
+ $source = $this->makeIndex()->newDoc('1')->source();
+ $this->assertSame('Elasticsearch Guide', $source['title']);
+ }
+
+ public function testExists(): void
+ {
+ $index = $this->makeIndex();
+ $this->assertTrue($index->newDoc('1')->exists());
+ $this->assertFalse($index->newDoc('999')->exists());
+ }
+
+ public function testUpdate(): void
+ {
+ $index = $this->makeIndex();
+ $index->newDoc('1')->update(['price' => 99]);
+ $this->refreshIndex();
+ $this->assertEquals(99, $index->newDoc('1')->source()['price']);
+ }
+
+ public function testUpsert(): void
+ {
+ $index = $this->makeIndex();
+ $index->newDoc('999')->update(['title' => 'upserted'], true);
+ $this->refreshIndex();
+ $this->assertTrue($index->newDoc('999')->exists());
+ }
+
+ public function testCreateConflict(): void
+ {
+ $index = $this->makeIndex();
+ try {
+ $index->newDoc('1')->create(['title' => 'duplicate']);
+ $this->fail('Expected version conflict for create on existing id');
+ } catch (\Throwable $e) {
+ $this->assertStringContainsString('version_conflict', $e->getMessage());
+ }
+ }
+
+ public function testDelete(): void
+ {
+ $index = $this->makeIndex();
+ $index->newDoc('1')->delete();
+ $this->refreshIndex();
+ $this->assertFalse($index->newDoc('1')->exists());
+ }
+
+ public function testAutoId(): void
+ {
+ $result = $this->makeIndex()->newDoc(null)->index(['title' => 'auto']);
+ $this->refreshIndex();
+ $this->assertNotEmpty($result['_id'] ?? null);
+ }
+
+ public function testUpdateRequiresId(): void
+ {
+ $this->expectException(\RuntimeException::class);
+ $this->makeIndex()->newDoc(null)->update(['title' => 'x']);
+ }
+
+ public function testCreateSuccess(): void
+ {
+ $index = $this->makeIndex();
+ $index->newDoc('40')->create(['title' => 'fresh']);
+ $this->refreshIndex();
+ $this->assertSame('fresh', $index->newDoc('40')->source()['title']);
+ }
+
+ public function testSave(): void
+ {
+ $index = $this->makeIndex();
+ $index->newDoc('41')->save(['title' => 'saved']);
+ $this->refreshIndex();
+ $this->assertSame('saved', $index->newDoc('41')->source()['title']);
+ }
+
+ public function testRetryOnConflictChain(): void
+ {
+ $index = $this->makeIndex();
+ $index->newDoc('1')->retryOnConflict(3)->update(['price' => 77]);
+ $this->refreshIndex();
+ $this->assertEquals(77, $index->newDoc('1')->source()['price']);
+ }
+
+ public function testRefreshOption(): void
+ {
+ $index = $this->makeIndex();
+ $index->newDoc('42')->refresh('wait_for')->index(['title' => 'refreshed']);
+ // refresh=wait_for makes it searchable immediately
+ $this->assertSame('refreshed', $index->newDoc('42')->source()['title']);
+ }
+}
diff --git a/tests/Integration/Index/EventContractTest.php b/tests/Integration/Index/EventContractTest.php
new file mode 100644
index 0000000..8c0f1c6
--- /dev/null
+++ b/tests/Integration/Index/EventContractTest.php
@@ -0,0 +1,60 @@
+name;
+ });
+ $this->makeIndex()->newQuery()->matchAll()->get();
+ $this->assertContains('search.query.before', $events);
+ $this->assertContains('search.query.after', $events);
+ }
+
+ public function testBulkEvents(): void
+ {
+ $events = [];
+ EventDispatcher::listen('bulk.*', function (Event $e) use (&$events) {
+ $events[] = $e->name;
+ });
+ (new Bulk($this->makeIndex()))->index('10', ['title' => 'x'])->flush();
+ $this->assertContains('bulk.flush.before', $events);
+ $this->assertContains('bulk.flush.after', $events);
+ }
+
+ public function testMultipleListeners(): void
+ {
+ $count = 0;
+ EventDispatcher::listen('search.query.before', function () use (&$count) {
+ $count++;
+ });
+ EventDispatcher::listen('search.query.before', function () use (&$count) {
+ $count++;
+ });
+ $this->makeIndex()->newQuery()->matchAll()->get();
+ $this->assertSame(2, $count);
+ }
+
+ public function testNoListenersDoesNotError(): void
+ {
+ $this->makeIndex()->newQuery()->matchAll()->get();
+ $this->assertTrue(true);
+ }
+}
diff --git a/tests/Integration/Index/ManagerContractTest.php b/tests/Integration/Index/ManagerContractTest.php
new file mode 100644
index 0000000..1fa7212
--- /dev/null
+++ b/tests/Integration/Index/ManagerContractTest.php
@@ -0,0 +1,115 @@
+assertTrue((new Manager($this->makeIndex()))->exists());
+ }
+
+ public function testPutMapping(): void
+ {
+ $name = $this->indexName;
+ $index = new class ($name) extends Index {
+ public function __construct(string $name)
+ {
+ $this->name = $name;
+ $this->mappings = ['properties' => ['extra' => ['type' => 'keyword']]];
+ }
+ };
+ $manager = new Manager($index);
+ $manager->putMapping();
+ $mapping = $manager->getMapping();
+ $this->assertArrayHasKey('extra', $mapping[$name]['mappings']['properties']);
+ }
+
+ public function testAddAndRemoveAlias(): void
+ {
+ $manager = new Manager($this->makeIndex());
+ $manager->addAlias('ek_alias_test');
+ $aliases = $manager->getAliases();
+ $this->assertArrayHasKey('ek_alias_test', $aliases[$this->indexName]['aliases']);
+ $manager->removeAlias('ek_alias_test');
+ $aliases = $manager->getAliases();
+ $this->assertArrayNotHasKey('ek_alias_test', $aliases[$this->indexName]['aliases']);
+ }
+
+ public function testRefresh(): void
+ {
+ // refresh must run without error on a real index
+ (new Manager($this->makeIndex()))->refresh();
+ $this->assertTrue((new Manager($this->makeIndex()))->exists());
+ }
+
+ public function testDelete(): void
+ {
+ $name = 'ek_mgr_' . bin2hex(random_bytes(4));
+ $index = new class ($name) extends Index {
+ public function __construct(string $name)
+ {
+ $this->name = $name;
+ }
+ };
+ $manager = new Manager($index);
+ $manager->create();
+ $this->assertTrue($manager->exists());
+ $manager->delete();
+ $this->assertFalse($manager->exists());
+ }
+
+ public function testCreate(): void
+ {
+ $name = 'ek_mgr_' . bin2hex(random_bytes(4));
+ $index = new class ($name) extends Index {
+ public function __construct(string $name)
+ {
+ $this->name = $name;
+ $this->mappings = ['properties' => ['title' => ['type' => 'text']]];
+ }
+ };
+ $manager = new Manager($index);
+ $this->assertFalse($manager->exists());
+ $manager->create();
+ $this->assertTrue($manager->exists());
+ }
+
+ public function testGet(): void
+ {
+ $info = (new Manager($this->makeIndex()))->get();
+ $this->assertArrayHasKey($this->indexName, $info);
+ $this->assertArrayHasKey('mappings', $info[$this->indexName]);
+ $this->assertArrayHasKey('settings', $info[$this->indexName]);
+ }
+
+ public function testPutSettings(): void
+ {
+ $manager = new Manager($this->makeIndex());
+ $manager->putSettings(['index' => ['number_of_replicas' => 0]]);
+ $settings = $manager->getSettings();
+ $this->assertSame('0', $settings[$this->indexName]['settings']['index']['number_of_replicas'] ?? null);
+ }
+
+ public function testCloseAndOpen(): void
+ {
+ $name = 'ek_mgr_' . bin2hex(random_bytes(4));
+ $index = new class ($name) extends Index {
+ public function __construct(string $name)
+ {
+ $this->name = $name;
+ }
+ };
+ $manager = new Manager($index);
+ $manager->create();
+ $manager->close();
+ $manager->open();
+ $this->assertTrue($manager->exists());
+ }
+}
diff --git a/tests/Integration/Index/RebuildContractTest.php b/tests/Integration/Index/RebuildContractTest.php
new file mode 100644
index 0000000..53ac043
--- /dev/null
+++ b/tests/Integration/Index/RebuildContractTest.php
@@ -0,0 +1,164 @@
+name = $alias;
+ }
+
+ public function source(array $context = []): iterable
+ {
+ yield 1 => ['title' => 'A'];
+ yield 2 => ['title' => 'B'];
+ }
+ };
+
+ $result = (new Rebuild($index))->run();
+ $this->assertNotEmpty($result['newIndex']);
+ $this->assertNull($result['oldIndex']);
+
+ // alias now resolves to the backing index with the 2 imported docs
+ $index->getClient()->indices()->refresh(['index' => $result['newIndex']]);
+ $this->assertSame(2, $index->newQuery()->matchAll()->count());
+ }
+
+ public function testRunSwapsAlias(): void
+ {
+ $alias = 'ek_rebuild_' . bin2hex(random_bytes(4));
+ $index = new class ($alias) extends Index {
+ public function __construct(string $alias)
+ {
+ $this->name = $alias;
+ }
+
+ public function rebuildName(): string
+ {
+ return $this->name . '_' . bin2hex(random_bytes(2));
+ }
+
+ public function source(array $context = []): iterable
+ {
+ yield 1 => ['title' => 'A'];
+ }
+ };
+
+ $first = (new Rebuild($index))->run();
+ $this->assertNull($first['oldIndex']);
+
+ $second = (new Rebuild($index))->run();
+ $this->assertSame($first['newIndex'], $second['oldIndex']);
+ }
+
+ public function testRunRejectsRealIndex(): void
+ {
+ // $this->indexName is a real index created by setUp, not an alias
+ $name = $this->indexName;
+ $index = new class ($name) extends Index {
+ public function __construct(string $name)
+ {
+ $this->name = $name;
+ }
+
+ public function source(array $context = []): iterable
+ {
+ return [];
+ }
+ };
+
+ $this->expectException(\RuntimeException::class);
+ $this->expectExceptionMessage('is a real index');
+ (new Rebuild($index))->allowEmpty()->run();
+ }
+
+ private function rebuildIndex(string $alias, bool $empty = false): Index
+ {
+ return new class ($alias, $empty) extends Index {
+ private bool $empty;
+
+ public function __construct(string $alias, bool $empty)
+ {
+ $this->name = $alias;
+ $this->empty = $empty;
+ }
+
+ public function rebuildName(): string
+ {
+ return $this->name . '_' . bin2hex(random_bytes(2));
+ }
+
+ public function source(array $context = []): iterable
+ {
+ if ($this->empty) {
+ return [];
+ }
+ yield 1 => ['title' => 'A'];
+ }
+ };
+ }
+
+ public function testRollback(): void
+ {
+ $alias = 'ek_rebuild_' . bin2hex(random_bytes(4));
+ $index = $this->rebuildIndex($alias);
+ $first = (new Rebuild($index))->run();
+ $second = (new Rebuild($index))->run();
+ $rolledBackFrom = (new Rebuild($index))->rollback($first['newIndex']);
+ $this->assertSame($second['newIndex'], $rolledBackFrom);
+ }
+
+ public function testClean(): void
+ {
+ $alias = 'ek_rebuild_' . bin2hex(random_bytes(4));
+ $index = $this->rebuildIndex($alias);
+ $result = (new Rebuild($index))->run();
+ (new Rebuild($index))->clean($result['newIndex']);
+ $this->assertFalse($index->getClient()->indices()->exists(['index' => $result['newIndex']])->asBool());
+ }
+
+ public function testForceUnlockIsIdempotent(): void
+ {
+ $alias = 'ek_rebuild_' . bin2hex(random_bytes(4));
+ $index = $this->rebuildIndex($alias);
+ // no lock held yet -> forceUnlock tolerates the 404
+ (new Rebuild($index))->forceUnlock();
+ $this->assertFalse((new Rebuild($index))->isLocked());
+ }
+
+ public function testIsLockedFalseAfterRun(): void
+ {
+ $alias = 'ek_rebuild_' . bin2hex(random_bytes(4));
+ $index = $this->rebuildIndex($alias);
+ (new Rebuild($index))->run();
+ $this->assertFalse((new Rebuild($index))->isLocked());
+ }
+
+ public function testEmptySourceThrowsWithoutAllowEmpty(): void
+ {
+ $alias = 'ek_rebuild_' . bin2hex(random_bytes(4));
+ $index = $this->rebuildIndex($alias, empty: true);
+ $this->expectException(\RuntimeException::class);
+ (new Rebuild($index))->run();
+ }
+
+ public function testAllowEmpty(): void
+ {
+ $alias = 'ek_rebuild_' . bin2hex(random_bytes(4));
+ $index = $this->rebuildIndex($alias, empty: true);
+ $result = (new Rebuild($index))->allowEmpty()->run();
+ $this->assertNotEmpty($result['newIndex']);
+ $this->assertNull($result['oldIndex']);
+ }
+}
diff --git a/tests/Integration/Index/SearchContractTest.php b/tests/Integration/Index/SearchContractTest.php
new file mode 100644
index 0000000..b008896
--- /dev/null
+++ b/tests/Integration/Index/SearchContractTest.php
@@ -0,0 +1,77 @@
+makeIndex()->newQuery()->matchAll()->get();
+ $this->assertSame(3, $results->total());
+ $this->assertCount(3, $results->hits());
+ }
+
+ public function testFirst(): void
+ {
+ $doc = $this->makeIndex()->newQuery()->match('content', 'elasticsearch')->first();
+ $this->assertIsArray($doc);
+ $this->assertContains($doc['title'], ['Elasticsearch Guide', 'PHP Development']);
+ }
+
+ public function testFirstEmpty(): void
+ {
+ $doc = $this->makeIndex()->newQuery()->term('status', 'nonexistent')->first();
+ $this->assertNull($doc);
+ }
+
+ public function testCount(): void
+ {
+ $count = $this->makeIndex()->newQuery()->term('status', 'published')->count();
+ $this->assertSame(2, $count);
+ }
+
+ public function testPaginate(): void
+ {
+ $results = $this->makeIndex()->newQuery()->matchAll()->paginate(1, 2);
+ $this->assertSame(3, $results->total());
+ $this->assertSame(1, $results->page());
+ $this->assertSame(2, $results->perPage());
+ $this->assertSame(2, $results->lastPage());
+ $this->assertCount(2, $results->items());
+ }
+
+ public function testPaginateLastPage(): void
+ {
+ $results = $this->makeIndex()->newQuery()->matchAll()->paginate(2, 2);
+ $this->assertCount(1, $results->items());
+ }
+
+ public function testScroll(): void
+ {
+ $results = $this->makeIndex()->newQuery()->matchAll()->scroll(null, '1m');
+ $this->assertNotEmpty($results->scrollId());
+ $this->assertGreaterThanOrEqual(1, count($results->hits()));
+ }
+
+ public function testChunk(): void
+ {
+ $count = 0;
+ foreach ($this->makeIndex()->newQuery()->matchAll()->chunk('1m') as $results) {
+ $count += count($results->hits());
+ }
+ $this->assertSame(3, $count);
+ }
+
+ public function testCursor(): void
+ {
+ $count = 0;
+ foreach ($this->makeIndex()->newQuery()->matchAll()->cursor('1m') as $hit) {
+ $count++;
+ }
+ $this->assertSame(3, $count);
+ }
+}
diff --git a/tests/Integration/Index/StatsSupportContractTest.php b/tests/Integration/Index/StatsSupportContractTest.php
new file mode 100644
index 0000000..f7a07dd
--- /dev/null
+++ b/tests/Integration/Index/StatsSupportContractTest.php
@@ -0,0 +1,39 @@
+assertEquals(35.0, $this->makeIndex()->newQuery()->matchAll()->max('price'));
+ }
+
+ public function testMin(): void
+ {
+ $this->assertEquals(25.0, $this->makeIndex()->newQuery()->matchAll()->min('price'));
+ }
+
+ public function testSum(): void
+ {
+ $this->assertEquals(90.0, $this->makeIndex()->newQuery()->matchAll()->sum('price'));
+ }
+
+ public function testAvg(): void
+ {
+ $this->assertEqualsWithDelta(30.0, $this->makeIndex()->newQuery()->matchAll()->avg('price'), 0.01);
+ }
+
+ public function testStats(): void
+ {
+ $stats = $this->makeIndex()->newQuery()->matchAll()->stats('price');
+ $this->assertSame(3, $stats['count']);
+ $this->assertEquals(25.0, $stats['min']);
+ $this->assertEquals(35.0, $stats['max']);
+ $this->assertEquals(90.0, $stats['sum']);
+ }
+}
diff --git a/tests/Integration/IntegrationTestCase.php b/tests/Integration/IntegrationTestCase.php
new file mode 100644
index 0000000..30c5fbc
--- /dev/null
+++ b/tests/Integration/IntegrationTestCase.php
@@ -0,0 +1,122 @@
+ test class -> its shared index name */
+ private static array $indices = [];
+
+ protected string $indexName;
+
+ protected function setUp(): void
+ {
+ $host = getenv('ELASTICKIT_TEST_HOST');
+ if (!$host) {
+ $this->markTestSkipped('ELASTICKIT_TEST_HOST not set');
+ return;
+ }
+
+ if (static::$esClient === null) {
+ static::$esClient = ClientBuilder::create()->setHosts([$host])->build();
+ }
+
+ $class = static::class;
+ if (!isset(self::$indices[$class])) {
+ // first test of this class: create the index
+ $this->indexName = 'ek_it_' . bin2hex(random_bytes(4));
+ self::$indices[$class] = $this->indexName;
+ static::createIndex(static::$esClient, $this->indexName);
+ } else {
+ // subsequent tests: clear docs left by the previous test
+ $this->indexName = self::$indices[$class];
+ static::$esClient->deleteByQuery([
+ 'index' => $this->indexName,
+ 'body' => ['query' => ['match_all' => new \stdClass()]],
+ 'refresh' => true,
+ ]);
+ }
+
+ // fresh seed for every test (cheap vs. creating the index)
+ static::seedData(static::$esClient, $this->indexName);
+
+ Index::setClient(static::$esClient);
+ }
+
+ protected function tearDown(): void
+ {
+ ClientManager::reset();
+ }
+
+ public static function tearDownAfterClass(): void
+ {
+ $class = static::class;
+ if (isset(self::$indices[$class]) && static::$esClient !== null) {
+ try {
+ static::$esClient->indices()->delete(['index' => self::$indices[$class]]);
+ } catch (\Throwable $e) {
+ // best-effort cleanup
+ }
+ unset(self::$indices[$class]);
+ }
+ }
+
+ /**
+ * Anonymous Index subclass bound to the shared test index.
+ */
+ protected function makeIndex(): Index
+ {
+ $name = $this->indexName;
+ return new class ($name) extends Index {
+ public function __construct(string $name)
+ {
+ $this->name = $name;
+ $this->trackTotalHits = true;
+ }
+ };
+ }
+
+ /**
+ * Send the query to ES; assert it is accepted. Optionally assert hit count.
+ *
+ * @return array raw ES response
+ */
+ protected function assertQueryEs(Query $query, ?int $expectedHits = null): array
+ {
+ $response = static::$esClient->search([
+ 'index' => $this->indexName,
+ 'body' => $query->toArray(),
+ ])->asArray();
+
+ if ($expectedHits !== null) {
+ $this->assertSame(
+ $expectedHits,
+ $response['hits']['total']['value'] ?? 0,
+ 'Hit count mismatch for query: ' . json_encode($query->toArray())
+ );
+ }
+
+ return $response;
+ }
+
+ /**
+ * Refresh the shared index so writes are immediately searchable.
+ */
+ protected function refreshIndex(): void
+ {
+ static::$esClient->indices()->refresh(['index' => $this->indexName]);
+ }
+}
diff --git a/tests/Integration/SmokeTest.php b/tests/Integration/SmokeTest.php
new file mode 100644
index 0000000..bfdacc9
--- /dev/null
+++ b/tests/Integration/SmokeTest.php
@@ -0,0 +1,20 @@
+assertQueryEs((new Query())->matchAll(), 3);
+ }
+}
diff --git a/tests/JoiningQueriesTest.php b/tests/JoiningQueriesTest.php
index cce2f8a..9d4fcca 100644
--- a/tests/JoiningQueriesTest.php
+++ b/tests/JoiningQueriesTest.php
@@ -15,7 +15,7 @@ class JoiningQueriesTest extends DslTestCase
{
public function testNested()
{
-$exampleJson = <<parentId(function (ParentId $parentId) {
+ $query->parentId(function (ParentId $parentId) {
$parentId->type('my-child');
$parentId->id('1');
});
$this->assertQuery($exampleJson, $query);
}
-}
\ No newline at end of file
+}
diff --git a/tests/MatchAllTest.php b/tests/MatchAllTest.php
index 5e53674..36e1dd9 100644
--- a/tests/MatchAllTest.php
+++ b/tests/MatchAllTest.php
@@ -7,7 +7,7 @@ class MatchAllTest extends DslTestCase
{
public function testMatchAll()
{
-$exampleJson = <<<'JSON'
+ $exampleJson = <<<'JSON'
{
"query": {
"match_all": {}
@@ -21,7 +21,7 @@ public function testMatchAll()
public function testMatchNone()
{
-$exampleJson = <<<'JSON'
+ $exampleJson = <<<'JSON'
{
"query": {
"match_none": {}
diff --git a/tests/NodeInvariantsTest.php b/tests/NodeInvariantsTest.php
new file mode 100644
index 0000000..571fd1b
--- /dev/null
+++ b/tests/NodeInvariantsTest.php
@@ -0,0 +1,75 @@
+ shorthand (no valueKey wrap)
+ public function testValueOnlyProducesShorthand()
+ {
+ $t = new Term('status', 'published');
+ $this->assertSame(['status' => 'published'], $t->toArray());
+ }
+
+ // value set + extra property -> value promoted under $_valueKey
+ public function testValueWithPropertyPromotesToValueKey()
+ {
+ $t = new Term('status', 'published');
+ $t->boost(2.0);
+ $this->assertEquals(['status' => ['value' => 'published', 'boost' => 2.0]], $t->toArray());
+ }
+
+ // value set + property occupying $_valueKey -> property wins, value NOT promoted
+ public function testValueDoesNotOverwritePropertyAtValueKey()
+ {
+ $t = new Term('status', 'published');
+ $t->value('override');
+ $this->assertSame(['status' => ['value' => 'override']], $t->toArray());
+ }
+
+ // no value, no properties, fieldKeyed -> null (not stdClass, not omitted)
+ public function testEmptyFieldKeyedProducesNull()
+ {
+ $t = new Term('status', function (Term $t) {
+ // empty closure: no clauses set
+ });
+ $this->assertSame(['status' => null], $t->toArray());
+ }
+
+ // no value, properties set -> properties only (value key absent)
+ public function testPropertiesOnlyWithoutValue()
+ {
+ $t = new Term('status', ['value' => 'x']);
+ $this->assertSame(['status' => ['value' => 'x']], $t->toArray());
+ }
+
+ // float value survives serialization (the JSON_PRESERVE_ZERO_FRACTION boundary)
+ public function testFloatValueSurvivesToJson()
+ {
+ $t = new Term('status', 'published');
+ $t->boost(2.0);
+ // boost 2.0 must stay a float in JSON, not collapse to int 2
+ $this->assertStringContainsString('"boost": 2.0', $t->toJson());
+ }
+
+ // field-keyed node with no field set -> LogicException (not uncatchable Error)
+ public function testFieldKeyedWithoutFieldThrows()
+ {
+ $this->expectException(\LogicException::class);
+ (new Term())->toArray();
+ }
+
+ // field-keyed node that overrides toArray() (Intervals) must be guarded too
+ public function testFieldKeyedOverrideWithoutFieldThrows()
+ {
+ $this->expectException(\LogicException::class);
+ (new \ElasticKit\DSL\Queries\FullText\Intervals())->toArray();
+ }
+}
diff --git a/tests/ParamsTest.php b/tests/ParamsTest.php
index e26e5f9..1d26cc6 100644
--- a/tests/ParamsTest.php
+++ b/tests/ParamsTest.php
@@ -7,7 +7,7 @@ class ParamsTest extends DslTestCase
{
public function testSizeWithQuery()
{
-$expectedJson = <<assertQuery($expectedJson, $query);
}
+ public function testIndicesBoostChained()
+ {
+ $expectedJson = <<matchAll();
+ $query->indicesBoost(['index-1' => 1.4])
+ ->indicesBoost(['index-2' => 1.2]);
+ $this->assertQuery($expectedJson, $query);
+ }
+
public function testChainingParamsAndQuery()
{
-$expectedJson = <<assertQuery('{"query":{"match":{"title":{"query":"test","fuzziness":"AUTO"}}}}', $query);
}
+ public function testDuplicateClauseKeyThrows()
+ {
+ $query = new Query();
+ $query->match('title', 'A');
+ $query->match('content', 'B');
+
+ $this->expectException(\RuntimeException::class);
+ $this->expectExceptionMessage('Duplicate query clause key "match"');
+ $query->toArray();
+ }
+
public function testTermClosure()
{
$query = new Query();
diff --git a/tests/ResultsTest.php b/tests/ResultsTest.php
new file mode 100644
index 0000000..438277d
--- /dev/null
+++ b/tests/ResultsTest.php
@@ -0,0 +1,273 @@
+ [
+ 'total' => ['value' => 0, 'relation' => 'eq'],
+ 'hits' => [],
+ ],
+ ], $overrides);
+ }
+
+ public function testTotal()
+ {
+ $results = new Results($this->makeResponse([
+ 'hits' => [
+ 'total' => ['value' => 42, 'relation' => 'eq'],
+ 'hits' => [],
+ ],
+ ]));
+ $this->assertEquals(42, $results->total());
+ }
+
+ public function testHits()
+ {
+ $hits = [
+ ['_id' => '1', '_source' => ['title' => 'foo']],
+ ['_id' => '2', '_source' => ['title' => 'bar']],
+ ];
+ $results = new Results($this->makeResponse([
+ 'hits' => [
+ 'total' => ['value' => 2, 'relation' => 'eq'],
+ 'hits' => $hits,
+ ],
+ ]));
+ $this->assertEquals($hits, $results->hits());
+ }
+
+ public function testDocs()
+ {
+ $results = new Results($this->makeResponse([
+ 'hits' => [
+ 'total' => ['value' => 2, 'relation' => 'eq'],
+ 'hits' => [
+ ['_id' => '1', '_source' => ['title' => 'foo']],
+ ['_id' => '2', '_source' => ['title' => 'bar']],
+ ],
+ ],
+ ]));
+ $this->assertEquals([['title' => 'foo'], ['title' => 'bar']], $results->docs());
+ }
+
+ public function testAggregations()
+ {
+ $aggs = ['price_avg' => ['value' => 100.5]];
+ $results = new Results($this->makeResponse(['aggregations' => $aggs]));
+ $this->assertEquals($aggs, $results->aggregations());
+ }
+
+ public function testAggregationsReturnsNullWhenAbsent()
+ {
+ $results = new Results($this->makeResponse());
+ $this->assertNull($results->aggregations());
+ }
+
+ public function testScrollId()
+ {
+ $results = new Results($this->makeResponse(['_scroll_id' => 'abc123']));
+ $this->assertEquals('abc123', $results->scrollId());
+ }
+
+ public function testScrollIdReturnsNullWhenAbsent()
+ {
+ $results = new Results($this->makeResponse());
+ $this->assertNull($results->scrollId());
+ }
+
+ public function testTotalRelationEq()
+ {
+ $results = new Results($this->makeResponse([
+ 'hits' => [
+ 'total' => ['value' => 42, 'relation' => 'eq'],
+ 'hits' => [],
+ ],
+ ]));
+ $this->assertEquals('eq', $results->totalRelation());
+ }
+
+ public function testTotalRelationGte()
+ {
+ $results = new Results($this->makeResponse([
+ 'hits' => [
+ 'total' => ['value' => 10000, 'relation' => 'gte'],
+ 'hits' => [],
+ ],
+ ]));
+ $this->assertEquals('gte', $results->totalRelation());
+ }
+
+ public function testRaw()
+ {
+ $response = $this->makeResponse(['took' => 5]);
+ $results = new Results($response);
+ $this->assertEquals($response, $results->raw());
+ }
+
+ public function testPaginateSetsMetadata()
+ {
+ $results = new Results($this->makeResponse([
+ 'hits' => [
+ 'total' => ['value' => 50, 'relation' => 'eq'],
+ 'hits' => [],
+ ],
+ ]));
+ $results->paginate(3, 10);
+
+ $this->assertEquals(3, $results->page());
+ $this->assertEquals(10, $results->perPage());
+ $this->assertEquals(5, $results->lastPage());
+ }
+
+ public function testLastPageMinimumIsOne()
+ {
+ $results = new Results($this->makeResponse());
+ $results->paginate(1, 15);
+
+ $this->assertEquals(1, $results->lastPage());
+ }
+
+ public function testLastPageWithZeroPerPageDoesNotCrash()
+ {
+ $results = new Results($this->makeResponse([
+ 'hits' => ['total' => ['value' => 50, 'relation' => 'eq'], 'hits' => []],
+ ]));
+ $results->paginate(1, 0);
+
+ $this->assertEquals(1, $results->lastPage()); // guarded, no DivisionByZeroError
+ }
+
+ public function testItemsReturnsDocs()
+ {
+ $results = new Results($this->makeResponse([
+ 'hits' => [
+ 'total' => ['value' => 2, 'relation' => 'eq'],
+ 'hits' => [
+ ['_id' => '1', '_source' => ['title' => 'foo']],
+ ['_id' => '2', '_source' => ['title' => 'bar']],
+ ],
+ ],
+ ]));
+ $this->assertEquals($results->docs(), $results->items());
+ }
+
+ public function testIsEmpty()
+ {
+ $results = new Results($this->makeResponse());
+ $this->assertTrue($results->isEmpty());
+
+ $results = new Results($this->makeResponse([
+ 'hits' => [
+ 'total' => ['value' => 1, 'relation' => 'eq'],
+ 'hits' => [['_source' => ['title' => 'foo']]],
+ ],
+ ]));
+ $this->assertFalse($results->isEmpty());
+ }
+
+ public function testToPaginatorCallsResolver()
+ {
+ Pagination::setPaginatorResolver(function (Results $results) {
+ return ['total' => $results->total(), 'page' => $results->page()];
+ });
+
+ $results = new Results($this->makeResponse([
+ 'hits' => [
+ 'total' => ['value' => 50, 'relation' => 'eq'],
+ 'hits' => [],
+ ],
+ ]));
+ $results->paginate(2, 10);
+
+ $paginator = $results->toPaginator();
+ $this->assertEquals(['total' => 50, 'page' => 2], $paginator);
+ }
+
+ public function testToPaginatorThrowsWithoutResolver()
+ {
+ $results = new Results($this->makeResponse());
+ $this->expectException(\RuntimeException::class);
+ $results->toPaginator();
+ }
+
+ public function testTook()
+ {
+ $results = new Results($this->makeResponse(['took' => 5]));
+ $this->assertEquals(5, $results->took());
+ }
+
+ public function testTimedOut()
+ {
+ $results = new Results($this->makeResponse(['timed_out' => true]));
+ $this->assertTrue($results->timedOut());
+ }
+
+ public function testTimedOutFalse()
+ {
+ $results = new Results($this->makeResponse(['timed_out' => false]));
+ $this->assertFalse($results->timedOut());
+ }
+
+ public function testTotalIsNullWhenOmitted()
+ {
+ // track_total_hits=false omits hits.total entirely.
+ $results = new Results(['hits' => ['hits' => []]]);
+ $this->assertNull($results->total());
+ $this->assertNull($results->totalRelation());
+ }
+
+ public function testLastPageIsNullWhenTotalUnavailable()
+ {
+ $results = (new Results(['hits' => ['hits' => []]]))->paginate(1, 15);
+ $this->assertNull($results->lastPage());
+ }
+
+ public function testHasMorePagesUsesTotalWhenKnown()
+ {
+ $results = (new Results($this->makeResponse([
+ 'hits' => ['total' => ['value' => 33, 'relation' => 'eq'], 'hits' => []],
+ ])))->paginate(1, 15);
+ $this->assertTrue($results->hasMorePages()); // page 1 < lastPage 3
+ }
+
+ public function testHasMorePagesFullPageHeuristicWhenNoTotal()
+ {
+ $full = array_fill(0, 15, ['_id' => 'x', '_source' => []]);
+ $results = (new Results(['hits' => ['hits' => $full]]))->paginate(1, 15);
+ $this->assertTrue($results->hasMorePages()); // full page -> probably more
+
+ $partial = array_fill(0, 10, ['_id' => 'x', '_source' => []]);
+ $results = (new Results(['hits' => ['hits' => $partial]]))->paginate(1, 15);
+ $this->assertFalse($results->hasMorePages()); // partial page -> last
+ }
+
+ public function testToPaginatorThrowsWhenTotalUnavailable()
+ {
+ $results = (new Results(['hits' => ['hits' => []]]))->paginate(1, 15);
+ $this->expectException(PaginationTotalUnavailableException::class);
+ $results->toPaginator();
+ }
+}
diff --git a/tests/ShapeQueriesTest.php b/tests/ShapeQueriesTest.php
index 2ccc68a..afec235 100644
--- a/tests/ShapeQueriesTest.php
+++ b/tests/ShapeQueriesTest.php
@@ -9,7 +9,7 @@ class ShapeQueriesTest extends DslTestCase
{
public function testShape()
{
-$exampleJson = <<<'JSON'
+ $exampleJson = <<<'JSON'
{
"query": {
"shape": {
diff --git a/tests/SpanQueriesTest.php b/tests/SpanQueriesTest.php
index bf22d03..b1152fc 100644
--- a/tests/SpanQueriesTest.php
+++ b/tests/SpanQueriesTest.php
@@ -240,12 +240,34 @@ public function testSpanOr()
$this->assertQuery($exampleJson, $query);
}
+ public function testSpanOrArrayForm()
+ {
+ $exampleJson = <<spanOr(['clauses' => function (Query $query) {
+ $query->spanTerm('field', 'value1');
+ $query->spanTerm('field', 'value2');
+ }]);
+ $this->assertQuery($exampleJson, $query);
+ }
+
public function testSpanTerm()
{
$exampleJson = <<<'JSON'
{
"query": {
- "span_term" : { "user.id" : { "term" : "kimchy", "boost" : 2.0 } }
+ "span_term" : { "user.id" : { "value" : "kimchy", "boost" : 2.0 } }
}
}
JSON;
@@ -312,10 +334,10 @@ public function testSpanOrAddClause()
JSON;
$query = new Query();
$query->spanOr(function (SpanOr $spanOr) {
- $spanOr->addClause(function (Query $q) {
+ $spanOr->clauses(function (Query $q) {
$q->spanTerm('field1', 'bar');
});
- $spanOr->addClause(function (Query $q) {
+ $spanOr->clauses(function (Query $q) {
$q->spanTerm('field2', 'baz');
});
});
@@ -340,10 +362,10 @@ public function testSpanNearAddClause()
JSON;
$query = new Query();
$query->spanNear(function (SpanNear $spanNear) {
- $spanNear->addClause(function (Query $q) {
+ $spanNear->clauses(function (Query $q) {
$q->spanTerm('field1', 'bar');
});
- $spanNear->addClause(function (Query $q) {
+ $spanNear->clauses(function (Query $q) {
$q->spanTerm('field2', 'baz');
});
$spanNear->slop(5)->inOrder(true);
diff --git a/tests/SpecializedQueriesTest.php b/tests/SpecializedQueriesTest.php
index 5d5e9cb..51d6511 100644
--- a/tests/SpecializedQueriesTest.php
+++ b/tests/SpecializedQueriesTest.php
@@ -15,7 +15,7 @@ class SpecializedQueriesTest extends DslTestCase
{
public function testDistanceFeature()
{
-$expectedJson = <<assertQuery($exampleJson, $query);
}
- public function testMoreLikeThisWithQuery()
+ public function testPercolate()
{
-$exampleJson = <<script(function (\ElasticKit\DSL\Queries\Script $script) {
$script->source('doc[\'my-int\'].value / 10 ');
});
+ $scriptScore->minScore(5.5);
});
$this->assertQuery($exampleJson, $query);
}
public function testWrapper()
{
-$exampleJson = <<assertQuery($exampleJson, $query);
}
-}
\ No newline at end of file
+}
diff --git a/tests/TermLevelTest.php b/tests/TermLevelTest.php
index 01c0e9d..f18cb7a 100644
--- a/tests/TermLevelTest.php
+++ b/tests/TermLevelTest.php
@@ -6,8 +6,6 @@
use ElasticKit\DSL\Queries\TermLevel\Prefix;
use ElasticKit\DSL\Queries\TermLevel\Range;
use ElasticKit\DSL\Queries\TermLevel\Regexp;
-use ElasticKit\DSL\Queries\TermLevel\Term;
-use ElasticKit\DSL\Queries\TermLevel\Terms;
use ElasticKit\DSL\Queries\TermLevel\TermsSet;
use ElasticKit\DSL\Queries\TermLevel\Wildcard;
@@ -15,7 +13,7 @@ class TermLevelTest extends DslTestCase
{
public function testExists()
{
-$exampleJson = <<assertQuery($expectedJson, $query);
}
+ public function testRangeShorthandRejectsExtraPositionalElements()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $query = new Query();
+ $query->range('price', [10, 20, 30]);
+ }
+
public function testRangeOperators()
{
-$expectedJson = <<assertQuery($expectedJson, $query);
}
-}
\ No newline at end of file
+}
diff --git a/tests/TestClient.php b/tests/TestClient.php
deleted file mode 100644
index 3b9711e..0000000
--- a/tests/TestClient.php
+++ /dev/null
@@ -1,247 +0,0 @@
-data = $data;
- }
-
- public function asArray(): array
- {
- return $this->data;
- }
-}
-
-/**
- * Minimal response object that mimics ES8 exists-family response's asBool() method.
- */
-class BoolResponse
-{
- private bool $value;
-
- public function __construct(bool $value)
- {
- $this->value = $value;
- }
-
- public function asBool(): bool
- {
- return $this->value;
- }
-}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index f4aa43b..d21c14d 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -1,4 +1,3 @@