From 19ceb794fe00dc8f763f85b01ae7c209d7d03f0b Mon Sep 17 00:00:00 2001 From: ykan821 Date: Mon, 8 Jun 2026 23:33:14 +0800 Subject: [PATCH 01/70] feat(index): Rebuild concurrency lock to prevent data loss from concurrent rebuilds Implements a distributed lock via ES doc create (op_type=create): - run() auto-acquires/releases the lock (try/finally) - forceUnlock() manually releases a stale lock - isLocked() queries lock state - ensureLockIndex() auto-creates the .ek_locks system index Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 16 +++ phpstan-baseline.neon | 25 +++- src/Index/Rebuild.php | 191 ++++++++++++++++++++++---- tests/Index/RebuildTest.php | 259 +++++++++++++++++++++++++++++++++++- 4 files changed, 456 insertions(+), 35 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 61b6b43..e3b8a61 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,6 +36,20 @@ Scope 可选:dsl / index / agg / query / docs。Breaking change 加 `!` 后缀 PSR-5 规范。 +## 待办 + +- [ ] **clone query 后 try-finally 恢复**:first() / paginate() 等方法 clone query 后修改状态,需 try-finally 确保恢复 +- [ ] **ClausesSupport API 统一**:废弃 addXXX() 方法,原 API(如 must())直接使用追加语义 +- [ ] **hasMore() 判断逻辑修复**:待确认(看代码) +- [ ] **Rebuild::rollback() 支持多索引别名**:当前只移除 `$currentList[0]`,应遍历全部 remove,参考 `doRun()` 循环写法 +- [ ] **Index::$name 空验证**:子类未设置 `$name` 时抛异常 +- [ ] **锁抽取为独立类**:通用分布式锁(acquire/release/forceUnlock/isLocked + ensureLockIndex),Bulk 等可复用 +- [ ] **$client 抽到 Registry 类**:新建 Registry 持有 client / pageResolver / paginatorResolver / listeners,Index 不再持有静态状态。旧 API 保留作 deprecated 代理,前期统一放 Registry,后续按需拆分 +- [ ] **Node 构造函数重构**:提取 applyValue,消除重复 +- [ ] **Bulk skipErrors() 设计**:参考 Rebuild 的 skipErrors 模式 +- [ ] **补核心路径的边界测试**:scroll、bulk 分批、rebuild 失败回滚 +- [ ] **搭建集成测试基建**:`ELASTICKIT_TEST_HOST` 驱动,随机索引名隔离 + ## 测试 测试在 Docker 容器中运行,需要设置以下环境变量: @@ -45,10 +59,12 @@ PSR-5 规范。 | `PHP_CONTAINER` | Docker 容器名 | | `PROJECT_PATH` | 项目在容器内的路径 | | `PROXY_PORT` | HTTP 代理端口(推送用) | +| `ELASTICKIT_TEST_HOST` | ES 集成测试地址(如 `https://localhost:9200`),不设置则跳过集成测试 | ```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/phpmd src text phpmd.xml" diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 400bc2b..1321b33 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -57,7 +57,30 @@ parameters: - message: '#^Call to an undefined method Elastic\\Elasticsearch\\ClientInterface\:\:indices\(\)\.$#' identifier: method.notFound - count: 4 + count: 5 + path: src/Index/Rebuild.php + + - + message: '#^Call to an undefined method Elastic\\Elasticsearch\\ClientInterface\:\:exists\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Index/Rebuild.php + + - + message: '#^Call to an undefined method Elastic\\Elasticsearch\\ClientInterface\:\:index\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Index/Rebuild.php + + - + message: '#^Call to an undefined method Elastic\\Elasticsearch\\ClientInterface\:\:delete\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Index/Rebuild.php + + - + message: '#^If condition is always false\.$#' + identifier: if.alwaysFalse path: src/Index/Rebuild.php - diff --git a/src/Index/Rebuild.php b/src/Index/Rebuild.php index 534064e..e8b130e 100644 --- a/src/Index/Rebuild.php +++ b/src/Index/Rebuild.php @@ -2,6 +2,7 @@ namespace ElasticKit\Index; +use Elastic\Elasticsearch\Exception\ClientResponseException; use RuntimeException; use stdClass; @@ -13,6 +14,11 @@ */ class Rebuild { + /** + * Lock index name. + */ + private const LOCK_INDEX = '.ek_locks'; + /** * @var Index */ @@ -107,6 +113,98 @@ public function source($source) * @return array{newIndex: string, oldIndex: string|null} */ public function run(array $context = []) + { + $this->acquireLock(); + + try { + $result = $this->doRun($context); + } finally { + $this->releaseLock(); + } + + return $result; + } + + /** + * 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. + */ + public function forceUnlock(): void + { + $this->releaseLock(); + } + + /** + * Check whether a rebuild lock is currently held for this 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) { + return false; + } + } + + /** + * Delete a specific backing index by name. + * + * @param string $indexName the backing index to delete + */ + public function clean(string $indexName): void + { + $this->index->getClient()->indices()->delete([ + 'index' => $indexName, + ]); + } + + /** + * Rollback alias to a specific previous backing index. + * + * @param string $targetIndex the backing index to roll back to + * @return string the index name that was rolled back from + * @throws RuntimeException if alias or target index does not exist + */ + public function rollback(string $targetIndex): string + { + $name = $this->index->name(); + $client = $this->index->getClient()->indices(); + + $currentList = array_keys($client->getAlias(['name' => $name])->asArray()); + if (empty($currentList)) { + throw new RuntimeException("No index found for alias [{$name}]"); + } + + if (!$client->exists(['index' => $targetIndex])->asBool()) { + throw new RuntimeException("Target index [{$targetIndex}] does not exist"); + } + + $client->updateAliases([ + 'body' => [ + 'actions' => [ + ['remove' => ['index' => $currentList[0], 'alias' => $name]], + ['add' => ['index' => $targetIndex, 'alias' => $name]], + ], + ], + ]); + + 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(); @@ -155,48 +253,83 @@ public function run(array $context = []) } /** - * Delete a specific backing index by name. + * Acquire the distributed lock using ES document create (op_type=create). * - * @param string $indexName the backing index to delete + * @throws RuntimeException if the lock is already held */ - public function clean(string $indexName): void + private function acquireLock(): void { - $this->index->getClient()->indices()->delete([ - 'index' => $indexName, - ]); + $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; + } } /** - * Rollback alias to a specific previous backing index. - * - * @param string $targetIndex the backing index to roll back to - * @return string the index name that was rolled back from - * @throws RuntimeException if alias or target index does not exist + * Release the distributed lock. Idempotent — silently ignores 404. */ - public function rollback(string $targetIndex): string + private function releaseLock(): void { - $name = $this->index->name(); - $client = $this->index->getClient()->indices(); - - $currentList = array_keys($client->getAlias(['name' => $name])->asArray()); - if (empty($currentList)) { - throw new RuntimeException("No index found for alias [{$name}]"); + try { + $this->index->getClient()->delete([ + 'index' => self::LOCK_INDEX, + 'id' => $this->index->name(), + ]); + } catch (ClientResponseException $e) { + if ($e->getResponse()->getStatusCode() !== 404) { + throw $e; + } + // Lock already gone — idempotent + } catch (\Throwable $e) { + // Swallow transport / server errors in finally to avoid masking the original exception } + } - if (!$client->exists(['index' => $targetIndex])->asBool()) { - throw new RuntimeException("Target index [{$targetIndex}] does not exist"); + /** + * 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; } - $client->updateAliases([ - 'body' => [ - 'actions' => [ - ['remove' => ['index' => $currentList[0], 'alias' => $name]], - ['add' => ['index' => $targetIndex, 'alias' => $name]], + try { + $indices->create([ + 'index' => self::LOCK_INDEX, + 'body' => [ + 'settings' => [ + 'number_of_shards' => 1, + 'number_of_replicas' => 0, + 'index.hidden' => true, + ], ], - ], - ]); - - return $currentList[0]; + ]); + } catch (ClientResponseException $e) { + // Race: another process may have created it concurrently + if ($indices->exists(['index' => self::LOCK_INDEX])->asBool()) { + return; + } + throw $e; + } } /** diff --git a/tests/Index/RebuildTest.php b/tests/Index/RebuildTest.php index 7e8b79c..97b18b2 100644 --- a/tests/Index/RebuildTest.php +++ b/tests/Index/RebuildTest.php @@ -40,13 +40,20 @@ public function testRunCreatesBackingIndexAndSetsAlias() }))->willReturn(new ArrayResponse(['acknowledged' => true])); $indices->method('existsAlias')->willReturn(new BoolResponse(false)); - $indices->method('exists')->willReturn(new BoolResponse(false)); + $indices->method('exists')->willReturnCallback(function ($params) { + if (($params['index'] ?? '') === '.ek_locks') { + return new BoolResponse(true); + } + return 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->method('index')->willReturn(new ArrayResponse(['result' => 'created'])); + $client->method('delete')->willReturn(new ArrayResponse(['result' => 'deleted'])); $client->expects($this->once())->method('bulk')->with($this->callback(function ($params) { return count($params['body']) === 4 && $params['body'][0]['index']['_id'] === 1 @@ -78,6 +85,7 @@ public function testRunSwapsAliasAtomically() { $indices = $this->createMock(TestIndices::class); $indices->expects($this->once())->method('create')->willReturn(new ArrayResponse(['acknowledged' => true])); + $indices->method('exists')->willReturn(new BoolResponse(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) { @@ -90,6 +98,8 @@ public function testRunSwapsAliasAtomically() $client = $this->createMock(TestClient::class); $client->method('indices')->willReturn($indices); + $client->method('index')->willReturn(new ArrayResponse(['result' => 'created'])); + $client->method('delete')->willReturn(new ArrayResponse(['result' => 'deleted'])); $client->method('bulk')->willReturn(new ArrayResponse(['items' => []])); Index::setClient($client); @@ -119,6 +129,8 @@ public function testRunThrowsWhenNameIsRealIndex() $client = $this->createMock(TestClient::class); $client->method('indices')->willReturn($indices); + $client->method('index')->willReturn(new ArrayResponse(['result' => 'created'])); + $client->method('delete')->willReturn(new ArrayResponse(['result' => 'deleted'])); $client->method('bulk')->willReturn(new ArrayResponse(['items' => []])); Index::setClient($client); @@ -144,11 +156,18 @@ 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('exists')->willReturnCallback(function ($params) { + if (($params['index'] ?? '') === '.ek_locks') { + return new BoolResponse(true); + } + return new BoolResponse(false); + }); $indices->method('putAlias')->willReturn(new ArrayResponse(['acknowledged' => true])); $client = $this->createMock(TestClient::class); $client->method('indices')->willReturn($indices); + $client->method('index')->willReturn(new ArrayResponse(['result' => 'created'])); + $client->method('delete')->willReturn(new ArrayResponse(['result' => 'deleted'])); $client->expects($this->exactly(2))->method('bulk')->willReturn(new ArrayResponse(['items' => []])); Index::setClient($client); @@ -174,11 +193,18 @@ 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('exists')->willReturnCallback(function ($params) { + if (($params['index'] ?? '') === '.ek_locks') { + return new BoolResponse(true); + } + return new BoolResponse(false); + }); $indices->method('putAlias')->willReturn(new ArrayResponse(['acknowledged' => true])); $client = $this->createMock(TestClient::class); $client->method('indices')->willReturn($indices); + $client->method('index')->willReturn(new ArrayResponse(['result' => 'created'])); + $client->method('delete')->willReturn(new ArrayResponse(['result' => 'deleted'])); $client->expects($this->once())->method('bulk')->willReturn(new ArrayResponse(['items' => []])); Index::setClient($client); @@ -199,11 +225,18 @@ public function testRunWithCustomRealName() 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('exists')->willReturnCallback(function ($params) { + if (($params['index'] ?? '') === '.ek_locks') { + return new BoolResponse(true); + } + return new BoolResponse(false); + }); $indices->method('putAlias')->willReturn(new ArrayResponse(['acknowledged' => true])); $client = $this->createMock(TestClient::class); $client->method('indices')->willReturn($indices); + $client->method('index')->willReturn(new ArrayResponse(['result' => 'created'])); + $client->method('delete')->willReturn(new ArrayResponse(['result' => 'deleted'])); $client->method('bulk')->willReturn(new ArrayResponse(['items' => []])); Index::setClient($client); @@ -305,12 +338,15 @@ public function testRunThrowsOnEmptyImport() { $indices = $this->createMock(TestIndices::class); $indices->method('create')->willReturn(new ArrayResponse(['acknowledged' => true])); + $indices->method('exists')->willReturn(new BoolResponse(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); + $client->method('index')->willReturn(new ArrayResponse(['result' => 'created'])); + $client->method('delete')->willReturn(new ArrayResponse(['result' => 'deleted'])); Index::setClient($client); $index = new class extends Index { @@ -335,11 +371,18 @@ 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('exists')->willReturnCallback(function ($params) { + if (($params['index'] ?? '') === '.ek_locks') { + return new BoolResponse(true); + } + return new BoolResponse(false); + }); $indices->method('putAlias')->willReturn(new ArrayResponse(['acknowledged' => true])); $client = $this->createMock(TestClient::class); $client->method('indices')->willReturn($indices); + $client->method('index')->willReturn(new ArrayResponse(['result' => 'created'])); + $client->method('delete')->willReturn(new ArrayResponse(['result' => 'deleted'])); $client->method('bulk')->willReturn(new ArrayResponse(['items' => []])); Index::setClient($client); @@ -363,12 +406,15 @@ public function testRunDeletesNewIndexOnImportFailure() { $indices = $this->createMock(TestIndices::class); $indices->method('create')->willReturn(new ArrayResponse(['acknowledged' => true])); + $indices->method('exists')->willReturn(new BoolResponse(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('index')->willReturn(new ArrayResponse(['result' => 'created'])); + $client->method('delete')->willReturn(new ArrayResponse(['result' => 'deleted'])); $client->method('bulk')->willReturn(new ArrayResponse(['items' => [], 'errors' => true])); Index::setClient($client); @@ -387,4 +433,207 @@ public function source(array $context = []): iterable $this->expectException(\RuntimeException::class); (new Rebuild($index))->run(); } + + public function testRunAcquiresAndReleasesLock() + { + $indices = $this->createMock(TestIndices::class); + $indices->method('create')->willReturn(new ArrayResponse(['acknowledged' => true])); + $indices->method('exists')->willReturnCallback(function ($params) { + if (($params['index'] ?? '') === '.ek_locks') { + return new BoolResponse(true); + } + return new BoolResponse(false); + }); + $indices->method('existsAlias')->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' => []])); + $client->expects($this->once())->method('index')->with($this->callback(function ($params) { + return $params['index'] === '.ek_locks' + && $params['id'] === 'products' + && ($params['op_type'] ?? null) === 'create'; + }))->willReturn(new ArrayResponse(['result' => 'created'])); + $client->expects($this->once())->method('delete')->with($this->callback(function ($params) { + return $params['index'] === '.ek_locks' && $params['id'] === 'products'; + }))->willReturn(new ArrayResponse(['result' => 'deleted'])); + Index::setClient($client); + + $index = new class extends Index { + public function __construct() + { + $this->name = 'products'; + } + + public function source(array $context = []): iterable + { + return []; + } + }; + + (new Rebuild($index))->allowEmpty()->run(); + } + + public function testRunReleasesLockOnFailure() + { + $indices = $this->createMock(TestIndices::class); + $indices->method('create')->willReturn(new ArrayResponse(['acknowledged' => true])); + $indices->method('exists')->willReturn(new BoolResponse(true)); + $indices->method('delete')->willReturn(new ArrayResponse(['acknowledged' => true])); + + $lockReleased = false; + $client = $this->createMock(TestClient::class); + $client->method('indices')->willReturn($indices); + $client->method('index')->willReturn(new ArrayResponse(['result' => 'created'])); + $client->method('delete')->willReturnCallback(function () use (&$lockReleased) { + $lockReleased = true; + return new ArrayResponse(['result' => 'deleted']); + }); + $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']; + } + }; + + try { + (new Rebuild($index))->run(); + } catch (\RuntimeException $e) { + // Expected: bulk import error + } + + $this->assertTrue($lockReleased, 'Lock should be released even when run() fails'); + } + + public function testRunThrowsWhenLocked() + { + $indices = $this->createMock(TestIndices::class); + $indices->method('exists')->willReturn(new BoolResponse(true)); + + $response = $this->createMock(\Psr\Http\Message\ResponseInterface::class); + $response->method('getStatusCode')->willReturn(409); + + $exception = new \Elastic\Elasticsearch\Exception\ClientResponseException(); + $exception->setResponse($response); + + $client = $this->createMock(TestClient::class); + $client->method('indices')->willReturn($indices); + $client->method('index')->willThrowException($exception); + Index::setClient($client); + + $index = $this->createIndex('products'); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('already running'); + (new Rebuild($index))->source([1 => ['title' => 'A']])->run(); + } + + public function testForceUnlock() + { + $client = $this->createMock(TestClient::class); + $client->expects($this->once())->method('delete')->with($this->callback(function ($params) { + return $params['index'] === '.ek_locks' && $params['id'] === 'products'; + }))->willReturn(new ArrayResponse(['result' => 'deleted'])); + Index::setClient($client); + + $index = $this->createIndex('products'); + (new Rebuild($index))->forceUnlock(); + } + + public function testForceUnlockIsIdempotent() + { + $response = $this->createMock(\Psr\Http\Message\ResponseInterface::class); + $response->method('getStatusCode')->willReturn(404); + + $exception = new \Elastic\Elasticsearch\Exception\ClientResponseException(); + $exception->setResponse($response); + + $client = $this->createMock(TestClient::class); + $client->method('delete')->willThrowException($exception); + Index::setClient($client); + + $index = $this->createIndex('products'); + + // Should not throw despite 404 + (new Rebuild($index))->forceUnlock(); + $this->assertTrue(true); // Reached without exception + } + + public function testIsLockedReturnsTrue() + { + $client = $this->createMock(TestClient::class); + $client->method('exists')->with($this->callback(function ($params) { + return $params['index'] === '.ek_locks' && $params['id'] === 'products'; + }))->willReturn(new BoolResponse(true)); + Index::setClient($client); + + $index = $this->createIndex('products'); + $this->assertTrue((new Rebuild($index))->isLocked()); + } + + public function testIsLockedReturnsFalse() + { + $client = $this->createMock(TestClient::class); + $client->method('exists')->willReturn(new BoolResponse(false)); + Index::setClient($client); + + $index = $this->createIndex('products'); + $this->assertFalse((new Rebuild($index))->isLocked()); + } + + public function testEnsureLockIndexCreatesWhenNotExists() + { + $indices = $this->createMock(TestIndices::class); + $indices->method('exists')->willReturnCallback(function ($params) { + if (($params['index'] ?? '') === '.ek_locks') { + return new BoolResponse(false); + } + return new BoolResponse(false); + }); + $indices->method('create')->willReturnCallback(function ($params) { + return new ArrayResponse(['acknowledged' => true]); + }); + $indices->method('existsAlias')->willReturn(new BoolResponse(false)); + $indices->method('putAlias')->willReturn(new ArrayResponse(['acknowledged' => true])); + + $lockIndexCreated = false; + $indices->expects($this->exactly(2))->method('create')->willReturnCallback(function ($params) use (&$lockIndexCreated) { + if (($params['index'] ?? '') === '.ek_locks') { + $lockIndexCreated = true; + $this->assertEquals(1, $params['body']['settings']['number_of_shards'] ?? 0); + $this->assertEquals(0, $params['body']['settings']['number_of_replicas'] ?? 1); + } + return new ArrayResponse(['acknowledged' => true]); + }); + + $client = $this->createMock(TestClient::class); + $client->method('indices')->willReturn($indices); + $client->method('index')->willReturn(new ArrayResponse(['result' => 'created'])); + $client->method('delete')->willReturn(new ArrayResponse(['result' => 'deleted'])); + $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 []; + } + }; + + (new Rebuild($index))->allowEmpty()->run(); + } } From 79534c48f70600a79b4ac56968c9a9ab44bc53ab Mon Sep 17 00:00:00 2001 From: ykan821 Date: Thu, 11 Jun 2026 21:47:22 +0800 Subject: [PATCH 02/70] fix(index): restore query state via try-finally after clone, validate count() response first()/scroll()/paginate()/aggregateScalar() failed to restore query state when doSearch() threw; now wrapped in try-finally to guarantee restoration. count() now throws RuntimeException when the ES response lacks the count field. --- src/Index/AggregationShortcut.php | 9 ++++---- src/Index/Search.php | 35 ++++++++++++++++++++----------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/Index/AggregationShortcut.php b/src/Index/AggregationShortcut.php index 87581f2..21ac81b 100644 --- a/src/Index/AggregationShortcut.php +++ b/src/Index/AggregationShortcut.php @@ -64,10 +64,11 @@ private function aggregateScalar($type, $field) $this->query = clone $this->query; $this->query->size(0); $this->query->aggs('__scalar', [$type => ['field' => $field]]); - - $response = $this->doSearch($type); - - $this->query = $saved; + try { + $response = $this->doSearch($type); + } finally { + $this->query = $saved; + } return $response['aggregations']['__scalar']['value'] ?? null; } diff --git a/src/Index/Search.php b/src/Index/Search.php index ea2335a..344857f 100644 --- a/src/Index/Search.php +++ b/src/Index/Search.php @@ -4,6 +4,7 @@ use BadMethodCallException; use ElasticKit\DSL\Query; +use RuntimeException; use stdClass; /** @@ -96,10 +97,11 @@ public function first() $saved = $this->query; $this->query = clone $this->query; $this->query->size(1); - - $response = $this->doSearch('first'); - - $this->query = $saved; + try { + $response = $this->doSearch('first'); + } finally { + $this->query = $saved; + } $docs = (new Results($response))->docs(); return $docs[0] ?? null; @@ -112,7 +114,13 @@ public function first() */ public function count() { - return $this->doCount()['count']; + $response = $this->doCount(); + + if (!isset($response['count'])) { + throw new RuntimeException('Missing "count" in Elasticsearch response.'); + } + + return $response['count']; } /** @@ -135,9 +143,11 @@ public function scroll($scrollId = null, $duration = '5m') $this->query->size(1000); } - $response = $this->doSearch('scroll', ['scroll' => $duration]); - - $this->query = $saved; + try { + $response = $this->doSearch('scroll', ['scroll' => $duration]); + } finally { + $this->query = $saved; + } return new Results($response); } @@ -252,10 +262,11 @@ public function paginate($page = null, $perPage = null) $this->query = clone $this->query; $this->query->from(($page - 1) * $perPage); $this->query->size($perPage); - - $response = $this->doSearch('paginate'); - - $this->query = $saved; + try { + $response = $this->doSearch('paginate'); + } finally { + $this->query = $saved; + } return (new Results($response))->paginate($page, $perPage); } From a34f6b00d3aead7803b41b82a2f849f8a4234e81 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Fri, 12 Jun 2026 00:32:12 +0800 Subject: [PATCH 03/70] feat(dsl): unify ClausesSupport on append semantics - must()/should()/filter() etc. on Boolean/DisjunctionMax/SpanOr/SpanNear now use append semantics - each clause key holds a single Query(multi=true); closures forward the same instance - Query instances are stored via addQuery(); buildQuery() flattens them into clauses - add Query::getQueries(); rename _queryClauses to _queries - remove Compound::wrapQueryAsClosure() --- CLAUDE.md | 4 +- README.md | 2 +- docs/guide.md | 10 ++-- src/DSL/Queries/Compound/Boolean.php | 58 +++----------------- src/DSL/Queries/Compound/DisjunctionMax.php | 15 +---- src/DSL/Queries/Span/SpanNear.php | 15 +---- src/DSL/Queries/Span/SpanOr.php | 15 +---- src/DSL/Query.php | 40 ++++++++++---- src/DSL/Shared/ClausesSupport.php | 61 +++++---------------- tests/CompoundQueriesTest.php | 12 ++-- tests/SpanQueriesTest.php | 8 +-- 11 files changed, 75 insertions(+), 165 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e3b8a61..a3e419c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,8 +38,8 @@ PSR-5 规范。 ## 待办 -- [ ] **clone query 后 try-finally 恢复**:first() / paginate() 等方法 clone query 后修改状态,需 try-finally 确保恢复 -- [ ] **ClausesSupport API 统一**:废弃 addXXX() 方法,原 API(如 must())直接使用追加语义 +- [x] **clone query 后 try-finally 恢复**:first() / paginate() 等方法 clone query 后修改状态,需 try-finally 确保恢复 +- [x] **ClausesSupport API 统一**:must()/should()/filter() 等改为追加语义,移除 addXXX() 方法 - [ ] **hasMore() 判断逻辑修复**:待确认(看代码) - [ ] **Rebuild::rollback() 支持多索引别名**:当前只移除 `$currentList[0]`,应遍历全部 remove,参考 `doRun()` 循环写法 - [ ] **Index::$name 空验证**:子类未设置 `$name` 时抛异常 diff --git a/README.md b/README.md index 85a0b34..fcca21f 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ $bool = Boolean::create() // 增量构建 if ($filterByPrice) { - $bool->addFilter(Range::create('price', [10, 100])); + $bool->filter(Range::create('price', [10, 100])); } $query = Query::create($bool); diff --git a/docs/guide.md b/docs/guide.md index b95f499..543b2de 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -128,17 +128,17 @@ class OrderIndex extends Index // 精确筛选(不需要评分,放 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) 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)); @@ -160,7 +160,7 @@ public function index(Request $request) } ``` -> 条件用 `if` 逐个判断,只有传了值才加查询。`addShould()` 实现 OR 搜索。深分页场景用 `cursor()` 替代 `paginate()`。 +> 条件用 `if` 逐个判断,只有传了值才加查询。`should()` 实现 OR 搜索。深分页场景用 `cursor()` 替代 `paginate()`。 运营还要导出筛选结果到 Excel。ES 默认 `max_result_window = 10000`,`from/size` 翻不到后面的数据,用 `cursor()` 基于 scroll 遍历: diff --git a/src/DSL/Queries/Compound/Boolean.php b/src/DSL/Queries/Compound/Boolean.php index 658fe51..fe6c768 100644 --- a/src/DSL/Queries/Compound/Boolean.php +++ b/src/DSL/Queries/Compound/Boolean.php @@ -4,7 +4,6 @@ use ElasticKit\DSL\Shared\ClausesSupport; use ElasticKit\DSL\Node; -use ElasticKit\DSL\Query; /** * A query that matches documents matching boolean combinations of other queries. The bool query maps to Lucene BooleanQuery. It is built using one or more boolean clauses, each clause with a typed occurrence. The occurrence types are: @@ -17,91 +16,50 @@ class Boolean extends Node /** * The clause (query) must appear in matching documents and will contribute to the score. + * Supports multiple calls to incrementally build the bool query. * * @param mixed $must * @return static */ public function must($must) { - return $this->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 - * @return static - */ - public function addMust($must) - { - return $this->pushClause('must', $must); + return $this->addClause('must', $must); } /** * 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 - * @return static - */ - public function addShould($should) - { - return $this->pushClause('should', $should); + return $this->addClause('should', $should); } /** * 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 * @return static */ public function filter($filter) { - 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', $filter); } /** * 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 - * @return static - */ - public function addMustNot($mustNot) - { - return $this->pushClause('must_not', $mustNot); + return $this->addClause('must_not', $mustNot); } /** diff --git a/src/DSL/Queries/Compound/DisjunctionMax.php b/src/DSL/Queries/Compound/DisjunctionMax.php index 520cc02..a1bbe34 100644 --- a/src/DSL/Queries/Compound/DisjunctionMax.php +++ b/src/DSL/Queries/Compound/DisjunctionMax.php @@ -4,7 +4,6 @@ use ElasticKit\DSL\Shared\ClausesSupport; use ElasticKit\DSL\Node; -use ElasticKit\DSL\Query; /** * Returns documents matching one or more wrapped queries, called query clauses or clauses. @@ -17,24 +16,14 @@ class DisjunctionMax extends Node /** * Contains one or more query clauses. Returned documents must match one or more of these queries. If a document matches multiple queries, Elasticsearch uses the highest relevance score. + * Supports multiple calls to incrementally build. * * @param mixed $queries * @return static */ public function queries($queries) { - return $this->addProperty('queries', Query::create($queries)->multi(true)); - } - - /** - * Append a query clause. Supports multiple calls to incrementally build. - * - * @param mixed $query - * @return static - */ - public function addQuery($query) - { - return $this->pushClause('queries', $query); + return $this->addClause('queries', $queries); } /** diff --git a/src/DSL/Queries/Span/SpanNear.php b/src/DSL/Queries/Span/SpanNear.php index c5a5ce6..aec40fc 100644 --- a/src/DSL/Queries/Span/SpanNear.php +++ b/src/DSL/Queries/Span/SpanNear.php @@ -4,7 +4,6 @@ use ElasticKit\DSL\Shared\ClausesSupport; use ElasticKit\DSL\Node; -use ElasticKit\DSL\Query; /** * Matches spans that are near each other, with configurable slop and ordering. @@ -17,24 +16,14 @@ class SpanNear extends Node /** * The list of span query clauses that must appear near each other. + * Supports multiple calls to incrementally build. * * @param mixed $clauses * @return static */ public function clauses($clauses) { - return $this->addProperty('clauses', Query::create($clauses)->multi(true)); - } - - /** - * Append a span query clause. Supports multiple calls to incrementally build. - * - * @param mixed $clause - * @return static - */ - public function addClause($clause) - { - return $this->pushClause('clauses', $clause); + return $this->addClause('clauses', $clauses); } /** diff --git a/src/DSL/Queries/Span/SpanOr.php b/src/DSL/Queries/Span/SpanOr.php index df9a3c9..840aa73 100644 --- a/src/DSL/Queries/Span/SpanOr.php +++ b/src/DSL/Queries/Span/SpanOr.php @@ -4,7 +4,6 @@ use ElasticKit\DSL\Shared\ClausesSupport; use ElasticKit\DSL\Node; -use ElasticKit\DSL\Query; /** * Matches the union of multiple span queries, combining their results. @@ -17,23 +16,13 @@ class SpanOr extends Node /** * The list of span query clauses to combine. + * Supports multiple calls to incrementally build. * * @param mixed $clauses * @return static */ public function clauses($clauses) { - return $this->addProperty('clauses', Query::create($clauses)->multi(true)); - } - - /** - * Append a span query clause. Supports multiple calls to incrementally build. - * - * @param mixed $clause - * @return static - */ - public function addClause($clause) - { - return $this->pushClause('clauses', $clause); + return $this->addClause('clauses', $clauses); } } diff --git a/src/DSL/Query.php b/src/DSL/Query.php index 30acc37..45975f6 100644 --- a/src/DSL/Query.php +++ b/src/DSL/Query.php @@ -48,7 +48,7 @@ class Query extends Node * * @var array */ - protected $_queryClauses = []; + protected $_queries = []; /** * Aggregation nodes stored independently from type properties. @@ -101,18 +101,28 @@ public function __construct($field = null, $value = null) if ($field instanceof Closure) { $field($this); } 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; + $this->_queries[] = $field; } } elseif ($field !== null) { $this->_properties = $field; } } + /** + * Get all query clauses. + * + * @return array + */ + public function getQueries() + { + return $this->_queries; + } + /** * Add a query clause to the query container. * @@ -121,7 +131,7 @@ public function __construct($field = null, $value = null) */ public function addQuery($query) { - $this->_queryClauses[] = $query; + $this->_queries[] = $query; return $this; } @@ -234,17 +244,25 @@ public function toArray() */ private function buildQuery() { - if (empty($this->_queryClauses)) { + if (empty($this->_queries)) { return $this->_multi ? (object)[] : []; } - $clauses = []; - foreach ($this->_queryClauses as $query) { - if ($query instanceof Query) { - foreach ($query->toArray()['query'] as $field => $item) { - $clauses[] = [$field => $item]; + // Flatten nested Query instances + $flat = []; + foreach ($this->_queries as $item) { + if ($item instanceof self) { + foreach ($item->getQueries() as $clause) { + $flat[] = $clause; } - } elseif ($query instanceof Node) { + } else { + $flat[] = $item; + } + } + + $clauses = []; + foreach ($flat as $query) { + if ($query instanceof Node) { $clauses[] = [$query->key() => $query->toArray()]; } elseif (is_array($query)) { foreach ($query as $field => $item) { diff --git a/src/DSL/Shared/ClausesSupport.php b/src/DSL/Shared/ClausesSupport.php index d543853..ec2224b 100644 --- a/src/DSL/Shared/ClausesSupport.php +++ b/src/DSL/Shared/ClausesSupport.php @@ -6,64 +6,31 @@ use ElasticKit\DSL\Query; /** - * Provides deferred clause merging for compound query nodes. + * Provides clause accumulation for compound query nodes. * - * Accumulates clauses added via pushClause() method, - * then merges them into _properties during serialization. + * Each clause key holds a Query(multi=true) instance. + * Closures receive the same Query object across calls. */ trait ClausesSupport { /** - * Clauses keyed by property name, merged at serialization time. - * - * @var array> - */ - protected $_clauses = []; - - /** - * Push a clause for the given property. + * Append 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) + protected function addClause(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); - } - } + if (!isset($this->_properties[$key])) { + $this->_properties[$key] = (new Query())->multi(true); } - $this->_clauses = []; + $target = $this->_properties[$key]; + if ($clause instanceof Closure) { + $clause($target); + } else { + $target->addQuery($clause); + } + return $this; } } diff --git a/tests/CompoundQueriesTest.php b/tests/CompoundQueriesTest.php index d65a4ec..65907fd 100644 --- a/tests/CompoundQueriesTest.php +++ b/tests/CompoundQueriesTest.php @@ -585,8 +585,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/SpanQueriesTest.php b/tests/SpanQueriesTest.php index bf22d03..3fd9b00 100644 --- a/tests/SpanQueriesTest.php +++ b/tests/SpanQueriesTest.php @@ -312,10 +312,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 +340,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); From d22085b303870a64bbde2897bd0834e51cb67cbb Mon Sep 17 00:00:00 2001 From: ykan821 Date: Fri, 12 Jun 2026 19:54:51 +0800 Subject: [PATCH 04/70] fix(index): add totalRelation(), return null from aggregations() - add totalRelation() mapping hits.total.relation directly - aggregations() now returns null by default instead of an empty array for clearer semantics - confirmed hasMore() has no bug; !empty(hits) is correct for scroll Co-Authored-By: Claude --- CLAUDE.md | 2 +- src/Index/Results.php | 20 ++++- tests/Index/ResultsTest.php | 168 ++++++++++++++++++------------------ 3 files changed, 99 insertions(+), 91 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a3e419c..43450b6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,7 +40,7 @@ PSR-5 规范。 - [x] **clone query 后 try-finally 恢复**:first() / paginate() 等方法 clone query 后修改状态,需 try-finally 确保恢复 - [x] **ClausesSupport API 统一**:must()/should()/filter() 等改为追加语义,移除 addXXX() 方法 -- [ ] **hasMore() 判断逻辑修复**:待确认(看代码) +- [x] **hasMore() 确认无 bug**:scroll 场景 !empty(hits) 正确,分页应用 page() + * @return array|null */ public function aggregations() { - return $this->response['aggregations'] ?? []; + return $this->response['aggregations'] ?? null; } /** @@ -124,7 +124,19 @@ public function scrollId() } /** - * 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 "eq" or "gte" + */ + public function totalRelation() + { + return $this->response['hits']['total']['relation']; + } + + /** + * Return whether the current batch contains hits. * * @return bool */ diff --git a/tests/Index/ResultsTest.php b/tests/Index/ResultsTest.php index 76fb5d5..73cc39a 100644 --- a/tests/Index/ResultsTest.php +++ b/tests/Index/ResultsTest.php @@ -6,21 +6,25 @@ class ResultsTest extends TestCase { - public function testTotal() + private function makeResponse(array $overrides = []): array { - $results = new Results([ + return array_merge([ 'hits' => [ - 'total' => ['value' => 42], + 'total' => ['value' => 0, 'relation' => 'eq'], 'hits' => [], ], - ]); - $this->assertEquals(42, $results->total()); + ], $overrides); } - public function testTotalDefaultsToZero() + public function testTotal() { - $results = new Results([]); - $this->assertEquals(0, $results->total()); + $results = new Results($this->makeResponse([ + 'hits' => [ + 'total' => ['value' => 42, 'relation' => 'eq'], + 'hits' => [], + ], + ])); + $this->assertEquals(42, $results->total()); } public function testHits() @@ -29,103 +33,108 @@ public function testHits() ['_id' => '1', '_source' => ['title' => 'foo']], ['_id' => '2', '_source' => ['title' => 'bar']], ]; - $results = new Results([ - 'hits' => ['total' => ['value' => 2], 'hits' => $hits], - ]); + $results = new Results($this->makeResponse([ + 'hits' => [ + 'total' => ['value' => 2, 'relation' => 'eq'], + 'hits' => $hits, + ], + ])); $this->assertEquals($hits, $results->hits()); } - public function testHitsDefaultsToEmpty() - { - $results = new Results([]); - $this->assertEquals([], $results->hits()); - } - public function testDocs() { - $results = new Results([ + $results = new Results($this->makeResponse([ 'hits' => [ - 'total' => ['value' => 2], + '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 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, - ]); + $results = new Results($this->makeResponse(['aggregations' => $aggs])); $this->assertEquals($aggs, $results->aggregations()); } - public function testAggregationsDefaultsToEmpty() + public function testAggregationsReturnsNullWhenAbsent() { - $results = new Results(['hits' => []]); - $this->assertEquals([], $results->aggregations()); + $results = new Results($this->makeResponse()); + $this->assertNull($results->aggregations()); } public function testScrollId() { - $results = new Results([ - '_scroll_id' => 'abc123', - 'hits' => ['total' => ['value' => 0], 'hits' => []], - ]); + $results = new Results($this->makeResponse(['_scroll_id' => 'abc123'])); $this->assertEquals('abc123', $results->scrollId()); } - public function testScrollIdDefaultsToNull() + public function testScrollIdReturnsNullWhenAbsent() { - $results = new Results([]); + $results = new Results($this->makeResponse()); $this->assertNull($results->scrollId()); } - public function testHasMore() + public function testHasMoreScrollWithHits() { - $results = new Results([ + $results = new Results($this->makeResponse([ 'hits' => [ - 'total' => ['value' => 1], + 'total' => ['value' => 1, 'relation' => 'eq'], 'hits' => [['_source' => ['title' => 'foo']]], ], - ]); + ])); $this->assertTrue($results->hasMore()); } - public function testHasMoreReturnsFalseWhenEmpty() + public function testHasMoreScrollEmpty() { - $results = new Results([ - 'hits' => ['total' => ['value' => 0], 'hits' => []], - ]); + $results = new Results($this->makeResponse()); $this->assertFalse($results->hasMore()); } + 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 = [ - 'took' => 5, - 'hits' => ['total' => ['value' => 1], 'hits' => []], - ]; + $response = $this->makeResponse(['took' => 5]); $results = new Results($response); $this->assertEquals($response, $results->raw()); } public function testPaginateSetsMetadata() { - $results = new Results([ - 'hits' => ['total' => ['value' => 50], 'hits' => []], - ]); + $results = new Results($this->makeResponse([ + 'hits' => [ + 'total' => ['value' => 50, 'relation' => 'eq'], + 'hits' => [], + ], + ])); $results->paginate(3, 10); $this->assertEquals(3, $results->page()); @@ -135,9 +144,7 @@ public function testPaginateSetsMetadata() public function testLastPageMinimumIsOne() { - $results = new Results([ - 'hits' => ['total' => ['value' => 0], 'hits' => []], - ]); + $results = new Results($this->makeResponse()); $results->paginate(1, 15); $this->assertEquals(1, $results->lastPage()); @@ -145,31 +152,29 @@ public function testLastPageMinimumIsOne() public function testItemsReturnsDocs() { - $results = new Results([ + $results = new Results($this->makeResponse([ 'hits' => [ - 'total' => ['value' => 2], + '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([ - 'hits' => ['total' => ['value' => 0], 'hits' => []], - ]); + $results = new Results($this->makeResponse()); $this->assertTrue($results->isEmpty()); - $results = new Results([ + $results = new Results($this->makeResponse([ 'hits' => [ - 'total' => ['value' => 1], + 'total' => ['value' => 1, 'relation' => 'eq'], 'hits' => [['_source' => ['title' => 'foo']]], ], - ]); + ])); $this->assertFalse($results->isEmpty()); } @@ -179,9 +184,12 @@ public function testToPaginatorCallsResolver() return ['total' => $results->total(), 'page' => $results->page()]; }); - $results = new Results([ - 'hits' => ['total' => ['value' => 50], 'hits' => []], - ]); + $results = new Results($this->makeResponse([ + 'hits' => [ + 'total' => ['value' => 50, 'relation' => 'eq'], + 'hits' => [], + ], + ])); $results->paginate(2, 10); $paginator = $results->toPaginator(); @@ -195,38 +203,26 @@ public function testToPaginatorCallsResolver() public function testToPaginatorThrowsWithoutResolver() { - $results = new Results(['hits' => []]); + $results = new Results($this->makeResponse()); $this->expectException(\RuntimeException::class); $results->toPaginator(); } public function testTook() { - $results = new Results([ - 'took' => 5, - 'hits' => ['total' => ['value' => 1], 'hits' => []], - ]); + $results = new Results($this->makeResponse(['took' => 5])); $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' => []], - ]); + $results = new Results($this->makeResponse(['timed_out' => true])); $this->assertTrue($results->timedOut()); } - public function testTimedOutDefaultsToFalse() + public function testTimedOutFalse() { - $results = new Results([]); + $results = new Results($this->makeResponse(['timed_out' => false])); $this->assertFalse($results->timedOut()); } } From b58fdff20b3fe718e4098e3bad99a6ba30d5684b Mon Sep 17 00:00:00 2001 From: ykan821 Date: Fri, 12 Jun 2026 20:04:15 +0800 Subject: [PATCH 05/70] fix(index): Rebuild::rollback() iterates all aliases, validate Index::name() - rollback() removes aliases from all backing indices, mirroring doRun() - name() throws when $name is not set in a subclass Co-Authored-By: Claude --- src/Index/Index.php | 6 ++++++ src/Index/Rebuild.php | 11 +++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Index/Index.php b/src/Index/Index.php index 6d93164..75f4b52 100644 --- a/src/Index/Index.php +++ b/src/Index/Index.php @@ -102,6 +102,12 @@ public function getClient() */ public function name() { + if (empty($this->name)) { + throw new RuntimeException( + sprintf('Index $name is not set in %s', static::class) + ); + } + return $this->name; } diff --git a/src/Index/Rebuild.php b/src/Index/Rebuild.php index e8b130e..101e195 100644 --- a/src/Index/Rebuild.php +++ b/src/Index/Rebuild.php @@ -186,12 +186,15 @@ 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, ], ]); From dc76f10c59c5b034fe596056b2f01d7497bf8baf Mon Sep 17 00:00:00 2001 From: ykan821 Date: Fri, 12 Jun 2026 20:24:15 +0800 Subject: [PATCH 06/70] refactor(dsl): split Node/Query constructors into from* methods, update TODO - Node: extract fromKeyValue/fromClosure/fromArrayField/fromScalar - Query: reuse fromClosure, add fromArray to handle the query key - constructor becomes a clear dispatch table, each method handles one input form - update CLAUDE.md TODO status Co-Authored-By: Claude --- CLAUDE.md | 6 ++-- src/DSL/Node.php | 92 +++++++++++++++++++++++++++++++++-------------- src/DSL/Query.php | 26 ++++++++++---- 3 files changed, 88 insertions(+), 36 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 43450b6..73f650e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,11 +41,11 @@ PSR-5 规范。 - [x] **clone query 后 try-finally 恢复**:first() / paginate() 等方法 clone query 后修改状态,需 try-finally 确保恢复 - [x] **ClausesSupport API 统一**:must()/should()/filter() 等改为追加语义,移除 addXXX() 方法 - [x] **hasMore() 确认无 bug**:scroll 场景 !empty(hits) 正确,分页应用 page()_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); + $this->fromClosure($field); } 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; - } + $this->fromArrayField($field); } elseif ($this->_isPropertyField && is_scalar($field)) { - $this->_rawValue = $field; - $this->_properties = []; + $this->fromScalar($field); } else { $this->_properties = $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->_rawValue = $value; + $this->_properties = []; + } else { + $this->_properties = $value; + } + if ($this->_isPropertyField) { + $this->field($field); + } + } + + /** + * Initialize from a closure. + * + * @param Closure $closure + */ + protected function fromClosure(Closure $closure): void + { + $closure($this); + } + + /** + * Initialize from a single-element array where key is field name. + * + * @param array $field + */ + protected function fromArrayField(array $field): void + { + foreach ($field as $key => $val) { + $this->field($key); + if (is_scalar($val)) { + $this->_rawValue = $val; + $this->_properties = []; + } else { + $this->_properties = $val; + } + break; + } + } + + /** + * Initialize from a scalar value. + * + * @param mixed $value + */ + protected function fromScalar($value): void + { + $this->_rawValue = $value; + $this->_properties = []; + } + /** * Set whether this node uses a field name as the top-level attribute. * diff --git a/src/DSL/Query.php b/src/DSL/Query.php index 45975f6..93d41db 100644 --- a/src/DSL/Query.php +++ b/src/DSL/Query.php @@ -19,6 +19,7 @@ * Query container that combines multiple query conditions into an Elasticsearch DSL query. * * @phpstan-consistent-constructor + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ class Query extends Node { @@ -99,20 +100,33 @@ public function __construct($field = null, $value = null) return; } if ($field instanceof Closure) { - $field($this); + $this->fromClosure($field); } elseif ($field instanceof Node) { $this->_queries[] = $field; } elseif (is_array($field)) { - if (array_key_exists('query', $field)) { - $this->_properties = $field; - } else { - $this->_queries[] = $field; - } + $this->fromArray($field); } elseif ($field !== null) { $this->_properties = $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. * From 8c359fffa0949c0bf134879dfdfebff2364ddce4 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Fri, 12 Jun 2026 21:55:42 +0800 Subject: [PATCH 07/70] feat(index): onError replaces skipErrors in Bulk/Rebuild, drop rebuild.import.failed event - Bulk gains onError(callable); execute() throws on errors by default - Rebuild drops skipErrors, gains onError(callable) that delegates to Bulk internally - remove the rebuild.import.failed event; error handling goes through the onError callback uniformly - batchSize auto-flush errors are also caught via onError, no longer lost - update docs and TODO Co-Authored-By: Claude --- CLAUDE.md | 4 +- docs/index.md | 33 +++++++++++---- src/Index/Bulk.php | 34 ++++++++++++++++ src/Index/Rebuild.php | 36 +++++++---------- tests/Index/BulkTest.php | 81 +++++++++++++++++++++++++++++++++++++ tests/Index/RebuildTest.php | 81 +++++++++++++++++++++++++++++++++++++ 6 files changed, 240 insertions(+), 29 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 73f650e..3833668 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,7 +46,7 @@ PSR-5 规范。 - [ ] **锁抽取为独立类**:通用分布式锁(acquire/release/forceUnlock/isLocked + ensureLockIndex),Bulk 等可复用 - [ ] **$client 抽到 Registry 类**:新建 Registry 持有 client / pageResolver / paginatorResolver / listeners,Index 不再持有静态状态。旧 API 保留作 deprecated 代理,前期统一放 Registry,后续按需拆分 - [x] **Node 构造函数重构**:拆分为 fromKeyValue/fromClosure/fromArrayField/fromScalar -- [ ] **Bulk skipErrors() 设计**:参考 Rebuild 的 skipErrors 模式 +- [x] **Bulk/Rebuild onError 设计**:Bulk 加 onError(callback) 默认 throw,Rebuild 删 skipErrors 加 onError,删 rebuild.import.failed 事件 - [ ] **补核心路径的边界测试**:scroll、bulk 分批、rebuild 失败回滚 - [ ] **搭建集成测试基建**:`ELASTICKIT_TEST_HOST` 驱动,随机索引名隔离 @@ -61,6 +61,8 @@ PSR-5 规范。 | `PROXY_PORT` | HTTP 代理端口(推送用) | | `ELASTICKIT_TEST_HOST` | ES 集成测试地址(如 `https://localhost:9200`),不设置则跳过集成测试 | +## 推送代码前需要执行4件套 + ```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" diff --git a/docs/index.md b/docs/index.md index 996048e..67c38c0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -164,6 +164,28 @@ $bulk->delete(3); $bulk->execute(); // 执行所有操作,执行后清空状态 ``` +### 错误处理 + +`execute()` 默认在响应包含错误时抛出 `RuntimeException`。使用 `onError()` 自定义处理。回调接收 ES 原始响应,不抛出则继续,抛出则中断: + +```php +$bulk = new Bulk(new ProductIndex()); + +// 不设 onError → 有错误就抛异常 +$bulk->execute(); + +// 设 onError → 回调内不抛则继续,抛则中断 +$bulk->onError(function (array $response) { + $failures = count(array_filter($response['items'], fn($i) => isset($i['index']['error']))); + if ($failures > 100) { + throw new RuntimeException("失败超过阈值: {$failures}"); + } + Log::warning("部分失败: {$failures} 条"); +})->execute(); +``` + +> `batchSize` 自动 flush 时的错误同样走 `onError`,不会丢失。 + ## 零停机重建 创建新索引 → 导入数据 → 切换别名。 @@ -242,14 +264,12 @@ $rebuild->run(['after' => '2024-01-01']); ### 错误处理 -导入错误会触发 `rebuild.import.failed` 事件(始终),并抛出异常(默认)。使用 `skipErrors()` 抑制异常但保留事件通知: +Rebuild 内部使用 Bulk 执行导入,`onError()` 用法与 [批量操作 > 错误处理](#错误处理) 一致: ```php -Index::listen('rebuild.import.failed', function (Event $e) { - Log::warning("重建导入错误", $e->response); -}); - -$rebuild->skipErrors()->run(); // 通过事件记录错误,不中断 +$rebuild->onError(function (array $response) { + Log::warning("重建导入错误", $response); +})->run(); ``` > rebuild 期间 DB 仍在变更,新索引是开始时刻的快照,建议 rebuild 后通过 `updated_at` 增量同步补齐。 @@ -351,4 +371,3 @@ Index::setClient($client); | `manager.swap_alias.after` | `$response` | | `rebuild.run.before` | | | `rebuild.run.after` | `$newIndex`, `$oldIndex` | -| `rebuild.import.failed` | `$response` | diff --git a/src/Index/Bulk.php b/src/Index/Bulk.php index 2ab5c3b..8f2336e 100644 --- a/src/Index/Bulk.php +++ b/src/Index/Bulk.php @@ -3,6 +3,7 @@ namespace ElasticKit\Index; use InvalidArgumentException; +use RuntimeException; /** * Batch document operations using the ES _bulk API. @@ -39,6 +40,11 @@ class Bulk */ private $docCount = 0; + /** + * @var callable|null + */ + private $errorHandler = null; + /** * @param Index $index */ @@ -78,6 +84,22 @@ public function batchSize($size) return $this; } + /** + * Set a callback to handle bulk errors. + * + * The callback receives the raw ES response. To continue execution, simply + * return without throwing. To abort, throw an exception from the callback. + * Without an error handler, execute() throws RuntimeException on errors. + * + * @param callable $handler function (array $response): void + * @return $this + */ + public function onError($handler) + { + $this->errorHandler = $handler; + return $this; + } + /** * Set retry_on_conflict for all update actions in this batch. * @@ -213,6 +235,18 @@ public function execute(array $options = []) $e->duration = $duration; Index::dispatch($e); + if (!empty($response['errors'])) { + if ($this->errorHandler) { + ($this->errorHandler)($response); + } else { + $json = json_encode($response, JSON_UNESCAPED_UNICODE); + if (strlen($json) > 4096) { + $json = substr($json, 0, 4096) . '... [truncated]'; + } + throw new RuntimeException("Bulk request has errors: {$json}"); + } + } + return $response; } diff --git a/src/Index/Rebuild.php b/src/Index/Rebuild.php index 101e195..7d21f56 100644 --- a/src/Index/Rebuild.php +++ b/src/Index/Rebuild.php @@ -30,9 +30,9 @@ class Rebuild private $batchSize = 1000; /** - * @var bool + * @var callable|null */ - private $skipErrors = false; + private $errorHandler = null; /** * @var bool @@ -65,14 +65,18 @@ public function batchSize($size) } /** - * Skip bulk import errors instead of throwing. Defaults to false. + * Set a callback to handle bulk import errors. + * + * The callback receives the raw ES response. To continue the rebuild, simply + * return without throwing. To abort, throw an exception from the callback. + * Without an error handler, import errors cause the rebuild to abort. * - * @param bool $skip + * @param callable $handler function (array $response): void * @return $this */ - public function skipErrors($skip = true) + public function onError($handler) { - $this->skipErrors = $skip; + $this->errorHandler = $handler; return $this; } @@ -372,6 +376,10 @@ protected function import($newName, array $context): void } $bulk = (new Bulk($this->index))->target($newName)->batchSize($this->batchSize); + if ($this->errorHandler) { + $bulk->onError($this->errorHandler); + } + $count = 0; foreach ($items as $id => $doc) { if (!is_array($doc)) { @@ -391,20 +399,6 @@ protected function import($newName, array $context): void ); } - $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->execute(); } } diff --git a/tests/Index/BulkTest.php b/tests/Index/BulkTest.php index ee2f9c6..90539a8 100644 --- a/tests/Index/BulkTest.php +++ b/tests/Index/BulkTest.php @@ -269,4 +269,85 @@ public function testExecuteReturnsEmptyWhenBodyIsEmpty() $this->assertEquals([], $result); } + + public function testExecuteThrowsOnErrors() + { + $client = $this->createMock(TestClient::class); + $client->method('bulk')->willReturn(new ArrayResponse([ + 'errors' => true, + 'items' => [ + ['index' => ['_id' => '1', 'status' => 400, 'error' => ['type' => 'mapper_parsing_exception']]], + ], + ])); + Index::setClient($client); + + $index = $this->createIndex('products'); + $this->expectException(\RuntimeException::class); + (new Bulk($index))->index('1', ['title' => 'foo'])->execute(); + } + + public function testExecuteCallsOnError() + { + $client = $this->createMock(TestClient::class); + $errorResponse = [ + 'errors' => true, + 'items' => [ + ['index' => ['_id' => '1', 'status' => 400, 'error' => ['type' => 'mapper_parsing_exception']]], + ], + ]; + $client->method('bulk')->willReturn(new ArrayResponse($errorResponse)); + Index::setClient($client); + + $received = null; + $index = $this->createIndex('products'); + (new Bulk($index)) + ->onError(function ($response) use (&$received) { + $received = $response; + }) + ->index('1', ['title' => 'foo']) + ->execute(); + + $this->assertEquals($errorResponse, $received); + } + + public function testAutoFlushCallsOnError() + { + $errorResponse = [ + 'errors' => true, + 'items' => [ + ['index' => ['_id' => '1', 'status' => 400, 'error' => ['type' => 'mapper_parsing_exception']]], + ], + ]; + $client = $this->createMock(TestClient::class); + $client->method('bulk')->willReturn(new ArrayResponse($errorResponse)); + Index::setClient($client); + + $received = null; + $index = $this->createIndex('products'); + $bulk = (new Bulk($index)) + ->batchSize(1) + ->onError(function ($response) use (&$received) { + $received = $response; + }); + + $bulk->index('1', ['title' => 'foo']); // triggers auto-flush + + $this->assertEquals($errorResponse, $received); + } + + public function testAutoFlushThrowsOnErrorsWithoutHandler() + { + $client = $this->createMock(TestClient::class); + $client->method('bulk')->willReturn(new ArrayResponse([ + 'errors' => true, + 'items' => [], + ])); + Index::setClient($client); + + $index = $this->createIndex('products'); + $bulk = (new Bulk($index))->batchSize(1); + + $this->expectException(\RuntimeException::class); + $bulk->index('1', ['title' => 'foo']); // auto-flush throws + } } diff --git a/tests/Index/RebuildTest.php b/tests/Index/RebuildTest.php index 97b18b2..2dd1abf 100644 --- a/tests/Index/RebuildTest.php +++ b/tests/Index/RebuildTest.php @@ -636,4 +636,85 @@ public function source(array $context = []): iterable (new Rebuild($index))->allowEmpty()->run(); } + + public function testOnErrorReceivesResponse() + { + $indices = $this->createMock(TestIndices::class); + $indices->method('create')->willReturn(new ArrayResponse(['acknowledged' => true])); + $indices->method('exists')->willReturn(new BoolResponse(false)); + $indices->method('existsAlias')->willReturn(new BoolResponse(false)); + $indices->method('putAlias')->willReturn(new ArrayResponse(['acknowledged' => true])); + + $errorResponse = [ + 'errors' => true, + 'items' => [ + ['index' => ['_id' => '1', 'status' => 400, 'error' => ['type' => 'mapper_parsing_exception']]], + ], + ]; + $client = $this->createMock(TestClient::class); + $client->method('indices')->willReturn($indices); + $client->method('index')->willReturn(new ArrayResponse(['result' => 'created'])); + $client->method('delete')->willReturn(new ArrayResponse(['result' => 'deleted'])); + $client->method('bulk')->willReturn(new ArrayResponse($errorResponse)); + Index::setClient($client); + + $index = new class extends Index { + public function __construct() + { + $this->name = 'products'; + } + + public function source(array $context = []): iterable + { + yield 1 => ['title' => 'A']; + } + }; + + $received = null; + (new Rebuild($index)) + ->onError(function ($response) use (&$received) { + $received = $response; + }) + ->run(); + + $this->assertEquals($errorResponse, $received); + } + + public function testOnErrorPreventsException() + { + $indices = $this->createMock(TestIndices::class); + $indices->method('create')->willReturn(new ArrayResponse(['acknowledged' => true])); + $indices->method('exists')->willReturn(new BoolResponse(false)); + $indices->method('existsAlias')->willReturn(new BoolResponse(false)); + $indices->method('putAlias')->willReturn(new ArrayResponse(['acknowledged' => true])); + + $client = $this->createMock(TestClient::class); + $client->method('indices')->willReturn($indices); + $client->method('index')->willReturn(new ArrayResponse(['result' => 'created'])); + $client->method('delete')->willReturn(new ArrayResponse(['result' => 'deleted'])); + $client->method('bulk')->willReturn(new ArrayResponse([ + 'errors' => true, + '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']; + } + }; + + // Without onError, would throw. With onError (empty callback), completes successfully. + $result = (new Rebuild($index)) + ->onError(function () {}) + ->run(); + + $this->assertStringStartsWith('products_', $result['newIndex']); + } } From 1ce21567a3dd6d05eca14a7a496a266127a9d6af Mon Sep 17 00:00:00 2001 From: ykan821 Date: Fri, 12 Jun 2026 22:52:09 +0800 Subject: [PATCH 08/70] refactor(index): rename AggregationShortcut to StatsSupport, add stats() --- src/Index/Search.php | 2 +- ...gregationShortcut.php => StatsSupport.php} | 34 ++++++++- ...nShortcutTest.php => StatsSupportTest.php} | 71 +++++++++++++++++-- 3 files changed, 101 insertions(+), 6 deletions(-) rename src/Index/{AggregationShortcut.php => StatsSupport.php} (61%) rename tests/Index/{AggregationShortcutTest.php => StatsSupportTest.php} (62%) diff --git a/src/Index/Search.php b/src/Index/Search.php index 344857f..2152981 100644 --- a/src/Index/Search.php +++ b/src/Index/Search.php @@ -14,7 +14,7 @@ */ class Search { - use AggregationShortcut; + use StatsSupport; /** * @var Query diff --git a/src/Index/AggregationShortcut.php b/src/Index/StatsSupport.php similarity index 61% rename from src/Index/AggregationShortcut.php rename to src/Index/StatsSupport.php index 21ac81b..0d09a0b 100644 --- a/src/Index/AggregationShortcut.php +++ b/src/Index/StatsSupport.php @@ -5,7 +5,7 @@ /** * Shortcut methods for common metric aggregations on Search. */ -trait AggregationShortcut +trait StatsSupport { /** * Return the maximum value of a field. @@ -51,6 +51,38 @@ public function sum($field) 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($field) + { + $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. * diff --git a/tests/Index/AggregationShortcutTest.php b/tests/Index/StatsSupportTest.php similarity index 62% rename from tests/Index/AggregationShortcutTest.php rename to tests/Index/StatsSupportTest.php index 2b3972d..507cff7 100644 --- a/tests/Index/AggregationShortcutTest.php +++ b/tests/Index/StatsSupportTest.php @@ -4,7 +4,7 @@ use ElasticKit\Index\Index; use ElasticKit\Index\Search; -class AggregationShortcutTest extends TestCase +class StatsSupportTest extends TestCase { protected function setUp(): void { @@ -78,7 +78,70 @@ public function testSum() $this->assertEquals(1500.0, $result); } - public function testAggregationShortcutSendsCorrectBody() + public function testStatsReturnsAllMetrics() + { + $client = $this->createMock(TestClient::class); + $client->method('search')->willReturn(new ArrayResponse([ + 'hits' => ['total' => ['value' => 0], 'hits' => []], + 'aggregations' => [ + '__stats' => [ + 'count' => 100, + 'min' => 9.99, + 'max' => 199.99, + 'avg' => 49.5, + 'sum' => 4950.0, + ], + ], + ])); + Index::setClient($client); + + $index = $this->createIndex(); + $result = $index->query()->stats('price'); + + $this->assertEquals([ + 'count' => 100, + 'min' => 9.99, + 'max' => 199.99, + 'avg' => 49.5, + 'sum' => 4950.0, + ], $result); + } + + public function testStatsReturnsNullWhenNoAggregation() + { + $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()->stats('nonexistent'); + + $this->assertNull($result); + } + + public function testStatsSendsCorrectBody() + { + $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']['__stats']['stats']['field']) + && $body['aggs']['__stats']['stats']['field'] === 'price'; + }))->willReturn(new ArrayResponse([ + 'hits' => ['total' => ['value' => 0], 'hits' => []], + 'aggregations' => [ + '__stats' => ['count' => 0, 'min' => null, 'max' => null, 'avg' => null, 'sum' => 0], + ], + ])); + Index::setClient($client); + + $index = $this->createIndex(); + $index->query()->stats('price'); + } + + public function testScalarSendsCorrectBody() { $client = $this->createMock(TestClient::class); $client->expects($this->once())->method('search')->with($this->callback(function ($params) { @@ -96,7 +159,7 @@ public function testAggregationShortcutSendsCorrectBody() $index->query()->max('price'); } - public function testAggregationShortcutDoesNotMutateQuery() + public function testScalarDoesNotMutateQuery() { $lastBody = null; $client = $this->createMock(TestClient::class); @@ -118,7 +181,7 @@ public function testAggregationShortcutDoesNotMutateQuery() $this->assertEquals(0, $lastBody['size']); } - public function testAggregationShortcutReturnsNullWhenNoValue() + public function testScalarReturnsNullWhenNoValue() { $client = $this->createMock(TestClient::class); $client->method('search')->willReturn(new ArrayResponse([ From a9e46860f1ad69b0aaec940b675af6e22912db78 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Fri, 12 Jun 2026 23:19:56 +0800 Subject: [PATCH 09/70] refactor(index): extract ClientManager, EventDispatcher, Pagination from Index Index static methods are kept as @deprecated proxies; call sites migrate to the new classes. Tests swap ReflectionProperty resets for the new classes' reset(). --- src/Index/Bulk.php | 4 +- src/Index/ClientManager.php | 62 ++++++++++++++++++++++++++ src/Index/EventDispatcher.php | 72 ++++++++++++++++++++++++++++++ src/Index/Index.php | 75 +++++++------------------------- src/Index/Manager.php | 12 ++--- src/Index/Pagination.php | 72 ++++++++++++++++++++++++++++++ src/Index/Rebuild.php | 4 +- src/Index/Results.php | 2 +- src/Index/Search.php | 14 +++--- tests/Index/BulkTest.php | 5 +-- tests/Index/DocTest.php | 5 +-- tests/Index/EventTest.php | 11 ++--- tests/Index/IndexTest.php | 22 +++------- tests/Index/ManagerTest.php | 5 +-- tests/Index/RebuildTest.php | 5 +-- tests/Index/ResultsTest.php | 5 +-- tests/Index/StatsSupportTest.php | 5 +-- 17 files changed, 260 insertions(+), 120 deletions(-) create mode 100644 src/Index/ClientManager.php create mode 100644 src/Index/EventDispatcher.php create mode 100644 src/Index/Pagination.php diff --git a/src/Index/Bulk.php b/src/Index/Bulk.php index 8f2336e..9001e08 100644 --- a/src/Index/Bulk.php +++ b/src/Index/Bulk.php @@ -217,7 +217,7 @@ public function execute(array $options = []) $e = new Event('bulk.execute.before', $indexName); $e->actions = $actions; - Index::dispatch($e); + EventDispatcher::dispatch($e); $start = microtime(true); $response = $this->index->getClient()->bulk( @@ -233,7 +233,7 @@ public function execute(array $options = []) $e->actions = $actions; $e->response = $response; $e->duration = $duration; - Index::dispatch($e); + EventDispatcher::dispatch($e); if (!empty($response['errors'])) { if ($this->errorHandler) { diff --git a/src/Index/ClientManager.php b/src/Index/ClientManager.php new file mode 100644 index 0000000..077cb71 --- /dev/null +++ b/src/Index/ClientManager.php @@ -0,0 +1,62 @@ + + */ + private static $clients = []; + + /** + * Register an Elasticsearch client. Optionally name the connection. + * + * @param ClientInterface $client + * @param string|null $name connection name, null for default + * @return void + */ + public static function set(ClientInterface $client, $name = null) + { + self::$clients[$name ?? 'default'] = $client; + } + + /** + * Return the Elasticsearch client for the given connection name. + * + * Falls back to 'default' if the named connection is not registered. + * + * @param string $connection + * @return ClientInterface + * @throws RuntimeException if no client is registered + */ + public static function get($connection = 'default') + { + if (isset(self::$clients[$connection])) { + return self::$clients[$connection]; + } + if (isset(self::$clients['default'])) { + return self::$clients['default']; + } + 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() + { + self::$clients = []; + } +} diff --git a/src/Index/EventDispatcher.php b/src/Index/EventDispatcher.php new file mode 100644 index 0000000..e3ac9df --- /dev/null +++ b/src/Index/EventDispatcher.php @@ -0,0 +1,72 @@ +> + */ + private static $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($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 === '*' || self::matchesCategory($pattern, $event->name)) { + foreach ($listeners as $listener) { + $listener($event); + } + } + } + } + + /** + * Reset all registered listeners. Mainly for testing. + * + * @return void + */ + public static function reset() + { + self::$listeners = []; + } + + /** + * Check if a wildcard pattern matches an event by category. + * + * @param string $pattern + * @param string $event + * @return bool + */ + private 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/Index.php b/src/Index/Index.php index 75f4b52..1b44bd1 100644 --- a/src/Index/Index.php +++ b/src/Index/Index.php @@ -14,11 +14,6 @@ */ abstract class Index { - /** - * @var array - */ - protected static $clients = []; - /** * @var string */ @@ -49,31 +44,17 @@ abstract class Index */ protected $maxPerPage = 100; - /** - * @var callable|null - */ - protected static $pageResolver; - - /** - * @var callable|null - */ - protected static $paginatorResolver; - - /** - * @var array> - */ - protected static $listeners = []; - /** * Register an Elasticsearch client. Optionally name the connection. * * @param ClientInterface $client * @param string|null $name connection name, null for default * @return void + * @deprecated Use ClientManager::set() instead */ public static function setClient(ClientInterface $client, $name = null) { - self::$clients[$name ?? 'default'] = $client; + ClientManager::set($client, $name); } /** @@ -83,16 +64,7 @@ public static function setClient(ClientInterface $client, $name = null) */ public function getClient() { - 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 ClientManager::get($this->connection); } /** @@ -206,10 +178,11 @@ public function maxPerPage() * * @param callable $resolver returns [$page, $perPage] * @return void + * @deprecated Use Pagination::setPageResolver() instead */ public static function setPageResolver(callable $resolver) { - self::$pageResolver = $resolver; + Pagination::setPageResolver($resolver); } /** @@ -217,30 +190,33 @@ public static function setPageResolver(callable $resolver) * * @param callable $resolver receives (Results $results, int $page, int $perPage) * @return void + * @deprecated Use Pagination::setPaginatorResolver() instead */ public static function setPaginatorResolver(callable $resolver) { - self::$paginatorResolver = $resolver; + Pagination::setPaginatorResolver($resolver); } /** * Return the registered page resolver, or null. * * @return callable|null + * @deprecated Use Pagination::getPageResolver() instead */ public static function getPageResolver() { - return self::$pageResolver; + return Pagination::getPageResolver(); } /** * Return the registered paginator resolver, or null. * * @return callable|null + * @deprecated Use Pagination::getPaginatorResolver() instead */ public static function getPaginatorResolver() { - return self::$paginatorResolver; + return Pagination::getPaginatorResolver(); } /** @@ -263,10 +239,11 @@ public function source(array $context = []): iterable * @param string $event * @param callable $listener receives (Event $event) * @return void + * @deprecated Use EventDispatcher::listen() instead */ public static function listen($event, callable $listener) { - self::$listeners[$event][] = $listener; + EventDispatcher::listen($event, $listener); } /** @@ -274,32 +251,10 @@ public static function listen($event, callable $listener) * * @param Event $event * @return void + * @deprecated Use EventDispatcher::dispatch() instead */ 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; + EventDispatcher::dispatch($event); } } diff --git a/src/Index/Manager.php b/src/Index/Manager.php index f166ac3..00ee232 100644 --- a/src/Index/Manager.php +++ b/src/Index/Manager.php @@ -36,7 +36,7 @@ public function create() $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,7 +51,7 @@ public function create() $e = new Event('manager.create.after', $indexName); $e->response = $response; - Index::dispatch($e); + EventDispatcher::dispatch($e); return $response; } @@ -66,7 +66,7 @@ public function delete() $indexName = $this->resolveIndexName(); $e = new Event('manager.delete.before', $indexName); - Index::dispatch($e); + EventDispatcher::dispatch($e); $response = $this->index->getClient()->indices()->delete([ 'index' => $indexName, @@ -74,7 +74,7 @@ public function delete() $e = new Event('manager.delete.after', $indexName); $e->response = $response; - Index::dispatch($e); + EventDispatcher::dispatch($e); return $response; } @@ -256,7 +256,7 @@ public function swapAlias($alias, $fromIndex) $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 +269,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; } diff --git a/src/Index/Pagination.php b/src/Index/Pagination.php new file mode 100644 index 0000000..9a933b3 --- /dev/null +++ b/src/Index/Pagination.php @@ -0,0 +1,72 @@ +index->name(); $client = $this->index->getClient()->indices(); - Index::dispatch(new Event('rebuild.run.before', $name)); + EventDispatcher::dispatch(new Event('rebuild.run.before', $name)); $newName = $this->createIndex(); @@ -254,7 +254,7 @@ private function doRun(array $context): array $e = new Event('rebuild.run.after', $name); $e->newIndex = $newName; $e->oldIndex = $oldIndex; - Index::dispatch($e); + EventDispatcher::dispatch($e); return ['newIndex' => $newName, 'oldIndex' => $oldIndex]; } diff --git a/src/Index/Results.php b/src/Index/Results.php index 0efd61e..5216d1b 100644 --- a/src/Index/Results.php +++ b/src/Index/Results.php @@ -239,7 +239,7 @@ public function toPaginator() ); } - $resolver = Index::getPaginatorResolver(); + $resolver = Pagination::getPaginatorResolver(); if ($resolver === null) { throw new RuntimeException( 'Paginator resolver not registered. Call Index::setPaginatorResolver() first.' diff --git a/src/Index/Search.php b/src/Index/Search.php index 2152981..c275c2a 100644 --- a/src/Index/Search.php +++ b/src/Index/Search.php @@ -194,7 +194,7 @@ protected function doScroll($scrollId, $duration) $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([ @@ -209,7 +209,7 @@ protected function doScroll($scrollId, $duration) $e->scrollId = $scrollId; $e->response = $response; $e->duration = $durationTime; - Index::dispatch($e); + EventDispatcher::dispatch($e); return new Results($response); } @@ -244,7 +244,7 @@ public function cursor($duration = '5m') public function paginate($page = null, $perPage = null) { if ($page === null && $perPage === null) { - $resolver = Index::getPageResolver(); + $resolver = Pagination::getPageResolver(); if ($resolver !== null) { [$page, $perPage] = $resolver(); } @@ -284,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([ @@ -299,7 +299,7 @@ protected function doCount() $e->response = $response; $e->duration = $duration; $e->action = 'count'; - Index::dispatch($e); + EventDispatcher::dispatch($e); return $response; } @@ -319,7 +319,7 @@ protected function doSearch($action, array $extra = []) $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); @@ -333,7 +333,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/tests/Index/BulkTest.php b/tests/Index/BulkTest.php index 90539a8..82dee70 100644 --- a/tests/Index/BulkTest.php +++ b/tests/Index/BulkTest.php @@ -2,6 +2,7 @@ use PHPUnit\Framework\TestCase; use ElasticKit\Index\Bulk; +use ElasticKit\Index\ClientManager; use ElasticKit\Index\Index; class BulkTest extends TestCase @@ -13,9 +14,7 @@ protected function setUp(): void protected function tearDown(): void { - $ref = new ReflectionProperty(Index::class, 'clients'); - $ref->setAccessible(true); - $ref->setValue(null, []); + ClientManager::reset(); } protected function createIndex($name = 'products') diff --git a/tests/Index/DocTest.php b/tests/Index/DocTest.php index 71168f4..7b3058f 100644 --- a/tests/Index/DocTest.php +++ b/tests/Index/DocTest.php @@ -1,6 +1,7 @@ setAccessible(true); - $ref->setValue(null, []); + ClientManager::reset(); } protected function createIndex($name = 'products') diff --git a/tests/Index/EventTest.php b/tests/Index/EventTest.php index 09edd1f..bb15d11 100644 --- a/tests/Index/EventTest.php +++ b/tests/Index/EventTest.php @@ -1,20 +1,17 @@ setAccessible(true); - $ref->setValue(null, []); - - $clientRef = new ReflectionProperty(Index::class, 'clients'); - $clientRef->setAccessible(true); - $clientRef->setValue(null, []); + EventDispatcher::reset(); + ClientManager::reset(); } protected function createIndex($name = 'products') diff --git a/tests/Index/IndexTest.php b/tests/Index/IndexTest.php index acc5d73..ea9403e 100644 --- a/tests/Index/IndexTest.php +++ b/tests/Index/IndexTest.php @@ -2,7 +2,9 @@ use PHPUnit\Framework\TestCase; use ElasticKit\DSL\Query; +use ElasticKit\Index\ClientManager; use ElasticKit\Index\Index; +use ElasticKit\Index\Pagination; use ElasticKit\Index\Results; use ElasticKit\Index\Search; @@ -15,19 +17,8 @@ protected function setUp(): void 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); + ClientManager::reset(); + Pagination::reset(); } protected function createIndex($name = 'products') @@ -703,10 +694,7 @@ public function __construct($name) public function testGetClientThrowsWhenNotRegistered() { - // Ensure no clients are registered - $ref = new ReflectionProperty(Index::class, 'clients'); - $ref->setAccessible(true); - $ref->setValue(null, []); + ClientManager::reset(); $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('nonexistent'); diff --git a/tests/Index/ManagerTest.php b/tests/Index/ManagerTest.php index d1bdbac..5039b72 100644 --- a/tests/Index/ManagerTest.php +++ b/tests/Index/ManagerTest.php @@ -1,6 +1,7 @@ setAccessible(true); - $ref->setValue(null, []); + ClientManager::reset(); } protected function createIndex($name = 'products', $mappings = [], $settings = []) diff --git a/tests/Index/RebuildTest.php b/tests/Index/RebuildTest.php index 2dd1abf..8d25dbb 100644 --- a/tests/Index/RebuildTest.php +++ b/tests/Index/RebuildTest.php @@ -1,6 +1,7 @@ setAccessible(true); - $ref->setValue(null, []); + ClientManager::reset(); } protected function createIndex($name = 'products', $mappings = [], $settings = []) diff --git a/tests/Index/ResultsTest.php b/tests/Index/ResultsTest.php index 73cc39a..40108a1 100644 --- a/tests/Index/ResultsTest.php +++ b/tests/Index/ResultsTest.php @@ -2,6 +2,7 @@ use PHPUnit\Framework\TestCase; use ElasticKit\Index\Index; +use ElasticKit\Index\Pagination; use ElasticKit\Index\Results; class ResultsTest extends TestCase @@ -196,9 +197,7 @@ public function testToPaginatorCallsResolver() $this->assertEquals(['total' => 50, 'page' => 2], $paginator); // Clean up - $ref = new ReflectionProperty(Index::class, 'paginatorResolver'); - $ref->setAccessible(true); - $ref->setValue(null, null); + Pagination::reset(); } public function testToPaginatorThrowsWithoutResolver() diff --git a/tests/Index/StatsSupportTest.php b/tests/Index/StatsSupportTest.php index 507cff7..715c084 100644 --- a/tests/Index/StatsSupportTest.php +++ b/tests/Index/StatsSupportTest.php @@ -1,6 +1,7 @@ setAccessible(true); - $ref->setValue(null, []); + ClientManager::reset(); } protected function createIndex($name = 'products') From 8a5ea87dc9eab764d2856970b5f1be188556ec0d Mon Sep 17 00:00:00 2001 From: ykan821 Date: Sat, 13 Jun 2026 00:38:40 +0800 Subject: [PATCH 10/70] refactor(index): drop insert/deprecated proxies, add on/newQuery/newDoc instance entry points - remove Index::insert(); equivalent functionality is covered by Doc::save() - remove 6 deprecated proxy methods (listen/dispatch/setPageResolver etc.) - add on(string $connection), newQuery(), newDoc() instance methods - add setConnection()/getConnection() instance methods - query()/doc() now delegate to newQuery()/newDoc() - ClientManager drops the implicit fallback and gains type declarations - update tests and README --- README.md | 2 +- src/Index/ClientManager.php | 17 ++-- src/Index/Index.php | 151 ++++++++++++++---------------------- tests/Index/EventTest.php | 32 ++++---- tests/Index/IndexTest.php | 140 +++++++++++++++++++++++++++++---- tests/Index/ResultsTest.php | 12 ++- 6 files changed, 217 insertions(+), 137 deletions(-) diff --git a/README.md b/README.md index fcca21f..ceffd8f 100644 --- a/README.md +++ b/README.md @@ -189,7 +189,7 @@ foreach (ProductIndex::query()->cursor() as $batch) { ### 文档 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] diff --git a/src/Index/ClientManager.php b/src/Index/ClientManager.php index 077cb71..886b042 100644 --- a/src/Index/ClientManager.php +++ b/src/Index/ClientManager.php @@ -19,31 +19,26 @@ class ClientManager * Register an Elasticsearch client. Optionally name the connection. * * @param ClientInterface $client - * @param string|null $name connection name, null for default + * @param string $name connection name, defaults to 'default' * @return void */ - public static function set(ClientInterface $client, $name = null) + public static function set(ClientInterface $client, string $name = 'default'): void { - self::$clients[$name ?? 'default'] = $client; + self::$clients[$name] = $client; } /** * Return the Elasticsearch client for the given connection name. * - * Falls back to 'default' if the named connection is not registered. - * * @param string $connection * @return ClientInterface - * @throws RuntimeException if no client is registered + * @throws RuntimeException if the connection is not registered */ - public static function get($connection = 'default') + public static function get(string $connection = 'default'): ClientInterface { if (isset(self::$clients[$connection])) { return self::$clients[$connection]; } - if (isset(self::$clients['default'])) { - return self::$clients['default']; - } throw new RuntimeException( "Elasticsearch client not registered for connection '{$connection}'. " . 'Call ClientManager::set($client) first.' @@ -55,7 +50,7 @@ public static function get($connection = 'default') * * @return void */ - public static function reset() + public static function reset(): void { self::$clients = []; } diff --git a/src/Index/Index.php b/src/Index/Index.php index 1b44bd1..0dd9854 100644 --- a/src/Index/Index.php +++ b/src/Index/Index.php @@ -48,11 +48,10 @@ abstract class Index * Register an Elasticsearch client. Optionally name the connection. * * @param ClientInterface $client - * @param string|null $name connection name, null for default + * @param string $name connection name, defaults to 'default' * @return void - * @deprecated Use ClientManager::set() instead */ - public static function setClient(ClientInterface $client, $name = null) + public static function setClient(ClientInterface $client, string $name = 'default'): void { ClientManager::set($client, $name); } @@ -62,11 +61,45 @@ public static function setClient(ClientInterface $client, $name = null) * * @return ClientInterface */ - public function getClient() + public function getClient(): ClientInterface { return ClientManager::get($this->connection); } + /** + * Set the connection name for this index instance. + * + * @param string $connection + * @return $this + */ + public function setConnection(string $connection) + { + $this->connection = $connection; + + return $this; + } + + /** + * Return the connection name for this index. + * + * @return string + */ + public function getConnection(): string + { + return $this->connection; + } + + /** + * Create a new index instance with the given connection. + * + * @param string $connection + * @return static + */ + public static function on(string $connection) + { + return (new static())->setConnection($connection); + } + /** * Return the index name. * @@ -84,43 +117,47 @@ public function 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 function newQuery(Query $query = null) + { + return new Search($this, $query); + } + + /** + * Create a Search instance. Delegates to newQuery() with a fresh instance. + * + * @param Query|null $query * @return Search */ public static function query(Query $query = null) { - return new Search(new static(), $query); + return (new static())->newQuery($query); } /** - * Create a DocReference for a single document. Supports both static and instance call. + * Create a Doc reference from this index instance. * * @param string|int $id * @return Doc */ - public static function doc($id) + public function newDoc($id) { - return new Doc(new static(), $id); + return new Doc($this, $id); } /** - * Insert (create or overwrite) a single document. + * Create a Doc reference. Delegates to newDoc() with a fresh 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 $id + * @return Doc */ - public static function insert($id, array $document) + public static function doc($id) { - $index = new static(); - $params = ['index' => $index->name(), 'body' => $document]; - - if ($id !== null && $id !== '') { - $params['id'] = $id; - } - - return $index->getClient()->index($params)->asArray(); + return (new static())->newDoc($id); } /** @@ -173,52 +210,6 @@ public function maxPerPage() return $this->maxPerPage; } - /** - * Register a resolver that extracts page and perPage from the request. - * - * @param callable $resolver returns [$page, $perPage] - * @return void - * @deprecated Use Pagination::setPageResolver() instead - */ - public static function setPageResolver(callable $resolver) - { - Pagination::setPageResolver($resolver); - } - - /** - * Register a resolver that converts Results into a framework paginator. - * - * @param callable $resolver receives (Results $results, int $page, int $perPage) - * @return void - * @deprecated Use Pagination::setPaginatorResolver() instead - */ - public static function setPaginatorResolver(callable $resolver) - { - Pagination::setPaginatorResolver($resolver); - } - - /** - * Return the registered page resolver, or null. - * - * @return callable|null - * @deprecated Use Pagination::getPageResolver() instead - */ - public static function getPageResolver() - { - return Pagination::getPageResolver(); - } - - /** - * Return the registered paginator resolver, or null. - * - * @return callable|null - * @deprecated Use Pagination::getPaginatorResolver() instead - */ - public static function getPaginatorResolver() - { - return Pagination::getPaginatorResolver(); - } - /** * Yield documents as [id => doc] pairs. Override to provide a default data source for rebuild. * @@ -233,28 +224,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 - * @deprecated Use EventDispatcher::listen() instead - */ - public static function listen($event, callable $listener) - { - EventDispatcher::listen($event, $listener); - } - - /** - * Dispatch an event to all matching listeners. - * - * @param Event $event - * @return void - * @deprecated Use EventDispatcher::dispatch() instead - */ - public static function dispatch(Event $event) - { - EventDispatcher::dispatch($event); - } } diff --git a/tests/Index/EventTest.php b/tests/Index/EventTest.php index bb15d11..8d01c92 100644 --- a/tests/Index/EventTest.php +++ b/tests/Index/EventTest.php @@ -37,7 +37,7 @@ protected function mockClient() public function testListenAndDispatch() { $received = null; - Index::listen('search.query.before', function (Event $e) use (&$received) { + EventDispatcher::listen('search.query.before', function (Event $e) use (&$received) { $received = $e; }); @@ -53,7 +53,7 @@ public function testListenAndDispatch() public function testSearchBeforePassesDsl() { $dsl = null; - Index::listen('search.query.before', function (Event $e) use (&$dsl) { + EventDispatcher::listen('search.query.before', function (Event $e) use (&$dsl) { $dsl = $e->dsl; }); @@ -68,7 +68,7 @@ public function testSearchBeforePassesDsl() public function testSearchAfterPassesResponse() { $response = null; - Index::listen('search.query.after', function (Event $e) use (&$response) { + EventDispatcher::listen('search.query.after', function (Event $e) use (&$response) { $response = $e->response; }); @@ -83,7 +83,7 @@ public function testSearchAfterPassesResponse() public function testSearchAfterContainsDuration() { $duration = null; - Index::listen('search.query.after', function (Event $e) use (&$duration) { + EventDispatcher::listen('search.query.after', function (Event $e) use (&$duration) { $duration = $e->duration; }); @@ -98,7 +98,7 @@ public function testSearchAfterContainsDuration() public function testSearchEventPassesAction() { $action = null; - Index::listen('search.query.before', function (Event $e) use (&$action) { + EventDispatcher::listen('search.query.before', function (Event $e) use (&$action) { $action = $e->action; }); @@ -112,7 +112,7 @@ public function testSearchEventPassesAction() public function testFirstTriggersSearchWithAction() { $action = null; - Index::listen('search.query.before', function (Event $e) use (&$action) { + EventDispatcher::listen('search.query.before', function (Event $e) use (&$action) { $action = $e->action; }); @@ -126,7 +126,7 @@ public function testFirstTriggersSearchWithAction() public function testWildcardListener() { $events = []; - Index::listen('*', function (Event $e) use (&$events) { + EventDispatcher::listen('*', function (Event $e) use (&$events) { $events[] = $e->name; }); @@ -141,7 +141,7 @@ public function testWildcardListener() public function testCategoryWildcardListener() { $events = []; - Index::listen('search.*', function (Event $e) use (&$events) { + EventDispatcher::listen('search.*', function (Event $e) use (&$events) { $events[] = $e->name; }); @@ -156,7 +156,7 @@ public function testCategoryWildcardListener() public function testCategoryWildcardMatchesSearchQueryEvents() { $events = []; - Index::listen('search.*', function (Event $e) use (&$events) { + EventDispatcher::listen('search.*', function (Event $e) use (&$events) { $events[] = $e->name; }); @@ -178,10 +178,10 @@ public function testCategoryWildcardMatchesSearchQueryEvents() public function testMultipleListeners() { $count = 0; - Index::listen('search.query.before', function (Event $e) use (&$count) { + EventDispatcher::listen('search.query.before', function (Event $e) use (&$count) { $count++; }); - Index::listen('search.query.before', function (Event $e) use (&$count) { + EventDispatcher::listen('search.query.before', function (Event $e) use (&$count) { $count++; }); @@ -204,7 +204,7 @@ public function testNoListenersDoesNotError() public function testBulkExecutePassesActions() { $actions = null; - Index::listen('bulk.execute.before', function (Event $e) use (&$actions) { + EventDispatcher::listen('bulk.execute.before', function (Event $e) use (&$actions) { $actions = $e->actions; }); @@ -224,7 +224,7 @@ public function testBulkExecutePassesActions() public function testManagerCreateEvents() { $events = []; - Index::listen('manager.create.*', function (Event $e) use (&$events) { + EventDispatcher::listen('manager.create.*', function (Event $e) use (&$events) { $events[] = $e->name; }); @@ -245,7 +245,7 @@ public function testManagerCreateEvents() public function testManagerDeleteEvents() { $events = []; - Index::listen('manager.delete.*', function (Event $e) use (&$events) { + EventDispatcher::listen('manager.delete.*', function (Event $e) use (&$events) { $events[] = $e->name; }); @@ -267,7 +267,7 @@ public function testManagerDeleteEvents() public function testManagerReadOperationsHaveNoEvents() { $events = []; - Index::listen('*', function (Event $e) use (&$events) { + EventDispatcher::listen('*', function (Event $e) use (&$events) { $events[] = $e->name; }); @@ -294,7 +294,7 @@ public function testManagerReadOperationsHaveNoEvents() public function testRebuildRunBeforeAndAfterEvents() { $events = []; - Index::listen('rebuild.*', function (Event $e) use (&$events) { + EventDispatcher::listen('rebuild.*', function (Event $e) use (&$events) { $events[] = $e->name; }); diff --git a/tests/Index/IndexTest.php b/tests/Index/IndexTest.php index ea9403e..08b732b 100644 --- a/tests/Index/IndexTest.php +++ b/tests/Index/IndexTest.php @@ -467,7 +467,7 @@ public function testPaginateWithExplicitParams() public function testPaginateWithPageResolver() { - Index::setPageResolver(function () { + Pagination::setPageResolver(function () { return [2, 20]; }); @@ -597,7 +597,7 @@ public function __construct($name = 'products') public function testToPaginatorReturnsFrameworkPaginator() { - Index::setPaginatorResolver(function (Results $results) { + Pagination::setPaginatorResolver(function (Results $results) { return [ 'data' => $results->items(), 'total' => $results->total(), @@ -676,11 +676,14 @@ public function __construct($name) $this->assertSame($logClient, $logs->getClient()); } - public function testGetClientFallsBackToDefault() + public function testGetClientThrowsForUnknownConnection() { $defaultClient = $this->createMock(TestClient::class); Index::setClient($defaultClient); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('nonexistent'); + $index = new class('unknown') extends Index { public function __construct($name) { @@ -689,7 +692,7 @@ public function __construct($name) } }; - $this->assertSame($defaultClient, $index->getClient()); + $index->getClient(); } public function testGetClientThrowsWhenNotRegistered() @@ -697,16 +700,127 @@ public function testGetClientThrowsWhenNotRegistered() ClientManager::reset(); $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('nonexistent'); - - $index = new class('test') extends Index { - public function __construct($name) - { - $this->name = $name; - $this->connection = 'nonexistent'; - } - }; + $index = $this->createIndex('test'); $index->getClient(); } + + public function testSetConnectionAndGetConnection() + { + $index = $this->createIndex('products'); + + $this->assertEquals('default', $index->getConnection()); + + $result = $index->setConnection('secondary'); + $this->assertSame($index, $result); + $this->assertEquals('secondary', $index->getConnection()); + } + + public function testOnCreatesInstanceWithConnection() + { + $defaultClient = $this->createMock(TestClient::class); + $secondaryClient = $this->createMock(TestClient::class); + Index::setClient($defaultClient); + Index::setClient($secondaryClient, 'secondary'); + + $index = TestConcreteIndex::on('secondary'); + + $this->assertInstanceOf(TestConcreteIndex::class, $index); + $this->assertEquals('secondary', $index->getConnection()); + $this->assertEquals('test', $index->name()); + $this->assertSame($secondaryClient, $index->getClient()); + } + + public function testNewQueryReturnsSearch() + { + $client = $this->createMock(TestClient::class); + $client->method('search')->willReturn(new ArrayResponse([ + 'hits' => ['total' => ['value' => 1], 'hits' => [['_source' => ['title' => 'test']]]], + ])); + Index::setClient($client); + + $index = $this->createIndex('products'); + $this->assertInstanceOf(Search::class, $index->newQuery()); + + $query = Query::create()->match('title', 'test'); + $results = $index->newQuery($query)->get(); + $this->assertEquals(1, $results->total()); + } + + public function testNewQueryUsesInstanceConnection() + { + $secondaryClient = $this->createMock(TestClient::class); + $secondaryClient->method('search')->willReturn(new ArrayResponse([ + 'hits' => ['total' => ['value' => 0], 'hits' => []], + ])); + Index::setClient($secondaryClient, 'secondary'); + + $index = $this->createIndex('products'); + $index->setConnection('secondary'); + $results = $index->newQuery()->matchAll()->get(); + + $this->assertEquals(0, $results->total()); + } + + public function testNewDocUsesInstanceConnection() + { + $secondaryClient = $this->createMock(TestClient::class); + $secondaryClient->method('getSource')->willReturn(new ArrayResponse([ + 'found' => true, + '_source' => ['title' => 'test'], + ])); + Index::setClient($secondaryClient, 'secondary'); + + $index = $this->createIndex('products'); + $index->setConnection('secondary'); + $doc = $index->newDoc(1); + + $this->assertInstanceOf(\ElasticKit\Index\Doc::class, $doc); + $this->assertEquals([ + 'found' => true, + '_source' => ['title' => 'test'], + ], $doc->source()); + } + + public function testQueryDocDelegateToNew() + { + $this->assertInstanceOf(Search::class, TestConcreteIndex::query()); + $this->assertInstanceOf(\ElasticKit\Index\Doc::class, TestConcreteIndex::doc(1)); + } + + public function testOnNewQueryChain() + { + $secondaryClient = $this->createMock(TestClient::class); + $secondaryClient->method('search')->willReturn(new ArrayResponse([ + 'hits' => ['total' => ['value' => 1], 'hits' => [['_source' => ['title' => 'from_secondary']]]], + ])); + Index::setClient($secondaryClient, 'secondary'); + + $results = TestConcreteIndex::on('secondary')->newQuery()->matchAll()->get(); + + $this->assertEquals(1, $results->total()); + $this->assertEquals([['title' => 'from_secondary']], $results->docs()); + } + + public function testOnNewDocChain() + { + $secondaryClient = $this->createMock(TestClient::class); + $secondaryClient->method('getSource')->willReturn(new ArrayResponse([ + 'found' => true, + '_source' => ['title' => 'secondary_doc'], + ])); + Index::setClient($secondaryClient, 'secondary'); + + $doc = TestConcreteIndex::on('secondary')->newDoc(42); + + $this->assertEquals([ + 'found' => true, + '_source' => ['title' => 'secondary_doc'], + ], $doc->source()); + } +} + +class TestConcreteIndex extends Index +{ + protected $name = 'test'; } diff --git a/tests/Index/ResultsTest.php b/tests/Index/ResultsTest.php index 40108a1..853b0a4 100644 --- a/tests/Index/ResultsTest.php +++ b/tests/Index/ResultsTest.php @@ -1,12 +1,19 @@ $results->total(), 'page' => $results->page()]; }); @@ -195,9 +202,6 @@ public function testToPaginatorCallsResolver() $paginator = $results->toPaginator(); $this->assertEquals(['total' => 50, 'page' => 2], $paginator); - - // Clean up - Pagination::reset(); } public function testToPaginatorThrowsWithoutResolver() From b5553f5e29a4dfd4f4835129f227661c13a146a2 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Sat, 13 Jun 2026 01:26:06 +0800 Subject: [PATCH 11/70] =?UTF-8?q?docs:=20update=20TODO=20=E2=80=94=20mark?= =?UTF-8?q?=20completed=20items,=20add=20PHP=208=20modernization=20and=20R?= =?UTF-8?q?ebuild=20exception-handling=20items?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3833668..1bd3c95 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,8 +43,14 @@ PSR-5 规范。 - [x] **hasMore() 确认无 bug**:scroll 场景 !empty(hits) 正确,分页应用 page() Date: Sat, 13 Jun 2026 01:30:46 +0800 Subject: [PATCH 12/70] refactor(dsl): rename Shared/ to Support/ --- src/DSL/Queries/Compound/Boolean.php | 2 +- src/DSL/Queries/Compound/DisjunctionMax.php | 2 +- src/DSL/Queries/FullText/Intervals/Range.php | 2 +- src/DSL/Queries/Span/SpanNear.php | 2 +- src/DSL/Queries/Span/SpanOr.php | 2 +- src/DSL/Queries/TermLevel/Range.php | 2 +- src/DSL/{Shared => Support}/ClausesSupport.php | 2 +- src/DSL/{Shared => Support}/RangeSupport.php | 2 +- tests/Index/IndexTest.php | 16 ++++------------ 9 files changed, 12 insertions(+), 20 deletions(-) rename src/DSL/{Shared => Support}/ClausesSupport.php (96%) rename src/DSL/{Shared => Support}/RangeSupport.php (97%) diff --git a/src/DSL/Queries/Compound/Boolean.php b/src/DSL/Queries/Compound/Boolean.php index fe6c768..4b142ac 100644 --- a/src/DSL/Queries/Compound/Boolean.php +++ b/src/DSL/Queries/Compound/Boolean.php @@ -2,7 +2,7 @@ namespace ElasticKit\DSL\Queries\Compound; -use ElasticKit\DSL\Shared\ClausesSupport; +use ElasticKit\DSL\Support\ClausesSupport; use ElasticKit\DSL\Node; /** diff --git a/src/DSL/Queries/Compound/DisjunctionMax.php b/src/DSL/Queries/Compound/DisjunctionMax.php index a1bbe34..ec0a92d 100644 --- a/src/DSL/Queries/Compound/DisjunctionMax.php +++ b/src/DSL/Queries/Compound/DisjunctionMax.php @@ -2,7 +2,7 @@ namespace ElasticKit\DSL\Queries\Compound; -use ElasticKit\DSL\Shared\ClausesSupport; +use ElasticKit\DSL\Support\ClausesSupport; use ElasticKit\DSL\Node; /** diff --git a/src/DSL/Queries/FullText/Intervals/Range.php b/src/DSL/Queries/FullText/Intervals/Range.php index f40a01b..9237c3d 100644 --- a/src/DSL/Queries/FullText/Intervals/Range.php +++ b/src/DSL/Queries/FullText/Intervals/Range.php @@ -3,7 +3,7 @@ namespace ElasticKit\DSL\Queries\FullText\Intervals; use ElasticKit\DSL\Node; -use ElasticKit\DSL\Shared\RangeSupport; +use ElasticKit\DSL\Support\RangeSupport; /** * The range rule matches terms that fall within a specified range of values. diff --git a/src/DSL/Queries/Span/SpanNear.php b/src/DSL/Queries/Span/SpanNear.php index aec40fc..eaa8f20 100644 --- a/src/DSL/Queries/Span/SpanNear.php +++ b/src/DSL/Queries/Span/SpanNear.php @@ -2,7 +2,7 @@ namespace ElasticKit\DSL\Queries\Span; -use ElasticKit\DSL\Shared\ClausesSupport; +use ElasticKit\DSL\Support\ClausesSupport; use ElasticKit\DSL\Node; /** diff --git a/src/DSL/Queries/Span/SpanOr.php b/src/DSL/Queries/Span/SpanOr.php index 840aa73..3999878 100644 --- a/src/DSL/Queries/Span/SpanOr.php +++ b/src/DSL/Queries/Span/SpanOr.php @@ -2,7 +2,7 @@ namespace ElasticKit\DSL\Queries\Span; -use ElasticKit\DSL\Shared\ClausesSupport; +use ElasticKit\DSL\Support\ClausesSupport; use ElasticKit\DSL\Node; /** diff --git a/src/DSL/Queries/TermLevel/Range.php b/src/DSL/Queries/TermLevel/Range.php index 892e331..71d191b 100644 --- a/src/DSL/Queries/TermLevel/Range.php +++ b/src/DSL/Queries/TermLevel/Range.php @@ -3,7 +3,7 @@ namespace ElasticKit\DSL\Queries\TermLevel; use ElasticKit\DSL\Node; -use ElasticKit\DSL\Shared\RangeSupport; +use ElasticKit\DSL\Support\RangeSupport; class Range extends Node { diff --git a/src/DSL/Shared/ClausesSupport.php b/src/DSL/Support/ClausesSupport.php similarity index 96% rename from src/DSL/Shared/ClausesSupport.php rename to src/DSL/Support/ClausesSupport.php index ec2224b..a80534e 100644 --- a/src/DSL/Shared/ClausesSupport.php +++ b/src/DSL/Support/ClausesSupport.php @@ -1,6 +1,6 @@ =, >, <=, <) and [start, end] to ES range keys. diff --git a/tests/Index/IndexTest.php b/tests/Index/IndexTest.php index 08b732b..130191f 100644 --- a/tests/Index/IndexTest.php +++ b/tests/Index/IndexTest.php @@ -766,8 +766,7 @@ public function testNewDocUsesInstanceConnection() { $secondaryClient = $this->createMock(TestClient::class); $secondaryClient->method('getSource')->willReturn(new ArrayResponse([ - 'found' => true, - '_source' => ['title' => 'test'], + 'title' => 'test', ])); Index::setClient($secondaryClient, 'secondary'); @@ -776,10 +775,7 @@ public function testNewDocUsesInstanceConnection() $doc = $index->newDoc(1); $this->assertInstanceOf(\ElasticKit\Index\Doc::class, $doc); - $this->assertEquals([ - 'found' => true, - '_source' => ['title' => 'test'], - ], $doc->source()); + $this->assertEquals(['title' => 'test'], $doc->source()); } public function testQueryDocDelegateToNew() @@ -806,17 +802,13 @@ public function testOnNewDocChain() { $secondaryClient = $this->createMock(TestClient::class); $secondaryClient->method('getSource')->willReturn(new ArrayResponse([ - 'found' => true, - '_source' => ['title' => 'secondary_doc'], + 'title' => 'secondary_doc', ])); Index::setClient($secondaryClient, 'secondary'); $doc = TestConcreteIndex::on('secondary')->newDoc(42); - $this->assertEquals([ - 'found' => true, - '_source' => ['title' => 'secondary_doc'], - ], $doc->source()); + $this->assertEquals(['title' => 'secondary_doc'], $doc->source()); } } From fb133137770bb7b48bb82f64d031613a1b600e33 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Sat, 13 Jun 2026 15:26:02 +0800 Subject: [PATCH 13/70] refactor(index): PHP 8 modernization (Index layer) All 12 files: declare(strict_types=1) + constructor property promotion + readonly + native property/return/param types + union types. Main changes: - fix Manager::resolveIndexName() null-alias map causing null->TypeError (array_key_first() ?? $name) - Doc $id now supports string|int|null: index/create omit the id to let ES auto-generate it, while get/source/exists/update/delete guard with requireId() (closes the hole of silently sending a null id to ES) - Bulk::execute() gains a json_encode false guard (prevents strlen(false) TypeError under strict_types) - callable properties (resolver/errorHandler/dataSource) keep a docblock (PHP forbids callable as a property type) Sync: TestConcreteIndex and docs examples get property types; CLAUDE.md TODO split into Index/DSL layers. Co-Authored-By: Claude --- CLAUDE.md | 4 +- README.md | 4 +- docs/guide.md | 4 +- docs/index.md | 8 +-- src/Index/Bulk.php | 53 ++++++++------- src/Index/ClientManager.php | 4 +- src/Index/Doc.php | 121 +++++++++++++++++++++++----------- src/Index/Event.php | 16 +++-- src/Index/EventDispatcher.php | 12 ++-- src/Index/Index.php | 40 +++++------ src/Index/Manager.php | 52 +++++++-------- src/Index/Pagination.php | 16 +++-- src/Index/Rebuild.php | 34 ++++------ src/Index/Results.php | 50 +++++++------- src/Index/Search.php | 49 +++++++------- src/Index/StatsSupport.php | 14 ++-- tests/Index/DocTest.php | 23 +++++++ tests/Index/IndexTest.php | 2 +- 18 files changed, 284 insertions(+), 222 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1bd3c95..ccffaca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -48,13 +48,15 @@ PSR-5 规范。 - [x] **实例入口**:新增 on()/newQuery()/newDoc()/setConnection()/getConnection(),query()/doc() 委托实例方法 - [x] **测试 mock 污染修复**:所有测试文件补 tearDown 清理静态状态 - [ ] **命名参数一致性**:公开方法参数名是 API 的一部分,全库审查确保命名统一(如 connection/name/client 不混用) -- [ ] **PHP 8 现代化**:构造器属性提升、属性/返回/参数类型声明、readonly、联合类型 +- [x] **PHP 8 现代化(Index 层)**:全 12 文件 strict_types + 构造器提升 + readonly + 属性/返回/参数类型 + 联合类型;callable 属性(resolver / errorHandler / dataSource)因 PHP 禁止 callable 作属性类型,保留 docblock +- [ ] **PHP 8 现代化(DSL 层)**:Node/Query/Agg + 122 leaf 类。strict_types + 类型声明;`$_key`/`$_valueKey`/`$_isPropertyField`/`$_multi` 4 属性加类型需同步全部 leaf(原子操作,漏一个 fatal),field()/create()/boost()/toArray() 加返回类型需同步所有覆写;`$_properties`/`$_rawValue` 三模式统一留给批次 1 - [ ] **Rebuild 异常处理**:run() 改为 try-catch + releaseLock 分离,releaseLock 不吞任何异常,forceUnlock 单独处理 404(文档不存在 vs 索引不存在的 404 需区分);isLocked() 只吞 404 不吞其他 ClientResponseException;rebuild 失败优先抛原始异常;ensureLockIndex() replicas=0 在多节点集群有风险需注释说明 - [x] **$client 抽到 Registry 类**:拆为 ClientManager / EventDispatcher / Pagination,Index 不再持有静态状态 - [x] **Node 构造函数重构**:拆分为 fromKeyValue/fromClosure/fromArrayField/fromScalar - [x] **Bulk/Rebuild onError 设计**:Bulk 加 onError(callback) 默认 throw,Rebuild 删 skipErrors 加 onError,删 rebuild.import.failed 事件 - [ ] **补核心路径的边界测试**:scroll、bulk 分批、rebuild 失败回滚 - [ ] **搭建集成测试基建**:`ELASTICKIT_TEST_HOST` 驱动,随机索引名隔离 +- [ ] **cursor/chunk API 重构**:现 `cursor()` 返回批次(Results)命名不准。拆为 `chunk($duration): Generator`(按批,即现 cursor 改名)+ `cursor($duration): Generator`(逐条 doc,内部扁平化 chunk、复用其 finally clear);保留 `scroll()/next()/clear()` 作低层原语。待定:单条 yield 完整 hit(带 _id/_score)还是只 _source;底层未来可换 PIT+search_after(上层签名不变) ## 测试 diff --git a/README.md b/README.md index ceffd8f..5f5b1de 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,8 @@ Index::setClient($client); // 2. 定义索引 class ProductIndex extends Index { - protected $name = 'products'; - protected $mappings = [ + protected string $name = 'products'; + protected array $mappings = [ 'properties' => [ 'title' => ['type' => 'text'], 'price' => ['type' => 'float'], diff --git a/docs/guide.md b/docs/guide.md index 543b2de..5cd09ff 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -40,9 +40,9 @@ 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'], // 精确匹配 'status' => ['type' => 'keyword'], // pending/paid/shipped/completed diff --git a/docs/index.md b/docs/index.md index 67c38c0..34535b0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -25,18 +25,18 @@ Index::setClient($logClient, 'logs'); ```php class ProductIndex extends Index { - protected $name = 'products'; // 索引名(必填) - protected $mappings = [ // 索引 mappings + protected string $name = 'products'; // 索引名(必填) + protected array $mappings = [ // 索引 mappings 'properties' => [ 'title' => ['type' => 'text'], 'price' => ['type' => 'float'], 'status' => ['type' => 'keyword'], ], ]; - protected $settings = [ // 索引 settings + protected array $settings = [ // 索引 settings 'number_of_shards' => 1, ]; - protected $connection = 'main'; // 连接名(默认 'default') + protected string $connection = 'main'; // 连接名(默认 'default') public function rebuildName(): string // 重建后的真实索引名(可重写自定义) { diff --git a/src/Index/Bulk.php b/src/Index/Bulk.php index 9001e08..470af0e 100644 --- a/src/Index/Bulk.php +++ b/src/Index/Bulk.php @@ -1,5 +1,7 @@ */ - private $body = []; + private array $body = []; /** * @var int */ - private $retryOnConflict = 0; + private int $retryOnConflict = 0; /** * @var string|null */ - private $targetIndex = null; + private ?string $targetIndex = null; /** * @var int */ - private $batchSize = 0; + private int $batchSize = 0; /** * @var int */ - private $docCount = 0; + private int $docCount = 0; /** * @var callable|null */ private $errorHandler = null; - /** - * @param Index $index - */ - public function __construct(Index $index) - { - $this->index = $index; + public function __construct( + private readonly Index $index + ) { } /** @@ -60,7 +54,7 @@ public function __construct(Index $index) * @return $this * @throws \InvalidArgumentException if indexName starts with a dot (system index) */ - public function target($indexName) + public function target(string $indexName): static { if (strpos($indexName, '.') === 0) { throw new InvalidArgumentException("System index names (starting with '.') are not allowed: {$indexName}"); @@ -77,7 +71,7 @@ public function target($indexName) * @param int $size * @return $this */ - public function batchSize($size) + public function batchSize(int $size): static { $this->batchSize = $size; @@ -94,7 +88,7 @@ public function batchSize($size) * @param callable $handler function (array $response): void * @return $this */ - public function onError($handler) + public function onError(callable $handler): static { $this->errorHandler = $handler; return $this; @@ -106,7 +100,7 @@ public function onError($handler) * @param int $count * @return $this */ - public function retryOnConflict($count) + public function retryOnConflict(int $count): static { $this->retryOnConflict = $count; @@ -120,7 +114,7 @@ public function retryOnConflict($count) * @param array $document * @return $this */ - public function index($id, $document) + public function index(string|int|null $id, array $document): static { $action = ['index' => ['_index' => $this->resolveIndex()]]; if ($id !== null && $id !== '') { @@ -140,7 +134,7 @@ public function index($id, $document) * @param array $document * @return $this */ - public function save($id, $document) + public function save(string|int|null $id, array $document): static { return $this->index($id, $document); } @@ -152,7 +146,7 @@ public function save($id, $document) * @param array $document * @return $this */ - public function create($id, $document) + public function create(string|int $id, array $document): static { $this->body[] = ['create' => ['_index' => $this->resolveIndex(), '_id' => $id]]; $this->body[] = $document; @@ -171,7 +165,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]]; @@ -192,7 +186,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(); @@ -206,7 +200,7 @@ public function delete($id) * @param array $options top-level bulk API params (refresh, timeout, etc) * @return array */ - public function execute(array $options = []) + public function execute(array $options = []): array { if (empty($this->body)) { return []; @@ -240,6 +234,11 @@ public function execute(array $options = []) ($this->errorHandler)($response); } 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 = substr($json, 0, 4096) . '... [truncated]'; } @@ -255,7 +254,7 @@ public function execute(array $options = []) * * @return string */ - private function resolveIndex() + private function resolveIndex(): string { return $this->targetIndex ?? $this->index->name(); } diff --git a/src/Index/ClientManager.php b/src/Index/ClientManager.php index 886b042..ae62175 100644 --- a/src/Index/ClientManager.php +++ b/src/Index/ClientManager.php @@ -1,5 +1,7 @@ */ - private static $clients = []; + private static array $clients = []; /** * Register an Elasticsearch client. Optionally name the connection. diff --git a/src/Index/Doc.php b/src/Index/Doc.php index c5a194f..dd8d2fd 100644 --- a/src/Index/Doc.php +++ b/src/Index/Doc.php @@ -1,48 +1,44 @@ 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; } @@ -53,7 +49,7 @@ public function id() * @param int $count * @return $this */ - public function retryOnConflict($count) + public function retryOnConflict(int $count): static { $this->retryOnConflict = $count; @@ -66,7 +62,7 @@ public function retryOnConflict($count) * @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, @@ -148,17 +152,23 @@ public function update($data, $upsert = false) /** * Index (create or overwrite) the document. * + * If $id is null or empty, ES auto-generates an id. + * * @param array $document * @return array */ - public function index($document) + public function index(array $document): array { $params = [ 'index' => $this->index->name(), - 'id' => $this->id, - 'body' => $document, ]; + if ($this->id !== null && $this->id !== '') { + $params['id'] = $this->id; + } + + $params['body'] = $document; + if ($this->refresh !== null) { $params['refresh'] = $this->refresh; } @@ -174,7 +184,7 @@ public function index($document) * @param array $document * @return array */ - public function save($document) + public function save(array $document): array { return $this->index($document); } @@ -182,18 +192,25 @@ public function save($document) /** * Create the document (fail if already exists). * + * If $id is null or empty, ES auto-generates an id (always a create, since + * auto-generated ids are unique). + * * @param array $document * @return array */ - public function create($document) + public function create(array $document): 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'] = $document; + $params['op_type'] = 'create'; + if ($this->refresh !== null) { $params['refresh'] = $this->refresh; } @@ -208,11 +225,13 @@ 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) { @@ -224,6 +243,28 @@ public function delete() return $this->index->getClient()->delete($params)->asArray(); } + /** + * 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 requireId(string $operation): string|int + { + 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; + } + /** * Reset pending options after a write operation. */ diff --git a/src/Index/Event.php b/src/Index/Event.php index 9c250d0..bd8edd1 100644 --- a/src/Index/Event.php +++ b/src/Index/Event.php @@ -1,5 +1,7 @@ */ - private $data = []; + private array $data = []; /** * @param string $name * @param string $index */ - public function __construct($name, $index) + public function __construct(string $name, string $index) { $this->name = $name; $this->index = $index; @@ -47,7 +49,7 @@ public function __construct($name, $index) * @param string $key * @return mixed */ - public function __get($key) + public function __get(string $key): mixed { return $this->data[$key] ?? null; } @@ -56,7 +58,7 @@ public function __get($key) * @param string $key * @param mixed $value */ - public function __set($key, $value) + public function __set(string $key, mixed $value): void { $this->data[$key] = $value; } @@ -65,7 +67,7 @@ public function __set($key, $value) * @param string $key * @return bool */ - public function __isset($key) + public function __isset(string $key): bool { return isset($this->data[$key]); } diff --git a/src/Index/EventDispatcher.php b/src/Index/EventDispatcher.php index e3ac9df..830611b 100644 --- a/src/Index/EventDispatcher.php +++ b/src/Index/EventDispatcher.php @@ -1,5 +1,7 @@ > */ - private static $listeners = []; + private static array $listeners = []; /** * Register an event listener. @@ -21,7 +23,7 @@ class EventDispatcher * @param callable $listener receives (Event $event) * @return void */ - public static function listen($event, callable $listener) + public static function listen(string $event, callable $listener): void { self::$listeners[$event][] = $listener; } @@ -32,7 +34,7 @@ public static function listen($event, callable $listener) * @param Event $event * @return void */ - public static function dispatch(Event $event) + public static function dispatch(Event $event): void { foreach (self::$listeners as $pattern => $listeners) { if ($pattern === $event->name || $pattern === '*' || self::matchesCategory($pattern, $event->name)) { @@ -48,7 +50,7 @@ public static function dispatch(Event $event) * * @return void */ - public static function reset() + public static function reset(): void { self::$listeners = []; } @@ -60,7 +62,7 @@ public static function reset() * @param string $event * @return bool */ - private static function matchesCategory($pattern, $event) + private static function matchesCategory(string $pattern, string $event): bool { if (substr($pattern, -2) !== '.*') { return false; diff --git a/src/Index/Index.php b/src/Index/Index.php index 0dd9854..a7c9c98 100644 --- a/src/Index/Index.php +++ b/src/Index/Index.php @@ -1,5 +1,7 @@ */ - 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; /** * Register an Elasticsearch client. Optionally name the connection. @@ -72,7 +74,7 @@ public function getClient(): ClientInterface * @param string $connection * @return $this */ - public function setConnection(string $connection) + public function setConnection(string $connection): static { $this->connection = $connection; @@ -95,7 +97,7 @@ public function getConnection(): string * @param string $connection * @return static */ - public static function on(string $connection) + public static function on(string $connection): static { return (new static())->setConnection($connection); } @@ -105,7 +107,7 @@ public static function on(string $connection) * * @return string */ - public function name() + public function name(): string { if (empty($this->name)) { throw new RuntimeException( @@ -122,7 +124,7 @@ public function name() * @param Query|null $query * @return Search */ - public function newQuery(Query $query = null) + public function newQuery(?Query $query = null): Search { return new Search($this, $query); } @@ -133,7 +135,7 @@ public function newQuery(Query $query = null) * @param Query|null $query * @return Search */ - public static function query(Query $query = null) + public static function query(?Query $query = null): Search { return (new static())->newQuery($query); } @@ -141,10 +143,10 @@ public static function query(Query $query = null) /** * Create a Doc reference from this index instance. * - * @param string|int $id + * @param string|int|null $id document id, or null/'' to let ES auto-generate * @return Doc */ - public function newDoc($id) + public function newDoc(string|int|null $id): Doc { return new Doc($this, $id); } @@ -152,10 +154,10 @@ public function newDoc($id) /** * Create a Doc reference. Delegates to newDoc() with a fresh instance. * - * @param string|int $id + * @param string|int|null $id document id, or null/'' to let ES auto-generate * @return Doc */ - public static function doc($id) + public static function doc(string|int|null $id): Doc { return (new static())->newDoc($id); } @@ -165,7 +167,7 @@ public static function doc($id) * * @return array */ - public function mappings() + public function mappings(): array { return $this->mappings; } @@ -175,7 +177,7 @@ public function mappings() * * @return array */ - public function settings() + public function settings(): array { return $this->settings; } @@ -195,7 +197,7 @@ public function rebuildName(): string * * @return int */ - public function perPage() + public function perPage(): int { return $this->perPage; } @@ -205,7 +207,7 @@ public function perPage() * * @return int */ - public function maxPerPage() + public function maxPerPage(): int { return $this->maxPerPage; } diff --git a/src/Index/Manager.php b/src/Index/Manager.php index 00ee232..1d6054e 100644 --- a/src/Index/Manager.php +++ b/src/Index/Manager.php @@ -1,5 +1,7 @@ index = $index; + public function __construct( + private readonly Index $index + ) { } /** @@ -31,7 +25,7 @@ public function __construct(Index $index) * * @return array */ - public function create() + public function create(): array { $indexName = $this->index->name(); @@ -61,7 +55,7 @@ public function create() * * @return array */ - public function delete() + public function delete(): array { $indexName = $this->resolveIndexName(); @@ -84,7 +78,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 +90,7 @@ public function exists() * * @return array */ - public function get() + public function get(): array { return $this->index->getClient()->indices()->get([ 'index' => $this->index->name(), @@ -108,7 +102,7 @@ public function get() * * @return array */ - public function putMapping() + public function putMapping(): array { return $this->index->getClient()->indices()->putMapping([ 'index' => $this->index->name(), @@ -121,7 +115,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 +128,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 +141,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 +153,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 +166,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 +178,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 +190,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 +205,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 +228,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,7 +245,7 @@ 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(); @@ -280,7 +274,7 @@ public function swapAlias($alias, $fromIndex) * * @return array */ - public function getAliases() + public function getAliases(): array { $indexName = $this->resolveIndexName(); @@ -294,14 +288,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/Pagination.php b/src/Index/Pagination.php index 9a933b3..7474381 100644 --- a/src/Index/Pagination.php +++ b/src/Index/Pagination.php @@ -1,5 +1,7 @@ >|null */ private $dataSource; - /** - * @param Index $index - */ - public function __construct(Index $index) - { - $this->index = $index; + public function __construct( + private readonly Index $index + ) { } /** @@ -58,7 +52,7 @@ 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; @@ -74,7 +68,7 @@ public function batchSize($size) * @param callable $handler function (array $response): void * @return $this */ - public function onError($handler) + public function onError(callable $handler): static { $this->errorHandler = $handler; return $this; @@ -86,7 +80,7 @@ public function onError($handler) * @param bool $allow * @return $this */ - public function allowEmpty($allow = true) + public function allowEmpty(bool $allow = true): static { $this->allowEmpty = $allow; return $this; @@ -99,7 +93,7 @@ public function allowEmpty($allow = true) * @param callable|\Iterator> $source * @return $this */ - public function source($source) + public function source($source): static { $this->dataSource = $source; return $this; @@ -116,7 +110,7 @@ public function source($source) * @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 { $this->acquireLock(); @@ -344,7 +338,7 @@ private function ensureLockIndex(): void * * @return string the new backing index name */ - protected function createIndex() + protected function createIndex(): string { $newName = $this->index->rebuildName(); $mappings = $this->index->mappings(); @@ -367,7 +361,7 @@ protected function createIndex() * @param string $newName * @param array $context */ - protected function import($newName, array $context): void + protected function import(string $newName, array $context): void { if ($this->dataSource !== null) { $items = is_callable($this->dataSource) ? ($this->dataSource)($context) : $this->dataSource; diff --git a/src/Index/Results.php b/src/Index/Results.php index 5216d1b..18bc275 100644 --- a/src/Index/Results.php +++ b/src/Index/Results.php @@ -1,5 +1,7 @@ */ - 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 +46,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; @@ -57,7 +59,7 @@ public function paginate($page, $perPage) * * @return int */ - public function total() + public function total(): int { return $this->response['hits']['total']['value'] ?? 0; } @@ -67,7 +69,7 @@ public function total() * * @return array> */ - public function hits() + public function hits(): array { return $this->response['hits']['hits'] ?? []; } @@ -77,7 +79,7 @@ public function hits() * * @return array|null> */ - public function docs() + public function docs(): array { return array_column($this->hits(), '_source'); } @@ -87,7 +89,7 @@ public function docs() * * @return array */ - public function ids() + public function ids(): array { return array_column($this->hits(), '_id'); } @@ -97,7 +99,7 @@ public function ids() * * @return array|null */ - public function first() + public function first(): ?array { $docs = $this->docs(); return $docs[0] ?? null; @@ -108,7 +110,7 @@ public function first() * * @return array|null */ - public function aggregations() + public function aggregations(): ?array { return $this->response['aggregations'] ?? null; } @@ -118,7 +120,7 @@ public function aggregations() * * @return string|null */ - public function scrollId() + public function scrollId(): ?string { return $this->response['_scroll_id'] ?? null; } @@ -128,11 +130,11 @@ public function scrollId() * * "eq" = total is exact, "gte" = total is a lower bound. * - * @return string "eq" or "gte" + * @return string|null "eq" or "gte" */ - public function totalRelation() + public function totalRelation(): ?string { - return $this->response['hits']['total']['relation']; + return $this->response['hits']['total']['relation'] ?? null; } /** @@ -140,7 +142,7 @@ public function totalRelation() * * @return bool */ - public function hasMore() + public function hasMore(): bool { return !empty($this->response['hits']['hits']); } @@ -150,7 +152,7 @@ public function hasMore() * * @return int */ - public function took() + public function took(): int { return $this->response['took'] ?? 0; } @@ -160,7 +162,7 @@ public function took() * * @return bool */ - public function timedOut() + public function timedOut(): bool { return $this->response['timed_out'] ?? false; } @@ -170,7 +172,7 @@ public function timedOut() * * @return array */ - public function raw() + public function raw(): array { return $this->response; } @@ -180,7 +182,7 @@ public function raw() * * @return int */ - public function page() + public function page(): int { return $this->page; } @@ -190,7 +192,7 @@ public function page() * * @return int */ - public function perPage() + public function perPage(): int { return $this->perPage; } @@ -200,7 +202,7 @@ public function perPage() * * @return int */ - public function lastPage() + public function lastPage(): int { return (int) ceil($this->total() / $this->perPage) ?: 1; } @@ -210,7 +212,7 @@ public function lastPage() * * @return array|null> */ - public function items() + public function items(): array { return $this->docs(); } @@ -220,7 +222,7 @@ public function items() * * @return bool */ - public function isEmpty() + public function isEmpty(): bool { return empty($this->response['hits']['hits']); } diff --git a/src/Index/Search.php b/src/Index/Search.php index c275c2a..88b78c8 100644 --- a/src/Index/Search.php +++ b/src/Index/Search.php @@ -1,5 +1,7 @@ */ - 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; } /** @@ -46,7 +41,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; @@ -57,10 +52,10 @@ 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( @@ -82,7 +77,7 @@ public function __call($method, $args) * * @return Results */ - public function get() + public function get(): Results { return new Results($this->doSearch('get')); } @@ -92,7 +87,7 @@ public function get() * * @return array|null */ - public function first() + public function first(): ?array { $saved = $this->query; $this->query = clone $this->query; @@ -112,7 +107,7 @@ public function first() * * @return int */ - public function count() + public function count(): int { $response = $this->doCount(); @@ -130,7 +125,7 @@ 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); @@ -159,7 +154,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); } @@ -170,7 +165,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) { @@ -187,7 +182,7 @@ 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(); @@ -220,7 +215,7 @@ protected function doScroll($scrollId, $duration) * @param string $duration * @return \Generator */ - public function cursor($duration = '5m') + public function cursor(string $duration = '5m'): \Generator { $results = $this->scroll(null, $duration); @@ -241,7 +236,7 @@ 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 = Pagination::getPageResolver(); @@ -276,7 +271,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(); @@ -311,7 +306,7 @@ protected function doCount() * @param array $extra extra request params (e.g. scroll) * @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(); diff --git a/src/Index/StatsSupport.php b/src/Index/StatsSupport.php index 0d09a0b..f25627e 100644 --- a/src/Index/StatsSupport.php +++ b/src/Index/StatsSupport.php @@ -1,5 +1,7 @@ aggregateScalar('max', $field); } @@ -24,7 +26,7 @@ public function max($field) * @param string $field * @return float|null */ - public function min($field) + public function min(string $field): ?float { return $this->aggregateScalar('min', $field); } @@ -35,7 +37,7 @@ public function min($field) * @param string $field * @return float|null */ - public function avg($field) + public function avg(string $field): ?float { return $this->aggregateScalar('avg', $field); } @@ -46,7 +48,7 @@ public function avg($field) * @param string $field * @return float|null */ - public function sum($field) + public function sum(string $field): ?float { return $this->aggregateScalar('sum', $field); } @@ -57,7 +59,7 @@ public function sum($field) * @param string $field * @return array{count: int, min: float|null, max: float|null, avg: float|null, sum: float|null}|null */ - public function stats($field) + public function stats(string $field): ?array { $saved = $this->query; $this->query = clone $this->query; @@ -90,7 +92,7 @@ public function stats($field) * @param string $field * @return float|null */ - private function aggregateScalar($type, $field) + private function aggregateScalar(string $type, string $field): ?float { $saved = $this->query; $this->query = clone $this->query; diff --git a/tests/Index/DocTest.php b/tests/Index/DocTest.php index 7b3058f..949fac1 100644 --- a/tests/Index/DocTest.php +++ b/tests/Index/DocTest.php @@ -331,4 +331,27 @@ public function testDeleteWithRefresh() $index = $this->createIndex('products'); $index->doc('1')->refresh('wait_for')->delete(); } + + public function testIndexAutoGeneratesIdWhenEmpty() + { + $client = $this->createMock(TestClient::class); + $client->expects($this->once()) + ->method('index') + ->with(['index' => 'products', 'body' => ['title' => 'foo']]) + ->willReturn(new ArrayResponse(['result' => 'created', '_id' => 'auto'])); + Index::setClient($client); + + $index = $this->createIndex('products'); + $result = $index->newDoc(null)->index(['title' => 'foo']); + + $this->assertEquals('created', $result['result']); + } + + public function testUpdateRequiresExplicitId() + { + $index = $this->createIndex('products'); + + $this->expectException(\RuntimeException::class); + $index->newDoc(null)->update(['title' => 'foo']); + } } diff --git a/tests/Index/IndexTest.php b/tests/Index/IndexTest.php index 130191f..f6cb55e 100644 --- a/tests/Index/IndexTest.php +++ b/tests/Index/IndexTest.php @@ -814,5 +814,5 @@ public function testOnNewDocChain() class TestConcreteIndex extends Index { - protected $name = 'test'; + protected string $name = 'test'; } From 32a62ebafa5c8962b3c2ebbf9cb7431d79489134 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Sat, 13 Jun 2026 15:55:14 +0800 Subject: [PATCH 14/70] docs: remove stray files and clean up 7.x content from README Co-Authored-By: Claude --- README.md | 3 +- docs/upgrade.md | 94 -------- ...07\345\215\227\350\247\204\345\210\222.md" | 225 ------------------ 3 files changed, 1 insertion(+), 321 deletions(-) delete mode 100644 docs/upgrade.md delete mode 100644 "docs/\345\256\236\346\210\230\346\214\207\345\215\227\350\247\204\345\210\222.md" diff --git a/README.md b/README.md index 5f5b1de..29d9ed8 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ PHP Elasticsearch DSL 查询构建库,覆盖查询、聚合、CRUD、批量写 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)。 +> 需要 PHP 8.1+、Elasticsearch 8.x。依赖 `elasticsearch-php` 自动安装。 ## 快速开始 @@ -277,7 +277,6 @@ ProductIndex::listen('search.*', function (Event $e) { - [实践指南](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)——查询类型和参数参考 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 集群调优(属于运维范畴) From accae8d39bcf0e4ceb1843a59dd77191b8064e5a Mon Sep 17 00:00:00 2001 From: ykan821 Date: Mon, 15 Jun 2026 22:49:27 +0800 Subject: [PATCH 15/70] refactor(dsl): PHP 8 modernization and redundancy cleanup (DSL layer) - strict_types across all 151 files; the 4 atomic properties $_key/$_valueKey/$_fieldKeyed/$_multi get types synced across all leaves; leaf parameter types completed against ES docblocks - unify $_properties as ?array; add $_raw to carry full pass-through - rename $_isPropertyField to $_fieldKeyed for naming consistency (aligned with $_multi) - remove the dead setMulti (Node/Query); multi() handles it uniformly - remove 30 field() overrides equivalent to the base, keeping Highlight - clean up redundant properties and toArray in AdjacencyMatrix/Composite; serialization pushed down to resolveProperties Co-Authored-By: Claude --- CLAUDE.md | 2 +- src/DSL/Agg.php | 34 +++--- src/DSL/Aggs/Bucket.php | 2 + src/DSL/Aggs/Bucket/AdjacencyMatrix.php | 44 ++----- src/DSL/Aggs/Bucket/AutoDateHistogram.php | 14 ++- src/DSL/Aggs/Bucket/CategorizeText.php | 22 ++-- src/DSL/Aggs/Bucket/Composite.php | 40 ++----- src/DSL/Aggs/Bucket/DateHistogram.php | 28 ++--- src/DSL/Aggs/Bucket/DateRange.php | 25 ++-- src/DSL/Aggs/Bucket/DiversifiedSampler.php | 10 +- src/DSL/Aggs/Bucket/FilterAgg.php | 12 +- src/DSL/Aggs/Bucket/Filters.php | 10 +- src/DSL/Aggs/Bucket/FrequentItemSets.php | 12 +- src/DSL/Aggs/Bucket/GeoDistance.php | 25 ++-- src/DSL/Aggs/Bucket/GeoHashGrid.php | 21 +--- src/DSL/Aggs/Bucket/GeohexGrid.php | 21 +--- src/DSL/Aggs/Bucket/GeotileGrid.php | 21 +--- src/DSL/Aggs/Bucket/GlobalAgg.php | 4 +- src/DSL/Aggs/Bucket/Histogram.php | 35 +++--- src/DSL/Aggs/Bucket/IpPrefix.php | 17 +-- src/DSL/Aggs/Bucket/IpRange.php | 21 +--- src/DSL/Aggs/Bucket/Missing.php | 17 +-- src/DSL/Aggs/Bucket/MultiTerms.php | 18 +-- src/DSL/Aggs/Bucket/Nested.php | 8 +- src/DSL/Aggs/Bucket/ParentAgg.php | 6 +- src/DSL/Aggs/Bucket/RandomSampler.php | 19 +--- src/DSL/Aggs/Bucket/Range.php | 23 ++-- src/DSL/Aggs/Bucket/RareTerms.php | 25 ++-- src/DSL/Aggs/Bucket/ReverseNested.php | 6 +- src/DSL/Aggs/Bucket/SignificantTerms.php | 31 ++--- src/DSL/Aggs/Bucket/SignificantText.php | 31 ++--- src/DSL/Aggs/Bucket/Terms.php | 37 +++--- src/DSL/Aggs/Bucket/TimeSeries.php | 19 +--- .../Aggs/Bucket/VariableWidthHistogram.php | 17 +-- src/DSL/Aggs/Metric.php | 2 + src/DSL/Aggs/Metric/Avg.php | 23 ++-- src/DSL/Aggs/Metric/Cardinality.php | 25 ++-- src/DSL/Aggs/Metric/ExtendedStats.php | 25 ++-- src/DSL/Aggs/Metric/Max.php | 23 ++-- src/DSL/Aggs/Metric/Min.php | 23 ++-- src/DSL/Aggs/Metric/Stats.php | 23 ++-- src/DSL/Aggs/Metric/Sum.php | 23 ++-- src/DSL/Aggs/Metric/ValueCount.php | 19 +--- src/DSL/Aggs/Pipeline.php | 2 + src/DSL/Aggs/Pipeline/AvgBucket.php | 14 ++- src/DSL/Aggs/Pipeline/BucketScript.php | 12 +- src/DSL/Aggs/Pipeline/CumulativeSum.php | 8 +- src/DSL/Aggs/Pipeline/Derivative.php | 12 +- src/DSL/Aggs/Pipeline/MaxBucket.php | 14 ++- src/DSL/Aggs/Pipeline/MinBucket.php | 14 ++- src/DSL/Aggs/Pipeline/StatsBucket.php | 14 ++- src/DSL/Aggs/Pipeline/SumBucket.php | 14 ++- src/DSL/Node.php | 107 ++++++++++-------- src/DSL/Param.php | 14 ++- src/DSL/Params/Collapse.php | 23 ++-- src/DSL/Params/Highlight.php | 38 ++++--- src/DSL/Params/Knn.php | 37 +++--- src/DSL/Params/Rescore.php | 16 +-- src/DSL/Params/Suggest.php | 10 +- src/DSL/Queries/Compound.php | 2 + src/DSL/Queries/Compound/Boolean.php | 16 +-- src/DSL/Queries/Compound/Boosting.php | 10 +- src/DSL/Queries/Compound/ConstantScore.php | 6 +- src/DSL/Queries/Compound/DisjunctionMax.php | 8 +- src/DSL/Queries/Compound/FunctionScore.php | 34 +++--- src/DSL/Queries/Compound/Functions/Exp.php | 22 ++-- .../Compound/Functions/FieldValueFactor.php | 23 ++-- .../Queries/Compound/Functions/Function_.php | 40 +++---- src/DSL/Queries/Compound/Functions/Gauss.php | 22 ++-- src/DSL/Queries/Compound/Functions/Linear.php | 22 ++-- .../Compound/Functions/RandomScore.php | 19 +--- .../Compound/Functions/ScriptScore.php | 8 +- src/DSL/Queries/FullText.php | 2 + src/DSL/Queries/FullText/CombinedFields.php | 18 +-- src/DSL/Queries/FullText/Intervals.php | 22 ++-- src/DSL/Queries/FullText/Intervals/AllOf.php | 18 +-- src/DSL/Queries/FullText/Intervals/AnyOf.php | 14 ++- src/DSL/Queries/FullText/Intervals/Filter.php | 22 ++-- src/DSL/Queries/FullText/Intervals/Fuzzy.php | 18 +-- src/DSL/Queries/FullText/Intervals/Match_.php | 16 +-- src/DSL/Queries/FullText/Intervals/Prefix.php | 10 +- src/DSL/Queries/FullText/Intervals/Range.php | 32 +++--- .../Queries/FullText/Intervals/Wildcard.php | 10 +- src/DSL/Queries/FullText/MatchBoolPrefix.php | 32 +++--- src/DSL/Queries/FullText/MatchPhrase.php | 16 +-- .../Queries/FullText/MatchPhrasePrefix.php | 18 +-- src/DSL/Queries/FullText/Match_.php | 38 ++++--- src/DSL/Queries/FullText/MultiMatch.php | 42 +++---- src/DSL/Queries/FullText/QueryString.php | 54 ++++----- .../Queries/FullText/SimpleQueryString.php | 32 +++--- src/DSL/Queries/Geo.php | 2 + src/DSL/Queries/Geo/GeoBoundingBox.php | 28 ++--- src/DSL/Queries/Geo/GeoDistance.php | 16 +-- src/DSL/Queries/Geo/GeoGrid.php | 12 +- src/DSL/Queries/Geo/GeoPolygon.php | 12 +- src/DSL/Queries/Geo/GeoShape.php | 14 ++- src/DSL/Queries/Joining.php | 2 + src/DSL/Queries/Joining/HasChild.php | 16 +-- src/DSL/Queries/Joining/HasParent.php | 12 +- src/DSL/Queries/Joining/Nested.php | 14 ++- src/DSL/Queries/Joining/ParentId.php | 10 +- src/DSL/Queries/MatchAll.php | 2 + src/DSL/Queries/MatchAll/MatchAll.php | 4 +- src/DSL/Queries/MatchAll/MatchNone.php | 4 +- src/DSL/Queries/Script.php | 20 ++-- src/DSL/Queries/Shape.php | 2 + src/DSL/Queries/Shape/Shape.php | 12 +- src/DSL/Queries/Span.php | 2 + src/DSL/Queries/Span/SpanContaining.php | 8 +- src/DSL/Queries/Span/SpanFieldMasking.php | 17 +-- src/DSL/Queries/Span/SpanFirst.php | 8 +- src/DSL/Queries/Span/SpanMulti.php | 6 +- src/DSL/Queries/Span/SpanNear.php | 10 +- src/DSL/Queries/Span/SpanNot.php | 14 ++- src/DSL/Queries/Span/SpanOr.php | 6 +- src/DSL/Queries/Span/SpanTerm.php | 14 ++- src/DSL/Queries/Span/SpanWithin.php | 8 +- src/DSL/Queries/Specialized.php | 2 + .../Queries/Specialized/DistanceFeature.php | 10 +- src/DSL/Queries/Specialized/MoreLikeThis.php | 12 +- src/DSL/Queries/Specialized/Percolate.php | 6 +- src/DSL/Queries/Specialized/Pinned.php | 10 +- src/DSL/Queries/Specialized/RankFeature.php | 12 +- src/DSL/Queries/Specialized/Script.php | 6 +- src/DSL/Queries/Specialized/ScriptScore.php | 10 +- src/DSL/Queries/Specialized/Wrapper.php | 8 +- src/DSL/Queries/TermLevel.php | 2 + src/DSL/Queries/TermLevel/Exists.php | 4 +- src/DSL/Queries/TermLevel/Fuzzy.php | 20 ++-- src/DSL/Queries/TermLevel/IDs.php | 6 +- src/DSL/Queries/TermLevel/Prefix.php | 12 +- src/DSL/Queries/TermLevel/Range.php | 36 +++--- src/DSL/Queries/TermLevel/Regexp.php | 16 +-- src/DSL/Queries/TermLevel/Term.php | 12 +- src/DSL/Queries/TermLevel/Terms.php | 10 +- src/DSL/Queries/TermLevel/TermsSet.php | 20 ++-- src/DSL/Queries/TermLevel/Wildcard.php | 14 ++- src/DSL/Query.php | 38 +++---- src/DSL/Support/ClausesSupport.php | 2 + src/DSL/Support/RangeSupport.php | 2 + tests/DslTestCase.php | 2 +- 141 files changed, 1151 insertions(+), 1254 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ccffaca..75b1dc9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,7 +49,7 @@ PSR-5 规范。 - [x] **测试 mock 污染修复**:所有测试文件补 tearDown 清理静态状态 - [ ] **命名参数一致性**:公开方法参数名是 API 的一部分,全库审查确保命名统一(如 connection/name/client 不混用) - [x] **PHP 8 现代化(Index 层)**:全 12 文件 strict_types + 构造器提升 + readonly + 属性/返回/参数类型 + 联合类型;callable 属性(resolver / errorHandler / dataSource)因 PHP 禁止 callable 作属性类型,保留 docblock -- [ ] **PHP 8 现代化(DSL 层)**:Node/Query/Agg + 122 leaf 类。strict_types + 类型声明;`$_key`/`$_valueKey`/`$_isPropertyField`/`$_multi` 4 属性加类型需同步全部 leaf(原子操作,漏一个 fatal),field()/create()/boost()/toArray() 加返回类型需同步所有覆写;`$_properties`/`$_rawValue` 三模式统一留给批次 1 +- [x] **PHP 8 现代化(DSL 层)**:Node/Query/Agg + 122 leaf 类全部完成。strict_types 全 151 文件;4 原子属性类型同步全 leaf;leaf 参数类型对照 ES docblock 完成;`$_properties` 三模式统一为 `?array`(新增 `$_raw` 承接整体透传,null 保留为合法空态);`toJson` 加 `:string` + false 检查;`toArray` 因多态返回(array/stdClass/null)不强加 PHP 返回类型 - [ ] **Rebuild 异常处理**:run() 改为 try-catch + releaseLock 分离,releaseLock 不吞任何异常,forceUnlock 单独处理 404(文档不存在 vs 索引不存在的 404 需区分);isLocked() 只吞 404 不吞其他 ClientResponseException;rebuild 失败优先抛原始异常;ensureLockIndex() replicas=0 在多节点集群有风险需注释说明 - [x] **$client 抽到 Registry 类**:拆为 ClientManager / EventDispatcher / Pagination,Index 不再持有静态状态 - [x] **Node 构造函数重构**:拆分为 fromKeyValue/fromClosure/fromArrayField/fromScalar diff --git a/src/DSL/Agg.php b/src/DSL/Agg.php index cb7c565..79a2aaa 100644 --- a/src/DSL/Agg.php +++ b/src/DSL/Agg.php @@ -1,5 +1,7 @@ */ - 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 +54,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 +81,7 @@ public static function create($agg = []) * @param Node $node * @return $this */ - protected function node($node) + protected function node($node): static { $this->_node = $node; $this->_properties = null; @@ -99,7 +97,7 @@ protected function node($node) * @param string $alias * @return $this */ - public function alias($alias) + public function alias($alias): static { $this->_alias = $alias; return $this; @@ -128,7 +126,7 @@ public function getAlias() * @return $this * @throws \BadMethodCallException if called with a string alias and no definition */ - public function aggs($alias, $aggs = null) + public function aggs($alias, $aggs = null): static { if ($aggs === null && !is_string($alias)) { $aggs = $alias; @@ -236,9 +234,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, int $depth = 512): string { - return json_encode($this->toArray(), $flags, $depth); + $json = json_encode($this->toArray(), $flags, $depth); + + return $json === false ? '' : $json; } /** @@ -246,7 +246,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..b9336c1 100644 --- a/src/DSL/Aggs/Bucket.php +++ b/src/DSL/Aggs/Bucket.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; } /** @@ -37,31 +34,8 @@ public function filters($key, $query) * @param string $separator * @return static */ - public function separator($separator) + public function separator(string $separator): 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; - } } diff --git a/src/DSL/Aggs/Bucket/AutoDateHistogram.php b/src/DSL/Aggs/Bucket/AutoDateHistogram.php index f2c1172..aa9fa77 100644 --- a/src/DSL/Aggs/Bucket/AutoDateHistogram.php +++ b/src/DSL/Aggs/Bucket/AutoDateHistogram.php @@ -1,5 +1,7 @@ addProperty('buckets', $buckets); } @@ -28,7 +30,7 @@ public function buckets($buckets) * @param string $format * @return static */ - public function format($format) + public function format(string $format): static { return $this->addProperty('format', $format); } @@ -39,7 +41,7 @@ public function format($format) * @param string $timeZone * @return static */ - public function timeZone($timeZone) + public function timeZone(string $timeZone): static { return $this->addProperty('time_zone', $timeZone); } @@ -50,7 +52,7 @@ public function timeZone($timeZone) * @param string $minimumInterval * @return static */ - public function minimumInterval($minimumInterval) + public function minimumInterval(string $minimumInterval): static { return $this->addProperty('minimum_interval', $minimumInterval); } @@ -61,7 +63,7 @@ public function minimumInterval($minimumInterval) * @param mixed $missing * @return static */ - public function missing($missing) + public function missing($missing): static { return $this->addProperty('missing', $missing); } diff --git a/src/DSL/Aggs/Bucket/CategorizeText.php b/src/DSL/Aggs/Bucket/CategorizeText.php index c54f088..09f74e8 100644 --- a/src/DSL/Aggs/Bucket/CategorizeText.php +++ b/src/DSL/Aggs/Bucket/CategorizeText.php @@ -1,5 +1,7 @@ addProperty('categorization_analyzer', $categorizationAnalyzer); } @@ -28,7 +30,7 @@ public function categorizationAnalyzer($categorizationAnalyzer) * @param array $categorizationFilters * @return static */ - public function categorizationFilters($categorizationFilters) + public function categorizationFilters(array $categorizationFilters): static { return $this->addProperty('categorization_filters', $categorizationFilters); } @@ -39,7 +41,7 @@ public function categorizationFilters($categorizationFilters) * @param int $maxMatchedTokens * @return static */ - public function maxMatchedTokens($maxMatchedTokens) + public function maxMatchedTokens(int $maxMatchedTokens): static { return $this->addProperty('max_matched_tokens', $maxMatchedTokens); } @@ -50,7 +52,7 @@ public function maxMatchedTokens($maxMatchedTokens) * @param int $maxUniqueTokens * @return static */ - public function maxUniqueTokens($maxUniqueTokens) + public function maxUniqueTokens(int $maxUniqueTokens): static { return $this->addProperty('max_unique_tokens', $maxUniqueTokens); } @@ -61,7 +63,7 @@ public function maxUniqueTokens($maxUniqueTokens) * @param int $minDocCount * @return static */ - public function minDocCount($minDocCount) + public function minDocCount(int $minDocCount): static { return $this->addProperty('min_doc_count', $minDocCount); } @@ -72,7 +74,7 @@ public function minDocCount($minDocCount) * @param int $shardMinDocCount * @return static */ - public function shardMinDocCount($shardMinDocCount) + public function shardMinDocCount(int $shardMinDocCount): static { return $this->addProperty('shard_min_doc_count', $shardMinDocCount); } @@ -83,7 +85,7 @@ public function shardMinDocCount($shardMinDocCount) * @param int $shardSize * @return static */ - public function shardSize($shardSize) + public function shardSize(int $shardSize): static { return $this->addProperty('shard_size', $shardSize); } @@ -94,7 +96,7 @@ public function shardSize($shardSize) * @param float $similarityThreshold * @return static */ - public function similarityThreshold($similarityThreshold) + public function similarityThreshold(float $similarityThreshold): static { return $this->addProperty('similarity_threshold', $similarityThreshold); } @@ -105,7 +107,7 @@ public function similarityThreshold($similarityThreshold) * @param int $size * @return static */ - public function size($size) + public function size(int $size): static { return $this->addProperty('size', $size); } diff --git a/src/DSL/Aggs/Bucket/Composite.php b/src/DSL/Aggs/Bucket/Composite.php index 681ef95..c3a57ae 100644 --- a/src/DSL/Aggs/Bucket/Composite.php +++ b/src/DSL/Aggs/Bucket/Composite.php @@ -1,5 +1,7 @@ addProperty('sources', $sources); } @@ -32,7 +30,7 @@ public function sources($sources) * @param mixed $after * @return static */ - public function after($after) + public function after($after): static { return $this->addProperty('after', $after); } @@ -43,7 +41,7 @@ public function after($after) * @param mixed $order * @return static */ - public function order($order) + public function order($order): static { return $this->addProperty('order', $order); } @@ -54,32 +52,8 @@ public function order($order) * @param int $size * @return static */ - public function size($size) + public function size(int $size): static { return $this->addProperty('size', $size); } - - /** - * {@inheritdoc} - * @SuppressWarnings(PHPMD.IfStatementAssignment) - */ - public function toArray() - { - $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; - } } diff --git a/src/DSL/Aggs/Bucket/DateHistogram.php b/src/DSL/Aggs/Bucket/DateHistogram.php index 2e1d01b..e95c988 100644 --- a/src/DSL/Aggs/Bucket/DateHistogram.php +++ b/src/DSL/Aggs/Bucket/DateHistogram.php @@ -1,5 +1,7 @@ addProperty('calendar_interval', $calendarInterval); } @@ -28,7 +30,7 @@ public function calendarInterval($calendarInterval) * @param string $interval * @return static */ - public function interval($interval) + public function interval(string $interval): static { return $this->addProperty('interval', $interval); } @@ -39,7 +41,7 @@ public function interval($interval) * @param string $fixedInterval * @return static */ - public function fixedInterval($fixedInterval) + public function fixedInterval(string $fixedInterval): static { return $this->addProperty('fixed_interval', $fixedInterval); } @@ -50,7 +52,7 @@ public function fixedInterval($fixedInterval) * @param string $format * @return static */ - public function format($format) + public function format(string $format): static { return $this->addProperty('format', $format); } @@ -61,7 +63,7 @@ public function format($format) * @param string $timeZone * @return static */ - public function timeZone($timeZone) + public function timeZone(string $timeZone): static { return $this->addProperty('time_zone', $timeZone); } @@ -72,7 +74,7 @@ public function timeZone($timeZone) * @param int $minDocCount * @return static */ - public function minDocCount($minDocCount) + public function minDocCount(int $minDocCount): static { return $this->addProperty('min_doc_count', $minDocCount); } @@ -83,7 +85,7 @@ public function minDocCount($minDocCount) * @param mixed $extendedBounds * @return static */ - public function extendedBounds($extendedBounds) + public function extendedBounds($extendedBounds): static { return $this->addProperty('extended_bounds', $extendedBounds); } @@ -94,7 +96,7 @@ public function extendedBounds($extendedBounds) * @param mixed $hardBounds * @return static */ - public function hardBounds($hardBounds) + public function hardBounds($hardBounds): static { return $this->addProperty('hard_bounds', $hardBounds); } @@ -105,7 +107,7 @@ public function hardBounds($hardBounds) * @param mixed $order * @return static */ - public function order($order) + public function order($order): static { return $this->addProperty('order', $order); } @@ -116,7 +118,7 @@ public function order($order) * @param bool $keyed * @return static */ - public function keyed($keyed) + public function keyed(bool $keyed): static { return $this->addProperty('keyed', $keyed); } @@ -127,7 +129,7 @@ public function keyed($keyed) * @param mixed $missing * @return static */ - public function missing($missing) + public function missing($missing): static { return $this->addProperty('missing', $missing); } @@ -138,7 +140,7 @@ public function missing($missing) * @param string $offset * @return static */ - public function offset($offset) + public function offset(string $offset): static { return $this->addProperty('offset', $offset); } diff --git a/src/DSL/Aggs/Bucket/DateRange.php b/src/DSL/Aggs/Bucket/DateRange.php index e7a6638..cd19739 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. @@ -28,7 +19,7 @@ public function field($field) * @param array $ranges * @return static */ - public function ranges($ranges) + public function ranges(array $ranges): static { return $this->addProperty('ranges', $ranges); } @@ -39,7 +30,7 @@ public function ranges($ranges) * @param bool $keyed * @return static */ - public function keyed($keyed) + public function keyed(bool $keyed): static { return $this->addProperty('keyed', $keyed); } @@ -50,7 +41,7 @@ public function keyed($keyed) * @param string $format * @return static */ - public function format($format) + public function format(string $format): static { return $this->addProperty('format', $format); } @@ -61,7 +52,7 @@ public function format($format) * @param mixed $missing * @return static */ - public function missing($missing) + public function missing($missing): static { return $this->addProperty('missing', $missing); } @@ -72,7 +63,7 @@ public function missing($missing) * @param string $timeZone * @return static */ - public function timeZone($timeZone) + public function timeZone(string $timeZone): static { return $this->addProperty('time_zone', $timeZone); } diff --git a/src/DSL/Aggs/Bucket/DiversifiedSampler.php b/src/DSL/Aggs/Bucket/DiversifiedSampler.php index 781676d..d50f4b7 100644 --- a/src/DSL/Aggs/Bucket/DiversifiedSampler.php +++ b/src/DSL/Aggs/Bucket/DiversifiedSampler.php @@ -1,5 +1,7 @@ addProperty('shard_size', $shardSize); } @@ -28,7 +30,7 @@ public function shardSize($shardSize) * @param int $maxDocsPerValue * @return static */ - public function maxDocsPerValue($maxDocsPerValue) + public function maxDocsPerValue(int $maxDocsPerValue): static { return $this->addProperty('max_docs_per_value', $maxDocsPerValue); } @@ -39,7 +41,7 @@ public function maxDocsPerValue($maxDocsPerValue) * @param string $executionHint * @return static */ - public function executionHint($executionHint) + public function executionHint(string $executionHint): static { return $this->addProperty('execution_hint', $executionHint); } diff --git a/src/DSL/Aggs/Bucket/FilterAgg.php b/src/DSL/Aggs/Bucket/FilterAgg.php index f2c8f54..630b2d8 100644 --- a/src/DSL/Aggs/Bucket/FilterAgg.php +++ b/src/DSL/Aggs/Bucket/FilterAgg.php @@ -1,5 +1,7 @@ _properties = $filter; + $this->_raw = $filter; return $this; } @@ -31,6 +33,6 @@ public function setFilter($filter) */ public function toArray() { - return Query::create($this->_properties)->toArray()['query']; + return Query::create($this->_raw)->toArray()['query']; } } diff --git a/src/DSL/Aggs/Bucket/Filters.php b/src/DSL/Aggs/Bucket/Filters.php index e6502f8..33282d8 100644 --- a/src/DSL/Aggs/Bucket/Filters.php +++ b/src/DSL/Aggs/Bucket/Filters.php @@ -1,5 +1,7 @@ addProperty('filters', $filters); } @@ -28,7 +30,7 @@ public function filters($filters) * @param string $otherBucketKey * @return static */ - public function otherBucketKey($otherBucketKey) + public function otherBucketKey(string $otherBucketKey): static { return $this->addProperty('other_bucket_key', $otherBucketKey); } @@ -39,7 +41,7 @@ public function otherBucketKey($otherBucketKey) * @param bool $keyed * @return static */ - public function keyed($keyed) + public function keyed(bool $keyed): static { return $this->addProperty('keyed', $keyed); } diff --git a/src/DSL/Aggs/Bucket/FrequentItemSets.php b/src/DSL/Aggs/Bucket/FrequentItemSets.php index a5c944e..7fd11af 100644 --- a/src/DSL/Aggs/Bucket/FrequentItemSets.php +++ b/src/DSL/Aggs/Bucket/FrequentItemSets.php @@ -1,5 +1,7 @@ addProperty('minimum_set_size', $minimumSetSize); } @@ -28,7 +30,7 @@ public function minimumSetSize($minimumSetSize) * @param array $fields * @return static */ - public function fields($fields) + public function fields(array $fields): static { return $this->addProperty('fields', $fields); } @@ -39,7 +41,7 @@ public function fields($fields) * @param int $size * @return static */ - public function size($size) + public function size(int $size): static { return $this->addProperty('size', $size); } @@ -50,7 +52,7 @@ public function size($size) * @param mixed $filter * @return static */ - public function filter($filter) + public function filter($filter): static { return $this->addProperty('filter', $filter); } diff --git a/src/DSL/Aggs/Bucket/GeoDistance.php b/src/DSL/Aggs/Bucket/GeoDistance.php index 3e6dcdd..8e05b02 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. @@ -28,7 +19,7 @@ public function field($field) * @param mixed $origin * @return static */ - public function origin($origin) + public function origin($origin): static { return $this->addProperty('origin', $origin); } @@ -39,7 +30,7 @@ public function origin($origin) * @param string $unit * @return static */ - public function unit($unit) + public function unit(string $unit): static { return $this->addProperty('unit', $unit); } @@ -50,7 +41,7 @@ public function unit($unit) * @param string $distanceType * @return static */ - public function distanceType($distanceType) + public function distanceType(string $distanceType): static { return $this->addProperty('distance_type', $distanceType); } @@ -61,7 +52,7 @@ public function distanceType($distanceType) * @param array $ranges * @return static */ - public function ranges($ranges) + public function ranges(array $ranges): static { return $this->addProperty('ranges', $ranges); } @@ -72,7 +63,7 @@ public function ranges($ranges) * @param bool $keyed * @return static */ - public function keyed($keyed) + public function keyed(bool $keyed): static { return $this->addProperty('keyed', $keyed); } diff --git a/src/DSL/Aggs/Bucket/GeoHashGrid.php b/src/DSL/Aggs/Bucket/GeoHashGrid.php index 6933f99..15b75b6 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. @@ -28,7 +19,7 @@ public function field($field) * @param int $precision * @return static */ - public function precision($precision) + public function precision(int $precision): static { return $this->addProperty('precision', $precision); } @@ -39,7 +30,7 @@ public function precision($precision) * @param int $size * @return static */ - public function size($size) + public function size(int $size): static { return $this->addProperty('size', $size); } @@ -50,7 +41,7 @@ public function size($size) * @param int $shardSize * @return static */ - public function shardSize($shardSize) + public function shardSize(int $shardSize): static { return $this->addProperty('shard_size', $shardSize); } diff --git a/src/DSL/Aggs/Bucket/GeohexGrid.php b/src/DSL/Aggs/Bucket/GeohexGrid.php index fa8b446..d8ad854 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. @@ -28,7 +19,7 @@ public function field($field) * @param int $precision * @return static */ - public function precision($precision) + public function precision(int $precision): static { return $this->addProperty('precision', $precision); } @@ -39,7 +30,7 @@ public function precision($precision) * @param int $size * @return static */ - public function size($size) + public function size(int $size): static { return $this->addProperty('size', $size); } @@ -50,7 +41,7 @@ public function size($size) * @param int $shardSize * @return static */ - public function shardSize($shardSize) + public function shardSize(int $shardSize): static { return $this->addProperty('shard_size', $shardSize); } diff --git a/src/DSL/Aggs/Bucket/GeotileGrid.php b/src/DSL/Aggs/Bucket/GeotileGrid.php index 6f60cec..2e95403 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. @@ -28,7 +19,7 @@ public function field($field) * @param int $precision * @return static */ - public function precision($precision) + public function precision(int $precision): static { return $this->addProperty('precision', $precision); } @@ -39,7 +30,7 @@ public function precision($precision) * @param int $size * @return static */ - public function size($size) + public function size(int $size): static { return $this->addProperty('size', $size); } @@ -50,7 +41,7 @@ public function size($size) * @param int $shardSize * @return static */ - public function shardSize($shardSize) + public function shardSize(int $shardSize): static { return $this->addProperty('shard_size', $shardSize); } diff --git a/src/DSL/Aggs/Bucket/GlobalAgg.php b/src/DSL/Aggs/Bucket/GlobalAgg.php index 9fd6f86..7d581d2 100644 --- a/src/DSL/Aggs/Bucket/GlobalAgg.php +++ b/src/DSL/Aggs/Bucket/GlobalAgg.php @@ -1,5 +1,7 @@ addProperty('field', $field); - } + protected string $_key = 'histogram'; /** * Interval size for each bucket. @@ -28,7 +19,7 @@ public function field($field) * @param float $interval * @return static */ - public function interval($interval) + public function interval(float $interval): static { return $this->addProperty('interval', $interval); } @@ -39,7 +30,7 @@ public function interval($interval) * @param int $minDocCount * @return static */ - public function minDocCount($minDocCount) + public function minDocCount(int $minDocCount): static { return $this->addProperty('min_doc_count', $minDocCount); } @@ -50,7 +41,7 @@ public function minDocCount($minDocCount) * @param mixed $bounds * @return static */ - public function extendedBounds($bounds) + public function extendedBounds($bounds): static { return $this->addProperty('extended_bounds', $bounds); } @@ -61,7 +52,7 @@ public function extendedBounds($bounds) * @param mixed $order * @return static */ - public function order($order) + public function order($order): static { return $this->addProperty('order', $order); } @@ -72,7 +63,7 @@ public function order($order) * @param bool $keyed * @return static */ - public function keyed($keyed) + public function keyed(bool $keyed): static { return $this->addProperty('keyed', $keyed); } @@ -83,7 +74,7 @@ public function keyed($keyed) * @param float $missing * @return static */ - public function missing($missing) + public function missing(float $missing): static { return $this->addProperty('missing', $missing); } @@ -94,7 +85,7 @@ public function missing($missing) * @param string $format * @return static */ - public function format($format) + public function format(string $format): static { return $this->addProperty('format', $format); } @@ -105,7 +96,7 @@ public function format($format) * @param string|callable $script * @return static */ - public function script($script) + public function script($script): static { return $this->addProperty('script', $script); } @@ -116,7 +107,7 @@ public function script($script) * @param float $offset * @return static */ - public function offset($offset) + public function offset(float $offset): static { return $this->addProperty('offset', $offset); } @@ -127,7 +118,7 @@ public function offset($offset) * @param mixed $hardBounds * @return static */ - public function hardBounds($hardBounds) + public function hardBounds($hardBounds): static { return $this->addProperty('hard_bounds', $hardBounds); } diff --git a/src/DSL/Aggs/Bucket/IpPrefix.php b/src/DSL/Aggs/Bucket/IpPrefix.php index 8d90deb..b59655b 100644 --- a/src/DSL/Aggs/Bucket/IpPrefix.php +++ b/src/DSL/Aggs/Bucket/IpPrefix.php @@ -1,21 +1,14 @@ addProperty('field', $field); - } + protected string $_key = 'ip_prefix'; /** * Length of the network prefix. @@ -23,7 +16,7 @@ public function field($field) * @param int $length * @return static */ - public function prefixLength($length) + public function prefixLength(int $length): static { return $this->addProperty('prefix_length', $length); } @@ -32,7 +25,7 @@ public function prefixLength($length) * @param int $length * @return static */ - public function minPrefixLength($length) + public function minPrefixLength(int $length): static { return $this->addProperty('min_prefix_length', $length); } diff --git a/src/DSL/Aggs/Bucket/IpRange.php b/src/DSL/Aggs/Bucket/IpRange.php index 46661ec..3ca179b 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. @@ -28,7 +19,7 @@ public function field($field) * @param array $ranges * @return static */ - public function ranges($ranges) + public function ranges(array $ranges): static { return $this->addProperty('ranges', $ranges); } @@ -39,7 +30,7 @@ public function ranges($ranges) * @param bool $keyed * @return static */ - public function keyed($keyed) + public function keyed(bool $keyed): static { return $this->addProperty('keyed', $keyed); } @@ -50,7 +41,7 @@ public function keyed($keyed) * @param mixed $missing * @return static */ - public function missing($missing) + public function missing($missing): static { return $this->addProperty('missing', $missing); } diff --git a/src/DSL/Aggs/Bucket/Missing.php b/src/DSL/Aggs/Bucket/Missing.php index bea6610..5da5b46 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. @@ -28,7 +19,7 @@ public function field($field) * @param mixed $missing * @return static */ - public function missing($missing) + public function missing($missing): static { return $this->addProperty('missing', $missing); } diff --git a/src/DSL/Aggs/Bucket/MultiTerms.php b/src/DSL/Aggs/Bucket/MultiTerms.php index 4d3859d..15d73bb 100644 --- a/src/DSL/Aggs/Bucket/MultiTerms.php +++ b/src/DSL/Aggs/Bucket/MultiTerms.php @@ -1,18 +1,20 @@ addProperty('terms', $terms, true); } @@ -21,7 +23,7 @@ public function terms($terms) * @param mixed $order * @return static */ - public function order($order) + public function order($order): static { return $this->addProperty('order', $order); } @@ -30,7 +32,7 @@ public function order($order) * @param int $size * @return static */ - public function size($size) + public function size(int $size): static { return $this->addProperty('size', $size); } @@ -39,7 +41,7 @@ public function size($size) * @param int $shardSize * @return static */ - public function shardSize($shardSize) + public function shardSize(int $shardSize): static { return $this->addProperty('shard_size', $shardSize); } @@ -48,7 +50,7 @@ public function shardSize($shardSize) * @param int $minDocCount * @return static */ - public function minDocCount($minDocCount) + public function minDocCount(int $minDocCount): static { return $this->addProperty('min_doc_count', $minDocCount); } @@ -57,7 +59,7 @@ public function minDocCount($minDocCount) * @param int $shardMinDocCount * @return static */ - public function shardMinDocCount($shardMinDocCount) + public function shardMinDocCount(int $shardMinDocCount): static { return $this->addProperty('shard_min_doc_count', $shardMinDocCount); } @@ -66,7 +68,7 @@ public function shardMinDocCount($shardMinDocCount) * @param string $collectMode * @return static */ - public function collectMode($collectMode) + public function collectMode(string $collectMode): static { return $this->addProperty('collect_mode', $collectMode); } diff --git a/src/DSL/Aggs/Bucket/Nested.php b/src/DSL/Aggs/Bucket/Nested.php index d30200b..2c69bad 100644 --- a/src/DSL/Aggs/Bucket/Nested.php +++ b/src/DSL/Aggs/Bucket/Nested.php @@ -1,5 +1,7 @@ addProperty('path', $path); } @@ -28,7 +30,7 @@ public function path($path) * @param bool $ignoreUnmapped * @return static */ - public function ignoreUnmapped($ignoreUnmapped) + public function ignoreUnmapped(bool $ignoreUnmapped): static { return $this->addProperty('ignore_unmapped', $ignoreUnmapped); } diff --git a/src/DSL/Aggs/Bucket/ParentAgg.php b/src/DSL/Aggs/Bucket/ParentAgg.php index b60e61b..c3a0cfd 100644 --- a/src/DSL/Aggs/Bucket/ParentAgg.php +++ b/src/DSL/Aggs/Bucket/ParentAgg.php @@ -1,5 +1,7 @@ addProperty('type', $type); } diff --git a/src/DSL/Aggs/Bucket/RandomSampler.php b/src/DSL/Aggs/Bucket/RandomSampler.php index de1b946..03a93f6 100644 --- a/src/DSL/Aggs/Bucket/RandomSampler.php +++ b/src/DSL/Aggs/Bucket/RandomSampler.php @@ -1,5 +1,7 @@ addProperty('probability', $probability); } @@ -28,19 +30,8 @@ public function probability($probability) * @param int $seed * @return static */ - public function seed($seed) + public function seed(int $seed): static { return $this->addProperty('seed', $seed); } - - /** - * Field used to maintain consistent random ordering across shards. - * - * @param string $field - * @return static - */ - public function field($field) - { - return $this->addProperty('field', $field); - } } diff --git a/src/DSL/Aggs/Bucket/Range.php b/src/DSL/Aggs/Bucket/Range.php index b488c66..24b2acf 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. @@ -28,7 +19,7 @@ public function field($field) * @param array $ranges * @return static */ - public function ranges($ranges) + public function ranges(array $ranges): static { return $this->addProperty('ranges', $ranges); } @@ -39,7 +30,7 @@ public function ranges($ranges) * @param bool $keyed * @return static */ - public function keyed($keyed) + public function keyed(bool $keyed): static { return $this->addProperty('keyed', $keyed); } @@ -50,7 +41,7 @@ public function keyed($keyed) * @param string|callable $script * @return static */ - public function script($script) + public function script($script): static { return $this->addProperty('script', $script); } @@ -61,7 +52,7 @@ public function script($script) * @param float $missing * @return static */ - public function missing($missing) + public function missing($missing): static { return $this->addProperty('missing', $missing); } diff --git a/src/DSL/Aggs/Bucket/RareTerms.php b/src/DSL/Aggs/Bucket/RareTerms.php index 55d678e..f89e835 100644 --- a/src/DSL/Aggs/Bucket/RareTerms.php +++ b/src/DSL/Aggs/Bucket/RareTerms.php @@ -1,27 +1,20 @@ addProperty('field', $field); - } + protected string $_key = 'rare_terms'; /** * @param int $maxDocCount * @return static */ - public function maxDocCount($maxDocCount) + public function maxDocCount(int $maxDocCount): static { return $this->addProperty('max_doc_count', $maxDocCount); } @@ -30,7 +23,7 @@ public function maxDocCount($maxDocCount) * @param mixed $precision * @return static */ - public function precision($precision) + public function precision($precision): static { return $this->addProperty('precision', $precision); } @@ -39,7 +32,7 @@ public function precision($precision) * @param mixed $include * @return static */ - public function include($include) + public function include($include): static { return $this->addProperty('include', $include); } @@ -48,7 +41,7 @@ public function include($include) * @param mixed $exclude * @return static */ - public function exclude($exclude) + public function exclude($exclude): static { return $this->addProperty('exclude', $exclude); } @@ -57,7 +50,7 @@ public function exclude($exclude) * @param mixed $missing * @return static */ - public function missing($missing) + public function missing($missing): static { return $this->addProperty('missing', $missing); } @@ -66,7 +59,7 @@ public function missing($missing) * @param int $shardSize * @return static */ - public function shardSize($shardSize) + public function shardSize(int $shardSize): static { return $this->addProperty('shard_size', $shardSize); } diff --git a/src/DSL/Aggs/Bucket/ReverseNested.php b/src/DSL/Aggs/Bucket/ReverseNested.php index ca434eb..b8c0292 100644 --- a/src/DSL/Aggs/Bucket/ReverseNested.php +++ b/src/DSL/Aggs/Bucket/ReverseNested.php @@ -1,5 +1,7 @@ addProperty('path', $path); } diff --git a/src/DSL/Aggs/Bucket/SignificantTerms.php b/src/DSL/Aggs/Bucket/SignificantTerms.php index 54fe10c..e933b57 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. @@ -28,7 +19,7 @@ public function field($field) * @param int $size * @return static */ - public function size($size) + public function size(int $size): static { return $this->addProperty('size', $size); } @@ -39,7 +30,7 @@ public function size($size) * @param int $shardSize * @return static */ - public function shardSize($shardSize) + public function shardSize(int $shardSize): static { return $this->addProperty('shard_size', $shardSize); } @@ -50,7 +41,7 @@ public function shardSize($shardSize) * @param int $minDocCount * @return static */ - public function minDocCount($minDocCount) + public function minDocCount(int $minDocCount): static { return $this->addProperty('min_doc_count', $minDocCount); } @@ -61,7 +52,7 @@ public function minDocCount($minDocCount) * @param int $shardMinDocCount * @return static */ - public function shardMinDocCount($shardMinDocCount) + public function shardMinDocCount(int $shardMinDocCount): static { return $this->addProperty('shard_min_doc_count', $shardMinDocCount); } @@ -72,7 +63,7 @@ public function shardMinDocCount($shardMinDocCount) * @param mixed $include * @return static */ - public function include($include) + public function include($include): static { return $this->addProperty('include', $include); } @@ -83,7 +74,7 @@ public function include($include) * @param mixed $exclude * @return static */ - public function exclude($exclude) + public function exclude($exclude): static { return $this->addProperty('exclude', $exclude); } @@ -94,7 +85,7 @@ public function exclude($exclude) * @param mixed $backgroundFilter * @return static */ - public function backgroundFilter($backgroundFilter) + public function backgroundFilter($backgroundFilter): static { return $this->addProperty('background_filter', $backgroundFilter); } @@ -105,7 +96,7 @@ public function backgroundFilter($backgroundFilter) * @param string $executionHint * @return static */ - public function executionHint($executionHint) + public function executionHint(string $executionHint): static { return $this->addProperty('execution_hint', $executionHint); } diff --git a/src/DSL/Aggs/Bucket/SignificantText.php b/src/DSL/Aggs/Bucket/SignificantText.php index d07832b..f7736f2 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. @@ -28,7 +19,7 @@ public function field($field) * @param int $size * @return static */ - public function size($size) + public function size(int $size): static { return $this->addProperty('size', $size); } @@ -39,7 +30,7 @@ public function size($size) * @param int $shardSize * @return static */ - public function shardSize($shardSize) + public function shardSize(int $shardSize): static { return $this->addProperty('shard_size', $shardSize); } @@ -50,7 +41,7 @@ public function shardSize($shardSize) * @param int $minDocCount * @return static */ - public function minDocCount($minDocCount) + public function minDocCount(int $minDocCount): static { return $this->addProperty('min_doc_count', $minDocCount); } @@ -61,7 +52,7 @@ public function minDocCount($minDocCount) * @param int $shardMinDocCount * @return static */ - public function shardMinDocCount($shardMinDocCount) + public function shardMinDocCount(int $shardMinDocCount): static { return $this->addProperty('shard_min_doc_count', $shardMinDocCount); } @@ -72,7 +63,7 @@ public function shardMinDocCount($shardMinDocCount) * @param mixed $include * @return static */ - public function include($include) + public function include($include): static { return $this->addProperty('include', $include); } @@ -83,7 +74,7 @@ public function include($include) * @param mixed $exclude * @return static */ - public function exclude($exclude) + public function exclude($exclude): static { return $this->addProperty('exclude', $exclude); } @@ -94,7 +85,7 @@ public function exclude($exclude) * @param mixed $backgroundFilter * @return static */ - public function backgroundFilter($backgroundFilter) + public function backgroundFilter($backgroundFilter): static { return $this->addProperty('background_filter', $backgroundFilter); } @@ -105,7 +96,7 @@ public function backgroundFilter($backgroundFilter) * @param bool $filter * @return static */ - public function filterDuplicateText($filter) + public function filterDuplicateText(bool $filter): static { return $this->addProperty('filter_duplicate_text', $filter); } diff --git a/src/DSL/Aggs/Bucket/Terms.php b/src/DSL/Aggs/Bucket/Terms.php index ad6531d..76526fa 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. @@ -28,7 +19,7 @@ public function field($field) * @param int $size * @return static */ - public function size($size) + public function size(int $size): static { return $this->addProperty('size', $size); } @@ -39,7 +30,7 @@ public function size($size) * @param mixed $order * @return static */ - public function order($order) + public function order($order): static { return $this->addProperty('order', $order); } @@ -50,7 +41,7 @@ public function order($order) * @param int $minDocCount * @return static */ - public function minDocCount($minDocCount) + public function minDocCount(int $minDocCount): static { return $this->addProperty('min_doc_count', $minDocCount); } @@ -61,7 +52,7 @@ public function minDocCount($minDocCount) * @param int $shardSize * @return static */ - public function shardSize($shardSize) + public function shardSize(int $shardSize): static { return $this->addProperty('shard_size', $shardSize); } @@ -72,7 +63,7 @@ public function shardSize($shardSize) * @param bool $show * @return static */ - public function showTermDocCountError($show) + public function showTermDocCountError(bool $show): static { return $this->addProperty('show_term_doc_count_error', $show); } @@ -83,7 +74,7 @@ public function showTermDocCountError($show) * @param int $shardMinDocCount * @return static */ - public function shardMinDocCount($shardMinDocCount) + public function shardMinDocCount(int $shardMinDocCount): static { return $this->addProperty('shard_min_doc_count', $shardMinDocCount); } @@ -94,7 +85,7 @@ public function shardMinDocCount($shardMinDocCount) * @param mixed $missing * @return static */ - public function missing($missing) + public function missing($missing): static { return $this->addProperty('missing', $missing); } @@ -105,7 +96,7 @@ public function missing($missing) * @param string $collectMode * @return static */ - public function collectMode($collectMode) + public function collectMode(string $collectMode): static { return $this->addProperty('collect_mode', $collectMode); } @@ -116,7 +107,7 @@ public function collectMode($collectMode) * @param mixed $include * @return static */ - public function include($include) + public function include($include): static { return $this->addProperty('include', $include); } @@ -127,7 +118,7 @@ public function include($include) * @param mixed $exclude * @return static */ - public function exclude($exclude) + public function exclude($exclude): static { return $this->addProperty('exclude', $exclude); } @@ -138,7 +129,7 @@ public function exclude($exclude) * @param string $executionHint * @return static */ - public function executionHint($executionHint) + public function executionHint(string $executionHint): static { return $this->addProperty('execution_hint', $executionHint); } diff --git a/src/DSL/Aggs/Bucket/TimeSeries.php b/src/DSL/Aggs/Bucket/TimeSeries.php index 2497ae8..f170918 100644 --- a/src/DSL/Aggs/Bucket/TimeSeries.php +++ b/src/DSL/Aggs/Bucket/TimeSeries.php @@ -1,27 +1,20 @@ addProperty('field', $field); - } + protected string $_key = 'time_series'; /** * @param string $calendarInterval * @return static */ - public function calendarInterval($calendarInterval) + public function calendarInterval(string $calendarInterval): static { return $this->addProperty('calendar_interval', $calendarInterval); } @@ -30,7 +23,7 @@ public function calendarInterval($calendarInterval) * @param string $fixedInterval * @return static */ - public function fixedInterval($fixedInterval) + public function fixedInterval(string $fixedInterval): static { return $this->addProperty('fixed_interval', $fixedInterval); } @@ -39,7 +32,7 @@ public function fixedInterval($fixedInterval) * @param mixed $missing * @return static */ - public function missing($missing) + public function missing($missing): static { return $this->addProperty('missing', $missing); } diff --git a/src/DSL/Aggs/Bucket/VariableWidthHistogram.php b/src/DSL/Aggs/Bucket/VariableWidthHistogram.php index 1f4d3ed..8f987eb 100644 --- a/src/DSL/Aggs/Bucket/VariableWidthHistogram.php +++ b/src/DSL/Aggs/Bucket/VariableWidthHistogram.php @@ -1,27 +1,20 @@ addProperty('field', $field); - } + protected string $_key = 'variable_width_histogram'; /** * @param int $buckets * @return static */ - public function buckets($buckets) + public function buckets(int $buckets): static { return $this->addProperty('buckets', $buckets); } @@ -30,7 +23,7 @@ public function buckets($buckets) * @param int $shardBuckets * @return static */ - public function shardBuckets($shardBuckets) + public function shardBuckets(int $shardBuckets): static { return $this->addProperty('shard_buckets', $shardBuckets); } diff --git a/src/DSL/Aggs/Metric.php b/src/DSL/Aggs/Metric.php index d66e88a..f7af4f0 100644 --- a/src/DSL/Aggs/Metric.php +++ b/src/DSL/Aggs/Metric.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 * @return static */ - public function missing($missing) + public function missing($missing): static { return $this->addProperty('missing', $missing); } /** - * (Optional) The script to use for the aggregation. + * The script to use for the aggregation. * * @param string|callable $script * @return static */ - public function script($script) + public function script($script): static { return $this->addProperty('script', $script); } diff --git a/src/DSL/Aggs/Metric/Cardinality.php b/src/DSL/Aggs/Metric/Cardinality.php index 9f45379..dacea20 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. @@ -28,29 +19,29 @@ public function field($field) * @param int $threshold * @return static */ - public function precisionThreshold($threshold) + public function precisionThreshold(int $threshold): static { return $this->addProperty('precision_threshold', $threshold); } /** - * (Optional) The value to use when the field is missing. + * The value to use when the field is missing. * * @param mixed $missing * @return static */ - public function missing($missing) + public function missing($missing): static { return $this->addProperty('missing', $missing); } /** - * (Optional) The script to use for the aggregation. + * The script to use for the aggregation. * * @param string|callable $script * @return static */ - public function script($script) + public function script($script): static { return $this->addProperty('script', $script); } diff --git a/src/DSL/Aggs/Metric/ExtendedStats.php b/src/DSL/Aggs/Metric/ExtendedStats.php index 616fd76..4a01725 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 * @return static */ - public function missing($missing) + public function missing($missing): static { return $this->addProperty('missing', $missing); } /** - * (Optional) The script to use for the aggregation. + * The script to use for the aggregation. * * @param string|callable $script * @return static */ - public function script($script) + public function script($script): static { return $this->addProperty('script', $script); } @@ -50,7 +41,7 @@ public function script($script) * @param int $sigma * @return static */ - public function sigma($sigma) + public function sigma(int $sigma): static { return $this->addProperty('sigma', $sigma); } diff --git a/src/DSL/Aggs/Metric/Max.php b/src/DSL/Aggs/Metric/Max.php index cb71a35..11775a9 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 * @return static */ - public function missing($missing) + public function missing($missing): static { return $this->addProperty('missing', $missing); } /** - * (Optional) The script to use for the aggregation. + * The script to use for the aggregation. * * @param string|callable $script * @return static */ - public function script($script) + public function script($script): static { return $this->addProperty('script', $script); } diff --git a/src/DSL/Aggs/Metric/Min.php b/src/DSL/Aggs/Metric/Min.php index cb9fd7c..94bf2d6 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 * @return static */ - public function missing($missing) + public function missing($missing): static { return $this->addProperty('missing', $missing); } /** - * (Optional) The script to use for the aggregation. + * The script to use for the aggregation. * * @param string|callable $script * @return static */ - public function script($script) + public function script($script): static { return $this->addProperty('script', $script); } diff --git a/src/DSL/Aggs/Metric/Stats.php b/src/DSL/Aggs/Metric/Stats.php index 9b7ac44..1903766 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 * @return static */ - public function missing($missing) + public function missing($missing): static { return $this->addProperty('missing', $missing); } /** - * (Optional) The script to use for the aggregation. + * The script to use for the aggregation. * * @param string|callable $script * @return static */ - public function script($script) + public function script($script): static { return $this->addProperty('script', $script); } diff --git a/src/DSL/Aggs/Metric/Sum.php b/src/DSL/Aggs/Metric/Sum.php index f86dfbc..c59dd63 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 * @return static */ - public function missing($missing) + public function missing($missing): static { return $this->addProperty('missing', $missing); } /** - * (Optional) The script to use for the aggregation. + * The script to use for the aggregation. * * @param string|callable $script * @return static */ - public function script($script) + public function script($script): static { return $this->addProperty('script', $script); } diff --git a/src/DSL/Aggs/Metric/ValueCount.php b/src/DSL/Aggs/Metric/ValueCount.php index bd7ef87..684e837 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 * @return static */ - public function script($script) + public function script($script): static { return $this->addProperty('script', $script); } diff --git a/src/DSL/Aggs/Pipeline.php b/src/DSL/Aggs/Pipeline.php index 48b1461..3be32cf 100644 --- a/src/DSL/Aggs/Pipeline.php +++ b/src/DSL/Aggs/Pipeline.php @@ -1,5 +1,7 @@ addProperty('buckets_path', $path); } @@ -28,7 +30,7 @@ public function bucketsPath($path) * @param string $policy * @return static */ - public function gapPolicy($policy) + public function gapPolicy(string $policy): static { return $this->addProperty('gap_policy', $policy); } @@ -39,18 +41,18 @@ public function gapPolicy($policy) * @param string $format * @return static */ - public function format($format) + public function format(string $format): static { return $this->addProperty('format', $format); } /** - * (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 * @return static */ - public function missing($missing) + public function missing($missing): static { return $this->addProperty('missing', $missing); } diff --git a/src/DSL/Aggs/Pipeline/BucketScript.php b/src/DSL/Aggs/Pipeline/BucketScript.php index 7df06b0..7da6e36 100644 --- a/src/DSL/Aggs/Pipeline/BucketScript.php +++ b/src/DSL/Aggs/Pipeline/BucketScript.php @@ -1,5 +1,7 @@ addProperty('buckets_path', $path); } @@ -28,7 +30,7 @@ public function bucketsPath($path) * @param string|callable $script * @return static */ - public function script($script) + public function script($script): static { return $this->addProperty('script', $script); } @@ -39,7 +41,7 @@ public function script($script) * @param string $policy * @return static */ - public function gapPolicy($policy) + public function gapPolicy(string $policy): static { return $this->addProperty('gap_policy', $policy); } @@ -50,7 +52,7 @@ public function gapPolicy($policy) * @param string $format * @return static */ - public function format($format) + public function format(string $format): static { return $this->addProperty('format', $format); } diff --git a/src/DSL/Aggs/Pipeline/CumulativeSum.php b/src/DSL/Aggs/Pipeline/CumulativeSum.php index a484122..0da75e1 100644 --- a/src/DSL/Aggs/Pipeline/CumulativeSum.php +++ b/src/DSL/Aggs/Pipeline/CumulativeSum.php @@ -1,5 +1,7 @@ addProperty('buckets_path', $path); } @@ -28,7 +30,7 @@ public function bucketsPath($path) * @param string $format * @return static */ - public function format($format) + public function format(string $format): static { return $this->addProperty('format', $format); } diff --git a/src/DSL/Aggs/Pipeline/Derivative.php b/src/DSL/Aggs/Pipeline/Derivative.php index d087cc7..d8305a3 100644 --- a/src/DSL/Aggs/Pipeline/Derivative.php +++ b/src/DSL/Aggs/Pipeline/Derivative.php @@ -1,5 +1,7 @@ addProperty('buckets_path', $path); } @@ -28,7 +30,7 @@ public function bucketsPath($path) * @param string $policy * @return static */ - public function gapPolicy($policy) + public function gapPolicy(string $policy): static { return $this->addProperty('gap_policy', $policy); } @@ -39,7 +41,7 @@ public function gapPolicy($policy) * @param string $format * @return static */ - public function format($format) + public function format(string $format): static { return $this->addProperty('format', $format); } @@ -50,7 +52,7 @@ public function format($format) * @param string $unit * @return static */ - public function unit($unit) + public function unit(string $unit): static { return $this->addProperty('unit', $unit); } diff --git a/src/DSL/Aggs/Pipeline/MaxBucket.php b/src/DSL/Aggs/Pipeline/MaxBucket.php index ea5942c..faa7ff5 100644 --- a/src/DSL/Aggs/Pipeline/MaxBucket.php +++ b/src/DSL/Aggs/Pipeline/MaxBucket.php @@ -1,5 +1,7 @@ addProperty('buckets_path', $path); } @@ -28,7 +30,7 @@ public function bucketsPath($path) * @param string $policy * @return static */ - public function gapPolicy($policy) + public function gapPolicy(string $policy): static { return $this->addProperty('gap_policy', $policy); } @@ -39,18 +41,18 @@ public function gapPolicy($policy) * @param string $format * @return static */ - public function format($format) + public function format(string $format): static { return $this->addProperty('format', $format); } /** - * (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 * @return static */ - public function missing($missing) + public function missing($missing): static { return $this->addProperty('missing', $missing); } diff --git a/src/DSL/Aggs/Pipeline/MinBucket.php b/src/DSL/Aggs/Pipeline/MinBucket.php index 7cf0cbe..12b0810 100644 --- a/src/DSL/Aggs/Pipeline/MinBucket.php +++ b/src/DSL/Aggs/Pipeline/MinBucket.php @@ -1,5 +1,7 @@ addProperty('buckets_path', $path); } @@ -28,7 +30,7 @@ public function bucketsPath($path) * @param string $policy * @return static */ - public function gapPolicy($policy) + public function gapPolicy(string $policy): static { return $this->addProperty('gap_policy', $policy); } @@ -39,18 +41,18 @@ public function gapPolicy($policy) * @param string $format * @return static */ - public function format($format) + public function format(string $format): static { return $this->addProperty('format', $format); } /** - * (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 * @return static */ - public function missing($missing) + public function missing($missing): static { return $this->addProperty('missing', $missing); } diff --git a/src/DSL/Aggs/Pipeline/StatsBucket.php b/src/DSL/Aggs/Pipeline/StatsBucket.php index 8eb4520..45af167 100644 --- a/src/DSL/Aggs/Pipeline/StatsBucket.php +++ b/src/DSL/Aggs/Pipeline/StatsBucket.php @@ -1,5 +1,7 @@ addProperty('buckets_path', $path); } @@ -28,7 +30,7 @@ public function bucketsPath($path) * @param string $policy * @return static */ - public function gapPolicy($policy) + public function gapPolicy(string $policy): static { return $this->addProperty('gap_policy', $policy); } @@ -39,18 +41,18 @@ public function gapPolicy($policy) * @param string $format * @return static */ - public function format($format) + public function format(string $format): static { return $this->addProperty('format', $format); } /** - * (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 * @return static */ - public function missing($missing) + public function missing($missing): static { return $this->addProperty('missing', $missing); } diff --git a/src/DSL/Aggs/Pipeline/SumBucket.php b/src/DSL/Aggs/Pipeline/SumBucket.php index 07a68d0..dcd5d8d 100644 --- a/src/DSL/Aggs/Pipeline/SumBucket.php +++ b/src/DSL/Aggs/Pipeline/SumBucket.php @@ -1,5 +1,7 @@ addProperty('buckets_path', $path); } @@ -28,7 +30,7 @@ public function bucketsPath($path) * @param string $policy * @return static */ - public function gapPolicy($policy) + public function gapPolicy(string $policy): static { return $this->addProperty('gap_policy', $policy); } @@ -39,18 +41,18 @@ public function gapPolicy($policy) * @param string $format * @return static */ - public function format($format) + public function format(string $format): static { return $this->addProperty('format', $format); } /** - * (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 * @return static */ - public function missing($missing) + public function missing($missing): static { return $this->addProperty('missing', $missing); } diff --git a/src/DSL/Node.php b/src/DSL/Node.php index 3f60791..0df9aa1 100644 --- a/src/DSL/Node.php +++ b/src/DSL/Node.php @@ -1,5 +1,7 @@ |null + */ + protected ?array $_properties = null; + + /** + * Raw whole-value pass-through. When set, toArray() emits this value + * verbatim — for nodes constructed with a single non-array, non-closure + * argument (a wrapped Node, a FilterAgg filter query, etc.). * - * @var array|mixed|null + * @var mixed */ - protected $_properties; + protected $_raw; /** * Raw scalar value stored separately from properties. @@ -33,14 +47,14 @@ abstract class Node * * @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. @@ -54,14 +68,14 @@ abstract class Node * * @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. @@ -83,12 +97,14 @@ public function __construct($field = null, $value = null) $this->fromKeyValue($field, $value); } elseif ($field instanceof Closure) { $this->fromClosure($field); - } elseif ($this->_isPropertyField && is_array($field)) { + } elseif ($this->_fieldKeyed && is_array($field)) { $this->fromArrayField($field); - } elseif ($this->_isPropertyField && is_scalar($field)) { + } elseif ($this->_fieldKeyed && is_scalar($field)) { $this->fromScalar($field); - } else { + } elseif (is_array($field)) { $this->_properties = $field; + } elseif ($field !== null) { + $this->_raw = $field; } } @@ -105,10 +121,12 @@ protected function fromKeyValue($field, $value): void } elseif (is_scalar($value)) { $this->_rawValue = $value; $this->_properties = []; - } else { + } elseif (is_array($value)) { $this->_properties = $value; + } else { + $this->_raw = $value; } - if ($this->_isPropertyField) { + if ($this->_fieldKeyed) { $this->field($field); } } @@ -135,8 +153,10 @@ protected function fromArrayField(array $field): void if (is_scalar($val)) { $this->_rawValue = $val; $this->_properties = []; - } else { + } elseif (is_array($val)) { $this->_properties = $val; + } else { + $this->_raw = $val; } break; } @@ -156,12 +176,12 @@ protected function fromScalar($value): void /** * Set whether this node uses a field name as the top-level attribute. * - * @param bool $isPropertyField + * @param bool $fieldKeyed * @return static */ - protected function isPropertyField($isPropertyField) + protected function fieldKeyed(bool $fieldKeyed): static { - $this->_isPropertyField = $isPropertyField; + $this->_fieldKeyed = $fieldKeyed; return $this; } @@ -171,19 +191,7 @@ protected function isPropertyField($isPropertyField) * @param bool $multi * @return static */ - protected function multi($multi) - { - $this->_multi = $multi; - return $this; - } - - /** - * Set whether the node supports multiple clauses. - * - * @param bool $multi - * @return static - */ - protected function setMulti($multi) + protected function multi(bool $multi): static { $this->_multi = $multi; return $this; @@ -194,7 +202,7 @@ protected function setMulti($multi) * * @return bool */ - protected function isMulti() + protected function isMulti(): bool { return $this->_multi; } @@ -204,7 +212,7 @@ protected function isMulti() * * @return string */ - public function key() + public function key(): string { return $this->_key; } @@ -215,9 +223,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; @@ -233,7 +241,7 @@ public function field($field) * @param bool $append * @return static */ - public function addProperty($attribute, $value, $append = false) + public function addProperty($attribute, $value, $append = false): static { if ($append) { $this->_properties[$attribute][] = $value; @@ -253,7 +261,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; @@ -267,7 +275,7 @@ 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) { @@ -287,28 +295,29 @@ protected function resolveProperties(array $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)) { + if ($this->_raw !== null) { + $properties = $this->_raw; + } elseif ($this->_rawValue !== null) { + $props = $this->_properties ?? []; + if ($props === []) { $properties = $this->_rawValue; } else { - $properties = $this->resolveProperties($this->_properties); + $properties = $this->resolveProperties($props); if (!isset($properties[$this->_valueKey])) { $properties = array_merge([$this->_valueKey => $this->_rawValue], $properties); } } } else { - $properties = is_array($this->_properties) - ? $this->resolveProperties($this->_properties) - : $this->_properties; + $properties = $this->_properties === null ? null : $this->resolveProperties($this->_properties); } - if ($this->_isPropertyField) { + if ($this->_fieldKeyed) { return [$this->_field => $properties]; } return $properties; @@ -321,9 +330,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, int $depth = 512): string { - return json_encode($this->toArray(), $flags, $depth); + $json = json_encode($this->toArray(), $flags, $depth); + + return $json === false ? '' : $json; } /** @@ -331,7 +342,7 @@ public function toJson($flags = JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT, $dep * * @return string */ - public function __toString() + public function __toString(): string { return $this->toJson(); } @@ -343,7 +354,7 @@ public function __toString() * @param float $boost * @return static */ - public function boost($boost) + public function boost($boost): static { return $this->addProperty('boost', $boost); } diff --git a/src/DSL/Param.php b/src/DSL/Param.php index 49c0dd8..ba982e3 100644 --- a/src/DSL/Param.php +++ b/src/DSL/Param.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); } @@ -60,7 +51,7 @@ public function innerHits($name, $hits = null) * @param int $max * @return static */ - public function maxConcurrentGroupSearches($max) + public function maxConcurrentGroupSearches(int $max): static { return $this->addProperty('max_concurrent_group_searches', $max); } diff --git a/src/DSL/Params/Highlight.php b/src/DSL/Params/Highlight.php index 9f8347b..460f116 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; @@ -53,7 +55,7 @@ public function field($field, $settings = []) * @param array $tags * @return static */ - public function preTags($tags) + public function preTags(array $tags): static { return $this->addProperty('pre_tags', $tags); } @@ -64,7 +66,7 @@ public function preTags($tags) * @param array $tags * @return static */ - public function postTags($tags) + public function postTags(array $tags): static { return $this->addProperty('post_tags', $tags); } @@ -75,7 +77,7 @@ public function postTags($tags) * @param int $size * @return static */ - public function fragmentSize($size) + public function fragmentSize(int $size): static { return $this->addProperty('fragment_size', $size); } @@ -86,7 +88,7 @@ public function fragmentSize($size) * @param int $num * @return static */ - public function numberOfFragments($num) + public function numberOfFragments(int $num): static { return $this->addProperty('number_of_fragments', $num); } @@ -97,7 +99,7 @@ public function numberOfFragments($num) * @param string $encoder * @return static */ - public function encoder($encoder) + public function encoder(string $encoder): static { return $this->addProperty('encoder', $encoder); } @@ -108,18 +110,18 @@ public function encoder($encoder) * @param string $order * @return static */ - public function order($order) + public function order(string $order): static { return $this->addProperty('order', $order); } /** - * (Optional) Highlight against a query other than the search query. + * Highlight against a query other than the search query. * * @param mixed $query * @return static */ - public function highlightQuery($query) + public function highlightQuery($query): static { return $this->addProperty('highlight_query', Query::create($query)); } @@ -130,7 +132,7 @@ public function highlightQuery($query) * @param string $type * @return static */ - public function type($type) + public function type(string $type): static { return $this->addProperty('type', $type); } @@ -141,7 +143,7 @@ public function type($type) * @param string $scanner * @return static */ - public function boundaryScanner($scanner) + public function boundaryScanner(string $scanner): static { return $this->addProperty('boundary_scanner', $scanner); } @@ -152,7 +154,7 @@ public function boundaryScanner($scanner) * @param string $locale * @return static */ - public function boundaryScannerLocale($locale) + public function boundaryScannerLocale(string $locale): static { return $this->addProperty('boundary_scanner_locale', $locale); } @@ -163,7 +165,7 @@ public function boundaryScannerLocale($locale) * @param int $max * @return static */ - public function boundaryMaxScan($max) + public function boundaryMaxScan(int $max): static { return $this->addProperty('boundary_max_scan', $max); } @@ -174,7 +176,7 @@ public function boundaryMaxScan($max) * @param int $size * @return static */ - public function noMatchSize($size) + public function noMatchSize(int $size): static { return $this->addProperty('no_match_size', $size); } @@ -185,7 +187,7 @@ public function noMatchSize($size) * @param string $fragmenter * @return static */ - public function fragmenter($fragmenter) + public function fragmenter(string $fragmenter): static { return $this->addProperty('fragmenter', $fragmenter); } diff --git a/src/DSL/Params/Knn.php b/src/DSL/Params/Knn.php index 01f80c2..358a382 100644 --- a/src/DSL/Params/Knn.php +++ b/src/DSL/Params/Knn.php @@ -1,5 +1,7 @@ addProperty('field', $field); - } + protected string $_key = 'knn'; /** * The query vector to search for. @@ -32,7 +23,7 @@ public function field($field) * @param array $vector * @return static */ - public function queryVector($vector) + public function queryVector(array $vector): static { return $this->addProperty('query_vector', $vector); } @@ -44,7 +35,7 @@ public function queryVector($vector) * @param int $k * @return static */ - public function k($k) + public function k(int $k): static { return $this->addProperty('k', $k); } @@ -56,7 +47,7 @@ public function k($k) * @param int $num * @return static */ - public function numCandidates($num) + public function numCandidates(int $num): static { return $this->addProperty('num_candidates', $num); } @@ -68,7 +59,7 @@ public function numCandidates($num) * @param float $similarity * @return static */ - public function similarity($similarity) + public function similarity(float $similarity): static { return $this->addProperty('similarity', $similarity); } @@ -79,41 +70,41 @@ public function similarity($similarity) * @param float $boost * @return static */ - public function boost($boost) + public function boost($boost): static { return $this->addProperty('boost', $boost); } /** - * (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 * @return static */ - public function filter($filter) + public function filter($filter): static { return $this->addProperty('filter', Query::create($filter)); } /** - * (Optional) Inner hits configuration for nested kNN search. + * Inner hits configuration for nested kNN search. * * @param mixed $innerHits * @return static */ - public function innerHits($innerHits) + public function innerHits($innerHits): static { return $this->addProperty('inner_hits', $innerHits); } /** - * (Optional) Rescore vector configuration for quantized vector rescoring. + * Rescore vector configuration for quantized vector rescoring. * * @param array $rescoreVector * @return static */ - public function rescoreVector($rescoreVector) + public function rescoreVector(array $rescoreVector): static { return $this->addProperty('rescore_vector', $rescoreVector); } diff --git a/src/DSL/Params/Rescore.php b/src/DSL/Params/Rescore.php index cef3e20..733229c 100644 --- a/src/DSL/Params/Rescore.php +++ b/src/DSL/Params/Rescore.php @@ -1,5 +1,7 @@ addProperty('window_size', $size); } /** - * (Required) The query to use for rescoring. + * The query to use for rescoring. * * @param mixed $query * @return static */ - public function query($query) + public function query($query): static { $this->_properties['query']['rescore_query'] = Query::create($query); return $this; @@ -44,7 +46,7 @@ public function query($query) * @param float $weight * @return static */ - public function rescoreQueryWeight($weight) + public function rescoreQueryWeight(float $weight): static { $this->_properties['query']['rescore_query_weight'] = $weight; return $this; @@ -56,7 +58,7 @@ public function rescoreQueryWeight($weight) * @param float $weight * @return static */ - public function queryWeight($weight) + public function queryWeight(float $weight): static { $this->_properties['query']['query_weight'] = $weight; return $this; @@ -69,7 +71,7 @@ public function queryWeight($weight) * @param string $mode * @return static */ - public function scoreMode($mode) + public function scoreMode(string $mode): static { $this->_properties['query']['score_mode'] = $mode; return $this; diff --git a/src/DSL/Params/Suggest.php b/src/DSL/Params/Suggest.php index 79eef40..0d53ef2 100644 --- a/src/DSL/Params/Suggest.php +++ b/src/DSL/Params/Suggest.php @@ -1,5 +1,7 @@ ['field' => $field]]; if ($text !== null) { @@ -38,7 +40,7 @@ public function term($alias, $field, $text = null) * @param string|null $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) { @@ -55,7 +57,7 @@ public function completion($alias, $field, $prefix = null) * @param string|null $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..79ad381 100644 --- a/src/DSL/Queries/Compound.php +++ b/src/DSL/Queries/Compound.php @@ -1,5 +1,7 @@ addClause('must', $must); } @@ -33,7 +35,7 @@ public function must($must) * @param mixed $should * @return static */ - public function should($should) + public function should($should): static { return $this->addClause('should', $should); } @@ -45,7 +47,7 @@ public function should($should) * @param mixed $filter * @return static */ - public function filter($filter) + public function filter($filter): static { return $this->addClause('filter', $filter); } @@ -57,7 +59,7 @@ public function filter($filter) * @param mixed $mustNot * @return static */ - public function mustNot($mustNot) + public function mustNot($mustNot): static { return $this->addClause('must_not', $mustNot); } @@ -69,10 +71,10 @@ public function mustNot($mustNot) * * For other valid values, see the minimum_should_match parameter. * - * @param int $minimumShouldMatch + * @param int|string $minimumShouldMatch * @return static */ - public function minimumShouldMatch($minimumShouldMatch) + public function minimumShouldMatch(int|string $minimumShouldMatch): static { return $this->addProperty('minimum_should_match', $minimumShouldMatch); } diff --git a/src/DSL/Queries/Compound/Boosting.php b/src/DSL/Queries/Compound/Boosting.php index 4279c18..c668cc5 100644 --- a/src/DSL/Queries/Compound/Boosting.php +++ b/src/DSL/Queries/Compound/Boosting.php @@ -1,5 +1,7 @@ addProperty('positive', Query::create($positive)); } @@ -29,7 +31,7 @@ public function positive($positive) * @param mixed $negative * @return static */ - public function negative($negative) + public function negative($negative): static { return $this->addProperty('negative', Query::create($negative)); } @@ -40,7 +42,7 @@ public function negative($negative) * @param float $negativeBoost * @return static */ - public function negativeBoost($negativeBoost) + public function negativeBoost(float $negativeBoost): static { return $this->addProperty('negative_boost', $negativeBoost); } diff --git a/src/DSL/Queries/Compound/ConstantScore.php b/src/DSL/Queries/Compound/ConstantScore.php index 4101f4c..80ba278 100644 --- a/src/DSL/Queries/Compound/ConstantScore.php +++ b/src/DSL/Queries/Compound/ConstantScore.php @@ -1,5 +1,7 @@ addProperty('filter', Query::create($query)); } diff --git a/src/DSL/Queries/Compound/DisjunctionMax.php b/src/DSL/Queries/Compound/DisjunctionMax.php index ec0a92d..5b63895 100644 --- a/src/DSL/Queries/Compound/DisjunctionMax.php +++ b/src/DSL/Queries/Compound/DisjunctionMax.php @@ -1,5 +1,7 @@ addClause('queries', $queries); } @@ -32,7 +34,7 @@ public function queries($queries) * @param float $tieBreaker * @return static */ - public function tieBreaker($tieBreaker) + public function tieBreaker(float $tieBreaker): static { return $this->addProperty('tie_breaker', $tieBreaker); } diff --git a/src/DSL/Queries/Compound/FunctionScore.php b/src/DSL/Queries/Compound/FunctionScore.php index d92fff5..ca16b3d 100644 --- a/src/DSL/Queries/Compound/FunctionScore.php +++ b/src/DSL/Queries/Compound/FunctionScore.php @@ -1,5 +1,7 @@ addProperty('score_mode', $scoreMode); } /** - * (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 * @return static */ - public function boostMode($boostMode) + public function boostMode(string $boostMode): static { return $this->addProperty('boost_mode', $boostMode); } /** - * (Optional) Excludes documents that do not meet the specified score threshold. + * Excludes documents that do not meet the specified score threshold. * * @param float $minScore * @return static */ - public function minScore($minScore) + public function minScore(float $minScore): static { return $this->addProperty('min_score', $minScore); } /** - * (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 * @return static */ - public function maxBoost($maxBoost) + public function maxBoost(float $maxBoost): static { return $this->addProperty('max_boost', $maxBoost); } /** - * (Required) The query to be scored. + * The query to be scored. * * @param mixed $query * @return static */ - public function query($query) + public function query($query): static { return $this->addProperty('query', Query::create($query)); } /** - * (Optional) Array of score functions to apply. + * Array of score functions to apply. * * @param array $functions * @return static */ - public function functions($functions) + public function functions(array $functions): static { return $this->addProperty('functions', $functions); } /** - * (Optional) Appends a score function to the functions array. + * Appends a score function to the functions array. * * @param mixed $function * @return static */ - public function addFunction($function) + public function addFunction($function): static { return $this->addProperty('functions', Function_::create($function), true); } @@ -114,7 +116,7 @@ public function toArray() $properties = $this->resolveProperties($properties); - if ($this->_isPropertyField) { + if ($this->_fieldKeyed) { return [$this->_field => $properties]; } return $properties; diff --git a/src/DSL/Queries/Compound/Functions/Exp.php b/src/DSL/Queries/Compound/Functions/Exp.php index 97bd842..2516317 100644 --- a/src/DSL/Queries/Compound/Functions/Exp.php +++ b/src/DSL/Queries/Compound/Functions/Exp.php @@ -1,5 +1,7 @@ addProperty('origin', $origin); } /** - * (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 * @return static */ - public function scale($scale) + public function scale($scale): static { return $this->addProperty('scale', $scale); } /** - * (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 * @return static */ - public function offset($offset) + public function offset($offset): static { return $this->addProperty('offset', $offset); } /** - * (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 * @return static */ - public function decay($decay) + public function decay(float $decay): static { return $this->addProperty('decay', $decay); } diff --git a/src/DSL/Queries/Compound/Functions/FieldValueFactor.php b/src/DSL/Queries/Compound/Functions/FieldValueFactor.php index fdb4f41..667455e 100644 --- a/src/DSL/Queries/Compound/Functions/FieldValueFactor.php +++ b/src/DSL/Queries/Compound/Functions/FieldValueFactor.php @@ -1,23 +1,14 @@ addProperty('field', $field); - } + protected string $_key = 'field_value_factor'; /** * Optional factor to multiply the field value with, defaults to 1. @@ -25,7 +16,7 @@ public function field($field) * @param float $factor * @return static */ - public function factor($factor) + public function factor(float $factor): static { return $this->addProperty('factor', $factor); } @@ -36,7 +27,7 @@ public function factor($factor) * @param string $modifier * @return static */ - public function modifier($modifier) + public function modifier(string $modifier): static { return $this->addProperty('modifier', $modifier); } @@ -44,10 +35,10 @@ public function modifier($modifier) /** * 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 $missing * @return static */ - public function missing($missing) + public function missing(string|int|float|bool $missing): static { return $this->addProperty('missing', $missing); } diff --git a/src/DSL/Queries/Compound/Functions/Function_.php b/src/DSL/Queries/Compound/Functions/Function_.php index cd09444..186e1d1 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)); } /** - * (Optional) Multiplies the score by the provided weight value. + * Multiplies the score by the provided weight value. * * @param float $weight * @return static */ - public function weight($weight) + public function weight(float $weight): static { return $this->addProperty('weight', $weight); } /** - * (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 * @return static */ - public function randomScore($randomScore = null) + public function randomScore($randomScore = null): static { return $this->addProperty('random_score', RandomScore::create($randomScore)); } /** - * (Optional) Wraps another query and customizes the scoring using a script. + * Wraps another query and customizes the scoring using a script. * * @param mixed $scriptScore * @return static */ - public function scriptScore($scriptScore) + public function scriptScore($scriptScore): static { return $this->addProperty('script_score', ScriptScore::create($scriptScore)); } /** - * (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 * @return static */ - public function script($script) + public function script($script): static { $scriptScore = (new ScriptScore())->script($script); return $this->addProperty('script_score', $scriptScore); } /** - * (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 * @return static */ - public function fieldValueFactor($field, $fieldValueFactor = null) + public function fieldValueFactor($field, $fieldValueFactor = null): static { return $this->addProperty('field_value_factor', FieldValueFactor::create($field, $fieldValueFactor)); } /** - * (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 * @return static */ - public function gauss($field, $gauss = null) + public function gauss($field, $gauss = null): static { return $this->addProperty('gauss', Gauss::create($field, $gauss)); } /** - * (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 * @return static */ - public function linear($field, $linear = null) + public function linear($field, $linear = null): static { return $this->addProperty('linear', Linear::create($field, $linear)); } /** - * (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 * @return static */ - public function exp($field, $exp = null) + public function exp($field, $exp = null): static { return $this->addProperty('exp', Exp::create($field, $exp)); } diff --git a/src/DSL/Queries/Compound/Functions/Gauss.php b/src/DSL/Queries/Compound/Functions/Gauss.php index 5bd3fa4..4bdaa3b 100644 --- a/src/DSL/Queries/Compound/Functions/Gauss.php +++ b/src/DSL/Queries/Compound/Functions/Gauss.php @@ -1,5 +1,7 @@ addProperty('origin', $origin); } /** - * (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 * @return static */ - public function scale($scale) + public function scale($scale): static { return $this->addProperty('scale', $scale); } /** - * (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 * @return static */ - public function offset($offset) + public function offset($offset): static { return $this->addProperty('offset', $offset); } /** - * (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 * @return static */ - public function decay($decay) + public function decay(float $decay): static { return $this->addProperty('decay', $decay); } diff --git a/src/DSL/Queries/Compound/Functions/Linear.php b/src/DSL/Queries/Compound/Functions/Linear.php index 32fe187..65a6896 100644 --- a/src/DSL/Queries/Compound/Functions/Linear.php +++ b/src/DSL/Queries/Compound/Functions/Linear.php @@ -1,5 +1,7 @@ addProperty('origin', $origin); } /** - * (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 * @return static */ - public function scale($scale) + public function scale($scale): static { return $this->addProperty('scale', $scale); } /** - * (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 * @return static */ - public function offset($offset) + public function offset($offset): static { return $this->addProperty('offset', $offset); } /** - * (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 * @return static */ - public function decay($decay) + public function decay(float $decay): static { return $this->addProperty('decay', $decay); } diff --git a/src/DSL/Queries/Compound/Functions/RandomScore.php b/src/DSL/Queries/Compound/Functions/RandomScore.php index 4a98aa9..25a6979 100644 --- a/src/DSL/Queries/Compound/Functions/RandomScore.php +++ b/src/DSL/Queries/Compound/Functions/RandomScore.php @@ -1,5 +1,7 @@ addProperty('seed', $seed); } - /** - * (Optional) Field used in combination with the seed to compute reproducible random scores. - * - * @param string $field - * @return static - */ - public function field($field) - { - return $this->addProperty('field', $field); - } - /** * {@inheritdoc} */ diff --git a/src/DSL/Queries/Compound/Functions/ScriptScore.php b/src/DSL/Queries/Compound/Functions/ScriptScore.php index 8175b3b..fbeea46 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)); } diff --git a/src/DSL/Queries/FullText.php b/src/DSL/Queries/FullText.php index 666c3fb..21b6808 100644 --- a/src/DSL/Queries/FullText.php +++ b/src/DSL/Queries/FullText.php @@ -1,5 +1,7 @@ addProperty('query', $query); } @@ -35,7 +37,7 @@ public function query($query) * @param array $fields * @return static */ - public function fields($fields) + public function fields(array $fields): static { return $this->addProperty('fields', $fields); } @@ -47,7 +49,7 @@ public function fields($fields) * @param bool $autoGenerateSynonymsPhraseQuery * @return static */ - public function autoGenerateSynonymsPhraseQuery($autoGenerateSynonymsPhraseQuery) + public function autoGenerateSynonymsPhraseQuery(bool $autoGenerateSynonymsPhraseQuery): static { return $this->addProperty('auto_generate_synonyms_phrase_query', $autoGenerateSynonymsPhraseQuery); } @@ -59,7 +61,7 @@ public function autoGenerateSynonymsPhraseQuery($autoGenerateSynonymsPhraseQuery * @param string $operator * @return static */ - public function operator($operator) + public function operator(string $operator): static { return $this->addProperty('operator', $operator); } @@ -68,10 +70,10 @@ public function operator($operator) * Minimum number of clauses that must match for a * document to be returned. * - * @param string $minimumShouldMatch + * @param int|string $minimumShouldMatch * @return static */ - public function minimumShouldMatch($minimumShouldMatch) + public function minimumShouldMatch(int|string $minimumShouldMatch): static { return $this->addProperty('minimum_should_match', $minimumShouldMatch); } @@ -84,7 +86,7 @@ public function minimumShouldMatch($minimumShouldMatch) * @param string $zeroTermsQuery * @return static */ - public function zeroTermsQuery($zeroTermsQuery) + public function zeroTermsQuery(string $zeroTermsQuery): static { return $this->addProperty('zero_terms_query', $zeroTermsQuery); } diff --git a/src/DSL/Queries/FullText/Intervals.php b/src/DSL/Queries/FullText/Intervals.php index 077167f..9b1b01d 100644 --- a/src/DSL/Queries/FullText/Intervals.php +++ b/src/DSL/Queries/FullText/Intervals.php @@ -1,5 +1,7 @@ */ protected $_intervals = []; @@ -24,7 +26,7 @@ class Intervals extends Node * @param mixed $match * @return static */ - public function match($match) + public function match($match): static { $this->_intervals[] = Intervals\Match_::create($match); return $this; @@ -37,7 +39,7 @@ public function match($match) * @param mixed $prefix * @return static */ - public function prefix($prefix) + public function prefix($prefix): static { $this->_intervals[] = Intervals\Prefix::create($prefix); return $this; @@ -49,7 +51,7 @@ public function prefix($prefix) * @param mixed $wildcard * @return static */ - public function wildcard($wildcard) + public function wildcard($wildcard): static { $this->_intervals[] = Intervals\Wildcard::create($wildcard); return $this; @@ -62,7 +64,7 @@ public function wildcard($wildcard) * @param mixed $fuzzy * @return static */ - public function fuzzy($fuzzy) + public function fuzzy($fuzzy): static { $this->_intervals[] = Intervals\Fuzzy::create($fuzzy); return $this; @@ -74,7 +76,7 @@ public function fuzzy($fuzzy) * @param mixed $range * @return static */ - public function range($range) + public function range($range): static { $this->_intervals[] = Intervals\Range::create($range); return $this; @@ -87,7 +89,7 @@ public function range($range) * @param mixed $allOf * @return static */ - public function allOf($allOf) + public function allOf($allOf): static { $this->_intervals[] = Intervals\AllOf::create($allOf); return $this; @@ -100,7 +102,7 @@ public function allOf($allOf) * @param mixed $anyOf * @return static */ - public function anyOf($anyOf) + public function anyOf($anyOf): static { $this->_intervals[] = Intervals\AnyOf::create($anyOf); return $this; @@ -127,7 +129,7 @@ public function toArray() $properties = $resolved; } - if ($this->_isPropertyField) { + if ($this->_fieldKeyed) { return [$this->_field => $properties]; } return $properties; diff --git a/src/DSL/Queries/FullText/Intervals/AllOf.php b/src/DSL/Queries/FullText/Intervals/AllOf.php index 817a3ec..777cf1b 100644 --- a/src/DSL/Queries/FullText/Intervals/AllOf.php +++ b/src/DSL/Queries/FullText/Intervals/AllOf.php @@ -1,5 +1,7 @@ isPropertyField(false) + ->fieldKeyed(false) ->multi(true); return $this->addProperty('intervals', $intervals); } @@ -33,10 +35,10 @@ public function intervals($intervals) * @param mixed $interval * @return static */ - public function addInterval($interval) + public function addInterval($interval): 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) { @@ -55,7 +57,7 @@ public function addInterval($interval) * @param int $maxGaps * @return static */ - public function maxGaps($maxGaps) + public function maxGaps(int $maxGaps): static { return $this->addProperty('max_gaps', $maxGaps); } @@ -67,7 +69,7 @@ public function maxGaps($maxGaps) * @param bool $ordered * @return static */ - public function ordered($ordered) + public function ordered(bool $ordered): static { return $this->addProperty('ordered', $ordered); } @@ -79,7 +81,7 @@ public function ordered($ordered) * @param mixed $filter * @return static */ - public function filter($filter) + public function filter($filter): static { return $this->addProperty('filter', $filter); } diff --git a/src/DSL/Queries/FullText/Intervals/AnyOf.php b/src/DSL/Queries/FullText/Intervals/AnyOf.php index b1da1e6..44bc2eb 100644 --- a/src/DSL/Queries/FullText/Intervals/AnyOf.php +++ b/src/DSL/Queries/FullText/Intervals/AnyOf.php @@ -1,5 +1,7 @@ isPropertyField(false) + ->fieldKeyed(false) ->multi(true); return $this->addProperty('intervals', $intervals); } @@ -32,10 +34,10 @@ public function intervals($intervals) * @param mixed $interval * @return static */ - public function addInterval($interval) + public function addInterval($interval): 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) { @@ -53,7 +55,7 @@ public function addInterval($interval) * @param mixed $filter * @return static */ - public function filter($filter) + public function filter($filter): static { return $this->addProperty('filter', Filter::create($filter)); } diff --git a/src/DSL/Queries/FullText/Intervals/Filter.php b/src/DSL/Queries/FullText/Intervals/Filter.php index 940b5a2..a2ae9a0 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)); } @@ -31,7 +35,7 @@ public function after($after) * @param mixed $before * @return static */ - public function before($before) + public function before($before): static { return $this->addProperty('before', Query::create($before)); } @@ -43,7 +47,7 @@ public function before($before) * @param mixed $containedBy * @return static */ - public function containedBy($containedBy) + public function containedBy($containedBy): static { return $this->addProperty('contained_by', Query::create($containedBy)); } @@ -55,7 +59,7 @@ public function containedBy($containedBy) * @param mixed $containing * @return static */ - public function containing($containing) + public function containing($containing): static { return $this->addProperty('containing', Query::create($containing)); } @@ -67,7 +71,7 @@ public function containing($containing) * @param mixed $notContaining * @return static */ - public function notContaining($notContaining) + public function notContaining($notContaining): static { return $this->addProperty('not_containing', Query::create($notContaining)); } @@ -79,7 +83,7 @@ public function notContaining($notContaining) * @param mixed $overlapping * @return static */ - public function overlapping($overlapping) + public function overlapping($overlapping): static { return $this->addProperty('overlapping', Query::create($overlapping)); } @@ -92,7 +96,7 @@ public function overlapping($overlapping) * @param mixed $script * @return static */ - public function script($script) + public function script($script): static { return $this->addProperty('script', Script::create($script)); } @@ -104,7 +108,7 @@ public function script($script) * @param mixed $notContainedBy * @return static */ - public function notContainedBy($notContainedBy) + public function notContainedBy($notContainedBy): static { return $this->addProperty('not_contained_by', Query::create($notContainedBy)); } @@ -116,7 +120,7 @@ public function notContainedBy($notContainedBy) * @param mixed $notOverlapping * @return static */ - public function notOverlapping($notOverlapping) + public function notOverlapping($notOverlapping): static { return $this->addProperty('not_overlapping', Query::create($notOverlapping)); } diff --git a/src/DSL/Queries/FullText/Intervals/Fuzzy.php b/src/DSL/Queries/FullText/Intervals/Fuzzy.php index 1a62ed3..9638bf8 100644 --- a/src/DSL/Queries/FullText/Intervals/Fuzzy.php +++ b/src/DSL/Queries/FullText/Intervals/Fuzzy.php @@ -1,5 +1,7 @@ addProperty('term', $term); } @@ -31,7 +33,7 @@ public function term($term) * @param int $prefixLength * @return static */ - public function prefixLength($prefixLength) + public function prefixLength(int $prefixLength): static { return $this->addProperty('prefix_length', $prefixLength); } @@ -43,7 +45,7 @@ public function prefixLength($prefixLength) * @param bool $transpositions * @return static */ - public function transpositions($transpositions) + public function transpositions(bool $transpositions): static { return $this->addProperty('transpositions', $transpositions); } @@ -52,10 +54,10 @@ public function transpositions($transpositions) * Maximum edit distance allowed for matching. * Defaults to auto. * - * @param string $fuzziness + * @param int|string $fuzziness * @return static */ - public function fuzziness($fuzziness) + public function fuzziness(int|string $fuzziness): static { return $this->addProperty('fuzziness', $fuzziness); } @@ -67,7 +69,7 @@ public function fuzziness($fuzziness) * @param string $analyzer * @return static */ - public function analyzer($analyzer) + public function analyzer(string $analyzer): static { return $this->addProperty('analyzer', $analyzer); } @@ -80,7 +82,7 @@ public function analyzer($analyzer) * @param string $useField * @return static */ - public function useField($useField) + public function useField(string $useField): static { return $this->addProperty('use_field', $useField); } diff --git a/src/DSL/Queries/FullText/Intervals/Match_.php b/src/DSL/Queries/FullText/Intervals/Match_.php index 6c7e8f9..1cadd4a 100644 --- a/src/DSL/Queries/FullText/Intervals/Match_.php +++ b/src/DSL/Queries/FullText/Intervals/Match_.php @@ -1,5 +1,7 @@ addProperty('query', $query); } @@ -33,7 +35,7 @@ public function query($query) * @param int $maxGaps * @return static */ - public function maxGaps($maxGaps) + public function maxGaps(int $maxGaps): static { return $this->addProperty('max_gaps', $maxGaps); } @@ -45,7 +47,7 @@ public function maxGaps($maxGaps) * @param bool $ordered * @return static */ - public function ordered($ordered = false) + public function ordered(bool $ordered = false): static { return $this->addProperty('ordered', $ordered); } @@ -57,7 +59,7 @@ public function ordered($ordered = false) * @param string $analyzer * @return static */ - public function analyzer($analyzer) + public function analyzer(string $analyzer): static { return $this->addProperty('analyzer', $analyzer); } @@ -68,7 +70,7 @@ public function analyzer($analyzer) * @param mixed $filter * @return static */ - public function filter($filter) + public function filter($filter): static { return $this->addProperty('filter', Filter::create($filter)); } @@ -81,7 +83,7 @@ public function filter($filter) * @param string $useField * @return static */ - public function useField($useField) + public function useField(string $useField): static { return $this->addProperty('use_field', $useField); } diff --git a/src/DSL/Queries/FullText/Intervals/Prefix.php b/src/DSL/Queries/FullText/Intervals/Prefix.php index 2ff087e..13eaef4 100644 --- a/src/DSL/Queries/FullText/Intervals/Prefix.php +++ b/src/DSL/Queries/FullText/Intervals/Prefix.php @@ -1,5 +1,7 @@ addProperty('prefix', $prefix); } @@ -32,7 +34,7 @@ public function prefix($prefix) * @param string $analyzer * @return static */ - public function analyzer($analyzer) + public function analyzer(string $analyzer): static { return $this->addProperty('analyzer', $analyzer); } @@ -45,7 +47,7 @@ public function analyzer($analyzer) * @param string $userField * @return static */ - public function useField($userField) + public function useField(string $userField): static { return $this->addProperty('use_field', $userField); } diff --git a/src/DSL/Queries/FullText/Intervals/Range.php b/src/DSL/Queries/FullText/Intervals/Range.php index 9237c3d..bfb7a6e 100644 --- a/src/DSL/Queries/FullText/Intervals/Range.php +++ b/src/DSL/Queries/FullText/Intervals/Range.php @@ -1,5 +1,7 @@ addProperty('gte', $gte); } /** - * (Optional) Greater than the specified value. + * Greater than the specified value. * - * @param mixed $gt + * @param string|int|float|bool $gt * @return static */ - public function gt($gt) + public function gt(string|int|float|bool $gt): static { return $this->addProperty('gt', $gt); } /** - * (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 $lte * @return static */ - public function lte($lte) + public function lte(string|int|float|bool $lte): static { return $this->addProperty('lte', $lte); } /** - * (Optional) Less than the specified value. + * Less than the specified value. * - * @param mixed $lt + * @param string|int|float|bool $lt * @return static */ - public function lt($lt) + public function lt(string|int|float|bool $lt): static { return $this->addProperty('lt', $lt); } @@ -66,7 +68,7 @@ public function lt($lt) * @param string $analyzer * @return static */ - public function analyzer($analyzer) + public function analyzer(string $analyzer): static { return $this->addProperty('analyzer', $analyzer); } @@ -78,7 +80,7 @@ public function analyzer($analyzer) * @param string $useField * @return static */ - public function useField($useField) + public function useField(string $useField): static { return $this->addProperty('use_field', $useField); } diff --git a/src/DSL/Queries/FullText/Intervals/Wildcard.php b/src/DSL/Queries/FullText/Intervals/Wildcard.php index 0a3174c..43fe8e3 100644 --- a/src/DSL/Queries/FullText/Intervals/Wildcard.php +++ b/src/DSL/Queries/FullText/Intervals/Wildcard.php @@ -1,5 +1,7 @@ addProperty('pattern', $pattern); } @@ -32,7 +34,7 @@ public function pattern($pattern) * @param string $analyzer * @return static */ - public function analyzer($analyzer) + public function analyzer(string $analyzer): static { return $this->addProperty('analyzer', $analyzer); } @@ -45,7 +47,7 @@ public function analyzer($analyzer) * @param string $useField * @return static */ - public function useField($useField) + public function useField(string $useField): static { return $this->addProperty('use_field', $useField); } diff --git a/src/DSL/Queries/FullText/MatchBoolPrefix.php b/src/DSL/Queries/FullText/MatchBoolPrefix.php index 9d41172..50ee452 100644 --- a/src/DSL/Queries/FullText/MatchBoolPrefix.php +++ b/src/DSL/Queries/FullText/MatchBoolPrefix.php @@ -1,5 +1,7 @@ addProperty('query', $query); } @@ -37,7 +39,7 @@ public function query($query) * @param int $maxExpansions * @return static */ - public function maxExpansions($maxExpansions) + public function maxExpansions(int $maxExpansions): static { return $this->addProperty('max_expansions', $maxExpansions); } @@ -49,7 +51,7 @@ public function maxExpansions($maxExpansions) * @param bool $lenient * @return static */ - public function lenient($lenient) + public function lenient(bool $lenient): static { return $this->addProperty('lenient', $lenient); } @@ -61,7 +63,7 @@ public function lenient($lenient) * @param string $analyzer * @return static */ - public function analyzer($analyzer) + public function analyzer(string $analyzer): static { return $this->addProperty('analyzer', $analyzer); } @@ -70,10 +72,10 @@ public function analyzer($analyzer) * Minimum number of clauses that must match for a * document to be returned. * - * @param string $minimumShouldMatch + * @param int|string $minimumShouldMatch * @return static */ - public function minimumShouldMatch($minimumShouldMatch) + public function minimumShouldMatch(int|string $minimumShouldMatch): static { return $this->addProperty('minimum_should_match', $minimumShouldMatch); } @@ -81,10 +83,10 @@ public function minimumShouldMatch($minimumShouldMatch) /** * Maximum edit distance allowed for fuzzy matching. * - * @param string $fuzziness + * @param int|string $fuzziness * @return static */ - public function fuzziness($fuzziness) + public function fuzziness(int|string $fuzziness): static { return $this->addProperty('fuzziness', $fuzziness); } @@ -96,7 +98,7 @@ public function fuzziness($fuzziness) * @param int $prefixLength * @return static */ - public function prefixLength($prefixLength) + public function prefixLength(int $prefixLength): static { return $this->addProperty('prefix_length', $prefixLength); } @@ -108,7 +110,7 @@ public function prefixLength($prefixLength) * @param bool $fuzzyTranspositions * @return static */ - public function fuzzyTranspositions($fuzzyTranspositions) + public function fuzzyTranspositions(bool $fuzzyTranspositions): static { return $this->addProperty('fuzzy_transpositions', $fuzzyTranspositions); } @@ -119,7 +121,7 @@ public function fuzzyTranspositions($fuzzyTranspositions) * @param string $fuzzyRewrite * @return static */ - public function fuzzyRewrite($fuzzyRewrite) + public function fuzzyRewrite(string $fuzzyRewrite): static { return $this->addProperty('fuzzy_rewrite', $fuzzyRewrite); } @@ -131,7 +133,7 @@ public function fuzzyRewrite($fuzzyRewrite) * @param string $operator * @return static */ - public function operator($operator) + public function operator(string $operator): static { return $this->addProperty('operator', $operator); } diff --git a/src/DSL/Queries/FullText/MatchPhrase.php b/src/DSL/Queries/FullText/MatchPhrase.php index 2ca8a03..83df915 100644 --- a/src/DSL/Queries/FullText/MatchPhrase.php +++ b/src/DSL/Queries/FullText/MatchPhrase.php @@ -1,5 +1,7 @@ addProperty('query', $query); } @@ -37,7 +39,7 @@ public function query($query) * @param string $analyzer * @return static */ - public function analyzer($analyzer) + public function analyzer(string $analyzer): static { return $this->addProperty('analyzer', $analyzer); } @@ -49,7 +51,7 @@ public function analyzer($analyzer) * @param int $slop * @return static */ - public function slop($slop) + public function slop(int $slop): static { return $this->addProperty('slop', $slop); } @@ -62,7 +64,7 @@ public function slop($slop) * @param string $zeroTermsQuery * @return static */ - public function zeroTermsQuery($zeroTermsQuery) + public function zeroTermsQuery(string $zeroTermsQuery): static { return $this->addProperty('zero_terms_query', $zeroTermsQuery); } diff --git a/src/DSL/Queries/FullText/MatchPhrasePrefix.php b/src/DSL/Queries/FullText/MatchPhrasePrefix.php index ee99983..c907d57 100644 --- a/src/DSL/Queries/FullText/MatchPhrasePrefix.php +++ b/src/DSL/Queries/FullText/MatchPhrasePrefix.php @@ -1,5 +1,7 @@ addProperty('query', $query); } @@ -38,7 +40,7 @@ public function query($query) * @param string $analyzer * @return static */ - public function analyzer($analyzer) + public function analyzer(string $analyzer): static { return $this->addProperty('analyzer', $analyzer); } @@ -50,7 +52,7 @@ public function analyzer($analyzer) * @param int $maxExpansions * @return static */ - public function maxExpansions($maxExpansions) + public function maxExpansions(int $maxExpansions): static { return $this->addProperty('max_expansions', $maxExpansions); } @@ -62,7 +64,7 @@ public function maxExpansions($maxExpansions) * @param int $slop * @return static */ - public function slop($slop) + public function slop(int $slop): static { return $this->addProperty('slop', $slop); } @@ -75,7 +77,7 @@ public function slop($slop) * @param string $zeroTermsQuery * @return static */ - public function zeroTermsQuery($zeroTermsQuery) + public function zeroTermsQuery(string $zeroTermsQuery): static { return $this->addProperty('zero_terms_query', $zeroTermsQuery); } diff --git a/src/DSL/Queries/FullText/Match_.php b/src/DSL/Queries/FullText/Match_.php index f57ca8b..45c8947 100644 --- a/src/DSL/Queries/FullText/Match_.php +++ b/src/DSL/Queries/FullText/Match_.php @@ -1,5 +1,7 @@ addProperty('query', $query); } @@ -39,7 +41,7 @@ public function query($query) * @param string $analyzer * @return static */ - public function analyzer($analyzer) + public function analyzer(string $analyzer): static { return $this->addProperty('analyzer', $analyzer); } @@ -51,7 +53,7 @@ public function analyzer($analyzer) * @param bool $autoGenerateSynonymsPhraseQuery * @return static */ - public function autoGenerateSynonymsPhraseQuery($autoGenerateSynonymsPhraseQuery) + public function autoGenerateSynonymsPhraseQuery(bool $autoGenerateSynonymsPhraseQuery): static { return $this->addProperty('auto_generate_synonyms_phrase_query', $autoGenerateSynonymsPhraseQuery); } @@ -60,10 +62,10 @@ public function autoGenerateSynonymsPhraseQuery($autoGenerateSynonymsPhraseQuery * Maximum edit distance allowed for matching. * See Fuzziness for valid values and more information. * - * @param string $fuzziness + * @param int|string $fuzziness * @return static */ - public function fuzziness($fuzziness) + public function fuzziness(int|string $fuzziness): static { return $this->addProperty('fuzziness', $fuzziness); } @@ -75,7 +77,7 @@ public function fuzziness($fuzziness) * @param int $maxExpansions * @return static */ - public function maxExpansions($maxExpansions) + public function maxExpansions(int $maxExpansions): static { return $this->addProperty('max_expansions', $maxExpansions); } @@ -87,7 +89,7 @@ public function maxExpansions($maxExpansions) * @param int $prefixLength * @return static */ - public function prefixLength($prefixLength) + public function prefixLength(int $prefixLength): static { return $this->addProperty('prefix_length', $prefixLength); } @@ -99,7 +101,7 @@ public function prefixLength($prefixLength) * @param bool $fuzzyTranspositions * @return static */ - public function fuzzyTranspositions($fuzzyTranspositions) + public function fuzzyTranspositions(bool $fuzzyTranspositions): static { return $this->addProperty('fuzzy_transpositions', $fuzzyTranspositions); } @@ -112,7 +114,7 @@ public function fuzzyTranspositions($fuzzyTranspositions) * @param string $fuzzyRewrite * @return static */ - public function fuzzyRewrite($fuzzyRewrite) + public function fuzzyRewrite(string $fuzzyRewrite): static { return $this->addProperty('fuzzy_rewrite', $fuzzyRewrite); } @@ -124,7 +126,7 @@ public function fuzzyRewrite($fuzzyRewrite) * @param bool $lenient * @return static */ - public function lenient($lenient) + public function lenient(bool $lenient): static { return $this->addProperty('lenient', $lenient); } @@ -136,7 +138,7 @@ public function lenient($lenient) * @param string $operator * @return static */ - public function operator($operator) + public function operator(string $operator): static { return $this->addProperty('operator', $operator); } @@ -145,10 +147,10 @@ public function operator($operator) * Minimum number of clauses that must match for a * document to be returned. * - * @param string $minimumShouldMatch + * @param int|string $minimumShouldMatch * @return static */ - public function minimumShouldMatch($minimumShouldMatch) + public function minimumShouldMatch(int|string $minimumShouldMatch): static { return $this->addProperty('minimum_should_match', $minimumShouldMatch); } @@ -161,7 +163,7 @@ public function minimumShouldMatch($minimumShouldMatch) * @param string $zeroTermsQuery * @return static */ - public function zeroTermsQuery($zeroTermsQuery) + public function zeroTermsQuery(string $zeroTermsQuery): static { return $this->addProperty('zero_terms_query', $zeroTermsQuery); } diff --git a/src/DSL/Queries/FullText/MultiMatch.php b/src/DSL/Queries/FullText/MultiMatch.php index 768bec5..048c222 100644 --- a/src/DSL/Queries/FullText/MultiMatch.php +++ b/src/DSL/Queries/FullText/MultiMatch.php @@ -1,5 +1,7 @@ addProperty('query', $query); } @@ -33,7 +35,7 @@ public function query($query) * @param array $fields * @return static */ - public function fields($fields) + public function fields(array $fields): static { return $this->addProperty('fields', $fields); } @@ -46,7 +48,7 @@ public function fields($fields) * @param string $type * @return static */ - public function type($type) + public function type(string $type): static { return $this->addProperty('type', $type); } @@ -58,7 +60,7 @@ public function type($type) * @param string $operator * @return static */ - public function operator($operator) + public function operator(string $operator): static { return $this->addProperty('operator', $operator); } @@ -70,7 +72,7 @@ public function operator($operator) * @param string $analyzer * @return static */ - public function analyzer($analyzer) + public function analyzer(string $analyzer): static { return $this->addProperty('analyzer', $analyzer); } @@ -79,10 +81,10 @@ public function analyzer($analyzer) * Minimum number of clauses that must match for a * document to be returned. * - * @param string $minimumShouldMatch + * @param int|string $minimumShouldMatch * @return static */ - public function minimumShouldMatch($minimumShouldMatch) + public function minimumShouldMatch(int|string $minimumShouldMatch): static { return $this->addProperty('minimum_should_match', $minimumShouldMatch); } @@ -94,7 +96,7 @@ public function minimumShouldMatch($minimumShouldMatch) * @param float $tieBreaker * @return static */ - public function tieBreaker($tieBreaker) + public function tieBreaker(float $tieBreaker): static { return $this->addProperty('tie_breaker', $tieBreaker); } @@ -102,10 +104,10 @@ public function tieBreaker($tieBreaker) /** * Maximum edit distance allowed for fuzzy matching. * - * @param string $fuzziness + * @param int|string $fuzziness * @return static */ - public function fuzziness($fuzziness) + public function fuzziness(int|string $fuzziness): static { return $this->addProperty('fuzziness', $fuzziness); } @@ -117,7 +119,7 @@ public function fuzziness($fuzziness) * @param int $prefixLength * @return static */ - public function prefixLength($prefixLength) + public function prefixLength(int $prefixLength): static { return $this->addProperty('prefix_length', $prefixLength); } @@ -129,7 +131,7 @@ public function prefixLength($prefixLength) * @param int $maxExpansions * @return static */ - public function maxExpansions($maxExpansions) + public function maxExpansions(int $maxExpansions): static { return $this->addProperty('max_expansions', $maxExpansions); } @@ -141,7 +143,7 @@ public function maxExpansions($maxExpansions) * @param bool $fuzzyTranspositions * @return static */ - public function fuzzyTranspositions($fuzzyTranspositions) + public function fuzzyTranspositions(bool $fuzzyTranspositions): static { return $this->addProperty('fuzzy_transpositions', $fuzzyTranspositions); } @@ -152,7 +154,7 @@ public function fuzzyTranspositions($fuzzyTranspositions) * @param string $fuzzyRewrite * @return static */ - public function fuzzyRewrite($fuzzyRewrite) + public function fuzzyRewrite(string $fuzzyRewrite): static { return $this->addProperty('fuzzy_rewrite', $fuzzyRewrite); } @@ -164,7 +166,7 @@ public function fuzzyRewrite($fuzzyRewrite) * @param bool $lenient * @return static */ - public function lenient($lenient) + public function lenient(bool $lenient): static { return $this->addProperty('lenient', $lenient); } @@ -177,7 +179,7 @@ public function lenient($lenient) * @param string $zeroTermsQuery * @return static */ - public function zeroTermsQuery($zeroTermsQuery) + public function zeroTermsQuery(string $zeroTermsQuery): static { return $this->addProperty('zero_terms_query', $zeroTermsQuery); } @@ -189,7 +191,7 @@ public function zeroTermsQuery($zeroTermsQuery) * @param int $slop * @return static */ - public function slop($slop) + public function slop(int $slop): static { return $this->addProperty('slop', $slop); } @@ -201,7 +203,7 @@ public function slop($slop) * @param bool $autoGenerateSynonymsPhraseQuery * @return static */ - public function autoGenerateSynonymsPhraseQuery($autoGenerateSynonymsPhraseQuery) + public function autoGenerateSynonymsPhraseQuery(bool $autoGenerateSynonymsPhraseQuery): static { return $this->addProperty('auto_generate_synonyms_phrase_query', $autoGenerateSynonymsPhraseQuery); } diff --git a/src/DSL/Queries/FullText/QueryString.php b/src/DSL/Queries/FullText/QueryString.php index 8446b3e..1d4b757 100644 --- a/src/DSL/Queries/FullText/QueryString.php +++ b/src/DSL/Queries/FullText/QueryString.php @@ -1,5 +1,7 @@ addProperty('query', $query); } @@ -32,7 +34,7 @@ public function query($query) * @param string $defaultField * @return static */ - public function defaultField($defaultField) + public function defaultField(string $defaultField): static { return $this->addProperty('default_field', $defaultField); } @@ -44,7 +46,7 @@ public function defaultField($defaultField) * @param bool $allowLeadingWildcard * @return static */ - public function allowLeadingWildcard($allowLeadingWildcard) + public function allowLeadingWildcard(bool $allowLeadingWildcard): static { return $this->addProperty('allow_leading_wildcard', $allowLeadingWildcard); } @@ -57,7 +59,7 @@ public function allowLeadingWildcard($allowLeadingWildcard) * @param string $analyzer * @return static */ - public function analyzer($analyzer) + public function analyzer(string $analyzer): static { return $this->addProperty('analyzer', $analyzer); } @@ -69,7 +71,7 @@ public function analyzer($analyzer) * @param bool $autoGenerateSynonymsPhraseQuery * @return static */ - public function autoGenerateSynonymsPhraseQuery($autoGenerateSynonymsPhraseQuery) + public function autoGenerateSynonymsPhraseQuery(bool $autoGenerateSynonymsPhraseQuery): static { return $this->addProperty('auto_generate_synonyms_phrase_query', $autoGenerateSynonymsPhraseQuery); } @@ -82,7 +84,7 @@ public function autoGenerateSynonymsPhraseQuery($autoGenerateSynonymsPhraseQuery * @param string $defaultOperator * @return static */ - public function defaultOperator($defaultOperator) + public function defaultOperator(string $defaultOperator): static { return $this->addProperty('default_operator', $defaultOperator); } @@ -94,7 +96,7 @@ public function defaultOperator($defaultOperator) * @param bool $enablePositionIncrements * @return static */ - public function enablePositionIncrements($enablePositionIncrements) + public function enablePositionIncrements(bool $enablePositionIncrements): static { return $this->addProperty('enable_position_increments', $enablePositionIncrements); } @@ -106,7 +108,7 @@ public function enablePositionIncrements($enablePositionIncrements) * @param array $fields * @return static */ - public function fields($fields) + public function fields(array $fields): static { return $this->addProperty('fields', $fields); } @@ -114,10 +116,10 @@ public function fields($fields) /** * Maximum edit distance allowed for fuzzy matching. * - * @param string $fuzziness + * @param int|string $fuzziness * @return static */ - public function fuzziness($fuzziness) + public function fuzziness(int|string $fuzziness): static { return $this->addProperty('fuzziness', $fuzziness); } @@ -129,7 +131,7 @@ public function fuzziness($fuzziness) * @param int $fuzzyMaxExpansions * @return static */ - public function fuzzyMaxExpansions($fuzzyMaxExpansions) + public function fuzzyMaxExpansions(int $fuzzyMaxExpansions): static { return $this->addProperty('fuzzy_max_expansions', $fuzzyMaxExpansions); } @@ -141,7 +143,7 @@ public function fuzzyMaxExpansions($fuzzyMaxExpansions) * @param int $fuzzyPrefixLength * @return static */ - public function fuzzyPrefixLength($fuzzyPrefixLength) + public function fuzzyPrefixLength(int $fuzzyPrefixLength): static { return $this->addProperty('fuzzy_prefix_length', $fuzzyPrefixLength); } @@ -153,7 +155,7 @@ public function fuzzyPrefixLength($fuzzyPrefixLength) * @param bool $fuzzyTranspositions * @return static */ - public function fuzzyTranspositions($fuzzyTranspositions) + public function fuzzyTranspositions(bool $fuzzyTranspositions): static { return $this->addProperty('fuzzy_transpositions', $fuzzyTranspositions); } @@ -165,7 +167,7 @@ public function fuzzyTranspositions($fuzzyTranspositions) * @param bool $lenient * @return static */ - public function lenient($lenient) + public function lenient(bool $lenient): static { return $this->addProperty('lenient', $lenient); } @@ -177,7 +179,7 @@ public function lenient($lenient) * @param int $maxDeterminizedStates * @return static */ - public function maxDeterminizedStates($maxDeterminizedStates) + public function maxDeterminizedStates(int $maxDeterminizedStates): static { return $this->addProperty('max_determinized_states', $maxDeterminizedStates); } @@ -186,10 +188,10 @@ public function maxDeterminizedStates($maxDeterminizedStates) * Minimum number of clauses that must match for a * document to be returned. * - * @param string $minimumShouldMatch + * @param int|string $minimumShouldMatch * @return static */ - public function minimumShouldMatch($minimumShouldMatch) + public function minimumShouldMatch(int|string $minimumShouldMatch): static { return $this->addProperty('minimum_should_match', $minimumShouldMatch); } @@ -202,7 +204,7 @@ public function minimumShouldMatch($minimumShouldMatch) * @param string $quoteAnalyzer * @return static */ - public function quoteAnalyzer($quoteAnalyzer) + public function quoteAnalyzer(string $quoteAnalyzer): static { return $this->addProperty('quote_analyzer', $quoteAnalyzer); } @@ -215,7 +217,7 @@ public function quoteAnalyzer($quoteAnalyzer) * @param int $phraseSlop * @return static */ - public function phraseSlop($phraseSlop) + public function phraseSlop(int $phraseSlop): static { return $this->addProperty('phrase_slop', $phraseSlop); } @@ -228,7 +230,7 @@ public function phraseSlop($phraseSlop) * @param string $quoteFieldSuffix * @return static */ - public function quoteFieldSuffix($quoteFieldSuffix) + public function quoteFieldSuffix(string $quoteFieldSuffix): static { return $this->addProperty('quote_field_suffix', $quoteFieldSuffix); } @@ -239,7 +241,7 @@ public function quoteFieldSuffix($quoteFieldSuffix) * @param string $rewrite * @return static */ - public function rewrite($rewrite) + public function rewrite(string $rewrite): static { return $this->addProperty('rewrite', $rewrite); } @@ -251,7 +253,7 @@ public function rewrite($rewrite) * @param string $timeZone * @return static */ - public function timeZone($timeZone) + public function timeZone(string $timeZone): static { return $this->addProperty('time_zone', $timeZone); } @@ -264,7 +266,7 @@ public function timeZone($timeZone) * @param string $type * @return static */ - public function type($type) + public function type(string $type): static { return $this->addProperty('type', $type); } @@ -276,7 +278,7 @@ public function type($type) * @param bool $analyzeWildcard * @return static */ - public function analyzeWildcard($analyzeWildcard) + public function analyzeWildcard(bool $analyzeWildcard): static { return $this->addProperty('analyze_wildcard', $analyzeWildcard); } @@ -288,7 +290,7 @@ public function analyzeWildcard($analyzeWildcard) * @param float $tieBreaker * @return static */ - public function tieBreaker($tieBreaker) + public function tieBreaker(float $tieBreaker): static { return $this->addProperty('tie_breaker', $tieBreaker); } diff --git a/src/DSL/Queries/FullText/SimpleQueryString.php b/src/DSL/Queries/FullText/SimpleQueryString.php index 5b475c9..84666d3 100644 --- a/src/DSL/Queries/FullText/SimpleQueryString.php +++ b/src/DSL/Queries/FullText/SimpleQueryString.php @@ -1,5 +1,7 @@ addProperty('query', $query); } @@ -33,7 +35,7 @@ public function query($query) * @param array $fields * @return static */ - public function fields($fields) + public function fields(array $fields): static { return $this->addProperty('fields', $fields); } @@ -46,7 +48,7 @@ public function fields($fields) * @param string $defaultOperator * @return static */ - public function defaultOperator($defaultOperator) + public function defaultOperator(string $defaultOperator): static { return $this->addProperty('default_operator', $defaultOperator); } @@ -58,7 +60,7 @@ public function defaultOperator($defaultOperator) * @param bool $analyzeWildcard * @return static */ - public function analyzeWildcard($analyzeWildcard) + public function analyzeWildcard(bool $analyzeWildcard): static { return $this->addProperty('analyze_wildcard', $analyzeWildcard); } @@ -71,7 +73,7 @@ public function analyzeWildcard($analyzeWildcard) * @param string $analyzer * @return static */ - public function analyzer($analyzer) + public function analyzer(string $analyzer): static { return $this->addProperty('analyzer', $analyzer); } @@ -83,7 +85,7 @@ public function analyzer($analyzer) * @param bool $autoGenerateSynonymsPhraseQuery * @return static */ - public function autoGenerateSynonymsPhraseQuery($autoGenerateSynonymsPhraseQuery) + public function autoGenerateSynonymsPhraseQuery(bool $autoGenerateSynonymsPhraseQuery): static { return $this->addProperty('auto_generate_synonyms_phrase_query', $autoGenerateSynonymsPhraseQuery); } @@ -95,7 +97,7 @@ public function autoGenerateSynonymsPhraseQuery($autoGenerateSynonymsPhraseQuery * @param string $flags * @return static */ - public function flags($flags) + public function flags(string $flags): static { return $this->addProperty('flags', $flags); } @@ -107,7 +109,7 @@ public function flags($flags) * @param int $fuzzyMaxExpansions * @return static */ - public function fuzzyMaxExpansions($fuzzyMaxExpansions) + public function fuzzyMaxExpansions(int $fuzzyMaxExpansions): static { return $this->addProperty('fuzzy_max_expansions', $fuzzyMaxExpansions); } @@ -119,7 +121,7 @@ public function fuzzyMaxExpansions($fuzzyMaxExpansions) * @param int $fuzzyPrefixLength * @return static */ - public function fuzzyPrefixLength($fuzzyPrefixLength) + public function fuzzyPrefixLength(int $fuzzyPrefixLength): static { return $this->addProperty('fuzzy_prefix_length', $fuzzyPrefixLength); } @@ -131,7 +133,7 @@ public function fuzzyPrefixLength($fuzzyPrefixLength) * @param bool $fuzzyTranspositions * @return static */ - public function fuzzyTranspositions($fuzzyTranspositions) + public function fuzzyTranspositions(bool $fuzzyTranspositions): static { return $this->addProperty('fuzzy_transpositions', $fuzzyTranspositions); } @@ -143,7 +145,7 @@ public function fuzzyTranspositions($fuzzyTranspositions) * @param bool $lenient * @return static */ - public function lenient($lenient) + public function lenient(bool $lenient): static { return $this->addProperty('lenient', $lenient); } @@ -152,10 +154,10 @@ public function lenient($lenient) * Minimum number of clauses that must match for a * document to be returned. * - * @param string $minimumShouldMatch + * @param int|string $minimumShouldMatch * @return static */ - public function minimumShouldMatch($minimumShouldMatch) + public function minimumShouldMatch(int|string $minimumShouldMatch): static { return $this->addProperty('minimum_should_match', $minimumShouldMatch); } @@ -168,7 +170,7 @@ public function minimumShouldMatch($minimumShouldMatch) * @param string $quoteFieldSuffix * @return static */ - public function quoteFieldSuffix($quoteFieldSuffix) + public function quoteFieldSuffix(string $quoteFieldSuffix): static { return $this->addProperty('quote_field_suffix', $quoteFieldSuffix); } diff --git a/src/DSL/Queries/Geo.php b/src/DSL/Queries/Geo.php index ddddf22..631bbb3 100644 --- a/src/DSL/Queries/Geo.php +++ b/src/DSL/Queries/Geo.php @@ -1,5 +1,7 @@ addProperty('top_left', $topLeft); } @@ -32,7 +34,7 @@ public function topLeft($topLeft) * @param mixed $bottomRight * @return static */ - public function bottomRight($bottomRight) + public function bottomRight($bottomRight): static { return $this->addProperty('bottom_right', $bottomRight); } @@ -43,7 +45,7 @@ public function bottomRight($bottomRight) * @param float $top * @return static */ - public function top($top) + public function top(float $top): static { return $this->addProperty('top', $top); } @@ -54,7 +56,7 @@ public function top($top) * @param float $left * @return static */ - public function left($left) + public function left(float $left): static { return $this->addProperty('left', $left); } @@ -65,7 +67,7 @@ public function left($left) * @param float $bottom * @return static */ - public function bottom($bottom) + public function bottom(float $bottom): static { return $this->addProperty('bottom', $bottom); } @@ -76,7 +78,7 @@ public function bottom($bottom) * @param float $right * @return static */ - public function right($right) + public function right(float $right): static { return $this->addProperty('right', $right); } @@ -87,7 +89,7 @@ public function right($right) * @param string $wkt * @return static */ - public function wkt($wkt) + public function wkt(string $wkt): static { return $this->addProperty('wkt', $wkt); } @@ -98,7 +100,7 @@ public function wkt($wkt) * @param mixed $topRight * @return static */ - public function topRight($topRight) + public function topRight($topRight): static { return $this->addProperty('top_right', $topRight); } @@ -109,7 +111,7 @@ public function topRight($topRight) * @param mixed $bottomLeft * @return static */ - public function bottomLeft($bottomLeft) + public function bottomLeft($bottomLeft): static { return $this->addProperty('bottom_left', $bottomLeft); } @@ -121,7 +123,7 @@ public function bottomLeft($bottomLeft) * @param string $validationMethod * @return static */ - public function validationMethod($validationMethod) + public function validationMethod(string $validationMethod): static { return $this->addProperty('validation_method', $validationMethod); } @@ -133,7 +135,7 @@ public function validationMethod($validationMethod) * @param bool $ignoreUnmapped * @return static */ - public function ignoreUnmapped($ignoreUnmapped) + public function ignoreUnmapped(bool $ignoreUnmapped): static { return $this->addProperty('ignore_unmapped', $ignoreUnmapped); } diff --git a/src/DSL/Queries/Geo/GeoDistance.php b/src/DSL/Queries/Geo/GeoDistance.php index fca2402..6f5b892 100644 --- a/src/DSL/Queries/Geo/GeoDistance.php +++ b/src/DSL/Queries/Geo/GeoDistance.php @@ -1,5 +1,7 @@ addProperty('distance', $distance); } @@ -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); } @@ -51,7 +53,7 @@ public function location($field, $location) * @param string $distanceType * @return static */ - public function distanceType($distanceType) + public function distanceType(string $distanceType): static { return $this->addProperty('distance_type', $distanceType); } @@ -64,7 +66,7 @@ public function distanceType($distanceType) * @SuppressWarnings(PHPMD.CamelCaseParameterName) * @SuppressWarnings(PHPMD.CamelCaseVariableName) */ - public function _name($_name) + public function _name(string $_name): static { return $this->addProperty('_name', $_name); } @@ -76,7 +78,7 @@ public function _name($_name) * @param string $validationMethod * @return static */ - public function validationMethod($validationMethod) + public function validationMethod(string $validationMethod): static { return $this->addProperty('validation_method', $validationMethod); } diff --git a/src/DSL/Queries/Geo/GeoGrid.php b/src/DSL/Queries/Geo/GeoGrid.php index d6fdb98..64bea1d 100644 --- a/src/DSL/Queries/Geo/GeoGrid.php +++ b/src/DSL/Queries/Geo/GeoGrid.php @@ -1,5 +1,7 @@ addProperty('geohex', $geohex); } @@ -35,7 +37,7 @@ public function geohex($geohex) * @param string $geotile * @return static */ - public function geotile($geotile) + public function geotile(string $geotile): static { return $this->addProperty('geotile', $geotile); } @@ -47,7 +49,7 @@ public function geotile($geotile) * @param string $geohash * @return static */ - public function geohash($geohash) + public function geohash(string $geohash): static { return $this->addProperty('geohash', $geohash); } diff --git a/src/DSL/Queries/Geo/GeoPolygon.php b/src/DSL/Queries/Geo/GeoPolygon.php index d456e23..9e3fbbf 100644 --- a/src/DSL/Queries/Geo/GeoPolygon.php +++ b/src/DSL/Queries/Geo/GeoPolygon.php @@ -1,5 +1,7 @@ > $points * @return static */ - public function points($points) + public function points(array $points): static { return $this->addProperty('points', $points); } @@ -34,7 +36,7 @@ public function points($points) * @param string $validationMethod * @return static */ - public function validationMethod($validationMethod) + public function validationMethod(string $validationMethod): static { return $this->addProperty('validation_method', $validationMethod); } @@ -46,7 +48,7 @@ public function validationMethod($validationMethod) * @param bool $ignoreUnmapped * @return static */ - public function ignoreUnmapped($ignoreUnmapped) + public function ignoreUnmapped(bool $ignoreUnmapped): static { return $this->addProperty('ignore_unmapped', $ignoreUnmapped); } diff --git a/src/DSL/Queries/Geo/GeoShape.php b/src/DSL/Queries/Geo/GeoShape.php index 638ed51..2dfb933 100644 --- a/src/DSL/Queries/Geo/GeoShape.php +++ b/src/DSL/Queries/Geo/GeoShape.php @@ -1,5 +1,7 @@ addProperty('shape', $shape); } @@ -37,7 +39,7 @@ public function shape($shape) * @param string $relation * @return static */ - public function relation($relation) + public function relation(string $relation): static { return $this->addProperty('relation', $relation); } @@ -49,7 +51,7 @@ public function relation($relation) * @param mixed $indexedShape * @return static */ - public function indexedShape($indexedShape) + public function indexedShape($indexedShape): static { return $this->addProperty('indexed_shape', $indexedShape); } @@ -61,7 +63,7 @@ public function indexedShape($indexedShape) * @param bool $ignoreUnmapped * @return static */ - public function ignoreUnmapped($ignoreUnmapped) + public function ignoreUnmapped(bool $ignoreUnmapped): static { return $this->addProperty('ignore_unmapped', $ignoreUnmapped); } diff --git a/src/DSL/Queries/Joining.php b/src/DSL/Queries/Joining.php index d814469..bb35ae9 100644 --- a/src/DSL/Queries/Joining.php +++ b/src/DSL/Queries/Joining.php @@ -1,5 +1,7 @@ addProperty('type', $type); } @@ -32,7 +34,7 @@ public function type($type) * @param mixed $query * @return static */ - public function query($query) + public function query($query): static { return $this->addProperty('query', Query::create($query)); } @@ -44,7 +46,7 @@ public function query($query) * @param bool $ignoreUnmapped * @return static */ - public function ignoreUnmapped($ignoreUnmapped) + public function ignoreUnmapped(bool $ignoreUnmapped): static { return $this->addProperty('ignore_unmapped', $ignoreUnmapped); } @@ -56,7 +58,7 @@ public function ignoreUnmapped($ignoreUnmapped) * @param int $maxChildren * @return static */ - public function maxChildren($maxChildren) + public function maxChildren(int $maxChildren): static { return $this->addProperty('max_children', $maxChildren); } @@ -69,7 +71,7 @@ public function maxChildren($maxChildren) * @param int $minChildren * @return static */ - public function minChildren($minChildren) + public function minChildren(int $minChildren): static { return $this->addProperty('min_children', $minChildren); } @@ -81,7 +83,7 @@ public function minChildren($minChildren) * @param string $scoreMode * @return static */ - public function scoreMode($scoreMode) + public function scoreMode(string $scoreMode): static { return $this->addProperty('score_mode', $scoreMode); } diff --git a/src/DSL/Queries/Joining/HasParent.php b/src/DSL/Queries/Joining/HasParent.php index 17c0c16..f13d68c 100644 --- a/src/DSL/Queries/Joining/HasParent.php +++ b/src/DSL/Queries/Joining/HasParent.php @@ -1,5 +1,7 @@ addProperty('parent_type', $parentType); } @@ -32,7 +34,7 @@ public function parentType($parentType) * @param mixed $query * @return static */ - public function query($query) + public function query($query): static { return $this->addProperty('query', Query::create($query)); } @@ -44,7 +46,7 @@ public function query($query) * @param bool $score * @return static */ - public function score($score) + public function score(bool $score): static { return $this->addProperty('score', $score); } @@ -56,7 +58,7 @@ public function score($score) * @param bool $ignoreUnmapped * @return static */ - public function ignoreUnmapped($ignoreUnmapped) + public function ignoreUnmapped(bool $ignoreUnmapped): static { return $this->addProperty('ignore_unmapped', $ignoreUnmapped); } diff --git a/src/DSL/Queries/Joining/Nested.php b/src/DSL/Queries/Joining/Nested.php index 4849437..97c762f 100644 --- a/src/DSL/Queries/Joining/Nested.php +++ b/src/DSL/Queries/Joining/Nested.php @@ -1,5 +1,7 @@ path($field); @@ -38,7 +40,7 @@ public static function create($field = null, $value = null) * @param string $path * @return static */ - public function path($path) + public function path(string $path): static { return $this->addProperty('path', $path); } @@ -50,7 +52,7 @@ public function path($path) * @param mixed $query * @return static */ - public function query($query) + public function query($query): static { return $this->addProperty('query', Query::create($query)); } @@ -62,7 +64,7 @@ public function query($query) * @param string $scoreMode * @return static */ - public function scoreMode($scoreMode) + public function scoreMode(string $scoreMode): static { return $this->addProperty('score_mode', $scoreMode); } @@ -74,7 +76,7 @@ public function scoreMode($scoreMode) * @param bool $ignoreUnmapped * @return static */ - public function ignoreUnmapped($ignoreUnmapped) + public function ignoreUnmapped(bool $ignoreUnmapped): static { return $this->addProperty('ignore_unmapped', $ignoreUnmapped); } diff --git a/src/DSL/Queries/Joining/ParentId.php b/src/DSL/Queries/Joining/ParentId.php index 6be7e8a..040b300 100644 --- a/src/DSL/Queries/Joining/ParentId.php +++ b/src/DSL/Queries/Joining/ParentId.php @@ -1,5 +1,7 @@ addProperty('type', $type); } @@ -30,7 +32,7 @@ public function type($type) * @param string $id * @return static */ - public function id($id) + public function id(string $id): static { return $this->addProperty('id', $id); } @@ -42,7 +44,7 @@ public function id($id) * @param bool $ignoreUnmapped * @return static */ - public function ignoreUnmapped($ignoreUnmapped) + public function ignoreUnmapped(bool $ignoreUnmapped): static { return $this->addProperty('ignore_unmapped', $ignoreUnmapped); } diff --git a/src/DSL/Queries/MatchAll.php b/src/DSL/Queries/MatchAll.php index efb892a..337b63f 100644 --- a/src/DSL/Queries/MatchAll.php +++ b/src/DSL/Queries/MatchAll.php @@ -1,5 +1,7 @@ addProperty('id', $id); } /** - * (Optional) The script language. Defaults to painless. + * The script language. Defaults to painless. * * @param string $lang * @return static */ - public function lang($lang) + public function lang(string $lang): static { return $this->addProperty('lang', $lang); } /** - * (Required) The inline script source to execute. + * The inline script source to execute. * * @param string $source * @return static */ - public function source($source) + public function source(string $source): static { return $this->addProperty('source', $source); } /** - * (Optional) Named parameters passed into the script. + * Named parameters passed into the script. * * @param array $params * @return static */ - public function params($params) + public function params(array $params): static { return $this->addProperty('params', $params); } diff --git a/src/DSL/Queries/Shape.php b/src/DSL/Queries/Shape.php index 1980745..1c2710a 100644 --- a/src/DSL/Queries/Shape.php +++ b/src/DSL/Queries/Shape.php @@ -1,5 +1,7 @@ addProperty('shape', $shape); } @@ -35,7 +37,7 @@ public function shape($shape) * @param string $relation * @return static */ - public function relation($relation) + public function relation(string $relation): static { return $this->addProperty('relation', $relation); } @@ -47,7 +49,7 @@ public function relation($relation) * @param mixed $indexedShape * @return static */ - public function indexedShape($indexedShape) + public function indexedShape($indexedShape): static { return $this->addProperty('indexed_shape', $indexedShape); } diff --git a/src/DSL/Queries/Span.php b/src/DSL/Queries/Span.php index 22bffbc..c083640 100644 --- a/src/DSL/Queries/Span.php +++ b/src/DSL/Queries/Span.php @@ -1,5 +1,7 @@ addProperty('little', Query::create($little)); } @@ -29,7 +31,7 @@ public function little($little) * @param mixed $big * @return static */ - public function big($big) + public function big($big): static { return $this->addProperty('big', Query::create($big)); } diff --git a/src/DSL/Queries/Span/SpanFieldMasking.php b/src/DSL/Queries/Span/SpanFieldMasking.php index ea62edf..5be991e 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 - * @return static - */ - public function field($field) - { - return $this->addProperty('field', $field); - } } diff --git a/src/DSL/Queries/Span/SpanFirst.php b/src/DSL/Queries/Span/SpanFirst.php index 2881f43..fc21193 100644 --- a/src/DSL/Queries/Span/SpanFirst.php +++ b/src/DSL/Queries/Span/SpanFirst.php @@ -1,5 +1,7 @@ addProperty('match', Query::create($match)); } @@ -29,7 +31,7 @@ public function match($match) * @param int $end * @return static */ - public function end($end) + public function end(int $end): static { return $this->addProperty('end', $end); } diff --git a/src/DSL/Queries/Span/SpanMulti.php b/src/DSL/Queries/Span/SpanMulti.php index ecdad4a..ebe0ffe 100644 --- a/src/DSL/Queries/Span/SpanMulti.php +++ b/src/DSL/Queries/Span/SpanMulti.php @@ -1,5 +1,7 @@ addProperty('match', Query::create($match)); } diff --git a/src/DSL/Queries/Span/SpanNear.php b/src/DSL/Queries/Span/SpanNear.php index eaa8f20..c7b49a5 100644 --- a/src/DSL/Queries/Span/SpanNear.php +++ b/src/DSL/Queries/Span/SpanNear.php @@ -1,5 +1,7 @@ addClause('clauses', $clauses); } @@ -32,7 +34,7 @@ public function clauses($clauses) * @param int $slop * @return static */ - public function slop($slop) + public function slop(int $slop): static { return $this->addProperty('slop', $slop); } @@ -43,7 +45,7 @@ public function slop($slop) * @param bool $inOrder * @return static */ - public function inOrder($inOrder) + public function inOrder(bool $inOrder): static { return $this->addProperty('in_order', $inOrder); } diff --git a/src/DSL/Queries/Span/SpanNot.php b/src/DSL/Queries/Span/SpanNot.php index 31b77c3..785f810 100644 --- a/src/DSL/Queries/Span/SpanNot.php +++ b/src/DSL/Queries/Span/SpanNot.php @@ -1,5 +1,7 @@ addProperty('include', Query::create($include)); } @@ -29,7 +31,7 @@ public function include($include) * @param mixed $exclude * @return static */ - public function exclude($exclude) + public function exclude($exclude): static { return $this->addProperty('exclude', Query::create($exclude)); } @@ -40,7 +42,7 @@ public function exclude($exclude) * @param int $pre * @return static */ - public function pre($pre) + public function pre(int $pre): static { return $this->addProperty('pre', $pre); } @@ -51,7 +53,7 @@ public function pre($pre) * @param int $post * @return static */ - public function post($post) + public function post(int $post): static { return $this->addProperty('post', $post); } @@ -62,7 +64,7 @@ public function post($post) * @param int $dist * @return static */ - public function dist($dist) + public function dist(int $dist): static { return $this->addProperty('dist', $dist); } diff --git a/src/DSL/Queries/Span/SpanOr.php b/src/DSL/Queries/Span/SpanOr.php index 3999878..2da6c7d 100644 --- a/src/DSL/Queries/Span/SpanOr.php +++ b/src/DSL/Queries/Span/SpanOr.php @@ -1,5 +1,7 @@ addClause('clauses', $clauses); } diff --git a/src/DSL/Queries/Span/SpanTerm.php b/src/DSL/Queries/Span/SpanTerm.php index ad34f62..972f096 100644 --- a/src/DSL/Queries/Span/SpanTerm.php +++ b/src/DSL/Queries/Span/SpanTerm.php @@ -1,5 +1,7 @@ addProperty('term', $term); } @@ -28,10 +30,10 @@ public function term($term) /** * 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..08edb3e 100644 --- a/src/DSL/Queries/Span/SpanWithin.php +++ b/src/DSL/Queries/Span/SpanWithin.php @@ -1,5 +1,7 @@ addProperty('little', Query::create($little)); } @@ -29,7 +31,7 @@ public function little($little) * @param mixed $big * @return static */ - public function big($big) + public function big($big): static { return $this->addProperty('big', Query::create($big)); } diff --git a/src/DSL/Queries/Specialized.php b/src/DSL/Queries/Specialized.php index 445d49c..1667af0 100644 --- a/src/DSL/Queries/Specialized.php +++ b/src/DSL/Queries/Specialized.php @@ -1,5 +1,7 @@ addProperty('origin', $origin); } @@ -25,10 +27,10 @@ public function origin($origin) /** * Distance from the origin at which relevance scores receive half of the boost value. * - * @param mixed $pivot + * @param string $pivot * @return static */ - public function pivot($pivot) + public function pivot(string $pivot): static { return $this->addProperty('pivot', $pivot); } diff --git a/src/DSL/Queries/Specialized/MoreLikeThis.php b/src/DSL/Queries/Specialized/MoreLikeThis.php index d7ab65b..4c7c6b1 100644 --- a/src/DSL/Queries/Specialized/MoreLikeThis.php +++ b/src/DSL/Queries/Specialized/MoreLikeThis.php @@ -1,5 +1,7 @@ $array * @return static */ - public function fields($array) + public function fields(array $array): static { return $this->addProperty('fields', $array); } @@ -28,7 +30,7 @@ public function fields($array) * @param mixed $string * @return static */ - public function like($string) + public function like($string): static { return $this->addProperty('like', $string); } @@ -39,7 +41,7 @@ public function like($string) * @param int $int * @return static */ - public function minTermFreq($int) + public function minTermFreq(int $int): static { return $this->addProperty('min_term_freq', $int); } @@ -50,7 +52,7 @@ public function minTermFreq($int) * @param int $int * @return static */ - public function maxQueryTerms($int) + public function maxQueryTerms(int $int): static { return $this->addProperty('max_query_terms', $int); } diff --git a/src/DSL/Queries/Specialized/Percolate.php b/src/DSL/Queries/Specialized/Percolate.php index 74d882e..61bf1a3 100644 --- a/src/DSL/Queries/Specialized/Percolate.php +++ b/src/DSL/Queries/Specialized/Percolate.php @@ -1,5 +1,7 @@ addProperty('document', $document); } diff --git a/src/DSL/Queries/Specialized/Pinned.php b/src/DSL/Queries/Specialized/Pinned.php index 0ed91d5..361bebc 100644 --- a/src/DSL/Queries/Specialized/Pinned.php +++ b/src/DSL/Queries/Specialized/Pinned.php @@ -1,5 +1,7 @@ $ids * @return static */ - public function ids($ids) + public function ids(array $ids): static { return $this->addProperty('ids', $ids); } @@ -29,7 +31,7 @@ public function ids($ids) * @param mixed $organic * @return static */ - public function organic($organic) + public function organic($organic): static { return $this->addProperty('organic', Query::create($organic)); } @@ -40,7 +42,7 @@ public function organic($organic) * @param mixed $doc * @return static */ - public function doc($doc) + public function doc($doc): static { return $this->addProperty('doc', $doc); } diff --git a/src/DSL/Queries/Specialized/RankFeature.php b/src/DSL/Queries/Specialized/RankFeature.php index 0cd8746..011b217 100644 --- a/src/DSL/Queries/Specialized/RankFeature.php +++ b/src/DSL/Queries/Specialized/RankFeature.php @@ -1,5 +1,7 @@ addProperty('saturation', $saturation); } @@ -28,7 +30,7 @@ public function saturation($saturation) * @param mixed $log * @return static */ - public function log($log) + public function log($log): static { return $this->addProperty('log', $log); } @@ -39,7 +41,7 @@ public function log($log) * @param mixed $sigmoid * @return static */ - public function sigmoid($sigmoid) + public function sigmoid($sigmoid): static { return $this->addProperty('sigmoid', $sigmoid); } @@ -50,7 +52,7 @@ public function sigmoid($sigmoid) * @param mixed $linear * @return static */ - public function linear($linear) + public function linear($linear): static { return $this->addProperty('linear', $linear); } diff --git a/src/DSL/Queries/Specialized/Script.php b/src/DSL/Queries/Specialized/Script.php index a51cd1e..e670d0b 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)); } diff --git a/src/DSL/Queries/Specialized/ScriptScore.php b/src/DSL/Queries/Specialized/ScriptScore.php index d58091b..aa9a94e 100644 --- a/src/DSL/Queries/Specialized/ScriptScore.php +++ b/src/DSL/Queries/Specialized/ScriptScore.php @@ -1,5 +1,7 @@ addProperty('query', Query::create($query)); } @@ -29,7 +31,7 @@ public function query($query) * @param mixed $script * @return static */ - public function script($script) + public function script($script): static { return $this->addProperty('script', \ElasticKit\DSL\Queries\Script::create($script)); } @@ -40,7 +42,7 @@ public function script($script) * @param float $minScore * @return static */ - public function minScore($minScore) + public function minScore(float $minScore): static { return $this->addProperty('min_score', Query::create($minScore)); } diff --git a/src/DSL/Queries/Specialized/Wrapper.php b/src/DSL/Queries/Specialized/Wrapper.php index 53f8b4d..84335df 100644 --- a/src/DSL/Queries/Specialized/Wrapper.php +++ b/src/DSL/Queries/Specialized/Wrapper.php @@ -1,5 +1,7 @@ query($field); @@ -35,7 +37,7 @@ public static function create($field = null, $value = null) * @param string $query * @return static */ - public function query($query) + public function query(string $query): static { return $this->addProperty('query', $query); } diff --git a/src/DSL/Queries/TermLevel.php b/src/DSL/Queries/TermLevel.php index 7e06f7a..946130d 100644 --- a/src/DSL/Queries/TermLevel.php +++ b/src/DSL/Queries/TermLevel.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,10 +26,10 @@ public function value($value) /** * Maximum edit distance allowed for matching. See Fuzziness for valid values and more information. * - * @param string $fuzziness + * @param int|string $fuzziness * @return static */ - public function fuzziness($fuzziness) + public function fuzziness(int|string $fuzziness): static { return $this->addProperty('fuzziness', $fuzziness); } @@ -38,7 +40,7 @@ public function fuzziness($fuzziness) * @param int $maxExpansions * @return static */ - public function maxExpansions($maxExpansions) + public function maxExpansions(int $maxExpansions): static { return $this->addProperty('max_expansions', $maxExpansions); } @@ -49,7 +51,7 @@ public function maxExpansions($maxExpansions) * @param int $prefixLength * @return static */ - public function prefixLength($prefixLength) + public function prefixLength(int $prefixLength): static { return $this->addProperty('prefix_length', $prefixLength); } @@ -60,7 +62,7 @@ public function prefixLength($prefixLength) * @param bool $transpositions * @return static */ - public function transpositions($transpositions) + public function transpositions(bool $transpositions): static { return $this->addProperty('transpositions', $transpositions); } @@ -71,7 +73,7 @@ public function transpositions($transpositions) * @param string $rewrite * @return static */ - public function rewrite($rewrite) + public function rewrite(string $rewrite): static { return $this->addProperty('rewrite', $rewrite); } diff --git a/src/DSL/Queries/TermLevel/IDs.php b/src/DSL/Queries/TermLevel/IDs.php index 2f36123..73efe22 100644 --- a/src/DSL/Queries/TermLevel/IDs.php +++ b/src/DSL/Queries/TermLevel/IDs.php @@ -1,5 +1,7 @@ $values * @return static */ - public function values($values) + public function values(array $values): static { return $this->addProperty('values', $values); } diff --git a/src/DSL/Queries/TermLevel/Prefix.php b/src/DSL/Queries/TermLevel/Prefix.php index 609dc7b..6f6305a 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); } @@ -27,7 +29,7 @@ public function value($value) * @param string $rewrite * @return static */ - public function rewrite($rewrite) + public function rewrite(string $rewrite): static { return $this->addProperty('rewrite', $rewrite); } @@ -38,7 +40,7 @@ public function rewrite($rewrite) * @param bool $caseInsensitive * @return static */ - public function caseInsensitive($caseInsensitive) + public function caseInsensitive(bool $caseInsensitive): static { return $this->addProperty('case_insensitive', $caseInsensitive); } diff --git a/src/DSL/Queries/TermLevel/Range.php b/src/DSL/Queries/TermLevel/Range.php index 71d191b..9686872 100644 --- a/src/DSL/Queries/TermLevel/Range.php +++ b/src/DSL/Queries/TermLevel/Range.php @@ -1,5 +1,7 @@ addProperty('gte', $gte); } /** - * (Optional) Greater than. + * Greater than. * - * @param mixed $gt + * @param string|int|float|bool $gt * @return static */ - public function gt($gt) + public function gt(string|int|float|bool $gt): static { return $this->addProperty('gt', $gt); } /** - * (Optional) Less than or equal to. + * Less than or equal to. * - * @param mixed $lte + * @param string|int|float|bool $lte * @return static */ - public function lte($lte) + public function lte(string|int|float|bool $lte): static { return $this->addProperty('lte', $lte); } /** - * (Optional) Less than. + * Less than. * - * @param mixed $lt + * @param string|int|float|bool $lt * @return static */ - public function lt($lt) + public function lt(string|int|float|bool $lt): static { return $this->addProperty('lt', $lt); } @@ -69,7 +71,7 @@ public function lt($lt) * @param string $format * @return static */ - public function format($format) + public function format(string $format): static { return $this->addProperty('format', $format); } @@ -87,7 +89,7 @@ public function format($format) * @param string $relation * @return static */ - public function relation($relation) + public function relation(string $relation): static { return $this->addProperty('relation', $relation); } @@ -98,7 +100,7 @@ public function relation($relation) * @param string $timeZone * @return static */ - public function timeZone($timeZone) + public function timeZone(string $timeZone): static { return $this->addProperty('time_zone', $timeZone); } diff --git a/src/DSL/Queries/TermLevel/Regexp.php b/src/DSL/Queries/TermLevel/Regexp.php index 80ed83c..f8db581 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); } @@ -31,7 +33,7 @@ public function value($value) * @param string $flags * @return static */ - public function flags($flags) + public function flags(string $flags): static { return $this->addProperty('flags', $flags); } @@ -42,7 +44,7 @@ public function flags($flags) * @param bool $caseInsensitive * @return static */ - public function caseInsensitive($caseInsensitive) + public function caseInsensitive(bool $caseInsensitive): static { return $this->addProperty('case_insensitive', $caseInsensitive); } @@ -57,7 +59,7 @@ public function caseInsensitive($caseInsensitive) * @param int $maxDeterminizedStates * @return static */ - public function maxDeterminizedStates($maxDeterminizedStates) + public function maxDeterminizedStates(int $maxDeterminizedStates): static { return $this->addProperty('max_determinized_states', $maxDeterminizedStates); } @@ -68,7 +70,7 @@ public function maxDeterminizedStates($maxDeterminizedStates) * @param string $rewrite * @return static */ - public function rewrite($rewrite) + public function rewrite(string $rewrite): static { return $this->addProperty('rewrite', $rewrite); } diff --git a/src/DSL/Queries/TermLevel/Term.php b/src/DSL/Queries/TermLevel/Term.php index 699d9cf..1ab8da5 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); } @@ -40,7 +42,7 @@ public function value($value) * @return static * @version 7.10.0 */ - public function caseInsensitive($caseInsensitive) + public function caseInsensitive(bool $caseInsensitive): static { return $this->addProperty('case_insensitive', $caseInsensitive); } 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..470f82f 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 $terms * @return static */ - public function terms($terms) + public function terms(array $terms): static { return $this->addProperty('terms', $terms); } /** - * (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 $minimumShouldMatch * @return static */ - public function minimumShouldMatch($minimumShouldMatch) + public function minimumShouldMatch(int|string $minimumShouldMatch): static { return $this->addProperty('minimum_should_match', $minimumShouldMatch); } @@ -44,7 +46,7 @@ public function minimumShouldMatch($minimumShouldMatch) * @param string $field * @return static */ - public function minimumShouldMatchField($field) + public function minimumShouldMatchField(string $field): static { return $this->addProperty('minimum_should_match_field', $field); } @@ -59,7 +61,7 @@ public function minimumShouldMatchField($field) * @param mixed $minimumShouldMatchScript * @return static */ - public function minimumShouldMatchScript($minimumShouldMatchScript) + public function minimumShouldMatchScript($minimumShouldMatchScript): static { return $this->addProperty('minimum_should_match_script', Script::create($minimumShouldMatchScript)); } diff --git a/src/DSL/Queries/TermLevel/Wildcard.php b/src/DSL/Queries/TermLevel/Wildcard.php index aaaff69..1efa192 100644 --- a/src/DSL/Queries/TermLevel/Wildcard.php +++ b/src/DSL/Queries/TermLevel/Wildcard.php @@ -1,14 +1,16 @@ addProperty('case_insensitive', $caseInsensitive); } @@ -28,7 +30,7 @@ public function caseInsensitive($caseInsensitive) * @param string $rewrite * @return static */ - public function rewrite($rewrite) + public function rewrite(string $rewrite): static { return $this->addProperty('rewrite', $rewrite); } @@ -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); } @@ -56,7 +58,7 @@ public function value($value) * @param string $wildcard * @return static */ - public function wildcard($wildcard) + public function wildcard(string $wildcard): static { return $this->addProperty('wildcard', $wildcard); } diff --git a/src/DSL/Query.php b/src/DSL/Query.php index 93d41db..57cff33 100644 --- a/src/DSL/Query.php +++ b/src/DSL/Query.php @@ -1,5 +1,7 @@ _multi = $multi; - return $this; - } - /** * Whether the query supports multiple clauses. * * @return bool */ - protected function isMulti() + protected function isMulti(): bool { return $this->_multi; } @@ -106,7 +96,7 @@ public function __construct($field = null, $value = null) } elseif (is_array($field)) { $this->fromArray($field); } elseif ($field !== null) { - $this->_properties = $field; + $this->_raw = $field; } } @@ -132,7 +122,7 @@ protected function fromArray(array $field): void * * @return array */ - public function getQueries() + public function getQueries(): array { return $this->_queries; } @@ -143,7 +133,7 @@ public function getQueries() * @param mixed $query * @return $this */ - public function addQuery($query) + public function addQuery($query): static { $this->_queries[] = $query; return $this; @@ -157,7 +147,7 @@ public function addQuery($query) * @param mixed $default * @return $this */ - public function when($condition, $query, $default = null) + public function when($condition, $query, $default = null): static { $truthy = is_callable($condition) ? $condition() : $condition; @@ -183,7 +173,7 @@ public function when($condition, $query, $default = null) * @return $this * @throws \BadMethodCallException if called with a string alias and no definition */ - public function aggs($alias, $aggs = null) + public function aggs($alias, $aggs = null): static { if ($aggs === null && !is_string($alias)) { $aggs = $alias; @@ -234,9 +224,9 @@ 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)) { @@ -303,7 +293,7 @@ private function buildQuery() * @param array $dsl * @return void */ - private function buildAggs(array &$dsl) + private function buildAggs(array &$dsl): void { if (empty($this->_aggregations)) { return; @@ -320,7 +310,7 @@ private function buildAggs(array &$dsl) * @param array $dsl * @return void */ - private function buildParams(array &$dsl) + private function buildParams(array &$dsl): void { foreach ($this->_params as $key => $value) { if ($value instanceof Query) { diff --git a/src/DSL/Support/ClausesSupport.php b/src/DSL/Support/ClausesSupport.php index a80534e..8a45877 100644 --- a/src/DSL/Support/ClausesSupport.php +++ b/src/DSL/Support/ClausesSupport.php @@ -1,5 +1,7 @@ Date: Tue, 16 Jun 2026 00:04:48 +0800 Subject: [PATCH 16/70] refactor(dsl)!: unify Bucket aggregation naming and simplify Node value access - rename FilterAgg/GlobalAgg/ParentAgg to Filter/Global_/Parent_ (drop the Agg suffix to align with ES keywords; global/parent are PHP reserved words, hence the _) - rename setFilter()/globalAggregation() to filter()/global() - remove the $_raw full pass-through mechanism from Node; rename $_rawValue to $_value and add a union type; the scalar construct branch no longer constrains _fieldKeyed - Query drops the $_raw branch accordingly; add rector.php with modernization rules Co-Authored-By: Claude --- src/DSL/Aggs/Bucket.php | 16 +++---- .../Aggs/Bucket/{FilterAgg.php => Filter.php} | 11 +++-- .../Bucket/{GlobalAgg.php => Global_.php} | 2 +- .../Bucket/{ParentAgg.php => Parent_.php} | 2 +- src/DSL/Node.php | 45 ++++++------------- src/DSL/Query.php | 2 - tests/AggsTest.php | 2 +- 7 files changed, 32 insertions(+), 48 deletions(-) rename src/DSL/Aggs/Bucket/{FilterAgg.php => Filter.php} (71%) rename src/DSL/Aggs/Bucket/{GlobalAgg.php => Global_.php} (93%) rename src/DSL/Aggs/Bucket/{ParentAgg.php => Parent_.php} (94%) diff --git a/src/DSL/Aggs/Bucket.php b/src/DSL/Aggs/Bucket.php index b9336c1..4d11bcb 100644 --- a/src/DSL/Aggs/Bucket.php +++ b/src/DSL/Aggs/Bucket.php @@ -11,9 +11,9 @@ use ElasticKit\DSL\Aggs\Bucket\DateHistogram; use ElasticKit\DSL\Aggs\Bucket\DateRange; use ElasticKit\DSL\Aggs\Bucket\DiversifiedSampler; -use ElasticKit\DSL\Aggs\Bucket\FilterAgg; +use ElasticKit\DSL\Aggs\Bucket\Filter; use ElasticKit\DSL\Aggs\Bucket\Filters; -use ElasticKit\DSL\Aggs\Bucket\GlobalAgg; +use ElasticKit\DSL\Aggs\Bucket\Global_; use ElasticKit\DSL\Aggs\Bucket\FrequentItemSets; use ElasticKit\DSL\Aggs\Bucket\GeoDistance; use ElasticKit\DSL\Aggs\Bucket\GeoHashGrid; @@ -25,7 +25,7 @@ use ElasticKit\DSL\Aggs\Bucket\Missing; use ElasticKit\DSL\Aggs\Bucket\MultiTerms; use ElasticKit\DSL\Aggs\Bucket\Nested; -use ElasticKit\DSL\Aggs\Bucket\ParentAgg; +use ElasticKit\DSL\Aggs\Bucket\Parent_; use ElasticKit\DSL\Aggs\Bucket\RandomSampler; use ElasticKit\DSL\Aggs\Bucket\Range; use ElasticKit\DSL\Aggs\Bucket\RareTerms; @@ -57,8 +57,8 @@ public function terms($params) */ public function filter($filter) { - $instance = new FilterAgg(); - $instance->setFilter($filter); + $instance = new Filter(); + $instance->filter($filter); return $this->node($instance); } @@ -210,9 +210,9 @@ public function geotileGrid($params) * * @return static */ - public function globalAggregation() + public function global() { - return $this->node(new GlobalAgg()); + return $this->node(new Global_()); } /** @@ -289,7 +289,7 @@ public function nested($params) */ public function parent($params) { - return $this->node(ParentAgg::create($params)); + return $this->node(Parent_::create($params)); } /** diff --git a/src/DSL/Aggs/Bucket/FilterAgg.php b/src/DSL/Aggs/Bucket/Filter.php similarity index 71% rename from src/DSL/Aggs/Bucket/FilterAgg.php rename to src/DSL/Aggs/Bucket/Filter.php index 630b2d8..5eb9abe 100644 --- a/src/DSL/Aggs/Bucket/FilterAgg.php +++ b/src/DSL/Aggs/Bucket/Filter.php @@ -10,19 +10,22 @@ /** * A single bucket aggregation that limits documents matching a query. */ -class FilterAgg extends Node +class Filter extends Node { protected string $_key = 'filter'; + /** @var mixed */ + protected $_filter; + /** * The filter query to apply. * * @param mixed $filter * @return static */ - public function setFilter($filter): static + public function filter($filter): static { - $this->_raw = $filter; + $this->_filter = $filter; return $this; } @@ -33,6 +36,6 @@ public function setFilter($filter): static */ public function toArray() { - return Query::create($this->_raw)->toArray()['query']; + return Query::create($this->_filter)->toArray()['query']; } } diff --git a/src/DSL/Aggs/Bucket/GlobalAgg.php b/src/DSL/Aggs/Bucket/Global_.php similarity index 93% rename from src/DSL/Aggs/Bucket/GlobalAgg.php rename to src/DSL/Aggs/Bucket/Global_.php index 7d581d2..8efd193 100644 --- a/src/DSL/Aggs/Bucket/GlobalAgg.php +++ b/src/DSL/Aggs/Bucket/Global_.php @@ -10,7 +10,7 @@ /** * A single bucket aggregation that defines all documents within the search context. */ -class GlobalAgg extends Node +class Global_ extends Node { protected string $_key = 'global'; diff --git a/src/DSL/Aggs/Bucket/ParentAgg.php b/src/DSL/Aggs/Bucket/Parent_.php similarity index 94% rename from src/DSL/Aggs/Bucket/ParentAgg.php rename to src/DSL/Aggs/Bucket/Parent_.php index c3a0cfd..9de4c8d 100644 --- a/src/DSL/Aggs/Bucket/ParentAgg.php +++ b/src/DSL/Aggs/Bucket/Parent_.php @@ -9,7 +9,7 @@ /** * A bucket aggregation that aggregates on parent documents from a join field. */ -class ParentAgg extends Node +class Parent_ extends Node { protected string $_key = 'parent'; diff --git a/src/DSL/Node.php b/src/DSL/Node.php index 0df9aa1..5aa823f 100644 --- a/src/DSL/Node.php +++ b/src/DSL/Node.php @@ -16,33 +16,23 @@ abstract class Node /** * Properties owned by a node. Either an array of attributes, or null when * the node carries no properties (empty construction / empty closure, - * which serializes to null). Whole-value pass-through uses $_raw; - * field-value shorthand uses $_rawValue. + * which serializes to null). Field-value shorthand uses $_value. * * @var array|null */ protected ?array $_properties = null; /** - * Raw whole-value pass-through. When set, toArray() emits this value - * verbatim — for nodes constructed with a single non-array, non-closure - * argument (a wrapped Node, a FilterAgg filter query, etc.). - * - * @var mixed - */ - protected $_raw; - - /** - * 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 @@ -61,7 +51,7 @@ abstract class Node * * @var string */ - protected $_field; + protected string $_field; /** * Whether the node supports multiple clauses. @@ -86,9 +76,10 @@ 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) @@ -99,12 +90,10 @@ public function __construct($field = null, $value = null) $this->fromClosure($field); } elseif ($this->_fieldKeyed && is_array($field)) { $this->fromArrayField($field); - } elseif ($this->_fieldKeyed && is_scalar($field)) { + } elseif (is_scalar($field)) { $this->fromScalar($field); } elseif (is_array($field)) { $this->_properties = $field; - } elseif ($field !== null) { - $this->_raw = $field; } } @@ -119,12 +108,10 @@ protected function fromKeyValue($field, $value): void if ($value instanceof Closure) { $value($this); } elseif (is_scalar($value)) { - $this->_rawValue = $value; + $this->_value = $value; $this->_properties = []; } elseif (is_array($value)) { $this->_properties = $value; - } else { - $this->_raw = $value; } if ($this->_fieldKeyed) { $this->field($field); @@ -151,12 +138,10 @@ protected function fromArrayField(array $field): void foreach ($field as $key => $val) { $this->field($key); if (is_scalar($val)) { - $this->_rawValue = $val; + $this->_value = $val; $this->_properties = []; } elseif (is_array($val)) { $this->_properties = $val; - } else { - $this->_raw = $val; } break; } @@ -169,7 +154,7 @@ protected function fromArrayField(array $field): void */ protected function fromScalar($value): void { - $this->_rawValue = $value; + $this->_value = $value; $this->_properties = []; } @@ -301,16 +286,14 @@ protected function resolveProperties(array $properties): array */ public function toArray() { - if ($this->_raw !== null) { - $properties = $this->_raw; - } elseif ($this->_rawValue !== null) { + if ($this->_value !== null) { $props = $this->_properties ?? []; if ($props === []) { - $properties = $this->_rawValue; + $properties = $this->_value; } else { $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 { diff --git a/src/DSL/Query.php b/src/DSL/Query.php index 57cff33..8ce35cd 100644 --- a/src/DSL/Query.php +++ b/src/DSL/Query.php @@ -95,8 +95,6 @@ public function __construct($field = null, $value = null) $this->_queries[] = $field; } elseif (is_array($field)) { $this->fromArray($field); - } elseif ($field !== null) { - $this->_raw = $field; } } diff --git a/tests/AggsTest.php b/tests/AggsTest.php index 2b39e8d..6de118f 100644 --- a/tests/AggsTest.php +++ b/tests/AggsTest.php @@ -745,7 +745,7 @@ public function testGlobalAggregation() $query = new Query(); $query->matchAll(); $query->aggs('all', function ($a) { - $a->globalAggregation(); + $a->global(); }); $this->assertQuery($expectedJson, $query); } From 440729f36125976cee96a099d985922a267974a3 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Fri, 19 Jun 2026 01:31:59 +0800 Subject: [PATCH 17/70] refactor(dsl): unify leaf setter parameter name to $value Single-argument public methods in all non-trait classes now use $value uniformly, avoiding named-argument BC risk. Multi-argument methods and generic trait methods are unaffected. - bulk-rename ~350 method parameters (docblocks and bodies kept in sync) - remove the duplicate Knn::boost() - fix inconsistent Suggest docblock types (string|null -> ?string) - fix the PHP_CS_FIXER_IGNORE_ENV deprecation warning --- .php-cs-fixer.php | 3 +- composer.json | 4 +- src/DSL/Agg.php | 10 +- src/DSL/Aggs/Bucket.php | 62 ++++---- src/DSL/Aggs/Bucket/AdjacencyMatrix.php | 6 +- src/DSL/Aggs/Bucket/AutoDateHistogram.php | 30 ++-- src/DSL/Aggs/Bucket/CategorizeText.php | 54 +++---- src/DSL/Aggs/Bucket/Composite.php | 24 +-- src/DSL/Aggs/Bucket/DateHistogram.php | 72 ++++----- src/DSL/Aggs/Bucket/DateRange.php | 30 ++-- src/DSL/Aggs/Bucket/DiversifiedSampler.php | 18 +-- src/DSL/Aggs/Bucket/Filter.php | 6 +- src/DSL/Aggs/Bucket/Filters.php | 18 +-- src/DSL/Aggs/Bucket/FrequentItemSets.php | 24 +-- src/DSL/Aggs/Bucket/GeoDistance.php | 30 ++-- src/DSL/Aggs/Bucket/GeoHashGrid.php | 18 +-- src/DSL/Aggs/Bucket/GeohexGrid.php | 18 +-- src/DSL/Aggs/Bucket/GeotileGrid.php | 18 +-- src/DSL/Aggs/Bucket/Histogram.php | 60 ++++---- src/DSL/Aggs/Bucket/IpPrefix.php | 12 +- src/DSL/Aggs/Bucket/IpRange.php | 18 +-- src/DSL/Aggs/Bucket/Missing.php | 6 +- src/DSL/Aggs/Bucket/MultiTerms.php | 42 +++--- src/DSL/Aggs/Bucket/Nested.php | 12 +- src/DSL/Aggs/Bucket/Parent_.php | 6 +- src/DSL/Aggs/Bucket/RandomSampler.php | 12 +- src/DSL/Aggs/Bucket/Range.php | 24 +-- src/DSL/Aggs/Bucket/RareTerms.php | 36 ++--- src/DSL/Aggs/Bucket/ReverseNested.php | 6 +- src/DSL/Aggs/Bucket/SignificantTerms.php | 48 +++--- src/DSL/Aggs/Bucket/SignificantText.php | 48 +++--- src/DSL/Aggs/Bucket/Terms.php | 66 ++++----- src/DSL/Aggs/Bucket/TimeSeries.php | 18 +-- .../Aggs/Bucket/VariableWidthHistogram.php | 12 +- src/DSL/Aggs/Metric.php | 16 +- src/DSL/Aggs/Metric/Avg.php | 12 +- src/DSL/Aggs/Metric/Cardinality.php | 18 +-- src/DSL/Aggs/Metric/ExtendedStats.php | 18 +-- src/DSL/Aggs/Metric/Max.php | 12 +- src/DSL/Aggs/Metric/Min.php | 12 +- src/DSL/Aggs/Metric/Stats.php | 12 +- src/DSL/Aggs/Metric/Sum.php | 12 +- src/DSL/Aggs/Metric/ValueCount.php | 6 +- src/DSL/Aggs/Pipeline.php | 16 +- src/DSL/Aggs/Pipeline/AvgBucket.php | 24 +-- src/DSL/Aggs/Pipeline/BucketScript.php | 24 +-- src/DSL/Aggs/Pipeline/CumulativeSum.php | 12 +- src/DSL/Aggs/Pipeline/Derivative.php | 24 +-- src/DSL/Aggs/Pipeline/MaxBucket.php | 24 +-- src/DSL/Aggs/Pipeline/MinBucket.php | 24 +-- src/DSL/Aggs/Pipeline/StatsBucket.php | 24 +-- src/DSL/Aggs/Pipeline/SumBucket.php | 24 +-- src/DSL/Node.php | 16 +- src/DSL/Param.php | 58 ++++---- src/DSL/Params/Collapse.php | 6 +- src/DSL/Params/Highlight.php | 78 +++++----- src/DSL/Params/Knn.php | 53 +++---- src/DSL/Params/Rescore.php | 30 ++-- src/DSL/Params/Suggest.php | 6 +- src/DSL/Queries/Compound.php | 10 +- src/DSL/Queries/Compound/Boolean.php | 30 ++-- src/DSL/Queries/Compound/Boosting.php | 18 +-- src/DSL/Queries/Compound/ConstantScore.php | 6 +- src/DSL/Queries/Compound/DisjunctionMax.php | 12 +- src/DSL/Queries/Compound/FunctionScore.php | 42 +++--- src/DSL/Queries/Compound/Functions/Exp.php | 24 +-- .../Compound/Functions/FieldValueFactor.php | 18 +-- .../Queries/Compound/Functions/Function_.php | 32 ++-- src/DSL/Queries/Compound/Functions/Gauss.php | 24 +-- src/DSL/Queries/Compound/Functions/Linear.php | 24 +-- .../Compound/Functions/RandomScore.php | 6 +- .../Compound/Functions/ScriptScore.php | 6 +- src/DSL/Queries/FullText.php | 18 +-- src/DSL/Queries/FullText/CombinedFields.php | 36 ++--- src/DSL/Queries/FullText/Intervals.php | 38 ++--- src/DSL/Queries/FullText/Intervals/AllOf.php | 38 ++--- src/DSL/Queries/FullText/Intervals/AnyOf.php | 26 ++-- src/DSL/Queries/FullText/Intervals/Filter.php | 54 +++---- src/DSL/Queries/FullText/Intervals/Fuzzy.php | 36 ++--- src/DSL/Queries/FullText/Intervals/Match_.php | 36 ++--- src/DSL/Queries/FullText/Intervals/Prefix.php | 18 +-- src/DSL/Queries/FullText/Intervals/Range.php | 36 ++--- .../Queries/FullText/Intervals/Wildcard.php | 18 +-- src/DSL/Queries/FullText/MatchBoolPrefix.php | 60 ++++---- src/DSL/Queries/FullText/MatchPhrase.php | 24 +-- .../Queries/FullText/MatchPhrasePrefix.php | 30 ++-- src/DSL/Queries/FullText/Match_.php | 72 ++++----- src/DSL/Queries/FullText/MultiMatch.php | 96 ++++++------ src/DSL/Queries/FullText/QueryString.php | 138 +++++++++--------- .../Queries/FullText/SimpleQueryString.php | 78 +++++----- src/DSL/Queries/Geo.php | 10 +- src/DSL/Queries/Geo/GeoBoundingBox.php | 66 ++++----- src/DSL/Queries/Geo/GeoDistance.php | 26 ++-- src/DSL/Queries/Geo/GeoGrid.php | 18 +-- src/DSL/Queries/Geo/GeoPolygon.php | 18 +-- src/DSL/Queries/Geo/GeoShape.php | 24 +-- src/DSL/Queries/Joining.php | 8 +- src/DSL/Queries/Joining/HasChild.php | 36 ++--- src/DSL/Queries/Joining/HasParent.php | 24 +-- src/DSL/Queries/Joining/Nested.php | 24 +-- src/DSL/Queries/Joining/ParentId.php | 18 +-- src/DSL/Queries/MatchAll.php | 4 +- src/DSL/Queries/Script.php | 24 +-- src/DSL/Queries/Shape.php | 2 +- src/DSL/Queries/Shape/Shape.php | 18 +-- src/DSL/Queries/Span.php | 18 +-- src/DSL/Queries/Span/SpanContaining.php | 12 +- src/DSL/Queries/Span/SpanFieldMasking.php | 6 +- src/DSL/Queries/Span/SpanFirst.php | 12 +- src/DSL/Queries/Span/SpanMulti.php | 6 +- src/DSL/Queries/Span/SpanNear.php | 18 +-- src/DSL/Queries/Span/SpanNot.php | 30 ++-- src/DSL/Queries/Span/SpanOr.php | 6 +- src/DSL/Queries/Span/SpanTerm.php | 6 +- src/DSL/Queries/Span/SpanWithin.php | 12 +- src/DSL/Queries/Specialized.php | 16 +- .../Queries/Specialized/DistanceFeature.php | 12 +- src/DSL/Queries/Specialized/MoreLikeThis.php | 24 +-- src/DSL/Queries/Specialized/Percolate.php | 6 +- src/DSL/Queries/Specialized/Pinned.php | 18 +-- src/DSL/Queries/Specialized/RankFeature.php | 24 +-- src/DSL/Queries/Specialized/Script.php | 6 +- src/DSL/Queries/Specialized/ScriptScore.php | 18 +-- src/DSL/Queries/Specialized/Wrapper.php | 6 +- src/DSL/Queries/TermLevel.php | 20 +-- src/DSL/Queries/TermLevel/Fuzzy.php | 30 ++-- src/DSL/Queries/TermLevel/IDs.php | 6 +- src/DSL/Queries/TermLevel/Prefix.php | 12 +- src/DSL/Queries/TermLevel/Range.php | 42 +++--- src/DSL/Queries/TermLevel/Regexp.php | 24 +-- src/DSL/Queries/TermLevel/Term.php | 6 +- src/DSL/Queries/TermLevel/TermsSet.php | 24 +-- src/DSL/Queries/TermLevel/Wildcard.php | 18 +-- src/DSL/Query.php | 42 +++--- src/DSL/Support/ClausesSupport.php | 2 +- src/DSL/Support/RangeSupport.php | 2 +- 136 files changed, 1655 insertions(+), 1685 deletions(-) diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 47a69ba..6bc47da 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -8,4 +8,5 @@ '@PSR12' => true, 'no_unused_imports' => true, ]) - ->setFinder($finder); + ->setFinder($finder) + ->setUnsupportedPhpVersionAllowed(true); diff --git a/composer.json b/composer.json index 058f8b6..ec1cc09 100644 --- a/composer.json +++ b/composer.json @@ -33,8 +33,8 @@ ], "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" + "cs-check": "php-cs-fixer fix --dry-run --diff", + "cs-fix": "php-cs-fixer fix" }, "config": { "audit": { diff --git a/src/DSL/Agg.php b/src/DSL/Agg.php index 79a2aaa..7e41b01 100644 --- a/src/DSL/Agg.php +++ b/src/DSL/Agg.php @@ -94,12 +94,12 @@ protected function node($node): static * 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): static + public function alias($value): static { - $this->_alias = $alias; + $this->_alias = $value; return $this; } @@ -108,7 +108,7 @@ public function alias($alias): static * * @return string|null */ - public function getAlias() + public function getAlias(): ?string { return $this->_alias; } @@ -175,7 +175,7 @@ public function aggs($alias, $aggs = null): static * @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) { diff --git a/src/DSL/Aggs/Bucket.php b/src/DSL/Aggs/Bucket.php index 4d11bcb..99fdb00 100644 --- a/src/DSL/Aggs/Bucket.php +++ b/src/DSL/Aggs/Bucket.php @@ -44,7 +44,7 @@ trait Bucket * @param mixed $params * @return static */ - public function terms($params) + public function terms($params): static { return $this->node(Terms::create(is_string($params) ? ['field' => $params] : $params)); } @@ -55,7 +55,7 @@ public function terms($params) * @param mixed $filter * @return static */ - public function filter($filter) + public function filter($filter): static { $instance = new Filter(); $instance->filter($filter); @@ -68,7 +68,7 @@ public function filter($filter) * @param mixed $params * @return static */ - public function filters($params) + public function filters($params): static { return $this->node(Filters::create($params)); } @@ -79,7 +79,7 @@ public function filters($params) * @param mixed $params * @return static */ - public function adjacencyMatrix($params) + public function adjacencyMatrix($params): static { return $this->node(AdjacencyMatrix::create($params)); } @@ -90,7 +90,7 @@ public function adjacencyMatrix($params) * @param mixed $params * @return static */ - public function autoDateHistogram($params) + public function autoDateHistogram($params): static { return $this->node(AutoDateHistogram::create(is_string($params) ? ['field' => $params] : $params)); } @@ -101,7 +101,7 @@ public function autoDateHistogram($params) * @param mixed $params * @return static */ - public function categorizeText($params) + public function categorizeText($params): static { return $this->node(CategorizeText::create(is_string($params) ? ['field' => $params] : $params)); } @@ -112,7 +112,7 @@ public function categorizeText($params) * @param mixed $params * @return static */ - public function composite($params) + public function composite($params): static { return $this->node(Composite::create($params)); } @@ -123,7 +123,7 @@ public function composite($params) * @param mixed $params * @return static */ - public function dateHistogram($params) + public function dateHistogram($params): static { return $this->node(DateHistogram::create(is_string($params) ? ['field' => $params] : $params)); } @@ -134,7 +134,7 @@ public function dateHistogram($params) * @param mixed $params * @return static */ - public function dateRange($params) + public function dateRange($params): static { return $this->node(DateRange::create(is_string($params) ? ['field' => $params] : $params)); } @@ -145,7 +145,7 @@ public function dateRange($params) * @param mixed $params * @return static */ - public function diversifiedSampler($params) + public function diversifiedSampler($params): static { return $this->node(DiversifiedSampler::create($params)); } @@ -156,7 +156,7 @@ public function diversifiedSampler($params) * @param mixed $params * @return static */ - public function frequentItemSets($params) + public function frequentItemSets($params): static { return $this->node(FrequentItemSets::create($params)); } @@ -167,7 +167,7 @@ public function frequentItemSets($params) * @param mixed $params * @return static */ - public function geoDistance($params) + public function geoDistance($params): static { return $this->node(GeoDistance::create(is_string($params) ? ['field' => $params] : $params)); } @@ -178,7 +178,7 @@ public function geoDistance($params) * @param mixed $params * @return static */ - public function geoHashGrid($params) + public function geoHashGrid($params): static { return $this->node(GeoHashGrid::create(is_string($params) ? ['field' => $params] : $params)); } @@ -189,7 +189,7 @@ public function geoHashGrid($params) * @param mixed $params * @return static */ - public function geohexGrid($params) + public function geohexGrid($params): static { return $this->node(GeohexGrid::create(is_string($params) ? ['field' => $params] : $params)); } @@ -200,7 +200,7 @@ public function geohexGrid($params) * @param mixed $params * @return static */ - public function geotileGrid($params) + public function geotileGrid($params): static { return $this->node(GeotileGrid::create(is_string($params) ? ['field' => $params] : $params)); } @@ -210,7 +210,7 @@ public function geotileGrid($params) * * @return static */ - public function global() + public function global(): static { return $this->node(new Global_()); } @@ -221,7 +221,7 @@ public function global() * @param mixed $params * @return static */ - public function histogram($params) + public function histogram($params): static { return $this->node(Histogram::create(is_string($params) ? ['field' => $params] : $params)); } @@ -232,7 +232,7 @@ public function histogram($params) * @param mixed $params * @return static */ - public function ipPrefix($params) + public function ipPrefix($params): static { return $this->node(IpPrefix::create(is_string($params) ? ['field' => $params] : $params)); } @@ -243,7 +243,7 @@ public function ipPrefix($params) * @param mixed $params * @return static */ - public function ipRange($params) + public function ipRange($params): static { return $this->node(IpRange::create($params)); } @@ -254,7 +254,7 @@ public function ipRange($params) * @param mixed $params * @return static */ - public function missing($params) + public function missing($params): static { return $this->node(Missing::create(is_string($params) ? ['field' => $params] : $params)); } @@ -265,7 +265,7 @@ public function missing($params) * @param mixed $params * @return static */ - public function multiTerms($params) + public function multiTerms($params): static { return $this->node(MultiTerms::create($params)); } @@ -276,7 +276,7 @@ public function multiTerms($params) * @param mixed $params * @return static */ - public function nested($params) + public function nested($params): static { return $this->node(Nested::create($params)); } @@ -287,7 +287,7 @@ public function nested($params) * @param mixed $params * @return static */ - public function parent($params) + public function parent($params): static { return $this->node(Parent_::create($params)); } @@ -298,7 +298,7 @@ public function parent($params) * @param mixed $params * @return static */ - public function randomSampler($params) + public function randomSampler($params): static { return $this->node(RandomSampler::create($params)); } @@ -309,7 +309,7 @@ public function randomSampler($params) * @param mixed $params * @return static */ - public function range($params) + public function range($params): static { return $this->node(Range::create(is_string($params) ? ['field' => $params] : $params)); } @@ -320,7 +320,7 @@ public function range($params) * @param mixed $params * @return static */ - public function rareTerms($params) + public function rareTerms($params): static { return $this->node(RareTerms::create(is_string($params) ? ['field' => $params] : $params)); } @@ -331,7 +331,7 @@ public function rareTerms($params) * @param mixed $params * @return static */ - public function reverseNested($params = []) + public function reverseNested($params = []): static { return $this->node(ReverseNested::create($params)); } @@ -342,7 +342,7 @@ public function reverseNested($params = []) * @param mixed $params * @return static */ - public function significantTerms($params) + public function significantTerms($params): static { return $this->node(SignificantTerms::create(is_string($params) ? ['field' => $params] : $params)); } @@ -353,7 +353,7 @@ public function significantTerms($params) * @param mixed $params * @return static */ - public function significantText($params) + public function significantText($params): static { return $this->node(SignificantText::create(is_string($params) ? ['field' => $params] : $params)); } @@ -364,7 +364,7 @@ public function significantText($params) * @param mixed $params * @return static */ - public function timeSeries($params) + public function timeSeries($params): static { return $this->node(TimeSeries::create($params)); } @@ -375,7 +375,7 @@ public function timeSeries($params) * @param mixed $params * @return static */ - public function variableWidthHistogram($params) + public function variableWidthHistogram($params): static { return $this->node(VariableWidthHistogram::create(is_string($params) ? ['field' => $params] : $params)); } diff --git a/src/DSL/Aggs/Bucket/AdjacencyMatrix.php b/src/DSL/Aggs/Bucket/AdjacencyMatrix.php index 36eb9a2..7bd2425 100644 --- a/src/DSL/Aggs/Bucket/AdjacencyMatrix.php +++ b/src/DSL/Aggs/Bucket/AdjacencyMatrix.php @@ -31,11 +31,11 @@ public function filters($key, $query): static /** * Separator used to concatenate filter names. Defaults to &. * - * @param string $separator + * @param string $value * @return static */ - public function separator(string $separator): static + public function separator(string $value): static { - return $this->addProperty('separator', $separator); + return $this->addProperty('separator', $value); } } diff --git a/src/DSL/Aggs/Bucket/AutoDateHistogram.php b/src/DSL/Aggs/Bucket/AutoDateHistogram.php index aa9fa77..518ec28 100644 --- a/src/DSL/Aggs/Bucket/AutoDateHistogram.php +++ b/src/DSL/Aggs/Bucket/AutoDateHistogram.php @@ -16,55 +16,55 @@ class AutoDateHistogram extends Node /** * Target number of buckets to return. * - * @param int $buckets + * @param int $value * @return static */ - public function buckets(int $buckets): static + public function buckets(int $value): static { - return $this->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(string $format): static + 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(string $timeZone): static + 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(string $minimumInterval): static + 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): static + 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 09f74e8..91bcdb5 100644 --- a/src/DSL/Aggs/Bucket/CategorizeText.php +++ b/src/DSL/Aggs/Bucket/CategorizeText.php @@ -16,99 +16,99 @@ class CategorizeText extends Node /** * Analyzer used for categorization. * - * @param mixed $categorizationAnalyzer + * @param mixed $value * @return static */ - public function categorizationAnalyzer($categorizationAnalyzer): static + public function categorizationAnalyzer($value): static { - return $this->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(array $categorizationFilters): static + 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(int $maxMatchedTokens): static + 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(int $maxUniqueTokens): static + 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(int $minDocCount): static + 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(int $shardMinDocCount): static + 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(int $shardSize): static + 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(float $similarityThreshold): static + 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(int $size): static + 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 c3a57ae..8614888 100644 --- a/src/DSL/Aggs/Bucket/Composite.php +++ b/src/DSL/Aggs/Bucket/Composite.php @@ -16,44 +16,44 @@ class Composite extends Node /** * List of source definitions used to build composite buckets. * - * @param mixed $sources + * @param mixed $value * @return static */ - public function sources($sources): static + public function sources($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): static + 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): static + 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(int $size): static + public function size(int $value): static { - return $this->addProperty('size', $size); + return $this->addProperty('size', $value); } } diff --git a/src/DSL/Aggs/Bucket/DateHistogram.php b/src/DSL/Aggs/Bucket/DateHistogram.php index e95c988..82110b2 100644 --- a/src/DSL/Aggs/Bucket/DateHistogram.php +++ b/src/DSL/Aggs/Bucket/DateHistogram.php @@ -16,132 +16,132 @@ class DateHistogram extends Node /** * Calendar-aware interval for bucketing (e.g. month, week). * - * @param string $calendarInterval + * @param string $value * @return static */ - public function calendarInterval(string $calendarInterval): static + public function calendarInterval(string $value): static { - return $this->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 + * @param string $value * @return static */ - public function interval(string $interval): static + 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(string $fixedInterval): static + 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(string $format): static + 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(string $timeZone): static + 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(int $minDocCount): static + 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): static + 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): static + 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): static + 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(bool $keyed): static + 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): static + 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(string $offset): static + 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 cd19739..e62b25d 100644 --- a/src/DSL/Aggs/Bucket/DateRange.php +++ b/src/DSL/Aggs/Bucket/DateRange.php @@ -16,55 +16,55 @@ class DateRange extends Node /** * Array of range definitions for bucketing. * - * @param array $ranges + * @param array $value * @return static */ - public function ranges(array $ranges): static + 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(bool $keyed): static + 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(string $format): static + 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): static + 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(string $timeZone): static + 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 d50f4b7..938127a 100644 --- a/src/DSL/Aggs/Bucket/DiversifiedSampler.php +++ b/src/DSL/Aggs/Bucket/DiversifiedSampler.php @@ -16,33 +16,33 @@ class DiversifiedSampler extends Node /** * Number of documents to sample per shard. * - * @param int $shardSize + * @param int $value * @return static */ - public function shardSize(int $shardSize): static + public function shardSize(int $value): static { - return $this->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(int $maxDocsPerValue): static + 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(string $executionHint): static + 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/Filter.php b/src/DSL/Aggs/Bucket/Filter.php index 5eb9abe..0adf209 100644 --- a/src/DSL/Aggs/Bucket/Filter.php +++ b/src/DSL/Aggs/Bucket/Filter.php @@ -20,12 +20,12 @@ class Filter extends Node /** * The filter query to apply. * - * @param mixed $filter + * @param mixed $value * @return static */ - public function filter($filter): static + public function filter($value): static { - $this->_filter = $filter; + $this->_filter = $value; return $this; } diff --git a/src/DSL/Aggs/Bucket/Filters.php b/src/DSL/Aggs/Bucket/Filters.php index 33282d8..0e42e6e 100644 --- a/src/DSL/Aggs/Bucket/Filters.php +++ b/src/DSL/Aggs/Bucket/Filters.php @@ -16,33 +16,33 @@ class Filters extends Node /** * Filter queries used to create buckets. * - * @param mixed $filters + * @param mixed $value * @return static */ - public function filters($filters): static + public function filters($value): static { - return $this->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(string $otherBucketKey): static + 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(bool $keyed): static + 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 7fd11af..7a66431 100644 --- a/src/DSL/Aggs/Bucket/FrequentItemSets.php +++ b/src/DSL/Aggs/Bucket/FrequentItemSets.php @@ -16,44 +16,44 @@ class FrequentItemSets extends Node /** * Minimum size of an item set. * - * @param int $minimumSetSize + * @param int $value * @return static */ - public function minimumSetSize(int $minimumSetSize): static + public function minimumSetSize(int $value): static { - return $this->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(array $fields): static + 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(int $size): static + 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): static + 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 8e05b02..77411db 100644 --- a/src/DSL/Aggs/Bucket/GeoDistance.php +++ b/src/DSL/Aggs/Bucket/GeoDistance.php @@ -16,55 +16,55 @@ class GeoDistance extends Node /** * The central geo point from which distances are measured. * - * @param mixed $origin + * @param mixed $value * @return static */ - public function origin($origin): static + 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(string $unit): static + 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(string $distanceType): static + 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(array $ranges): static + 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(bool $keyed): static + 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 15b75b6..9e46e3b 100644 --- a/src/DSL/Aggs/Bucket/GeoHashGrid.php +++ b/src/DSL/Aggs/Bucket/GeoHashGrid.php @@ -16,33 +16,33 @@ class GeoHashGrid extends Node /** * Geohash precision (length) for grid cells. * - * @param int $precision + * @param int $value * @return static */ - public function precision(int $precision): static + 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(int $size): static + 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(int $shardSize): static + 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 d8ad854..5f93015 100644 --- a/src/DSL/Aggs/Bucket/GeohexGrid.php +++ b/src/DSL/Aggs/Bucket/GeohexGrid.php @@ -16,33 +16,33 @@ class GeohexGrid extends Node /** * H3 resolution for grid cells. * - * @param int $precision + * @param int $value * @return static */ - public function precision(int $precision): static + 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(int $size): static + 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(int $shardSize): static + 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 2e95403..aa61d5a 100644 --- a/src/DSL/Aggs/Bucket/GeotileGrid.php +++ b/src/DSL/Aggs/Bucket/GeotileGrid.php @@ -16,33 +16,33 @@ class GeotileGrid extends Node /** * Zoom level (precision) for geotile grid cells. * - * @param int $precision + * @param int $value * @return static */ - public function precision(int $precision): static + 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(int $size): static + 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(int $shardSize): static + 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/Histogram.php b/src/DSL/Aggs/Bucket/Histogram.php index 196eb5a..c721b42 100644 --- a/src/DSL/Aggs/Bucket/Histogram.php +++ b/src/DSL/Aggs/Bucket/Histogram.php @@ -16,110 +16,110 @@ class Histogram extends Node /** * Interval size for each bucket. * - * @param float $interval + * @param float $value * @return static */ - public function interval(float $interval): static + 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(int $minDocCount): static + 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): static + 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): static + 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(bool $keyed): static + 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(float $missing): static + 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(string $format): static + 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): static + 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(float $offset): static + 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): static + 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 b59655b..433ad0c 100644 --- a/src/DSL/Aggs/Bucket/IpPrefix.php +++ b/src/DSL/Aggs/Bucket/IpPrefix.php @@ -13,20 +13,20 @@ class IpPrefix extends Node /** * Length of the network prefix. * - * @param int $length + * @param int $value * @return static */ - public function prefixLength(int $length): static + 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(int $length): static + 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 3ca179b..51f1cb3 100644 --- a/src/DSL/Aggs/Bucket/IpRange.php +++ b/src/DSL/Aggs/Bucket/IpRange.php @@ -16,33 +16,33 @@ class IpRange extends Node /** * Array of IP range definitions for bucketing. * - * @param array $ranges + * @param array $value * @return static */ - public function ranges(array $ranges): static + 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(bool $keyed): static + 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): static + 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 5da5b46..c445f03 100644 --- a/src/DSL/Aggs/Bucket/Missing.php +++ b/src/DSL/Aggs/Bucket/Missing.php @@ -16,11 +16,11 @@ class Missing extends Node /** * Value to treat as missing for the field. * - * @param mixed $missing + * @param mixed $value * @return static */ - public function missing($missing): static + 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 15d73bb..98620b4 100644 --- a/src/DSL/Aggs/Bucket/MultiTerms.php +++ b/src/DSL/Aggs/Bucket/MultiTerms.php @@ -11,65 +11,65 @@ class MultiTerms extends Node protected string $_key = 'multi_terms'; /** - * @param mixed $terms + * @param mixed $value * @return static */ - public function terms($terms): static + public function terms($value): static { - return $this->addProperty('terms', $terms, true); + return $this->addProperty('terms', $value, true); } /** - * @param mixed $order + * @param mixed $value * @return static */ - public function order($order): static + 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(int $size): static + 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(int $shardSize): static + 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(int $minDocCount): static + 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(int $shardMinDocCount): static + 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(string $collectMode): static + 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 2c69bad..5f185fc 100644 --- a/src/DSL/Aggs/Bucket/Nested.php +++ b/src/DSL/Aggs/Bucket/Nested.php @@ -16,22 +16,22 @@ class Nested extends Node /** * Path to the nested object to aggregate on. * - * @param string $path + * @param string $value * @return static */ - public function path(string $path): static + public function path(string $value): static { - return $this->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(bool $ignoreUnmapped): static + 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/Parent_.php b/src/DSL/Aggs/Bucket/Parent_.php index 9de4c8d..82b4154 100644 --- a/src/DSL/Aggs/Bucket/Parent_.php +++ b/src/DSL/Aggs/Bucket/Parent_.php @@ -16,11 +16,11 @@ class Parent_ extends Node /** * The child type that identifies the parent documents to aggregate on. * - * @param string $type + * @param string $value * @return static */ - public function type(string $type): static + public function type(string $value): static { - return $this->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 03a93f6..3e5217d 100644 --- a/src/DSL/Aggs/Bucket/RandomSampler.php +++ b/src/DSL/Aggs/Bucket/RandomSampler.php @@ -16,22 +16,22 @@ class RandomSampler extends Node /** * Probability that a document is included in the sample (between 0 and 1). * - * @param float $probability + * @param float $value * @return static */ - public function probability(float $probability): static + public function probability(float $value): static { - return $this->addProperty('probability', $probability); + return $this->addProperty('probability', $value); } /** * Seed for the random number generator to produce repeatable samples. * - * @param int $seed + * @param int $value * @return static */ - public function seed(int $seed): static + public function seed(int $value): static { - return $this->addProperty('seed', $seed); + return $this->addProperty('seed', $value); } } diff --git a/src/DSL/Aggs/Bucket/Range.php b/src/DSL/Aggs/Bucket/Range.php index 24b2acf..e87eaae 100644 --- a/src/DSL/Aggs/Bucket/Range.php +++ b/src/DSL/Aggs/Bucket/Range.php @@ -16,44 +16,44 @@ class Range extends Node /** * Array of range definitions for bucketing. * - * @param array $ranges + * @param array $value * @return static */ - public function ranges(array $ranges): static + 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(bool $keyed): static + 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): static + 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): static + 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 f89e835..ad0ae34 100644 --- a/src/DSL/Aggs/Bucket/RareTerms.php +++ b/src/DSL/Aggs/Bucket/RareTerms.php @@ -11,56 +11,56 @@ class RareTerms extends Node protected string $_key = 'rare_terms'; /** - * @param int $maxDocCount + * @param int $value * @return static */ - public function maxDocCount(int $maxDocCount): static + 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): static + 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): static + 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): static + 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): static + 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(int $shardSize): static + 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 b8c0292..39c8d64 100644 --- a/src/DSL/Aggs/Bucket/ReverseNested.php +++ b/src/DSL/Aggs/Bucket/ReverseNested.php @@ -27,11 +27,11 @@ public function toArray() /** * Path to the nested object to reverse out of. * - * @param string $path + * @param string $value * @return static */ - public function path(string $path): static + public function path(string $value): static { - return $this->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 e933b57..17fe658 100644 --- a/src/DSL/Aggs/Bucket/SignificantTerms.php +++ b/src/DSL/Aggs/Bucket/SignificantTerms.php @@ -16,88 +16,88 @@ class SignificantTerms extends Node /** * Maximum number of significant terms to return. * - * @param int $size + * @param int $value * @return static */ - public function size(int $size): static + 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(int $shardSize): static + 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(int $minDocCount): static + 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(int $shardMinDocCount): static + 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): static + 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): static + 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): static + 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(string $executionHint): static + 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 f7736f2..cd78ec1 100644 --- a/src/DSL/Aggs/Bucket/SignificantText.php +++ b/src/DSL/Aggs/Bucket/SignificantText.php @@ -16,88 +16,88 @@ class SignificantText extends Node /** * Maximum number of significant terms to return. * - * @param int $size + * @param int $value * @return static */ - public function size(int $size): static + 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(int $shardSize): static + 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(int $minDocCount): static + 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(int $shardMinDocCount): static + 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): static + 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): static + 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): static + 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(bool $filter): static + 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 76526fa..d7bf761 100644 --- a/src/DSL/Aggs/Bucket/Terms.php +++ b/src/DSL/Aggs/Bucket/Terms.php @@ -16,121 +16,121 @@ class Terms extends Node /** * Maximum number of term buckets to return. * - * @param int $size + * @param int $value * @return static */ - public function size(int $size): static + 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): static + 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(int $minDocCount): static + 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(int $shardSize): static + 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(bool $show): static + 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(int $shardMinDocCount): static + 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): static + 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(string $collectMode): static + 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): static + 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): static + 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(string $executionHint): static + 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 f170918..0d0b144 100644 --- a/src/DSL/Aggs/Bucket/TimeSeries.php +++ b/src/DSL/Aggs/Bucket/TimeSeries.php @@ -11,29 +11,29 @@ class TimeSeries extends Node protected string $_key = 'time_series'; /** - * @param string $calendarInterval + * @param string $value * @return static */ - public function calendarInterval(string $calendarInterval): static + 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(string $fixedInterval): static + 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): static + 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 8f987eb..df6091e 100644 --- a/src/DSL/Aggs/Bucket/VariableWidthHistogram.php +++ b/src/DSL/Aggs/Bucket/VariableWidthHistogram.php @@ -11,20 +11,20 @@ class VariableWidthHistogram extends Node protected string $_key = 'variable_width_histogram'; /** - * @param int $buckets + * @param int $value * @return static */ - public function buckets(int $buckets): static + 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(int $shardBuckets): static + 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 f7af4f0..e86c25f 100644 --- a/src/DSL/Aggs/Metric.php +++ b/src/DSL/Aggs/Metric.php @@ -21,7 +21,7 @@ trait Metric * @param mixed $params * @return static */ - public function avg($params) + public function avg($params): static { return $this->node(Avg::create(is_string($params) ? ['field' => $params] : $params)); } @@ -32,7 +32,7 @@ public function avg($params) * @param mixed $params * @return static */ - public function sum($params) + public function sum($params): static { return $this->node(Sum::create(is_string($params) ? ['field' => $params] : $params)); } @@ -43,7 +43,7 @@ public function sum($params) * @param mixed $params * @return static */ - public function min($params) + public function min($params): static { return $this->node(Min::create(is_string($params) ? ['field' => $params] : $params)); } @@ -54,7 +54,7 @@ public function min($params) * @param mixed $params * @return static */ - public function max($params) + public function max($params): static { return $this->node(Max::create(is_string($params) ? ['field' => $params] : $params)); } @@ -65,7 +65,7 @@ public function max($params) * @param mixed $params * @return static */ - public function cardinality($params) + public function cardinality($params): static { return $this->node(Cardinality::create(is_string($params) ? ['field' => $params] : $params)); } @@ -76,7 +76,7 @@ public function cardinality($params) * @param mixed $params * @return static */ - public function valueCount($params) + public function valueCount($params): static { return $this->node(ValueCount::create(is_string($params) ? ['field' => $params] : $params)); } @@ -87,7 +87,7 @@ public function valueCount($params) * @param mixed $params * @return static */ - public function stats($params) + public function stats($params): static { return $this->node(Stats::create(is_string($params) ? ['field' => $params] : $params)); } @@ -98,7 +98,7 @@ public function stats($params) * @param mixed $params * @return static */ - public function extendedStats($params) + public function extendedStats($params): static { return $this->node(ExtendedStats::create(is_string($params) ? ['field' => $params] : $params)); } diff --git a/src/DSL/Aggs/Metric/Avg.php b/src/DSL/Aggs/Metric/Avg.php index 485c956..9e1ae08 100644 --- a/src/DSL/Aggs/Metric/Avg.php +++ b/src/DSL/Aggs/Metric/Avg.php @@ -16,22 +16,22 @@ class Avg extends Node /** * The value to use when the field is missing. * - * @param mixed $missing + * @param mixed $value * @return static */ - public function missing($missing): static + public function missing($value): static { - return $this->addProperty('missing', $missing); + return $this->addProperty('missing', $value); } /** * The script to use for the aggregation. * - * @param string|callable $script + * @param string|callable $value * @return static */ - public function script($script): static + 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 dacea20..46d5337 100644 --- a/src/DSL/Aggs/Metric/Cardinality.php +++ b/src/DSL/Aggs/Metric/Cardinality.php @@ -16,33 +16,33 @@ class Cardinality extends Node /** * Controls the precision of the count. * - * @param int $threshold + * @param int $value * @return static */ - public function precisionThreshold(int $threshold): static + public function precisionThreshold(int $value): static { - return $this->addProperty('precision_threshold', $threshold); + return $this->addProperty('precision_threshold', $value); } /** * The value to use when the field is missing. * - * @param mixed $missing + * @param mixed $value * @return static */ - public function missing($missing): static + public function missing($value): static { - return $this->addProperty('missing', $missing); + return $this->addProperty('missing', $value); } /** * The script to use for the aggregation. * - * @param string|callable $script + * @param string|callable $value * @return static */ - public function script($script): static + 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 4a01725..57fb326 100644 --- a/src/DSL/Aggs/Metric/ExtendedStats.php +++ b/src/DSL/Aggs/Metric/ExtendedStats.php @@ -16,33 +16,33 @@ class ExtendedStats extends Node /** * The value to use when the field is missing. * - * @param mixed $missing + * @param mixed $value * @return static */ - public function missing($missing): static + public function missing($value): static { - return $this->addProperty('missing', $missing); + return $this->addProperty('missing', $value); } /** * The script to use for the aggregation. * - * @param string|callable $script + * @param string|callable $value * @return static */ - public function script($script): static + 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(int $sigma): static + 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 11775a9..4393603 100644 --- a/src/DSL/Aggs/Metric/Max.php +++ b/src/DSL/Aggs/Metric/Max.php @@ -16,22 +16,22 @@ class Max extends Node /** * The value to use when the field is missing. * - * @param mixed $missing + * @param mixed $value * @return static */ - public function missing($missing): static + public function missing($value): static { - return $this->addProperty('missing', $missing); + return $this->addProperty('missing', $value); } /** * The script to use for the aggregation. * - * @param string|callable $script + * @param string|callable $value * @return static */ - public function script($script): static + 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 94bf2d6..5f3121d 100644 --- a/src/DSL/Aggs/Metric/Min.php +++ b/src/DSL/Aggs/Metric/Min.php @@ -16,22 +16,22 @@ class Min extends Node /** * The value to use when the field is missing. * - * @param mixed $missing + * @param mixed $value * @return static */ - public function missing($missing): static + public function missing($value): static { - return $this->addProperty('missing', $missing); + return $this->addProperty('missing', $value); } /** * The script to use for the aggregation. * - * @param string|callable $script + * @param string|callable $value * @return static */ - public function script($script): static + 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 1903766..3467257 100644 --- a/src/DSL/Aggs/Metric/Stats.php +++ b/src/DSL/Aggs/Metric/Stats.php @@ -16,22 +16,22 @@ class Stats extends Node /** * The value to use when the field is missing. * - * @param mixed $missing + * @param mixed $value * @return static */ - public function missing($missing): static + public function missing($value): static { - return $this->addProperty('missing', $missing); + return $this->addProperty('missing', $value); } /** * The script to use for the aggregation. * - * @param string|callable $script + * @param string|callable $value * @return static */ - public function script($script): static + 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 c59dd63..063c905 100644 --- a/src/DSL/Aggs/Metric/Sum.php +++ b/src/DSL/Aggs/Metric/Sum.php @@ -16,22 +16,22 @@ class Sum extends Node /** * The value to use when the field is missing. * - * @param mixed $missing + * @param mixed $value * @return static */ - public function missing($missing): static + public function missing($value): static { - return $this->addProperty('missing', $missing); + return $this->addProperty('missing', $value); } /** * The script to use for the aggregation. * - * @param string|callable $script + * @param string|callable $value * @return static */ - public function script($script): static + 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 684e837..055fa7e 100644 --- a/src/DSL/Aggs/Metric/ValueCount.php +++ b/src/DSL/Aggs/Metric/ValueCount.php @@ -16,11 +16,11 @@ class ValueCount extends Node /** * The script to use for the aggregation. * - * @param string|callable $script + * @param string|callable $value * @return static */ - public function script($script): static + 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 3be32cf..d3ea21c 100644 --- a/src/DSL/Aggs/Pipeline.php +++ b/src/DSL/Aggs/Pipeline.php @@ -21,7 +21,7 @@ trait Pipeline * @param mixed $params * @return static */ - public function avgBucket($params) + public function avgBucket($params): static { return $this->node(AvgBucket::create(is_string($params) ? ['buckets_path' => $params] : $params)); } @@ -32,7 +32,7 @@ public function avgBucket($params) * @param mixed $params * @return static */ - public function sumBucket($params) + public function sumBucket($params): static { return $this->node(SumBucket::create(is_string($params) ? ['buckets_path' => $params] : $params)); } @@ -43,7 +43,7 @@ public function sumBucket($params) * @param mixed $params * @return static */ - public function maxBucket($params) + public function maxBucket($params): static { return $this->node(MaxBucket::create(is_string($params) ? ['buckets_path' => $params] : $params)); } @@ -54,7 +54,7 @@ public function maxBucket($params) * @param mixed $params * @return static */ - public function minBucket($params) + public function minBucket($params): static { return $this->node(MinBucket::create(is_string($params) ? ['buckets_path' => $params] : $params)); } @@ -65,7 +65,7 @@ public function minBucket($params) * @param mixed $params * @return static */ - public function statsBucket($params) + public function statsBucket($params): static { return $this->node(StatsBucket::create(is_string($params) ? ['buckets_path' => $params] : $params)); } @@ -76,7 +76,7 @@ public function statsBucket($params) * @param mixed $params * @return static */ - public function cumulativeSum($params) + public function cumulativeSum($params): static { return $this->node(CumulativeSum::create(is_string($params) ? ['buckets_path' => $params] : $params)); } @@ -87,7 +87,7 @@ public function cumulativeSum($params) * @param mixed $params * @return static */ - public function derivative($params) + public function derivative($params): static { return $this->node(Derivative::create(is_string($params) ? ['buckets_path' => $params] : $params)); } @@ -98,7 +98,7 @@ public function derivative($params) * @param mixed $params * @return static */ - public function bucketScript($params) + public function bucketScript($params): static { return $this->node(BucketScript::create(is_string($params) ? ['buckets_path' => $params] : $params)); } diff --git a/src/DSL/Aggs/Pipeline/AvgBucket.php b/src/DSL/Aggs/Pipeline/AvgBucket.php index 8e626eb..dabce8a 100644 --- a/src/DSL/Aggs/Pipeline/AvgBucket.php +++ b/src/DSL/Aggs/Pipeline/AvgBucket.php @@ -16,44 +16,44 @@ class AvgBucket extends Node /** * Path to the buckets to average. * - * @param string $path + * @param string $value * @return static */ - public function bucketsPath(string $path): static + public function bucketsPath(string $value): static { - return $this->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(string $policy): static + 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(string $format): static + public function format(string $value): static { - return $this->addProperty('format', $format); + return $this->addProperty('format', $value); } /** * The value to use when the aggregation is missing a value. * - * @param mixed $missing + * @param mixed $value * @return static */ - public function missing($missing): static + 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 7da6e36..98eef0b 100644 --- a/src/DSL/Aggs/Pipeline/BucketScript.php +++ b/src/DSL/Aggs/Pipeline/BucketScript.php @@ -16,44 +16,44 @@ class BucketScript extends Node /** * Path to the buckets to use in the script. * - * @param mixed $path + * @param mixed $value * @return static */ - public function bucketsPath($path): static + public function bucketsPath($value): static { - return $this->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): static + 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(string $policy): static + 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(string $format): static + 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 0da75e1..3287ab2 100644 --- a/src/DSL/Aggs/Pipeline/CumulativeSum.php +++ b/src/DSL/Aggs/Pipeline/CumulativeSum.php @@ -16,22 +16,22 @@ class CumulativeSum extends Node /** * Path to the buckets to cumulatively sum. * - * @param string $path + * @param string $value * @return static */ - public function bucketsPath(string $path): static + public function bucketsPath(string $value): static { - return $this->addProperty('buckets_path', $path); + return $this->addProperty('buckets_path', $value); } /** * Format for the output value. * - * @param string $format + * @param string $value * @return static */ - public function format(string $format): static + 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 d8305a3..1f32506 100644 --- a/src/DSL/Aggs/Pipeline/Derivative.php +++ b/src/DSL/Aggs/Pipeline/Derivative.php @@ -16,44 +16,44 @@ class Derivative extends Node /** * Path to the buckets to differentiate. * - * @param string $path + * @param string $value * @return static */ - public function bucketsPath(string $path): static + public function bucketsPath(string $value): static { - return $this->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(string $policy): static + 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(string $format): static + 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(string $unit): static + 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 faa7ff5..e762188 100644 --- a/src/DSL/Aggs/Pipeline/MaxBucket.php +++ b/src/DSL/Aggs/Pipeline/MaxBucket.php @@ -16,44 +16,44 @@ class MaxBucket extends Node /** * Path to the buckets to find the maximum. * - * @param string $path + * @param string $value * @return static */ - public function bucketsPath(string $path): static + public function bucketsPath(string $value): static { - return $this->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(string $policy): static + 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(string $format): static + public function format(string $value): static { - return $this->addProperty('format', $format); + return $this->addProperty('format', $value); } /** * The value to use when the aggregation is missing a value. * - * @param mixed $missing + * @param mixed $value * @return static */ - public function missing($missing): static + 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 12b0810..dcb4634 100644 --- a/src/DSL/Aggs/Pipeline/MinBucket.php +++ b/src/DSL/Aggs/Pipeline/MinBucket.php @@ -16,44 +16,44 @@ class MinBucket extends Node /** * Path to the buckets to find the minimum. * - * @param string $path + * @param string $value * @return static */ - public function bucketsPath(string $path): static + public function bucketsPath(string $value): static { - return $this->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(string $policy): static + 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(string $format): static + public function format(string $value): static { - return $this->addProperty('format', $format); + return $this->addProperty('format', $value); } /** * The value to use when the aggregation is missing a value. * - * @param mixed $missing + * @param mixed $value * @return static */ - public function missing($missing): static + 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 45af167..93649b1 100644 --- a/src/DSL/Aggs/Pipeline/StatsBucket.php +++ b/src/DSL/Aggs/Pipeline/StatsBucket.php @@ -16,44 +16,44 @@ class StatsBucket extends Node /** * Path to the buckets to compute stats on. * - * @param string $path + * @param string $value * @return static */ - public function bucketsPath(string $path): static + public function bucketsPath(string $value): static { - return $this->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(string $policy): static + 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(string $format): static + public function format(string $value): static { - return $this->addProperty('format', $format); + return $this->addProperty('format', $value); } /** * The value to use when the aggregation is missing a value. * - * @param mixed $missing + * @param mixed $value * @return static */ - public function missing($missing): static + 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 dcd5d8d..74dc3c2 100644 --- a/src/DSL/Aggs/Pipeline/SumBucket.php +++ b/src/DSL/Aggs/Pipeline/SumBucket.php @@ -16,44 +16,44 @@ class SumBucket extends Node /** * Path to the buckets to sum. * - * @param string $path + * @param string $value * @return static */ - public function bucketsPath(string $path): static + public function bucketsPath(string $value): static { - return $this->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(string $policy): static + 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(string $format): static + public function format(string $value): static { - return $this->addProperty('format', $format); + return $this->addProperty('format', $value); } /** * The value to use when the aggregation is missing a value. * - * @param mixed $missing + * @param mixed $value * @return static */ - public function missing($missing): static + public function missing($value): static { - return $this->addProperty('missing', $missing); + return $this->addProperty('missing', $value); } } diff --git a/src/DSL/Node.php b/src/DSL/Node.php index 5aa823f..13db4cb 100644 --- a/src/DSL/Node.php +++ b/src/DSL/Node.php @@ -182,16 +182,6 @@ protected function multi(bool $multi): static return $this; } - /** - * Whether the node supports multiple clauses. - * - * @return bool - */ - protected function isMulti(): bool - { - return $this->_multi; - } - /** * Get the Elasticsearch type identifier. * @@ -334,11 +324,11 @@ public function __toString(): string * 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): static + public function boost($value): static { - return $this->addProperty('boost', $boost); + return $this->addProperty('boost', $value); } } diff --git a/src/DSL/Param.php b/src/DSL/Param.php index ba982e3..f5efce6 100644 --- a/src/DSL/Param.php +++ b/src/DSL/Param.php @@ -14,7 +14,7 @@ trait Param * * @var array */ - protected $_params = []; + protected array $_params = []; /** * Check if a search request parameter has been set. @@ -22,7 +22,7 @@ trait Param * @param string $key * @return bool */ - public function hasParam($key) + public function hasParam($key): bool { return array_key_exists($key, $this->_params); } @@ -34,7 +34,7 @@ public function hasParam($key) * @param int $size * @return $this */ - public function size($size) + public function size($size): static { $this->_params['size'] = $size; return $this; @@ -47,7 +47,7 @@ public function size($size) * @param int $from * @return $this */ - public function from($from) + public function from($from): static { $this->_params['from'] = $from; return $this; @@ -60,7 +60,7 @@ public function from($from) * @param string $timeout * @return $this */ - public function timeout($timeout) + public function timeout($timeout): static { $this->_params['timeout'] = $timeout; return $this; @@ -73,7 +73,7 @@ public function timeout($timeout) * @param float $minScore * @return $this */ - public function minScore($minScore) + public function minScore($minScore): static { $this->_params['min_score'] = $minScore; return $this; @@ -86,7 +86,7 @@ public function minScore($minScore) * @param int $terminateAfter * @return $this */ - public function terminateAfter($terminateAfter) + public function terminateAfter($terminateAfter): static { $this->_params['terminate_after'] = $terminateAfter; return $this; @@ -99,7 +99,7 @@ public function terminateAfter($terminateAfter) * @param bool $explain * @return $this */ - public function explain($explain) + public function explain($explain): static { $this->_params['explain'] = $explain; return $this; @@ -112,7 +112,7 @@ public function explain($explain) * @param bool $version * @return $this */ - public function version($version) + public function version($version): static { $this->_params['version'] = $version; return $this; @@ -124,7 +124,7 @@ public function version($version) * @param bool $profile * @return $this */ - public function profile($profile) + public function profile($profile): static { $this->_params['profile'] = $profile; return $this; @@ -137,7 +137,7 @@ public function profile($profile) * @param bool|int $trackTotalHits * @return $this */ - public function trackTotalHits($trackTotalHits) + public function trackTotalHits($trackTotalHits): static { $this->_params['track_total_hits'] = $trackTotalHits; return $this; @@ -150,7 +150,7 @@ public function trackTotalHits($trackTotalHits) * @param bool $seqNoPrimaryTerm * @return $this */ - public function seqNoPrimaryTerm($seqNoPrimaryTerm) + public function seqNoPrimaryTerm($seqNoPrimaryTerm): static { $this->_params['seq_no_primary_term'] = $seqNoPrimaryTerm; return $this; @@ -167,7 +167,7 @@ public function seqNoPrimaryTerm($seqNoPrimaryTerm) * @param string|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]; @@ -186,7 +186,7 @@ public function sort($field, $order = null) * @param array|string $source * @return $this */ - public function source($source) + public function source($source): static { $this->_params['_source'] = $source; return $this; @@ -198,7 +198,7 @@ public function source($source) * @param array $searchAfter * @return $this */ - public function searchAfter($searchAfter) + public function searchAfter($searchAfter): static { $this->_params['search_after'] = $searchAfter; return $this; @@ -211,7 +211,7 @@ public function searchAfter($searchAfter) * @param array $storedFields * @return $this */ - public function storedFields($storedFields) + public function storedFields($storedFields): static { $this->_params['stored_fields'] = $storedFields; return $this; @@ -223,7 +223,7 @@ public function storedFields($storedFields) * @param array $docvalueFields * @return $this */ - public function docvalueFields($docvalueFields) + public function docvalueFields($docvalueFields): static { $this->_params['docvalue_fields'] = $docvalueFields; return $this; @@ -236,7 +236,7 @@ public function docvalueFields($docvalueFields) * @param array $indicesBoost * @return $this */ - public function indicesBoost($indicesBoost) + public function indicesBoost($indicesBoost): static { $this->_params['indices_boost'] = [$indicesBoost]; return $this; @@ -249,7 +249,7 @@ public function indicesBoost($indicesBoost) * @param bool $trackScores * @return $this */ - public function trackScores($trackScores) + public function trackScores($trackScores): static { $this->_params['track_scores'] = $trackScores; return $this; @@ -262,7 +262,7 @@ public function trackScores($trackScores) * @param array $fields * @return $this */ - public function fields($fields) + public function fields($fields): static { $this->_params['fields'] = $fields; return $this; @@ -274,7 +274,7 @@ public function fields($fields) * @param array $pit * @return $this */ - public function pit($pit) + public function pit($pit): static { $this->_params['pit'] = $pit; return $this; @@ -287,7 +287,7 @@ public function pit($pit) * @param mixed $filter * @return $this */ - public function postFilter($filter) + public function postFilter($filter): static { $this->_params['post_filter'] = Query::create($filter); return $this; @@ -299,7 +299,7 @@ public function postFilter($filter) * @param mixed $collapse * @return $this */ - public function collapse($collapse) + public function collapse($collapse): static { $this->_params['collapse'] = Params\Collapse::create($collapse); return $this; @@ -311,7 +311,7 @@ public function collapse($collapse) * @param mixed $rescore * @return $this */ - public function rescore($rescore) + public function rescore($rescore): static { $this->_params['rescore'] = Params\Rescore::create($rescore); return $this; @@ -324,7 +324,7 @@ public function rescore($rescore) * @param mixed $highlight * @return $this */ - public function highlight($highlight) + public function highlight($highlight): static { $new = Params\Highlight::create($highlight); @@ -357,7 +357,7 @@ public function highlight($highlight) * @param mixed $suggest * @return $this */ - public function suggest($suggest) + public function suggest($suggest): static { $this->_params['suggest'] = Params\Suggest::create($suggest); return $this; @@ -369,7 +369,7 @@ public function suggest($suggest) * @param array $scriptFields * @return $this */ - public function scriptFields($scriptFields) + public function scriptFields($scriptFields): static { $this->_params['script_fields'] = $scriptFields; return $this; @@ -381,7 +381,7 @@ public function scriptFields($scriptFields) * @param array $runtimeMappings * @return $this */ - public function runtimeMappings($runtimeMappings) + public function runtimeMappings($runtimeMappings): static { $this->_params['runtime_mappings'] = $runtimeMappings; return $this; @@ -399,7 +399,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 a2fd335..8f242da 100644 --- a/src/DSL/Params/Collapse.php +++ b/src/DSL/Params/Collapse.php @@ -48,11 +48,11 @@ public function innerHits(string $name, $hits = null): static /** * Maximum number of concurrent group searches. * - * @param int $max + * @param int $value * @return static */ - public function maxConcurrentGroupSearches(int $max): static + 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 460f116..57e8180 100644 --- a/src/DSL/Params/Highlight.php +++ b/src/DSL/Params/Highlight.php @@ -52,144 +52,144 @@ public function field($field, array $settings = []): static /** * Opening HTML tags for highlighted snippets. * - * @param array $tags + * @param array $value * @return static */ - public function preTags(array $tags): static + 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(array $tags): static + 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(int $size): static + 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(int $num): static + 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(string $encoder): static + 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(string $order): static + public function order(string $value): static { - return $this->addProperty('order', $order); + return $this->addProperty('order', $value); } /** * Highlight against a query other than the search query. * - * @param mixed $query + * @param mixed $value * @return static */ - public function highlightQuery($query): static + 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(string $type): static + 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(string $scanner): static + 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(string $locale): static + 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(int $max): static + 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(int $size): static + 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(string $fragmenter): static + public function fragmenter(string $value): static { - return $this->addProperty('fragmenter', $fragmenter); + return $this->addProperty('fragmenter', $value); } public function toArray() diff --git a/src/DSL/Params/Knn.php b/src/DSL/Params/Knn.php index 358a382..c033394 100644 --- a/src/DSL/Params/Knn.php +++ b/src/DSL/Params/Knn.php @@ -20,93 +20,82 @@ class Knn extends Node /** * The query vector to search for. * - * @param array $vector + * @param array $value * @return static */ - public function queryVector(array $vector): static + 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(int $k): static + 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(int $num): static + 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 + * @param float $value * @return static */ - public function similarity(float $similarity): static + public function similarity(float $value): static { - return $this->addProperty('similarity', $similarity); - } - - /** - * Boost value for the kNN score. - * - * @param float $boost - * @return static - */ - public function boost($boost): static - { - return $this->addProperty('boost', $boost); + return $this->addProperty('similarity', $value); } /** * 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): static + public function filter($value): static { - return $this->addProperty('filter', Query::create($filter)); + return $this->addProperty('filter', Query::create($value)); } /** * Inner hits configuration for nested kNN search. * - * @param mixed $innerHits + * @param mixed $value * @return static */ - public function innerHits($innerHits): static + public function innerHits($value): static { - return $this->addProperty('inner_hits', $innerHits); + return $this->addProperty('inner_hits', $value); } /** * Rescore vector configuration for quantized vector rescoring. * - * @param array $rescoreVector + * @param array $value * @return static */ - public function rescoreVector(array $rescoreVector): static + public function rescoreVector(array $value): static { - return $this->addProperty('rescore_vector', $rescoreVector); + return $this->addProperty('rescore_vector', $value); } public function toArray() diff --git a/src/DSL/Params/Rescore.php b/src/DSL/Params/Rescore.php index 733229c..632ea45 100644 --- a/src/DSL/Params/Rescore.php +++ b/src/DSL/Params/Rescore.php @@ -20,47 +20,47 @@ class Rescore extends Node /** * Number of documents to rescore per shard. * - * @param int $size + * @param int $value * @return static */ - public function windowSize(int $size): static + public function windowSize(int $value): static { - return $this->addProperty('window_size', $size); + return $this->addProperty('window_size', $value); } /** * The query to use for rescoring. * - * @param mixed $query + * @param mixed $value * @return static */ - public function query($query): static + 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(float $weight): static + 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(float $weight): static + public function queryWeight(float $value): static { - $this->_properties['query']['query_weight'] = $weight; + $this->_properties['query']['query_weight'] = $value; return $this; } @@ -68,12 +68,12 @@ public function queryWeight(float $weight): static * How scores are combined. Valid values: total, * multiply, max, avg. Defaults to total. * - * @param string $mode + * @param string $value * @return static */ - public function scoreMode(string $mode): static + public function scoreMode(string $value): static { - $this->_properties['query']['score_mode'] = $mode; + $this->_properties['query']['score_mode'] = $value; return $this; } diff --git a/src/DSL/Params/Suggest.php b/src/DSL/Params/Suggest.php index 0d53ef2..1b10c6e 100644 --- a/src/DSL/Params/Suggest.php +++ b/src/DSL/Params/Suggest.php @@ -20,7 +20,7 @@ class Suggest extends Node * * @param string $alias * @param string $field - * @param string|null $text + * @param ?string $text * @return static */ public function term(string $alias, string $field, ?string $text = null): static @@ -37,7 +37,7 @@ public function term(string $alias, string $field, ?string $text = null): static * * @param string $alias * @param string $field - * @param string|null $prefix + * @param ?string $prefix * @return static */ public function completion(string $alias, string $field, ?string $prefix = null): static @@ -54,7 +54,7 @@ public function completion(string $alias, string $field, ?string $prefix = null) * * @param string $alias * @param string $field - * @param string|null $text + * @param ?string $text * @return static */ public function phrase(string $alias, string $field, ?string $text = null): static diff --git a/src/DSL/Queries/Compound.php b/src/DSL/Queries/Compound.php index 79ad381..1b804ea 100644 --- a/src/DSL/Queries/Compound.php +++ b/src/DSL/Queries/Compound.php @@ -28,7 +28,7 @@ trait Compound * @param callable|Boolean|array $bool * @return $this */ - public function bool($bool) + public function bool($bool): static { if (is_array($bool)) { $boolean = new Boolean(); @@ -51,7 +51,7 @@ public function bool($bool) * @param callable|Boosting|array $boosting * @return $this */ - public function boosting($boosting) + public function boosting($boosting): static { if (is_array($boosting)) { $b = new Boosting(); @@ -74,7 +74,7 @@ public function boosting($boosting) * @param callable|ConstantScore|array $constantScore * @return $this */ - public function constantScore($constantScore) + public function constantScore($constantScore): static { if (is_array($constantScore)) { $cs = new ConstantScore(); @@ -96,7 +96,7 @@ public function constantScore($constantScore) * @param callable|DisjunctionMax|array $disMax * @return $this */ - public function disMax($disMax) + public function disMax($disMax): static { if (is_array($disMax)) { $dm = new DisjunctionMax(); @@ -118,7 +118,7 @@ public function disMax($disMax) * @param mixed $functionScore * @return $this */ - public function functionScore($functionScore) + public function functionScore($functionScore): static { return $this->addQuery(FunctionScore::create($functionScore)); } diff --git a/src/DSL/Queries/Compound/Boolean.php b/src/DSL/Queries/Compound/Boolean.php index aa8a71f..a1bae57 100644 --- a/src/DSL/Queries/Compound/Boolean.php +++ b/src/DSL/Queries/Compound/Boolean.php @@ -20,48 +20,48 @@ class Boolean extends Node * The clause (query) must appear in matching documents and will contribute to the score. * Supports multiple calls to incrementally build the bool query. * - * @param mixed $must + * @param mixed $value * @return static */ - public function must($must): static + public function must($value): static { - return $this->addClause('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 + * @param mixed $value * @return static */ - public function should($should): static + public function should($value): static { - return $this->addClause('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 mixed $value * @return static */ - public function filter($filter): static + public function filter($value): static { - return $this->addClause('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 + * @param mixed $value * @return static */ - public function mustNot($mustNot): static + public function mustNot($value): static { - return $this->addClause('must_not', $mustNot); + return $this->addClause('must_not', $value); } /** @@ -71,11 +71,11 @@ public function mustNot($mustNot): static * * For other valid values, see the minimum_should_match parameter. * - * @param int|string $minimumShouldMatch + * @param int|string $value * @return static */ - public function minimumShouldMatch(int|string $minimumShouldMatch): static + 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 c668cc5..679c041 100644 --- a/src/DSL/Queries/Compound/Boosting.php +++ b/src/DSL/Queries/Compound/Boosting.php @@ -17,33 +17,33 @@ class Boosting extends Node /** * Query you wish to run. Any returned documents must match this query. * - * @param mixed $positive + * @param mixed $value * @return static */ - public function positive($positive): static + public function positive($value): static { - return $this->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): static + 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(float $negativeBoost): static + 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 80ba278..1a80442 100644 --- a/src/DSL/Queries/Compound/ConstantScore.php +++ b/src/DSL/Queries/Compound/ConstantScore.php @@ -17,11 +17,11 @@ class ConstantScore extends Node /** * Filter query you wish to run. Any returned documents must match this query. * - * @param mixed $query + * @param mixed $value * @return static */ - public function filter($query): static + public function filter($value): static { - return $this->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 5b63895..ce6a5a7 100644 --- a/src/DSL/Queries/Compound/DisjunctionMax.php +++ b/src/DSL/Queries/Compound/DisjunctionMax.php @@ -20,22 +20,22 @@ class DisjunctionMax extends Node * Contains one or more query clauses. Returned documents must match one or more of these queries. If a document matches multiple queries, Elasticsearch uses the highest relevance score. * Supports multiple calls to incrementally build. * - * @param mixed $queries + * @param mixed $value * @return static */ - public function queries($queries): static + public function queries($value): static { - return $this->addClause('queries', $queries); + 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(float $tieBreaker): static + 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 ca16b3d..52338e6 100644 --- a/src/DSL/Queries/Compound/FunctionScore.php +++ b/src/DSL/Queries/Compound/FunctionScore.php @@ -19,79 +19,79 @@ class FunctionScore extends Node * Controls how the computed scores from multiple functions are combined. * Options: multiply (default), sum, avg, first, max, min. * - * @param string $scoreMode + * @param string $value * @return static */ - public function scoreMode(string $scoreMode): static + public function scoreMode(string $value): static { - return $this->addProperty('score_mode', $scoreMode); + return $this->addProperty('score_mode', $value); } /** * 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(string $boostMode): static + public function boostMode(string $value): static { - return $this->addProperty('boost_mode', $boostMode); + return $this->addProperty('boost_mode', $value); } /** * Excludes documents that do not meet the specified score threshold. * - * @param float $minScore + * @param float $value * @return static */ - public function minScore(float $minScore): static + public function minScore(float $value): static { - return $this->addProperty('min_score', $minScore); + return $this->addProperty('min_score', $value); } /** * 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(float $maxBoost): static + public function maxBoost(float $value): static { - return $this->addProperty('max_boost', $maxBoost); + return $this->addProperty('max_boost', $value); } /** * The query to be scored. * - * @param mixed $query + * @param mixed $value * @return static */ - public function query($query): static + public function query($value): static { - return $this->addProperty('query', Query::create($query)); + return $this->addProperty('query', Query::create($value)); } /** * Array of score functions to apply. * - * @param array $functions + * @param array $value * @return static */ - public function functions(array $functions): static + public function functions(array $value): static { - return $this->addProperty('functions', $functions); + return $this->addProperty('functions', $value); } /** * Appends a score function to the functions array. * - * @param mixed $function + * @param mixed $value * @return static */ - public function addFunction($function): static + public function addFunction($value): static { - return $this->addProperty('functions', Function_::create($function), true); + return $this->addProperty('functions', Function_::create($value), true); } /** diff --git a/src/DSL/Queries/Compound/Functions/Exp.php b/src/DSL/Queries/Compound/Functions/Exp.php index 2516317..adf6cc2 100644 --- a/src/DSL/Queries/Compound/Functions/Exp.php +++ b/src/DSL/Queries/Compound/Functions/Exp.php @@ -18,44 +18,44 @@ class Exp extends Node /** * The point of origin used for calculating distance. Must be a number for numeric fields, date for date fields, and geo point for geo fields. * - * @param mixed $origin + * @param mixed $value * @return static */ - public function origin($origin): static + public function origin($value): static { - return $this->addProperty('origin', $origin); + return $this->addProperty('origin', $value); } /** * 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): static + public function scale($value): static { - return $this->addProperty('scale', $scale); + return $this->addProperty('scale', $value); } /** * 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): static + public function offset($value): static { - return $this->addProperty('offset', $offset); + return $this->addProperty('offset', $value); } /** * 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(float $decay): static + 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 667455e..8e3b945 100644 --- a/src/DSL/Queries/Compound/Functions/FieldValueFactor.php +++ b/src/DSL/Queries/Compound/Functions/FieldValueFactor.php @@ -13,33 +13,33 @@ class FieldValueFactor extends Node /** * Optional factor to multiply the field value with, defaults to 1. * - * @param float $factor + * @param float $value * @return static */ - public function factor(float $factor): static + 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(string $modifier): static + 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 string|int|float|bool $missing + * @param string|int|float|bool $value * @return static */ - public function missing(string|int|float|bool $missing): static + 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 186e1d1..bb203fa 100644 --- a/src/DSL/Queries/Compound/Functions/Function_.php +++ b/src/DSL/Queries/Compound/Functions/Function_.php @@ -18,57 +18,57 @@ class Function_ extends Node /** * A filtering query that restricts the score function to matching documents only. * - * @param mixed $filter + * @param mixed $value * @return static */ - public function filter($filter): static + public function filter($value): static { - return $this->addProperty('filter', Query::create($filter)); + return $this->addProperty('filter', Query::create($value)); } /** * Multiplies the score by the provided weight value. * - * @param float $weight + * @param float $value * @return static */ - public function weight(float $weight): static + public function weight(float $value): static { - return $this->addProperty('weight', $weight); + return $this->addProperty('weight', $value); } /** * 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): static + public function randomScore($value = null): static { - return $this->addProperty('random_score', RandomScore::create($randomScore)); + return $this->addProperty('random_score', RandomScore::create($value)); } /** * Wraps another query and customizes the scoring using a script. * - * @param mixed $scriptScore + * @param mixed $value * @return static */ - public function scriptScore($scriptScore): static + public function scriptScore($value): static { - return $this->addProperty('script_score', ScriptScore::create($scriptScore)); + return $this->addProperty('script_score', ScriptScore::create($value)); } /** * Sets the script for script_score using a Script object or closure. * - * @param mixed $script + * @param mixed $value * @return static */ - public function script($script): static + 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); } /** diff --git a/src/DSL/Queries/Compound/Functions/Gauss.php b/src/DSL/Queries/Compound/Functions/Gauss.php index 4bdaa3b..d88dae3 100644 --- a/src/DSL/Queries/Compound/Functions/Gauss.php +++ b/src/DSL/Queries/Compound/Functions/Gauss.php @@ -18,44 +18,44 @@ class Gauss extends Node /** * The point of origin used for calculating distance. Must be a number for numeric fields, date for date fields, and geo point for geo fields. * - * @param mixed $origin + * @param mixed $value * @return static */ - public function origin($origin): static + public function origin($value): static { - return $this->addProperty('origin', $origin); + return $this->addProperty('origin', $value); } /** * 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): static + public function scale($value): static { - return $this->addProperty('scale', $scale); + return $this->addProperty('scale', $value); } /** * 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): static + public function offset($value): static { - return $this->addProperty('offset', $offset); + return $this->addProperty('offset', $value); } /** * 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(float $decay): static + 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 65a6896..8d64265 100644 --- a/src/DSL/Queries/Compound/Functions/Linear.php +++ b/src/DSL/Queries/Compound/Functions/Linear.php @@ -18,44 +18,44 @@ class Linear extends Node /** * The point of origin used for calculating distance. Must be a number for numeric fields, date for date fields, and geo point for geo fields. * - * @param mixed $origin + * @param mixed $value * @return static */ - public function origin($origin): static + public function origin($value): static { - return $this->addProperty('origin', $origin); + return $this->addProperty('origin', $value); } /** * 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): static + public function scale($value): static { - return $this->addProperty('scale', $scale); + return $this->addProperty('scale', $value); } /** * 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): static + public function offset($value): static { - return $this->addProperty('offset', $offset); + return $this->addProperty('offset', $value); } /** * 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(float $decay): static + 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 25a6979..a7ae078 100644 --- a/src/DSL/Queries/Compound/Functions/RandomScore.php +++ b/src/DSL/Queries/Compound/Functions/RandomScore.php @@ -16,12 +16,12 @@ class RandomScore extends Node /** * Seed value for reproducible random scores. * - * @param mixed $seed + * @param mixed $value * @return static */ - public function seed($seed): static + public function seed($value): static { - return $this->addProperty('seed', $seed); + return $this->addProperty('seed', $value); } /** diff --git a/src/DSL/Queries/Compound/Functions/ScriptScore.php b/src/DSL/Queries/Compound/Functions/ScriptScore.php index fbeea46..e2c9f7f 100644 --- a/src/DSL/Queries/Compound/Functions/ScriptScore.php +++ b/src/DSL/Queries/Compound/Functions/ScriptScore.php @@ -17,11 +17,11 @@ class ScriptScore extends Node /** * The script used to compute the custom score. * - * @param mixed $script + * @param mixed $value * @return static */ - public function script($script): static + public function script($value): static { - return $this->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 21b6808..298d46a 100644 --- a/src/DSL/Queries/FullText.php +++ b/src/DSL/Queries/FullText.php @@ -26,7 +26,7 @@ trait FullText * @param mixed $value * @return $this */ - public function intervals($field, $value = null) + public function intervals($field, $value = null): static { return $this->addQuery(Intervals::create($field, $value)); } @@ -40,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)); } @@ -52,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)); } @@ -64,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)); } @@ -76,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)); } @@ -89,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)); } @@ -100,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)); } @@ -111,7 +111,7 @@ public function combinedFields($value) * @param mixed $queryString * @return $this */ - public function queryString($queryString) + public function queryString($queryString): static { return $this->addQuery(QueryString::create($queryString)); } @@ -122,7 +122,7 @@ public function queryString($queryString) * @param mixed $simpleQueryString * @return $this */ - public function simpleQueryString($simpleQueryString) + public function simpleQueryString($simpleQueryString): static { return $this->addQuery(SimpleQueryString::create($simpleQueryString)); } diff --git a/src/DSL/Queries/FullText/CombinedFields.php b/src/DSL/Queries/FullText/CombinedFields.php index d63c934..5b29314 100644 --- a/src/DSL/Queries/FullText/CombinedFields.php +++ b/src/DSL/Queries/FullText/CombinedFields.php @@ -21,12 +21,12 @@ class CombinedFields extends Node * The combined_fields query analyzes the provided text before performing * a search. * - * @param string $query + * @param string $value * @return static */ - public function query(string $query): static + public function query(string $value): static { - return $this->addProperty('query', $query); + return $this->addProperty('query', $value); } /** @@ -34,48 +34,48 @@ public function query(string $query): static * 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(array $fields): static + 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(bool $autoGenerateSynonymsPhraseQuery): static + 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(string $operator): static + 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 int|string $minimumShouldMatch + * @param int|string $value * @return static */ - public function minimumShouldMatch(int|string $minimumShouldMatch): static + public function minimumShouldMatch(int|string $value): static { - return $this->addProperty('minimum_should_match', $minimumShouldMatch); + return $this->addProperty('minimum_should_match', $value); } /** @@ -83,11 +83,11 @@ public function minimumShouldMatch(int|string $minimumShouldMatch): static * 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(string $zeroTermsQuery): static + 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 9b1b01d..af4edf0 100644 --- a/src/DSL/Queries/FullText/Intervals.php +++ b/src/DSL/Queries/FullText/Intervals.php @@ -18,7 +18,7 @@ class Intervals extends Node protected bool $_fieldKeyed = true; /** @var array */ - protected $_intervals = []; + protected array $_intervals = []; /** * Add a match rule that matches analyzed text. @@ -36,24 +36,24 @@ public function match($match): static * 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): static + 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($wildcard): static + public function wildcard($value): static { - $this->_intervals[] = Intervals\Wildcard::create($wildcard); + $this->_intervals[] = Intervals\Wildcard::create($value); return $this; } @@ -61,24 +61,24 @@ public function wildcard($wildcard): static * 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): static + 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): static + public function range($value): static { - $this->_intervals[] = Intervals\Range::create($range); + $this->_intervals[] = Intervals\Range::create($value); return $this; } @@ -86,12 +86,12 @@ public function range($range): static * 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): static + public function allOf($value): static { - $this->_intervals[] = Intervals\AllOf::create($allOf); + $this->_intervals[] = Intervals\AllOf::create($value); return $this; } @@ -99,12 +99,12 @@ public function allOf($allOf): static * 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): static + public function anyOf($value): static { - $this->_intervals[] = Intervals\AnyOf::create($anyOf); + $this->_intervals[] = Intervals\AnyOf::create($value); return $this; } diff --git a/src/DSL/Queries/FullText/Intervals/AllOf.php b/src/DSL/Queries/FullText/Intervals/AllOf.php index 777cf1b..030d0c9 100644 --- a/src/DSL/Queries/FullText/Intervals/AllOf.php +++ b/src/DSL/Queries/FullText/Intervals/AllOf.php @@ -18,33 +18,33 @@ class AllOf extends Node * An array of rules to combine. All rules * must produce a match in a document for the overall source to match. * - * @param mixed $intervals + * @param mixed $value * @return static */ - public function intervals($intervals): static + public function intervals($value): static { - $intervals = Intervals::create($intervals) + $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): static + public function addInterval($value): static { if (!isset($this->_properties['intervals'])) { $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); + } elseif ($value instanceof Node) { + $target->addQuery($value); } return $this; } @@ -54,35 +54,35 @@ public function addInterval($interval): static * 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(int $maxGaps): static + 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(bool $ordered): static + 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): static + public function filter($value): static { - return $this->addProperty('filter', $filter); + return $this->addProperty('filter', $value); } } diff --git a/src/DSL/Queries/FullText/Intervals/AnyOf.php b/src/DSL/Queries/FullText/Intervals/AnyOf.php index 44bc2eb..77408ee 100644 --- a/src/DSL/Queries/FullText/Intervals/AnyOf.php +++ b/src/DSL/Queries/FullText/Intervals/AnyOf.php @@ -17,33 +17,33 @@ class AnyOf extends Node /** * An array of rules to match. * - * @param mixed $intervals + * @param mixed $value * @return static */ - public function intervals($intervals): static + public function intervals($value): static { - $intervals = Intervals::create($intervals) + $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): static + public function addInterval($value): static { if (!isset($this->_properties['intervals'])) { $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); + } elseif ($value instanceof Node) { + $target->addQuery($value); } return $this; } @@ -52,11 +52,11 @@ public function addInterval($interval): static * Rule used to filter returned * intervals. * - * @param mixed $filter + * @param mixed $value * @return static */ - public function filter($filter): static + 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 a2ae9a0..5957a91 100644 --- a/src/DSL/Queries/FullText/Intervals/Filter.php +++ b/src/DSL/Queries/FullText/Intervals/Filter.php @@ -20,72 +20,72 @@ class Filter extends Node * Query used to return intervals that follow an * interval from the filter rule. * - * @param mixed $after + * @param mixed $value * @return static */ - public function after($after): static + public function after($value): static { - return $this->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): static + 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): static + 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): static + 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): static + 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): static + public function overlapping($value): static { - return $this->addProperty('overlapping', Query::create($overlapping)); + return $this->addProperty('overlapping', Query::create($value)); } /** @@ -93,35 +93,35 @@ public function overlapping($overlapping): static * 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): static + 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): static + 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): static + 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 9638bf8..419849b 100644 --- a/src/DSL/Queries/FullText/Intervals/Fuzzy.php +++ b/src/DSL/Queries/FullText/Intervals/Fuzzy.php @@ -18,60 +18,60 @@ class Fuzzy extends Node /** * The term to match. * - * @param string $term + * @param string $value * @return static */ - public function term(string $term): static + public function term(string $value): static { - return $this->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(int $prefixLength): static + 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(bool $transpositions): static + 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 int|string $fuzziness + * @param int|string $value * @return static */ - public function fuzziness(int|string $fuzziness): static + 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(string $analyzer): static + public function analyzer(string $value): static { - return $this->addProperty('analyzer', $analyzer); + return $this->addProperty('analyzer', $value); } /** @@ -79,11 +79,11 @@ public function analyzer(string $analyzer): static * 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(string $useField): static + 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 1cadd4a..89657c4 100644 --- a/src/DSL/Queries/FullText/Intervals/Match_.php +++ b/src/DSL/Queries/FullText/Intervals/Match_.php @@ -18,12 +18,12 @@ class Match_ extends Node /** * Text you wish to find in the provided field. * - * @param string $query + * @param string $value * @return static */ - public function query(string $query): static + public function query(string $value): static { - return $this->addProperty('query', $query); + return $this->addProperty('query', $value); } /** @@ -32,47 +32,47 @@ public function query(string $query): static * 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(int $maxGaps): static + 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(bool $ordered = false): static + 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(string $analyzer): static + 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): static + public function filter($value): static { - return $this->addProperty('filter', Filter::create($filter)); + return $this->addProperty('filter', Filter::create($value)); } /** @@ -80,11 +80,11 @@ public function filter($filter): static * 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(string $useField): static + 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 13eaef4..38e7d60 100644 --- a/src/DSL/Queries/FullText/Intervals/Prefix.php +++ b/src/DSL/Queries/FullText/Intervals/Prefix.php @@ -19,24 +19,24 @@ class Prefix extends Node * Beginning characters of terms you wish to find in * the top-level field. * - * @param string $prefix + * @param string $value * @return static */ - public function prefix(string $prefix): static + public function prefix(string $value): static { - return $this->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(string $analyzer): static + public function analyzer(string $value): static { - return $this->addProperty('analyzer', $analyzer); + return $this->addProperty('analyzer', $value); } /** @@ -44,11 +44,11 @@ public function analyzer(string $analyzer): static * 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(string $userField): static + 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 bfb7a6e..b931ef9 100644 --- a/src/DSL/Queries/FullText/Intervals/Range.php +++ b/src/DSL/Queries/FullText/Intervals/Range.php @@ -20,68 +20,68 @@ class Range extends Node /** * Greater than or equal to the specified value. * - * @param string|int|float|bool $gte + * @param string|int|float|bool $value * @return static */ - public function gte(string|int|float|bool $gte): static + public function gte(string|int|float|bool $value): static { - return $this->addProperty('gte', $gte); + return $this->addProperty('gte', $value); } /** * Greater than the specified value. * - * @param string|int|float|bool $gt + * @param string|int|float|bool $value * @return static */ - public function gt(string|int|float|bool $gt): static + public function gt(string|int|float|bool $value): static { - return $this->addProperty('gt', $gt); + return $this->addProperty('gt', $value); } /** * Less than or equal to the specified value. * - * @param string|int|float|bool $lte + * @param string|int|float|bool $value * @return static */ - public function lte(string|int|float|bool $lte): static + public function lte(string|int|float|bool $value): static { - return $this->addProperty('lte', $lte); + return $this->addProperty('lte', $value); } /** * Less than the specified value. * - * @param string|int|float|bool $lt + * @param string|int|float|bool $value * @return static */ - public function lt(string|int|float|bool $lt): static + 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(string $analyzer): static + 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(string $useField): static + 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/Wildcard.php b/src/DSL/Queries/FullText/Intervals/Wildcard.php index 43fe8e3..92892ab 100644 --- a/src/DSL/Queries/FullText/Intervals/Wildcard.php +++ b/src/DSL/Queries/FullText/Intervals/Wildcard.php @@ -19,24 +19,24 @@ class Wildcard extends Node * Wildcard pattern used to find matching terms. * Supports ? (any single character) and * (zero or more characters). * - * @param string $pattern + * @param string $value * @return static */ - public function pattern(string $pattern): static + public function pattern(string $value): static { - return $this->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(string $analyzer): static + public function analyzer(string $value): static { - return $this->addProperty('analyzer', $analyzer); + return $this->addProperty('analyzer', $value); } /** @@ -44,11 +44,11 @@ public function analyzer(string $analyzer): static * 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(string $useField): static + 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 50ee452..1c4e311 100644 --- a/src/DSL/Queries/FullText/MatchBoolPrefix.php +++ b/src/DSL/Queries/FullText/MatchBoolPrefix.php @@ -24,117 +24,117 @@ class MatchBoolPrefix extends Node * The match_bool_prefix query analyzes any provided text into tokens * before performing a search. * - * @param string $query + * @param string $value * @return static */ - public function query(string $query): static + public function query(string $value): static { - return $this->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(int $maxExpansions): static + 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(bool $lenient): static + 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(string $analyzer): static + 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 int|string $minimumShouldMatch + * @param int|string $value * @return static */ - public function minimumShouldMatch(int|string $minimumShouldMatch): static + 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 int|string $fuzziness + * @param int|string $value * @return static */ - public function fuzziness(int|string $fuzziness): static + 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(int $prefixLength): static + 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(bool $fuzzyTranspositions): static + 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(string $fuzzyRewrite): static + 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(string $operator): static + 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 83df915..921ab50 100644 --- a/src/DSL/Queries/FullText/MatchPhrase.php +++ b/src/DSL/Queries/FullText/MatchPhrase.php @@ -24,36 +24,36 @@ class MatchPhrase extends Node * The match_phrase query analyzes any provided text into tokens before * performing a search. * - * @param string $query + * @param string $value * @return static */ - public function query(string $query): static + public function query(string $value): static { - return $this->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(string $analyzer): static + 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(int $slop): static + public function slop(int $value): static { - return $this->addProperty('slop', $slop); + return $this->addProperty('slop', $value); } /** @@ -61,11 +61,11 @@ public function slop(int $slop): static * 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(string $zeroTermsQuery): static + 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 c907d57..9b2fcc6 100644 --- a/src/DSL/Queries/FullText/MatchPhrasePrefix.php +++ b/src/DSL/Queries/FullText/MatchPhrasePrefix.php @@ -25,48 +25,48 @@ class MatchPhrasePrefix extends Node * before performing a search. The last term of this text is treated as a * prefix, matching any words that begin with that term. * - * @param string $query + * @param string $value * @return static */ - public function query(string $query): static + public function query(string $value): static { - return $this->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(string $analyzer): static + 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(int $maxExpansions): static + 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(int $slop): static + public function slop(int $value): static { - return $this->addProperty('slop', $slop); + return $this->addProperty('slop', $value); } /** @@ -74,11 +74,11 @@ public function slop(int $slop): static * 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(string $zeroTermsQuery): static + 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 45c8947..70a0466 100644 --- a/src/DSL/Queries/FullText/Match_.php +++ b/src/DSL/Queries/FullText/Match_.php @@ -26,84 +26,84 @@ class Match_ extends Node * in the provided field. The match query analyzes any provided text before * performing a search. * - * @param string|int|float|bool $query + * @param string|int|float|bool $value * @return static */ - public function query(string|int|float|bool $query): static + public function query(string|int|float|bool $value): static { - return $this->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(string $analyzer): static + 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(bool $autoGenerateSynonymsPhraseQuery): static + 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 int|string $fuzziness + * @param int|string $value * @return static */ - public function fuzziness(int|string $fuzziness): static + 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(int $maxExpansions): static + 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(int $prefixLength): static + 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(bool $fuzzyTranspositions): static + public function fuzzyTranspositions(bool $value): static { - return $this->addProperty('fuzzy_transpositions', $fuzzyTranspositions); + return $this->addProperty('fuzzy_transpositions', $value); } /** @@ -111,48 +111,48 @@ public function fuzzyTranspositions(bool $fuzzyTranspositions): static * 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(string $fuzzyRewrite): static + 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(bool $lenient): static + 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(string $operator): static + 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 int|string $minimumShouldMatch + * @param int|string $value * @return static */ - public function minimumShouldMatch(int|string $minimumShouldMatch): static + public function minimumShouldMatch(int|string $value): static { - return $this->addProperty('minimum_should_match', $minimumShouldMatch); + return $this->addProperty('minimum_should_match', $value); } /** @@ -160,11 +160,11 @@ public function minimumShouldMatch(int|string $minimumShouldMatch): static * 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(string $zeroTermsQuery): static + 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 048c222..38f35e3 100644 --- a/src/DSL/Queries/FullText/MultiMatch.php +++ b/src/DSL/Queries/FullText/MultiMatch.php @@ -19,12 +19,12 @@ class MultiMatch extends Node * Text, number, boolean value or date you wish to find * in the provided fields. * - * @param string|int|float|bool $query + * @param string|int|float|bool $value * @return static */ - public function query(string|int|float|bool $query): static + public function query(string|int|float|bool $value): static { - return $this->addProperty('query', $query); + return $this->addProperty('query', $value); } /** @@ -32,12 +32,12 @@ public function query(string|int|float|bool $query): static * wildcards (*). Individual fields can be boosted with the caret (^) * notation. * - * @param array $fields + * @param array $value * @return static */ - public function fields(array $fields): static + public function fields(array $value): static { - return $this->addProperty('fields', $fields); + return $this->addProperty('fields', $value); } /** @@ -45,130 +45,130 @@ public function fields(array $fields): static * 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(string $type): static + 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(string $operator): static + 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(string $analyzer): static + 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 int|string $minimumShouldMatch + * @param int|string $value * @return static */ - public function minimumShouldMatch(int|string $minimumShouldMatch): static + 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(float $tieBreaker): static + 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 int|string $fuzziness + * @param int|string $value * @return static */ - public function fuzziness(int|string $fuzziness): static + 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(int $prefixLength): static + 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(int $maxExpansions): static + 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(bool $fuzzyTranspositions): static + 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(string $fuzzyRewrite): static + 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(bool $lenient): static + public function lenient(bool $value): static { - return $this->addProperty('lenient', $lenient); + return $this->addProperty('lenient', $value); } /** @@ -176,35 +176,35 @@ public function lenient(bool $lenient): static * 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(string $zeroTermsQuery): static + 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(int $slop): static + 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(bool $autoGenerateSynonymsPhraseQuery): static + 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 1d4b757..b4145fb 100644 --- a/src/DSL/Queries/FullText/QueryString.php +++ b/src/DSL/Queries/FullText/QueryString.php @@ -19,36 +19,36 @@ class QueryString extends Node /** * Query string you wish to parse and use for search. * - * @param string $query + * @param string $value * @return static */ - public function query(string $query): static + public function query(string $value): static { - return $this->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(string $defaultField): static + 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(bool $allowLeadingWildcard): static + public function allowLeadingWildcard(bool $value): static { - return $this->addProperty('allow_leading_wildcard', $allowLeadingWildcard); + return $this->addProperty('allow_leading_wildcard', $value); } /** @@ -56,24 +56,24 @@ public function allowLeadingWildcard(bool $allowLeadingWildcard): static * into tokens. Defaults to the index-time analyzer mapped for the * default_field. * - * @param string $analyzer + * @param string $value * @return static */ - public function analyzer(string $analyzer): static + 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(bool $autoGenerateSynonymsPhraseQuery): static + public function autoGenerateSynonymsPhraseQuery(bool $value): static { - return $this->addProperty('auto_generate_synonyms_phrase_query', $autoGenerateSynonymsPhraseQuery); + return $this->addProperty('auto_generate_synonyms_phrase_query', $value); } /** @@ -81,119 +81,119 @@ public function autoGenerateSynonymsPhraseQuery(bool $autoGenerateSynonymsPhrase * query string if no operators are specified. Valid values are: * OR (Default), AND. * - * @param string $defaultOperator + * @param string $value * @return static */ - public function defaultOperator(string $defaultOperator): static + 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(bool $enablePositionIncrements): static + 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(array $fields): static + public function fields(array $value): static { - return $this->addProperty('fields', $fields); + return $this->addProperty('fields', $value); } /** * Maximum edit distance allowed for fuzzy matching. * - * @param int|string $fuzziness + * @param int|string $value * @return static */ - public function fuzziness(int|string $fuzziness): static + 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(int $fuzzyMaxExpansions): static + 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(int $fuzzyPrefixLength): static + 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(bool $fuzzyTranspositions): static + 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(bool $lenient): static + 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(int $maxDeterminizedStates): static + 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 int|string $minimumShouldMatch + * @param int|string $value * @return static */ - public function minimumShouldMatch(int|string $minimumShouldMatch): static + public function minimumShouldMatch(int|string $value): static { - return $this->addProperty('minimum_should_match', $minimumShouldMatch); + return $this->addProperty('minimum_should_match', $value); } /** @@ -201,12 +201,12 @@ public function minimumShouldMatch(int|string $minimumShouldMatch): static * 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(string $quoteAnalyzer): static + public function quoteAnalyzer(string $value): static { - return $this->addProperty('quote_analyzer', $quoteAnalyzer); + return $this->addProperty('quote_analyzer', $value); } /** @@ -214,12 +214,12 @@ public function quoteAnalyzer(string $quoteAnalyzer): static * 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(int $phraseSlop): static + public function phraseSlop(int $value): static { - return $this->addProperty('phrase_slop', $phraseSlop); + return $this->addProperty('phrase_slop', $value); } /** @@ -227,35 +227,35 @@ public function phraseSlop(int $phraseSlop): static * 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(string $quoteFieldSuffix): static + 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(string $rewrite): static + 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(string $timeZone): static + public function timeZone(string $value): static { - return $this->addProperty('time_zone', $timeZone); + return $this->addProperty('time_zone', $value); } /** @@ -263,35 +263,35 @@ public function timeZone(string $timeZone): static * 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(string $type): static + 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(bool $analyzeWildcard): static + 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(float $tieBreaker): static + 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 84666d3..700a93e 100644 --- a/src/DSL/Queries/FullText/SimpleQueryString.php +++ b/src/DSL/Queries/FullText/SimpleQueryString.php @@ -19,12 +19,12 @@ class SimpleQueryString extends Node /** * Query string you wish to parse and use for search. * - * @param string $query + * @param string $value * @return static */ - public function query(string $query): static + public function query(string $value): static { - return $this->addProperty('query', $query); + return $this->addProperty('query', $value); } /** @@ -32,12 +32,12 @@ public function query(string $query): static * Supports wildcard expressions and per-field boosting with caret (^) * notation. * - * @param array $fields + * @param array $value * @return static */ - public function fields(array $fields): static + public function fields(array $value): static { - return $this->addProperty('fields', $fields); + return $this->addProperty('fields', $value); } /** @@ -45,24 +45,24 @@ public function fields(array $fields): static * query string if no operators are specified. Valid values are: * OR (Default), AND. * - * @param string $defaultOperator + * @param string $value * @return static */ - public function defaultOperator(string $defaultOperator): static + 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(bool $analyzeWildcard): static + public function analyzeWildcard(bool $value): static { - return $this->addProperty('analyze_wildcard', $analyzeWildcard); + return $this->addProperty('analyze_wildcard', $value); } /** @@ -70,96 +70,96 @@ public function analyzeWildcard(bool $analyzeWildcard): static * into tokens. Defaults to the index-time analyzer mapped for the * default_field. * - * @param string $analyzer + * @param string $value * @return static */ - public function analyzer(string $analyzer): static + 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(bool $autoGenerateSynonymsPhraseQuery): static + 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(string $flags): static + 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(int $fuzzyMaxExpansions): static + 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(int $fuzzyPrefixLength): static + 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(bool $fuzzyTranspositions): static + 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(bool $lenient): static + 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 int|string $minimumShouldMatch + * @param int|string $value * @return static */ - public function minimumShouldMatch(int|string $minimumShouldMatch): static + public function minimumShouldMatch(int|string $value): static { - return $this->addProperty('minimum_should_match', $minimumShouldMatch); + return $this->addProperty('minimum_should_match', $value); } /** @@ -167,11 +167,11 @@ public function minimumShouldMatch(int|string $minimumShouldMatch): static * 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(string $quoteFieldSuffix): static + 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 631bbb3..51f567d 100644 --- a/src/DSL/Queries/Geo.php +++ b/src/DSL/Queries/Geo.php @@ -22,7 +22,7 @@ trait Geo * @param mixed $value * @return $this */ - public function geoBoundingBox($field, $value = null) + public function geoBoundingBox($field, $value = null): static { return $this->addQuery(GeoBoundingBox::create($field, $value)); } @@ -35,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)); } @@ -47,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)); } @@ -59,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)); } @@ -71,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 a5b5367..2502bda 100644 --- a/src/DSL/Queries/Geo/GeoBoundingBox.php +++ b/src/DSL/Queries/Geo/GeoBoundingBox.php @@ -20,123 +20,123 @@ class GeoBoundingBox extends Node /** * Top-left corner of the bounding box. * - * @param mixed $topLeft + * @param mixed $value * @return static */ - public function topLeft($topLeft): static + public function topLeft($value): static { - return $this->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): static + 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(float $top): static + 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(float $left): static + 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(float $bottom): static + 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(float $right): static + 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(string $wkt): static + 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): static + 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): static + 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(string $validationMethod): static + 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(bool $ignoreUnmapped): static + 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 6f5b892..e0ea3f4 100644 --- a/src/DSL/Queries/Geo/GeoDistance.php +++ b/src/DSL/Queries/Geo/GeoDistance.php @@ -25,12 +25,12 @@ class GeoDistance extends Node * Points which fall into this circle are considered to be matches. * The distance can be specified in various units. * - * @param string $distance + * @param string $value * @return static */ - public function distance(string $distance): static + public function distance(string $value): static { - return $this->addProperty('distance', $distance); + return $this->addProperty('distance', $value); } /** @@ -50,36 +50,34 @@ public function location(string $field, $location): static * 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 + * @param string $value * @return static */ - public function distanceType(string $distanceType): static + public function distanceType(string $value): static { - return $this->addProperty('distance_type', $distanceType); + return $this->addProperty('distance_type', $value); } /** * Optional name field to identify the query. * - * @param string $_name + * @param string $value * @return static - * @SuppressWarnings(PHPMD.CamelCaseParameterName) - * @SuppressWarnings(PHPMD.CamelCaseVariableName) */ - public function _name(string $_name): static + public function _name(string $value): static { - return $this->addProperty('_name', $_name); + return $this->addProperty('_name', $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(string $validationMethod): static + 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 64bea1d..66337a8 100644 --- a/src/DSL/Queries/Geo/GeoGrid.php +++ b/src/DSL/Queries/Geo/GeoGrid.php @@ -22,35 +22,35 @@ class GeoGrid extends Node /** * The geohex grid key to match. Only usable with geo_point fields. * - * @param string $geohex + * @param string $value * @return static */ - public function geohex(string $geohex): static + public function geohex(string $value): static { - return $this->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(string $geotile): static + 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(string $geohash): static + 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 9e3fbbf..bc5b05d 100644 --- a/src/DSL/Queries/Geo/GeoPolygon.php +++ b/src/DSL/Queries/Geo/GeoPolygon.php @@ -21,35 +21,35 @@ class GeoPolygon extends Node * Array of geo points that define the polygon. * At least three points are required to form a polygon. * - * @param array> $points + * @param array> $value * @return static */ - public function points(array $points): static + 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(string $validationMethod): static + 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(bool $ignoreUnmapped): static + 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 2dfb933..e600074 100644 --- a/src/DSL/Queries/Geo/GeoShape.php +++ b/src/DSL/Queries/Geo/GeoShape.php @@ -24,47 +24,47 @@ class GeoShape extends Node * Inline shape definition using GeoJSON or Well-Known Text (WKT). * Contains the shape type and coordinates. * - * @param mixed $shape + * @param mixed $value * @return static */ - public function shape($shape): static + public function shape($value): static { - return $this->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(string $relation): static + 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): static + 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(bool $ignoreUnmapped): static + 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 bb35ae9..98e3446 100644 --- a/src/DSL/Queries/Joining.php +++ b/src/DSL/Queries/Joining.php @@ -24,7 +24,7 @@ trait Joining * @param callable|Query|array|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); @@ -41,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(); @@ -59,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(); @@ -76,7 +76,7 @@ public function hasParent($type, $query = null) * @param mixed $parentId * @return $this */ - public function parentId($parentId) + public function parentId($parentId): static { return $this->addQuery(ParentId::create($parentId)); } diff --git a/src/DSL/Queries/Joining/HasChild.php b/src/DSL/Queries/Joining/HasChild.php index 267aa62..5f6ac72 100644 --- a/src/DSL/Queries/Joining/HasChild.php +++ b/src/DSL/Queries/Joining/HasChild.php @@ -19,48 +19,48 @@ class HasChild extends Node /** * Name of the child relationship mapped for the join field. * - * @param string $type + * @param string $value * @return static */ - public function type(string $type): static + public function type(string $value): static { - return $this->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): static + 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(bool $ignoreUnmapped): static + 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(int $maxChildren): static + public function maxChildren(int $value): static { - return $this->addProperty('max_children', $maxChildren); + return $this->addProperty('max_children', $value); } /** @@ -68,23 +68,23 @@ public function maxChildren(int $maxChildren): static * 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(int $minChildren): static + 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(string $scoreMode): static + 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 f13d68c..c01178b 100644 --- a/src/DSL/Queries/Joining/HasParent.php +++ b/src/DSL/Queries/Joining/HasParent.php @@ -19,47 +19,47 @@ class HasParent extends Node /** * Name of the parent relationship mapped for the join field. * - * @param string $parentType + * @param string $value * @return static */ - public function parentType(string $parentType): static + public function parentType(string $value): static { - return $this->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): static + 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(bool $score): static + 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(bool $ignoreUnmapped): static + 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 97c762f..5493c0d 100644 --- a/src/DSL/Queries/Joining/Nested.php +++ b/src/DSL/Queries/Joining/Nested.php @@ -37,47 +37,47 @@ public static function create($field = null, $value = null): static /** * Path to the nested object you wish to search. * - * @param string $path + * @param string $value * @return static */ - public function path(string $path): static + 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): static + 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(string $scoreMode): static + 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(bool $ignoreUnmapped): static + 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 040b300..9914e16 100644 --- a/src/DSL/Queries/Joining/ParentId.php +++ b/src/DSL/Queries/Joining/ParentId.php @@ -18,34 +18,34 @@ class ParentId extends Node /** * Name of the child relationship mapped for the join field. * - * @param string $type + * @param string $value * @return static */ - public function type(string $type): static + public function type(string $value): static { - return $this->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(string $id): static + 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(bool $ignoreUnmapped): static + 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 337b63f..74e4605 100644 --- a/src/DSL/Queries/MatchAll.php +++ b/src/DSL/Queries/MatchAll.php @@ -18,7 +18,7 @@ trait MatchAll * @param mixed $matchAll * @return $this */ - public function matchAll($matchAll = null) + public function matchAll($matchAll = null): static { return $this->addQuery(QMatchAll::create($matchAll)); } @@ -28,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/Script.php b/src/DSL/Queries/Script.php index 132637e..8a41289 100644 --- a/src/DSL/Queries/Script.php +++ b/src/DSL/Queries/Script.php @@ -16,44 +16,44 @@ class Script extends Node /** * The ID of a stored script. * - * @param string $id + * @param string $value * @return static */ - public function id(string $id): static + public function id(string $value): static { - return $this->addProperty('id', $id); + return $this->addProperty('id', $value); } /** * The script language. Defaults to painless. * - * @param string $lang + * @param string $value * @return static */ - public function lang(string $lang): static + public function lang(string $value): static { - return $this->addProperty('lang', $lang); + return $this->addProperty('lang', $value); } /** * The inline script source to execute. * - * @param string $source + * @param string $value * @return static */ - public function source(string $source): static + public function source(string $value): static { - return $this->addProperty('source', $source); + return $this->addProperty('source', $value); } /** * Named parameters passed into the script. * - * @param array $params + * @param array $value * @return static */ - public function params(array $params): static + 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 1c2710a..ee708b8 100644 --- a/src/DSL/Queries/Shape.php +++ b/src/DSL/Queries/Shape.php @@ -18,7 +18,7 @@ trait Shape * @param mixed $value * @return $this */ - public function shape($field, $value = null) + public function shape($field, $value = null): static { return $this->addQuery(QShape::create($field, $value)); } diff --git a/src/DSL/Queries/Shape/Shape.php b/src/DSL/Queries/Shape/Shape.php index c0cc763..b1de24e 100644 --- a/src/DSL/Queries/Shape/Shape.php +++ b/src/DSL/Queries/Shape/Shape.php @@ -22,35 +22,35 @@ class Shape extends Node /** * Inline shape definition. Contains the shape type and coordinates. * - * @param mixed $shape + * @param mixed $value * @return static */ - public function shape($shape): static + public function shape($value): static { - return $this->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(string $relation): static + 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): static + 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 c083640..111ef5c 100644 --- a/src/DSL/Queries/Span.php +++ b/src/DSL/Queries/Span.php @@ -26,7 +26,7 @@ trait Span * @param mixed $spanContaining * @return $this */ - public function spanContaining($spanContaining) + public function spanContaining($spanContaining): static { return $this->addQuery(SpanContaining::create($spanContaining)); } @@ -37,7 +37,7 @@ public function spanContaining($spanContaining) * @param mixed $spanFieldMasking * @return $this */ - public function spanFieldMasking($spanFieldMasking) + public function spanFieldMasking($spanFieldMasking): static { return $this->addQuery(SpanFieldMasking::create($spanFieldMasking)); } @@ -48,7 +48,7 @@ public function spanFieldMasking($spanFieldMasking) * @param mixed $spanFirst * @return $this */ - public function spanFirst($spanFirst) + public function spanFirst($spanFirst): static { return $this->addQuery(SpanFirst::create($spanFirst)); } @@ -59,7 +59,7 @@ public function spanFirst($spanFirst) * @param mixed $spanMulti * @return $this */ - public function spanMulti($spanMulti) + public function spanMulti($spanMulti): static { return $this->addQuery(SpanMulti::create($spanMulti)); } @@ -70,7 +70,7 @@ public function spanMulti($spanMulti) * @param mixed $spanNear * @return $this */ - public function spanNear($spanNear) + public function spanNear($spanNear): static { return $this->addQuery(SpanNear::create($spanNear)); } @@ -81,7 +81,7 @@ public function spanNear($spanNear) * @param mixed $spanNot * @return $this */ - public function spanNot($spanNot) + public function spanNot($spanNot): static { return $this->addQuery(SpanNot::create($spanNot)); } @@ -92,7 +92,7 @@ public function spanNot($spanNot) * @param mixed $spanOr * @return $this */ - public function spanOr($spanOr) + public function spanOr($spanOr): static { return $this->addQuery(SpanOr::create($spanOr)); } @@ -104,7 +104,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)); } @@ -115,7 +115,7 @@ public function spanTerm($field, $value = null) * @param mixed $spanWithin * @return $this */ - public function spanWithin($spanWithin) + public function spanWithin($spanWithin): static { return $this->addQuery(SpanWithin::create($spanWithin)); } diff --git a/src/DSL/Queries/Span/SpanContaining.php b/src/DSL/Queries/Span/SpanContaining.php index 7f5e397..d2593fa 100644 --- a/src/DSL/Queries/Span/SpanContaining.php +++ b/src/DSL/Queries/Span/SpanContaining.php @@ -17,22 +17,22 @@ class SpanContaining extends Node /** * The little span query whose matches must be contained within the big span. * - * @param mixed $little + * @param mixed $value * @return static */ - public function little($little): static + public function little($value): static { - return $this->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): static + 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 5be991e..458344d 100644 --- a/src/DSL/Queries/Span/SpanFieldMasking.php +++ b/src/DSL/Queries/Span/SpanFieldMasking.php @@ -17,11 +17,11 @@ class SpanFieldMasking extends Node /** * The inner span query to execute. * - * @param mixed $query + * @param mixed $value * @return static */ - public function query($query): static + public function query($value): static { - return $this->addProperty('query', Query::create($query)); + return $this->addProperty('query', Query::create($value)); } } diff --git a/src/DSL/Queries/Span/SpanFirst.php b/src/DSL/Queries/Span/SpanFirst.php index fc21193..660b048 100644 --- a/src/DSL/Queries/Span/SpanFirst.php +++ b/src/DSL/Queries/Span/SpanFirst.php @@ -17,22 +17,22 @@ class SpanFirst extends Node /** * The inner span query whose matches are restricted. * - * @param mixed $match + * @param mixed $value * @return static */ - public function match($match): static + public function match($value): static { - return $this->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(int $end): static + 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 ebe0ffe..61094f9 100644 --- a/src/DSL/Queries/Span/SpanMulti.php +++ b/src/DSL/Queries/Span/SpanMulti.php @@ -17,11 +17,11 @@ class SpanMulti extends Node /** * The non-span query to wrap as a span query. * - * @param mixed $match + * @param mixed $value * @return static */ - public function match($match): static + public function match($value): static { - return $this->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 c7b49a5..3cf7404 100644 --- a/src/DSL/Queries/Span/SpanNear.php +++ b/src/DSL/Queries/Span/SpanNear.php @@ -20,33 +20,33 @@ class SpanNear extends Node * The list of span query clauses that must appear near each other. * Supports multiple calls to incrementally build. * - * @param mixed $clauses + * @param mixed $value * @return static */ - public function clauses($clauses): static + public function clauses($value): static { - return $this->addClause('clauses', $clauses); + 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(int $slop): static + 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(bool $inOrder): static + 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 785f810..9a7d34f 100644 --- a/src/DSL/Queries/Span/SpanNot.php +++ b/src/DSL/Queries/Span/SpanNot.php @@ -17,55 +17,55 @@ class SpanNot extends Node /** * The span query whose matches are included. * - * @param mixed $include + * @param mixed $value * @return static */ - public function include($include): static + public function include($value): static { - return $this->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): static + 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(int $pre): static + 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(int $post): static + 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(int $dist): static + 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 2da6c7d..1b7152d 100644 --- a/src/DSL/Queries/Span/SpanOr.php +++ b/src/DSL/Queries/Span/SpanOr.php @@ -20,11 +20,11 @@ class SpanOr extends Node * The list of span query clauses to combine. * Supports multiple calls to incrementally build. * - * @param mixed $clauses + * @param mixed $value * @return static */ - public function clauses($clauses): static + public function clauses($value): static { - return $this->addClause('clauses', $clauses); + return $this->addClause('clauses', $value); } } diff --git a/src/DSL/Queries/Span/SpanTerm.php b/src/DSL/Queries/Span/SpanTerm.php index 972f096..1a468e8 100644 --- a/src/DSL/Queries/Span/SpanTerm.php +++ b/src/DSL/Queries/Span/SpanTerm.php @@ -19,12 +19,12 @@ class SpanTerm extends Node /** * The value of the term to match. * - * @param string|int|float|bool $term + * @param string|int|float|bool $value * @return static */ - public function term(string|int|float|bool $term): static + public function term(string|int|float|bool $value): static { - return $this->addProperty('term', $term); + return $this->addProperty('term', $value); } /** diff --git a/src/DSL/Queries/Span/SpanWithin.php b/src/DSL/Queries/Span/SpanWithin.php index 08edb3e..2e63664 100644 --- a/src/DSL/Queries/Span/SpanWithin.php +++ b/src/DSL/Queries/Span/SpanWithin.php @@ -17,22 +17,22 @@ class SpanWithin extends Node /** * The little span query whose matches must fall within the big span. * - * @param mixed $little + * @param mixed $value * @return static */ - public function little($little): static + public function little($value): static { - return $this->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): static + 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 1667af0..ea94d56 100644 --- a/src/DSL/Queries/Specialized.php +++ b/src/DSL/Queries/Specialized.php @@ -24,7 +24,7 @@ trait Specialized * @param mixed $distanceFeature * @return $this */ - public function distanceFeature($distanceFeature) + public function distanceFeature($distanceFeature): static { return $this->addQuery(DistanceFeature::create($distanceFeature)); } @@ -35,7 +35,7 @@ public function distanceFeature($distanceFeature) * @param mixed $moreLikeThis * @return $this */ - public function moreLikeThis($moreLikeThis) + public function moreLikeThis($moreLikeThis): static { return $this->addQuery(MoreLikeThis::create($moreLikeThis)); } @@ -46,7 +46,7 @@ public function moreLikeThis($moreLikeThis) * @param mixed $percolate * @return $this */ - public function percolate($percolate) + public function percolate($percolate): static { return $this->addQuery(Percolate::create($percolate)); } @@ -57,7 +57,7 @@ public function percolate($percolate) * @param mixed $rankFeature * @return $this */ - public function rankFeature($rankFeature) + public function rankFeature($rankFeature): static { return $this->addQuery(RankFeature::create($rankFeature)); } @@ -68,7 +68,7 @@ public function rankFeature($rankFeature) * @param mixed $script * @return $this */ - public function script($script) + public function script($script): static { return $this->addQuery(Script::create($script)); } @@ -79,7 +79,7 @@ public function script($script) * @param mixed $scriptScore * @return $this */ - public function scriptScore($scriptScore) + public function scriptScore($scriptScore): static { return $this->addQuery(ScriptScore::create($scriptScore)); } @@ -90,7 +90,7 @@ public function scriptScore($scriptScore) * @param mixed $wrapper * @return $this */ - public function wrapper($wrapper) + public function wrapper($wrapper): static { return $this->addQuery(Wrapper::create($wrapper)); } @@ -101,7 +101,7 @@ public function wrapper($wrapper) * @param mixed $pinned * @return $this */ - public function pinned($pinned) + public function pinned($pinned): static { return $this->addQuery(Pinned::create($pinned)); } diff --git a/src/DSL/Queries/Specialized/DistanceFeature.php b/src/DSL/Queries/Specialized/DistanceFeature.php index be85c11..7bd8715 100644 --- a/src/DSL/Queries/Specialized/DistanceFeature.php +++ b/src/DSL/Queries/Specialized/DistanceFeature.php @@ -16,22 +16,22 @@ class DistanceFeature extends Node /** * Location or date to use as the origin from which to calculate distance. * - * @param mixed $origin + * @param mixed $value * @return static */ - public function origin($origin): static + public function origin($value): static { - return $this->addProperty('origin', $origin); + return $this->addProperty('origin', $value); } /** * Distance from the origin at which relevance scores receive half of the boost value. * - * @param string $pivot + * @param string $value * @return static */ - public function pivot(string $pivot): static + 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 4c7c6b1..f12b310 100644 --- a/src/DSL/Queries/Specialized/MoreLikeThis.php +++ b/src/DSL/Queries/Specialized/MoreLikeThis.php @@ -16,44 +16,44 @@ class MoreLikeThis extends Node /** * List of fields to use for similarity comparison. * - * @param array $array + * @param array $value * @return static */ - public function fields(array $array): static + 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): static + 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 $int): static + 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 $int): static + 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 61bf1a3..635d058 100644 --- a/src/DSL/Queries/Specialized/Percolate.php +++ b/src/DSL/Queries/Specialized/Percolate.php @@ -16,11 +16,11 @@ class Percolate extends Node /** * The source document to percolate against registered queries. * - * @param mixed $document + * @param mixed $value * @return static */ - public function document($document): static + public function document($value): static { - return $this->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 361bebc..81c11a9 100644 --- a/src/DSL/Queries/Specialized/Pinned.php +++ b/src/DSL/Queries/Specialized/Pinned.php @@ -17,33 +17,33 @@ class Pinned extends Node /** * List of document IDs to pin to the top of the results. * - * @param array $ids + * @param array $value * @return static */ - public function ids(array $ids): static + 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): static + 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): static + 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 011b217..ed77d5b 100644 --- a/src/DSL/Queries/Specialized/RankFeature.php +++ b/src/DSL/Queries/Specialized/RankFeature.php @@ -16,44 +16,44 @@ class RankFeature extends Node /** * Saturation function to compute the score. Uses point: 2 by default. * - * @param mixed $saturation + * @param mixed $value * @return static */ - public function saturation($saturation): static + public function saturation($value): static { - return $this->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): static + 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): static + 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): static + 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 e670d0b..2c5cb2e 100644 --- a/src/DSL/Queries/Specialized/Script.php +++ b/src/DSL/Queries/Specialized/Script.php @@ -16,11 +16,11 @@ class Script extends Node /** * The script to use as the query filter. * - * @param mixed $script + * @param mixed $value * @return static */ - public function script($script): static + 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)); } } diff --git a/src/DSL/Queries/Specialized/ScriptScore.php b/src/DSL/Queries/Specialized/ScriptScore.php index aa9a94e..bcee851 100644 --- a/src/DSL/Queries/Specialized/ScriptScore.php +++ b/src/DSL/Queries/Specialized/ScriptScore.php @@ -17,33 +17,33 @@ class ScriptScore extends Node /** * The base query whose scores will be modified by the script. * - * @param mixed $query + * @param mixed $value * @return static */ - public function query($query): static + public function query($value): static { - return $this->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): static + 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(float $minScore): static + public function minScore(float $value): static { - return $this->addProperty('min_score', Query::create($minScore)); + return $this->addProperty('min_score', Query::create($value)); } } diff --git a/src/DSL/Queries/Specialized/Wrapper.php b/src/DSL/Queries/Specialized/Wrapper.php index 84335df..48c83c7 100644 --- a/src/DSL/Queries/Specialized/Wrapper.php +++ b/src/DSL/Queries/Specialized/Wrapper.php @@ -34,11 +34,11 @@ public static function create($field = null, $value = null): static /** * A query in base64 encoded format. * - * @param string $query + * @param string $value * @return static */ - public function query(string $query): static + 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 946130d..2b1583a 100644 --- a/src/DSL/Queries/TermLevel.php +++ b/src/DSL/Queries/TermLevel.php @@ -34,7 +34,7 @@ trait TermLevel * @param mixed $value * @return $this */ - public function fuzzy($field, $value = null) + public function fuzzy($field, $value = null): static { return $this->addQuery(Fuzzy::create($field, $value)); } @@ -52,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)); } @@ -63,7 +63,7 @@ public function exists($field) * @param mixed $ids * @return $this */ - public function ids($ids) + public function ids($ids): static { if (is_array($ids) && !isset($ids['values'])) { $ids = ['values' => $ids]; @@ -78,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)); } @@ -93,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)); } @@ -105,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)); } @@ -119,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)); } @@ -134,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)); } @@ -151,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)); } @@ -165,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/Fuzzy.php b/src/DSL/Queries/TermLevel/Fuzzy.php index 47301da..33a5fd0 100644 --- a/src/DSL/Queries/TermLevel/Fuzzy.php +++ b/src/DSL/Queries/TermLevel/Fuzzy.php @@ -26,55 +26,55 @@ public function value(string $value): static /** * Maximum edit distance allowed for matching. See Fuzziness for valid values and more information. * - * @param int|string $fuzziness + * @param int|string $value * @return static */ - public function fuzziness(int|string $fuzziness): static + 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(int $maxExpansions): static + 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(int $prefixLength): static + 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(bool $transpositions): static + 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(string $rewrite): static + 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 73efe22..fcab3a4 100644 --- a/src/DSL/Queries/TermLevel/IDs.php +++ b/src/DSL/Queries/TermLevel/IDs.php @@ -16,11 +16,11 @@ class IDs extends Node /** * An array of document IDs. * - * @param array $values + * @param array $value * @return static */ - public function values(array $values): static + 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 6f6305a..eec7485 100644 --- a/src/DSL/Queries/TermLevel/Prefix.php +++ b/src/DSL/Queries/TermLevel/Prefix.php @@ -26,22 +26,22 @@ public function value(string $value): static /** * 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(string $rewrite): static + 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(bool $caseInsensitive): static + 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 9686872..dc2192f 100644 --- a/src/DSL/Queries/TermLevel/Range.php +++ b/src/DSL/Queries/TermLevel/Range.php @@ -18,45 +18,45 @@ class Range extends Node /** * Greater than or equal to. * - * @param string|int|float|bool $gte + * @param string|int|float|bool $value * @return static */ - public function gte(string|int|float|bool $gte): static + public function gte(string|int|float|bool $value): static { - return $this->addProperty('gte', $gte); + return $this->addProperty('gte', $value); } /** * Greater than. * - * @param string|int|float|bool $gt + * @param string|int|float|bool $value * @return static */ - public function gt(string|int|float|bool $gt): static + public function gt(string|int|float|bool $value): static { - return $this->addProperty('gt', $gt); + return $this->addProperty('gt', $value); } /** * Less than or equal to. * - * @param string|int|float|bool $lte + * @param string|int|float|bool $value * @return static */ - public function lte(string|int|float|bool $lte): static + public function lte(string|int|float|bool $value): static { - return $this->addProperty('lte', $lte); + return $this->addProperty('lte', $value); } /** * Less than. * - * @param string|int|float|bool $lt + * @param string|int|float|bool $value * @return static */ - public function lt(string|int|float|bool $lt): static + public function lt(string|int|float|bool $value): static { - return $this->addProperty('lt', $lt); + return $this->addProperty('lt', $value); } /** @@ -68,12 +68,12 @@ public function lt(string|int|float|bool $lt): static * * 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(string $format): static + public function format(string $value): static { - return $this->addProperty('format', $format); + return $this->addProperty('format', $value); } /** @@ -86,22 +86,22 @@ public function format(string $format): static * 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(string $relation): static + 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(string $timeZone): static + 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 f8db581..f7d9a83 100644 --- a/src/DSL/Queries/TermLevel/Regexp.php +++ b/src/DSL/Queries/TermLevel/Regexp.php @@ -30,23 +30,23 @@ public function value(string $value): static /** * 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(string $flags): static + 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(bool $caseInsensitive): static + public function caseInsensitive(bool $value): static { - return $this->addProperty('case_insensitive', $caseInsensitive); + return $this->addProperty('case_insensitive', $value); } /** @@ -56,22 +56,22 @@ public function caseInsensitive(bool $caseInsensitive): static * * 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(int $maxDeterminizedStates): static + 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(string $rewrite): static + 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 1ab8da5..ad4f0e8 100644 --- a/src/DSL/Queries/TermLevel/Term.php +++ b/src/DSL/Queries/TermLevel/Term.php @@ -38,12 +38,12 @@ public function value(string|int|float|bool $value): static * * 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(bool $caseInsensitive): static + 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/TermsSet.php b/src/DSL/Queries/TermLevel/TermsSet.php index 470f82f..1c60488 100644 --- a/src/DSL/Queries/TermLevel/TermsSet.php +++ b/src/DSL/Queries/TermLevel/TermsSet.php @@ -21,12 +21,12 @@ class TermsSet extends Node * * 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(array $terms): static + public function terms(array $value): static { - return $this->addProperty('terms', $terms); + return $this->addProperty('terms', $value); } /** @@ -34,21 +34,21 @@ public function terms(array $terms): static * * For valid values, see minimum_should_match parameter. * - * @param int|string $minimumShouldMatch + * @param int|string $value * @return static */ - public function minimumShouldMatch(int|string $minimumShouldMatch): static + 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(string $field): static + public function minimumShouldMatchField(string $value): static { - return $this->addProperty('minimum_should_match_field', $field); + return $this->addProperty('minimum_should_match_field', $value); } /** @@ -58,11 +58,11 @@ public function minimumShouldMatchField(string $field): static * * 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): static + 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 1efa192..88c6d69 100644 --- a/src/DSL/Queries/TermLevel/Wildcard.php +++ b/src/DSL/Queries/TermLevel/Wildcard.php @@ -15,24 +15,24 @@ class Wildcard extends Node /** * Allows case insensitive matching of the pattern 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(bool $caseInsensitive): static + public function caseInsensitive(bool $value): static { - return $this->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(string $rewrite): static + public function rewrite(string $value): static { - return $this->addProperty('rewrite', $rewrite); + return $this->addProperty('rewrite', $value); } /** @@ -55,11 +55,11 @@ public function value(string $value): static /** * 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(string $wildcard): static + 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 8ce35cd..c6f0d63 100644 --- a/src/DSL/Query.php +++ b/src/DSL/Query.php @@ -51,24 +51,14 @@ class Query extends Node * * @var array */ - protected $_queries = []; + protected array $_queries = []; /** * Aggregation nodes stored independently from type properties. * * @var array */ - protected $_aggregations = []; - - /** - * Whether the query supports multiple clauses. - * - * @return bool - */ - protected function isMulti(): bool - { - return $this->_multi; - } + protected array $_aggregations = []; /** * Initialize the query container. @@ -128,12 +118,12 @@ public function getQueries(): array /** * Add a query clause to the query container. * - * @param mixed $query + * @param mixed $value * @return $this */ - public function addQuery($query): static + public function addQuery($value): static { - $this->_queries[] = $query; + $this->_queries[] = $value; return $this; } @@ -231,8 +221,8 @@ public function toArray(): array $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 !== []; @@ -242,9 +232,9 @@ public function toArray(): array /** * Build the query clause array from stored query clauses. * - * @return array|object + * @return array|object */ - private function buildQuery() + private function buildQuery(): array|object { if (empty($this->_queries)) { return $this->_multi ? (object)[] : []; @@ -277,7 +267,7 @@ private function buildQuery() } if ($this->_multi) { - return $clauses; // @phpstan-ignore return.type + return $clauses; } if (empty($clauses)) { return []; @@ -289,26 +279,27 @@ private function buildQuery() * Build aggregation entries into the DSL array. * * @param array $dsl - * @return void + * @return array */ - private function buildAggs(array &$dsl): void + 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): void + private function buildParams(array $dsl): array { foreach ($this->_params as $key => $value) { if ($value instanceof Query) { @@ -320,5 +311,6 @@ private function buildParams(array &$dsl): void } $dsl[$key] = $value; } + return $dsl; } } diff --git a/src/DSL/Support/ClausesSupport.php b/src/DSL/Support/ClausesSupport.php index 8a45877..7c28048 100644 --- a/src/DSL/Support/ClausesSupport.php +++ b/src/DSL/Support/ClausesSupport.php @@ -22,7 +22,7 @@ trait ClausesSupport * @param mixed $clause * @return static */ - protected function addClause(string $key, $clause) + protected function addClause(string $key, $clause): static { if (!isset($this->_properties[$key])) { $this->_properties[$key] = (new Query())->multi(true); diff --git a/src/DSL/Support/RangeSupport.php b/src/DSL/Support/RangeSupport.php index 723ba39..6a35899 100644 --- a/src/DSL/Support/RangeSupport.php +++ b/src/DSL/Support/RangeSupport.php @@ -29,7 +29,7 @@ public function __construct($field = null, $value = null) * @param array $props * @return array */ - private static function normalizeKeys(array $props) + private static function normalizeKeys(array $props): array { $operators = [ '>=' => 'gte', '>' => 'gt', '<=' => 'lte', '<' => 'lt', From 96d839cdb3e6b25b1bdc785d54968158b914ee93 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Fri, 19 Jun 2026 01:26:51 +0800 Subject: [PATCH 18/70] feat(query): add intervals regexp rule Fill in the missing regexp rule for the intervals query, placed between wildcard and fuzzy to match the ES docs. Add a Regexp leaf (pattern/analyzer/useField). Co-Authored-By: Claude --- src/DSL/Queries/FullText/Intervals.php | 19 ++++++- src/DSL/Queries/FullText/Intervals/Regexp.php | 55 +++++++++++++++++++ tests/FullTextQueriesTest.php | 24 ++++++++ 3 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 src/DSL/Queries/FullText/Intervals/Regexp.php diff --git a/src/DSL/Queries/FullText/Intervals.php b/src/DSL/Queries/FullText/Intervals.php index af4edf0..198f6ec 100644 --- a/src/DSL/Queries/FullText/Intervals.php +++ b/src/DSL/Queries/FullText/Intervals.php @@ -23,12 +23,12 @@ class Intervals extends Node /** * Add a match rule that matches analyzed text. * - * @param mixed $match + * @param mixed $value * @return static */ - public function match($match): static + public function match($value): static { - $this->_intervals[] = Intervals\Match_::create($match); + $this->_intervals[] = Intervals\Match_::create($value); return $this; } @@ -57,6 +57,19 @@ public function wildcard($value): static return $this; } + /** + * Add a regexp rule that matches terms using a regular expression + * pattern. + * + * @param mixed $value + * @return static + */ + public function regexp($value): static + { + $this->_intervals[] = Intervals\Regexp::create($value); + return $this; + } + /** * Add a fuzzy rule that matches terms that are similar to the provided * term, within a defined edit distance. 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/tests/FullTextQueriesTest.php b/tests/FullTextQueriesTest.php index 054e72e..5d07a84 100644 --- a/tests/FullTextQueriesTest.php +++ b/tests/FullTextQueriesTest.php @@ -503,6 +503,30 @@ public function testIntervalsWildcard() $this->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 = << Date: Fri, 19 Jun 2026 01:27:09 +0800 Subject: [PATCH 19/70] refactor(dsl): unify non-leaf parameter naming and types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Following 2e18121 (leaf setters unified to $value), extend the naming convention to the non-leaf layer — Query/Param/Agg/Function — and all builder traits: - Param: 26 single-arg setters use $value; sort() becomes pure append; indices_boost switches to append mode (chained calls accumulate instead of overwriting); fix sort docblock - Agg: $subAggs -> $_subAggs; add type declarations to node/alias/toArray - traits (Compound/Span/Specialized/FullText/Joining/MatchAll/TermLevel/Aggs): single-arg builders unify on $value; Bucket::filter synced - Function_: the second arg of the 4 decay methods becomes $value Co-Authored-By: Claude --- src/DSL/Agg.php | 24 +-- src/DSL/Aggs/Bucket.php | 180 +++++++++--------- src/DSL/Aggs/Metric.php | 48 ++--- src/DSL/Aggs/Pipeline.php | 48 ++--- src/DSL/Node.php | 4 +- src/DSL/Param.php | 172 +++++++++-------- src/DSL/Queries/Compound.php | 46 ++--- .../Queries/Compound/Functions/Function_.php | 24 +-- src/DSL/Queries/FullText.php | 12 +- src/DSL/Queries/FullText/Intervals/AllOf.php | 2 - src/DSL/Queries/FullText/Intervals/AnyOf.php | 2 - src/DSL/Queries/Joining.php | 6 +- src/DSL/Queries/MatchAll.php | 6 +- src/DSL/Queries/Span.php | 48 ++--- src/DSL/Queries/Specialized.php | 48 ++--- src/DSL/Queries/TermLevel.php | 10 +- src/DSL/Query.php | 8 +- tests/ParamsTest.php | 20 ++ 18 files changed, 365 insertions(+), 343 deletions(-) diff --git a/src/DSL/Agg.php b/src/DSL/Agg.php index 7e41b01..fd27e51 100644 --- a/src/DSL/Agg.php +++ b/src/DSL/Agg.php @@ -35,7 +35,7 @@ class Agg * * @var array */ - protected array $subAggs = []; + protected array $_subAggs = []; /** * Properties for array-based aggregation definitions (raw DSL mode). @@ -81,7 +81,7 @@ public static function create($agg = []): static * @param Node $node * @return $this */ - protected function node($node): static + protected function node(Node $node): static { $this->_node = $node; $this->_properties = null; @@ -97,7 +97,7 @@ protected function node($node): static * @param string $value * @return $this */ - public function alias($value): static + public function alias(string $value): static { $this->_alias = $value; return $this; @@ -137,7 +137,7 @@ public function aggs($alias, $aggs = null): static if ($alias !== null) { $aggs->alias($alias); } - $this->subAggs[$alias ?? $aggs->getAlias()] = $aggs; + $this->_subAggs[$alias ?? $aggs->getAlias()] = $aggs; return $this; } @@ -146,17 +146,17 @@ public function aggs($alias, $aggs = null): static if ($alias !== null) { $childAgg->alias($alias); } - $this->subAggs[$alias] = $childAgg; + $this->_subAggs[$alias] = $childAgg; return $this; } - if ($alias !== null && !isset($this->subAggs[$alias])) { - $this->subAggs[$alias] = new Agg(); - $this->subAggs[$alias]->alias($alias); + 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]); + $aggs($this->_subAggs[$alias]); return $this; } @@ -197,7 +197,7 @@ protected function resolveProperties(array $properties): array * * @return array */ - public function toArray() + public function toArray(): array { if ($this->_properties !== null) { $resolved = $this->resolveProperties($this->_properties); @@ -213,9 +213,9 @@ 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(); } } diff --git a/src/DSL/Aggs/Bucket.php b/src/DSL/Aggs/Bucket.php index 99fdb00..7519c2a 100644 --- a/src/DSL/Aggs/Bucket.php +++ b/src/DSL/Aggs/Bucket.php @@ -41,168 +41,168 @@ trait Bucket /** * Groups documents by field values into buckets. * - * @param mixed $params + * @param mixed $value * @return static */ - public function terms($params): static + public function terms($value): static { - return $this->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): static + public function filter($value): static { $instance = new Filter(); - $instance->filter($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): static + 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): static + 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): static + 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): static + 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): static + 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): static + 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): static + 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): static + 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): static + 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): static + 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): static + 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): static + 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): static + 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)); } /** @@ -218,165 +218,165 @@ public function global(): static /** * Groups documents into buckets by numeric interval. * - * @param mixed $params + * @param mixed $value * @return static */ - public function histogram($params): static + 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): static + 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): static + 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): static + 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): static + 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): static + 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): static + public function parent($value): static { - return $this->node(Parent_::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): static + 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): static + 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): static + 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 = []): static + 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): static + 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): static + 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): static + 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): static + 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/Metric.php b/src/DSL/Aggs/Metric.php index e86c25f..30a91ff 100644 --- a/src/DSL/Aggs/Metric.php +++ b/src/DSL/Aggs/Metric.php @@ -18,88 +18,88 @@ trait Metric /** * Computes the average of numeric values from a field. * - * @param mixed $params + * @param mixed $value * @return static */ - public function avg($params): static + public function avg($value): static { - return $this->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): static + 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): static + 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): static + 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): static + 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): static + 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): static + 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): static + 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/Pipeline.php b/src/DSL/Aggs/Pipeline.php index d3ea21c..34d7a74 100644 --- a/src/DSL/Aggs/Pipeline.php +++ b/src/DSL/Aggs/Pipeline.php @@ -18,88 +18,88 @@ trait Pipeline /** * Computes the average of a metric across sibling aggregation buckets. * - * @param mixed $params + * @param mixed $value * @return static */ - public function avgBucket($params): static + public function avgBucket($value): static { - return $this->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): static + 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): static + 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): static + 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): static + 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): static + 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): static + 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 + * @param mixed $value * @return static */ - public function bucketScript($params): static + public function bucketScript($value): static { - return $this->node(BucketScript::create(is_string($params) ? ['buckets_path' => $params] : $params)); + return $this->node(BucketScript::create(is_string($value) ? ['buckets_path' => $value] : $value)); } } diff --git a/src/DSL/Node.php b/src/DSL/Node.php index 13db4cb..e47e4af 100644 --- a/src/DSL/Node.php +++ b/src/DSL/Node.php @@ -185,6 +185,8 @@ protected function multi(bool $multi): static /** * Get the Elasticsearch type identifier. * + * @internal + * * @return string */ public function key(): string @@ -216,7 +218,7 @@ public function field($field): static * @param bool $append * @return static */ - public function addProperty($attribute, $value, $append = false): static + protected function addProperty($attribute, $value, $append = false): static { if ($append) { $this->_properties[$attribute][] = $value; diff --git a/src/DSL/Param.php b/src/DSL/Param.php index f5efce6..44eee44 100644 --- a/src/DSL/Param.php +++ b/src/DSL/Param.php @@ -31,12 +31,12 @@ public function hasParam($key): bool * Defines the maximum number of documents to return. * Defaults to 10. * - * @param int $size + * @param int $value * @return $this */ - public function size($size): static + public function size($value): static { - $this->_params['size'] = $size; + $this->_params['size'] = $value; return $this; } @@ -44,12 +44,12 @@ public function size($size): static * The starting document offset. * Defaults to 0. * - * @param int $from + * @param int $value * @return $this */ - public function from($from): static + public function from($value): static { - $this->_params['from'] = $from; + $this->_params['from'] = $value; return $this; } @@ -57,12 +57,12 @@ public function from($from): static * 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): static + public function timeout($value): static { - $this->_params['timeout'] = $timeout; + $this->_params['timeout'] = $value; return $this; } @@ -70,12 +70,12 @@ public function timeout($timeout): static * 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): static + public function minScore($value): static { - $this->_params['min_score'] = $minScore; + $this->_params['min_score'] = $value; return $this; } @@ -83,12 +83,12 @@ public function minScore($minScore): static * 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): static + public function terminateAfter($value): static { - $this->_params['terminate_after'] = $terminateAfter; + $this->_params['terminate_after'] = $value; return $this; } @@ -96,12 +96,12 @@ public function terminateAfter($terminateAfter): static * 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): static + public function explain($value): static { - $this->_params['explain'] = $explain; + $this->_params['explain'] = $value; return $this; } @@ -109,24 +109,24 @@ public function explain($explain): static * If true, returns document version as part * of a hit. * - * @param bool $version + * @param bool $value * @return $this */ - public function version($version): static + 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): static + public function profile($value): static { - $this->_params['profile'] = $profile; + $this->_params['profile'] = $value; return $this; } @@ -134,12 +134,12 @@ public function profile($profile): static * 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): static + public function trackTotalHits($value): static { - $this->_params['track_total_hits'] = $trackTotalHits; + $this->_params['track_total_hits'] = $value; return $this; } @@ -147,24 +147,26 @@ public function trackTotalHits($trackTotalHits): static * 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): static + 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): static @@ -172,7 +174,9 @@ 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; } @@ -183,24 +187,24 @@ public function sort($field, $order = null): static * Indicates which source fields are returned * for the search hits. * - * @param array|string $source + * @param array|string $value * @return $this */ - public function source($source): static + 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): static + public function searchAfter($value): static { - $this->_params['search_after'] = $searchAfter; + $this->_params['search_after'] = $value; return $this; } @@ -208,37 +212,37 @@ public function searchAfter($searchAfter): static * Controls which stored fields are returned * as part of a hit. * - * @param array $storedFields + * @param array $value * @return $this */ - public function storedFields($storedFields): static + 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): static + 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): static + public function indicesBoost($value): static { - $this->_params['indices_boost'] = [$indicesBoost]; + $this->_params['indices_boost'][] = $value; return $this; } @@ -246,12 +250,12 @@ public function indicesBoost($indicesBoost): static * 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): static + public function trackScores($value): static { - $this->_params['track_scores'] = $trackScores; + $this->_params['track_scores'] = $value; return $this; } @@ -259,24 +263,24 @@ public function trackScores($trackScores): static * 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): static + 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): static + public function pit($value): static { - $this->_params['pit'] = $pit; + $this->_params['pit'] = $value; return $this; } @@ -284,36 +288,36 @@ public function pit($pit): static * 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): static + public function postFilter($value): static { - $this->_params['post_filter'] = Query::create($filter); + $this->_params['post_filter'] = Query::create($value); return $this; } /** * Collapse search results by field value. * - * @param mixed $collapse + * @param mixed $value * @return $this */ - public function collapse($collapse): static + public function collapse($value): static { - $this->_params['collapse'] = Params\Collapse::create($collapse); + $this->_params['collapse'] = Params\Collapse::create($value); return $this; } /** * Rescore the top documents with a secondary query. * - * @param mixed $rescore + * @param mixed $value * @return $this */ - public function rescore($rescore): static + public function rescore($value): static { - $this->_params['rescore'] = Params\Rescore::create($rescore); + $this->_params['rescore'] = Params\Rescore::create($value); return $this; } @@ -321,12 +325,12 @@ public function rescore($rescore): static * 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): static + 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 @@ -338,9 +342,9 @@ public function highlight($highlight): static } // 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); } } } @@ -354,36 +358,36 @@ public function highlight($highlight): static /** * Search suggestions based on term, completion, or phrase. * - * @param mixed $suggest + * @param mixed $value * @return $this */ - public function suggest($suggest): static + 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): static + 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): static + public function runtimeMappings($value): static { - $this->_params['runtime_mappings'] = $runtimeMappings; + $this->_params['runtime_mappings'] = $value; return $this; } diff --git a/src/DSL/Queries/Compound.php b/src/DSL/Queries/Compound.php index 1b804ea..90e92d8 100644 --- a/src/DSL/Queries/Compound.php +++ b/src/DSL/Queries/Compound.php @@ -25,14 +25,14 @@ trait Compound * * @example $query->bool(function (Boolean $b) { $b->must(function (Query $q) { $q->match('title', 'test') }) }) * - * @param callable|Boolean|array $bool + * @param callable|Boolean|array $value * @return $this */ - public function bool($bool): static + public function bool($value): static { - if (is_array($bool)) { + if (is_array($value)) { $boolean = new Boolean(); - foreach ($bool as $clause => $val) { + foreach ($value as $clause => $val) { $method = $clause === 'must_not' ? 'mustNot' : $clause; if ($val instanceof \Closure || $val instanceof Query) { $boolean->$method($val); @@ -42,20 +42,20 @@ public function bool($bool): static } return $this->addQuery($boolean); } - return $this->addQuery(Boolean::create($bool)); + return $this->addQuery(Boolean::create($value)); } /** * Add a boosting query. * - * @param callable|Boosting|array $boosting + * @param callable|Boosting|array $value * @return $this */ - public function boosting($boosting): static + public function boosting($value): static { - if (is_array($boosting)) { + if (is_array($value)) { $b = new Boosting(); - foreach ($boosting as $key => $val) { + foreach ($value as $key => $val) { if (($key === 'positive' || $key === 'negative') && ($val instanceof \Closure || $val instanceof Query)) { $b->$key($val); @@ -65,20 +65,20 @@ public function boosting($boosting): static } return $this->addQuery($b); } - return $this->addQuery(Boosting::create($boosting)); + return $this->addQuery(Boosting::create($value)); } /** * Add a constant_score query. * - * @param callable|ConstantScore|array $constantScore + * @param callable|ConstantScore|array $value * @return $this */ - public function constantScore($constantScore): static + public function constantScore($value): static { - if (is_array($constantScore)) { + if (is_array($value)) { $cs = new ConstantScore(); - foreach ($constantScore as $key => $val) { + foreach ($value as $key => $val) { if ($key === 'filter' && ($val instanceof \Closure || $val instanceof Query)) { $cs->filter($val); } else { @@ -87,20 +87,20 @@ public function constantScore($constantScore): static } return $this->addQuery($cs); } - return $this->addQuery(ConstantScore::create($constantScore)); + return $this->addQuery(ConstantScore::create($value)); } /** * Add a dis_max query. * - * @param callable|DisjunctionMax|array $disMax + * @param callable|DisjunctionMax|array $value * @return $this */ - public function disMax($disMax): static + public function disMax($value): static { - if (is_array($disMax)) { + if (is_array($value)) { $dm = new DisjunctionMax(); - foreach ($disMax as $key => $val) { + foreach ($value as $key => $val) { if ($key === 'queries' && ($val instanceof \Closure || $val instanceof Query)) { $dm->queries($val); } else { @@ -109,17 +109,17 @@ public function disMax($disMax): static } return $this->addQuery($dm); } - return $this->addQuery(DisjunctionMax::create($disMax)); + return $this->addQuery(DisjunctionMax::create($value)); } /** * Add a function_score query. * - * @param mixed $functionScore + * @param mixed $value * @return $this */ - public function functionScore($functionScore): static + public function functionScore($value): static { - return $this->addQuery(FunctionScore::create($functionScore)); + return $this->addQuery(FunctionScore::create($value)); } } diff --git a/src/DSL/Queries/Compound/Functions/Function_.php b/src/DSL/Queries/Compound/Functions/Function_.php index bb203fa..dfbdbdb 100644 --- a/src/DSL/Queries/Compound/Functions/Function_.php +++ b/src/DSL/Queries/Compound/Functions/Function_.php @@ -75,48 +75,48 @@ public function script($value): static * 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): static + 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)); } /** * 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): static + public function gauss($field, $value = null): static { - return $this->addProperty('gauss', Gauss::create($field, $gauss)); + return $this->addProperty('gauss', Gauss::create($field, $value)); } /** * 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): static + public function linear($field, $value = null): static { - return $this->addProperty('linear', Linear::create($field, $linear)); + return $this->addProperty('linear', Linear::create($field, $value)); } /** * 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): static + 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/FullText.php b/src/DSL/Queries/FullText.php index 298d46a..7056c9b 100644 --- a/src/DSL/Queries/FullText.php +++ b/src/DSL/Queries/FullText.php @@ -108,22 +108,22 @@ public function combinedFields($value): static /** * Add a query_string query. * - * @param mixed $queryString + * @param mixed $value * @return $this */ - public function queryString($queryString): static + 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): static + public function simpleQueryString($value): static { - return $this->addQuery(SimpleQueryString::create($simpleQueryString)); + return $this->addQuery(SimpleQueryString::create($value)); } } diff --git a/src/DSL/Queries/FullText/Intervals/AllOf.php b/src/DSL/Queries/FullText/Intervals/AllOf.php index 030d0c9..c139ace 100644 --- a/src/DSL/Queries/FullText/Intervals/AllOf.php +++ b/src/DSL/Queries/FullText/Intervals/AllOf.php @@ -43,8 +43,6 @@ public function addInterval($value): static $target = $this->_properties['intervals']; if ($value instanceof \Closure) { $value($target); - } elseif ($value instanceof Node) { - $target->addQuery($value); } return $this; } diff --git a/src/DSL/Queries/FullText/Intervals/AnyOf.php b/src/DSL/Queries/FullText/Intervals/AnyOf.php index 77408ee..b5fd8e9 100644 --- a/src/DSL/Queries/FullText/Intervals/AnyOf.php +++ b/src/DSL/Queries/FullText/Intervals/AnyOf.php @@ -42,8 +42,6 @@ public function addInterval($value): static $target = $this->_properties['intervals']; if ($value instanceof \Closure) { $value($target); - } elseif ($value instanceof Node) { - $target->addQuery($value); } return $this; } diff --git a/src/DSL/Queries/Joining.php b/src/DSL/Queries/Joining.php index 98e3446..caf8527 100644 --- a/src/DSL/Queries/Joining.php +++ b/src/DSL/Queries/Joining.php @@ -73,11 +73,11 @@ public function hasParent($type, $query = null): static /** * Add a parent_id query. * - * @param mixed $parentId + * @param mixed $value * @return $this */ - public function parentId($parentId): static + public function parentId($value): static { - return $this->addQuery(ParentId::create($parentId)); + return $this->addQuery(ParentId::create($value)); } } diff --git a/src/DSL/Queries/MatchAll.php b/src/DSL/Queries/MatchAll.php index 74e4605..4f0af23 100644 --- a/src/DSL/Queries/MatchAll.php +++ b/src/DSL/Queries/MatchAll.php @@ -15,12 +15,12 @@ trait MatchAll /** * Add a match_all query. * - * @param mixed $matchAll + * @param mixed $value * @return $this */ - public function matchAll($matchAll = null): static + public function matchAll($value = null): static { - return $this->addQuery(QMatchAll::create($matchAll)); + return $this->addQuery(QMatchAll::create($value)); } /** diff --git a/src/DSL/Queries/Span.php b/src/DSL/Queries/Span.php index 111ef5c..d20e19a 100644 --- a/src/DSL/Queries/Span.php +++ b/src/DSL/Queries/Span.php @@ -23,78 +23,78 @@ trait Span /** * Add a span_containing query. * - * @param mixed $spanContaining + * @param mixed $value * @return $this */ - public function spanContaining($spanContaining): static + public function spanContaining($value): static { - return $this->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): static + 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): static + 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): static + 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 $value * @return $this */ - public function spanNear($spanNear): static + public function spanNear($value): static { - return $this->addQuery(SpanNear::create($spanNear)); + return $this->addQuery(SpanNear::create($value)); } /** * Add a span_not query. * - * @param mixed $spanNot + * @param mixed $value * @return $this */ - public function spanNot($spanNot): static + 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 $value * @return $this */ - public function spanOr($spanOr): static + public function spanOr($value): static { - return $this->addQuery(SpanOr::create($spanOr)); + return $this->addQuery(SpanOr::create($value)); } /** @@ -112,11 +112,11 @@ public function spanTerm($field, $value = null): static /** * Add a span_within query. * - * @param mixed $spanWithin + * @param mixed $value * @return $this */ - public function spanWithin($spanWithin): static + public function spanWithin($value): static { - return $this->addQuery(SpanWithin::create($spanWithin)); + return $this->addQuery(SpanWithin::create($value)); } } diff --git a/src/DSL/Queries/Specialized.php b/src/DSL/Queries/Specialized.php index ea94d56..0bdcb10 100644 --- a/src/DSL/Queries/Specialized.php +++ b/src/DSL/Queries/Specialized.php @@ -21,88 +21,88 @@ trait Specialized /** * Add a distance_feature query. * - * @param mixed $distanceFeature + * @param mixed $value * @return $this */ - public function distanceFeature($distanceFeature): static + public function distanceFeature($value): static { - return $this->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): static + 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): static + 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): static + 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): static + 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): static + 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): static + 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): static + public function pinned($value): static { - return $this->addQuery(Pinned::create($pinned)); + return $this->addQuery(Pinned::create($value)); } } diff --git a/src/DSL/Queries/TermLevel.php b/src/DSL/Queries/TermLevel.php index 2b1583a..6f1e34c 100644 --- a/src/DSL/Queries/TermLevel.php +++ b/src/DSL/Queries/TermLevel.php @@ -60,15 +60,15 @@ public function exists($field): static /** * 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): static + 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)); } /** diff --git a/src/DSL/Query.php b/src/DSL/Query.php index c6f0d63..24eaf55 100644 --- a/src/DSL/Query.php +++ b/src/DSL/Query.php @@ -118,12 +118,12 @@ public function getQueries(): array /** * Add a query clause to the query container. * - * @param mixed $value + * @param mixed $clause * @return $this */ - public function addQuery($value): static + public function addQuery($clause): static { - $this->_queries[] = $value; + $this->_queries[] = $clause; return $this; } @@ -135,7 +135,7 @@ public function addQuery($value): static * @param mixed $default * @return $this */ - public function when($condition, $query, $default = null): static + public function when(bool|callable $condition, $query, $default = null): static { $truthy = is_callable($condition) ? $condition() : $condition; diff --git a/tests/ParamsTest.php b/tests/ParamsTest.php index e26e5f9..bb22f33 100644 --- a/tests/ParamsTest.php +++ b/tests/ParamsTest.php @@ -317,6 +317,26 @@ public function testIndicesBoost() $this->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 = << Date: Fri, 19 Jun 2026 14:13:02 +0800 Subject: [PATCH 20/70] refactor(index): unify named parameters and type Rebuild::source - $name -> $connection (ClientManager::set / Index::setClient) - $newName -> $newIndex (Rebuild, aligning with the Index pattern) - $document -> $data (index/save/create in Doc/Bulk; unify document payload naming across the library) - add callable|iterable parameter type to Rebuild::source; fix the \Iterator docblock typo Co-Authored-By: Claude --- CLAUDE.md | 2 +- src/Index/Bulk.php | 18 +++++++++--------- src/Index/ClientManager.php | 6 +++--- src/Index/Doc.php | 18 +++++++++--------- src/Index/Index.php | 6 +++--- src/Index/Rebuild.php | 38 ++++++++++++++++++------------------- 6 files changed, 44 insertions(+), 44 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 75b1dc9..cec014b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,7 +47,7 @@ PSR-5 规范。 - [x] **移除 Index::insert()**:等价功能由 Doc::save() 覆盖 - [x] **实例入口**:新增 on()/newQuery()/newDoc()/setConnection()/getConnection(),query()/doc() 委托实例方法 - [x] **测试 mock 污染修复**:所有测试文件补 tearDown 清理静态状态 -- [ ] **命名参数一致性**:公开方法参数名是 API 的一部分,全库审查确保命名统一(如 connection/name/client 不混用) +- [x] **命名参数一致性**:公开方法参数名是 API 的一部分,全库审查确保命名统一(如 connection/name/client 不混用) - [x] **PHP 8 现代化(Index 层)**:全 12 文件 strict_types + 构造器提升 + readonly + 属性/返回/参数类型 + 联合类型;callable 属性(resolver / errorHandler / dataSource)因 PHP 禁止 callable 作属性类型,保留 docblock - [x] **PHP 8 现代化(DSL 层)**:Node/Query/Agg + 122 leaf 类全部完成。strict_types 全 151 文件;4 原子属性类型同步全 leaf;leaf 参数类型对照 ES docblock 完成;`$_properties` 三模式统一为 `?array`(新增 `$_raw` 承接整体透传,null 保留为合法空态);`toJson` 加 `:string` + false 检查;`toArray` 因多态返回(array/stdClass/null)不强加 PHP 返回类型 - [ ] **Rebuild 异常处理**:run() 改为 try-catch + releaseLock 分离,releaseLock 不吞任何异常,forceUnlock 单独处理 404(文档不存在 vs 索引不存在的 404 需区分);isLocked() 只吞 404 不吞其他 ClientResponseException;rebuild 失败优先抛原始异常;ensureLockIndex() replicas=0 在多节点集群有风险需注释说明 diff --git a/src/Index/Bulk.php b/src/Index/Bulk.php index 470af0e..f5c2ed3 100644 --- a/src/Index/Bulk.php +++ b/src/Index/Bulk.php @@ -111,17 +111,17 @@ public function retryOnConflict(int $count): static * 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(string|int|null $id, array $document): static + 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; @@ -131,25 +131,25 @@ public function index(string|int|null $id, array $document): static * 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(string|int|null $id, array $document): static + 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 array $data * @return $this */ - public function create(string|int $id, array $document): static + public function create(string|int $id, array $data): static { $this->body[] = ['create' => ['_index' => $this->resolveIndex(), '_id' => $id]]; - $this->body[] = $document; + $this->body[] = $data; $this->afterPush(); return $this; diff --git a/src/Index/ClientManager.php b/src/Index/ClientManager.php index ae62175..443ef51 100644 --- a/src/Index/ClientManager.php +++ b/src/Index/ClientManager.php @@ -21,12 +21,12 @@ class ClientManager * Register an Elasticsearch client. Optionally name the connection. * * @param ClientInterface $client - * @param string $name connection name, defaults to 'default' + * @param string $connection connection name, defaults to 'default' * @return void */ - public static function set(ClientInterface $client, string $name = 'default'): void + public static function set(ClientInterface $client, string $connection = 'default'): void { - self::$clients[$name] = $client; + self::$clients[$connection] = $client; } /** diff --git a/src/Index/Doc.php b/src/Index/Doc.php index dd8d2fd..3ab8178 100644 --- a/src/Index/Doc.php +++ b/src/Index/Doc.php @@ -154,10 +154,10 @@ public function update(array $data, bool $upsert = false): array * * If $id is null or empty, ES auto-generates an id. * - * @param array $document + * @param array $data * @return array */ - public function index(array $document): array + public function index(array $data): array { $params = [ 'index' => $this->index->name(), @@ -167,7 +167,7 @@ public function index(array $document): array $params['id'] = $this->id; } - $params['body'] = $document; + $params['body'] = $data; if ($this->refresh !== null) { $params['refresh'] = $this->refresh; @@ -181,12 +181,12 @@ public function index(array $document): array /** * Alias for index(). Create or overwrite the document. * - * @param array $document + * @param array $data * @return array */ - public function save(array $document): array + public function save(array $data): array { - return $this->index($document); + return $this->index($data); } /** @@ -195,10 +195,10 @@ public function save(array $document): array * If $id is null or empty, ES auto-generates an id (always a create, since * auto-generated ids are unique). * - * @param array $document + * @param array $data * @return array */ - public function create(array $document): array + public function create(array $data): array { $params = [ 'index' => $this->index->name(), @@ -208,7 +208,7 @@ public function create(array $document): array $params['id'] = $this->id; } - $params['body'] = $document; + $params['body'] = $data; $params['op_type'] = 'create'; if ($this->refresh !== null) { diff --git a/src/Index/Index.php b/src/Index/Index.php index a7c9c98..35426e0 100644 --- a/src/Index/Index.php +++ b/src/Index/Index.php @@ -50,12 +50,12 @@ abstract class Index * Register an Elasticsearch client. Optionally name the connection. * * @param ClientInterface $client - * @param string $name connection name, defaults to 'default' + * @param string $connection connection name, defaults to 'default' * @return void */ - public static function setClient(ClientInterface $client, string $name = 'default'): void + public static function setClient(ClientInterface $client, string $connection = 'default'): void { - ClientManager::set($client, $name); + ClientManager::set($client, $connection); } /** diff --git a/src/Index/Rebuild.php b/src/Index/Rebuild.php index 7979d3d..81f2be3 100644 --- a/src/Index/Rebuild.php +++ b/src/Index/Rebuild.php @@ -37,7 +37,7 @@ class Rebuild private bool $allowEmpty = false; /** - * @var callable|\Iterator>|null + * @var callable|iterable>|null */ private $dataSource; @@ -90,10 +90,10 @@ public function allowEmpty(bool $allow = true): static * 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): static + public function source(callable|iterable $source): static { $this->dataSource = $source; return $this; @@ -212,16 +212,16 @@ private function doRun(array $context): array EventDispatcher::dispatch(new Event('rebuild.run.before', $name)); - $newName = $this->createIndex(); + $newIndex = $this->createIndex(); try { - $this->import($newName, $context); + $this->import($newIndex, $context); } catch (\Throwable $e) { - $client->delete(['index' => $newName]); + $client->delete(['index' => $newIndex]); throw $e; } - $client->refresh(['index' => $newName]); + $client->refresh(['index' => $newIndex]); $oldIndex = null; @@ -232,25 +232,25 @@ private function doRun(array $context): array foreach ($oldIndices as $idx) { $actions[] = ['remove' => ['index' => $idx, 'alias' => $name]]; } - $actions[] = ['add' => ['index' => $newName, 'alias' => $name]]; + $actions[] = ['add' => ['index' => $newIndex, 'alias' => $name]]; $client->updateAliases(['body' => ['actions' => $actions]]); } elseif ($client->exists(['index' => $name])->asBool()) { - $client->delete(['index' => $newName]); + $client->delete(['index' => $newIndex]); 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' => $newName, 'name' => $name]); + $client->putAlias(['index' => $newIndex, 'name' => $name]); } $e = new Event('rebuild.run.after', $name); - $e->newIndex = $newName; + $e->newIndex = $newIndex; $e->oldIndex = $oldIndex; EventDispatcher::dispatch($e); - return ['newIndex' => $newName, 'oldIndex' => $oldIndex]; + return ['newIndex' => $newIndex, 'oldIndex' => $oldIndex]; } /** @@ -340,28 +340,28 @@ private function ensureLockIndex(): void */ 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(string $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; @@ -369,7 +369,7 @@ protected function import(string $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); } @@ -388,7 +388,7 @@ protected function import(string $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." ); } From b64de5ad0b7c6d7956e521d37efda53f5a490b42 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Fri, 19 Jun 2026 14:46:32 +0800 Subject: [PATCH 21/70] refactor(index)!: split chunk/cursor iteration API - the old cursor() (yielding Results per batch) is renamed chunk() - add cursor(): yields each full hit (_id/_score/_source), flattening chunk and reusing its scroll cleanup - keep the low-level scroll/next/clear; the backend may switch to PIT+search_after later with upper-level signatures unchanged - sync README/docs/IndexTest **BC:** cursor() return changes from Generator (per batch) to Generator (per hit) Co-Authored-By: Claude --- README.md | 12 ++++++--- docs/guide.md | 6 ++--- docs/index.md | 18 ++++++++++--- src/Index/Search.php | 27 +++++++++++++++++--- tests/Index/IndexTest.php | 53 +++++++++++++++++++++++++++++++++++++-- 5 files changed, 100 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 29d9ed8..d972849 100644 --- a/README.md +++ b/README.md @@ -178,12 +178,18 @@ $results->lastPage(); $results->items(); $results->toPaginator(); // 转为框架分页器(需注册 Paginator Resolver) -// 游标遍历(大批量导出) -foreach (ProductIndex::query()->cursor() as $batch) { - foreach ($batch->docs() as $doc) { +// 分批遍历(大批量导出/批处理,每次 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']; + // ... +} ``` ### 文档 CRUD diff --git a/docs/guide.md b/docs/guide.md index 5cd09ff..3ca10a5 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -160,7 +160,7 @@ public function index(Request $request) } ``` -> 条件用 `if` 逐个判断,只有传了值才加查询。`should()` 实现 OR 搜索。深分页场景用 `cursor()` 替代 `paginate()`。 +> 条件用 `if` 逐个判断,只有传了值才加查询。`should()` 实现 OR 搜索。深分页场景用 `chunk()`(按批)或 `cursor()`(逐条)替代 `paginate()`。 运营还要导出筛选结果到 Excel。ES 默认 `max_result_window = 10000`,`from/size` 翻不到后面的数据,用 `cursor()` 基于 scroll 遍历: @@ -169,8 +169,8 @@ public function export(array $filters) { $search = static::searchOrders($filters)->sort('created_at', 'desc'); - foreach ($search->cursor() as $batch) { - foreach ($batch->docs() as $doc) { + foreach ($search->chunk() as $results) { + foreach ($results->docs() as $doc) { // 写入 Excel } } diff --git a/docs/index.md b/docs/index.md index 34535b0..c1811bd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -116,17 +116,27 @@ while (count($results->docs()) > 0) { ProductIndex::query()->clear($scrollId); ``` -## Cursor +## Chunk / Cursor -Cursor 把 scroll 封装成 PHP 生成器: +把 scroll 封装成 PHP 生成器,scroll 自动清理。 + +**chunk** 按批遍历,每次 yield 一个 Results(含 docs/hits/total 等): ```php -foreach (ProductIndex::query()->cursor() as $results) { +foreach (ProductIndex::query()->chunk() as $results) { foreach ($results->docs() as $doc) { // 处理 } } -// scroll 自动清理 +``` + +**cursor** 逐条遍历,每次 yield 一个完整 hit(_id/_score/_source): + +```php +foreach (ProductIndex::query()->cursor() as $hit) { + $doc = $hit['_source']; + $id = $hit['_id']; +} ``` ## 文档 CRUD diff --git a/src/Index/Search.php b/src/Index/Search.php index 88b78c8..83cf276 100644 --- a/src/Index/Search.php +++ b/src/Index/Search.php @@ -210,12 +210,13 @@ protected function doScroll(string $scrollId, string $duration): Results } /** - * 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(string $duration = '5m'): \Generator + public function chunk(string $duration = '5m'): \Generator { $results = $this->scroll(null, $duration); @@ -229,6 +230,24 @@ public function cursor(string $duration = '5m'): \Generator } } + /** + * 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) { + foreach ($results->hits() as $hit) { + yield $hit; + } + } + } + /** * Execute a paginated search. Uses pageResolver if no arguments are given. * diff --git a/tests/Index/IndexTest.php b/tests/Index/IndexTest.php index f6cb55e..2c2b325 100644 --- a/tests/Index/IndexTest.php +++ b/tests/Index/IndexTest.php @@ -379,7 +379,7 @@ public function testClearSkipsWhenNoScrollId() $index->query()->clear($results); } - public function testCursorYieldsResultsBatches() + public function testChunkYieldsResultsBatches() { $client = $this->createMock(TestClient::class); @@ -417,7 +417,7 @@ public function testCursorYieldsResultsBatches() $index = $this->createIndex('products'); $batches = []; - foreach ($index->query()->matchAll()->cursor('1m') as $results) { + foreach ($index->query()->matchAll()->chunk('1m') as $results) { $this->assertInstanceOf(Results::class, $results); $batches[] = $results; } @@ -428,6 +428,55 @@ public function testCursorYieldsResultsBatches() $this->assertEquals(['3'], $batches[1]->ids()); } + public function testCursorYieldsIndividualHits() + { + $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'); + $hits = []; + foreach ($index->query()->matchAll()->cursor('1m') as $hit) { + $hits[] = $hit; + } + + // 3 docs flattened across 2 batches, each yielded as a raw hit + $this->assertCount(3, $hits); + $this->assertSame('1', $hits[0]['_id']); + $this->assertSame(['id' => 1], $hits[0]['_source']); + $this->assertSame('3', $hits[2]['_id']); + } + public function testNameReturnsIndexName() { $index = $this->createIndex('orders'); From c518cb79adffe0308a169209b5937705d957f463 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Fri, 19 Jun 2026 14:51:03 +0800 Subject: [PATCH 22/70] refactor(index): cursor delegates iteration via yield from Co-Authored-By: Claude --- src/Index/Search.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Index/Search.php b/src/Index/Search.php index 83cf276..e5f9e7f 100644 --- a/src/Index/Search.php +++ b/src/Index/Search.php @@ -242,9 +242,7 @@ public function chunk(string $duration = '5m'): \Generator public function cursor(string $duration = '5m'): \Generator { foreach ($this->chunk($duration) as $results) { - foreach ($results->hits() as $hit) { - yield $hit; - } + yield from $results->hits(); } } From 1c8d9d00cf1fc5ff3ef0a090e8dc1c4b4f3f3935 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Tue, 23 Jun 2026 22:42:18 +0800 Subject: [PATCH 23/70] refactor(index)!: tighten Rebuild exception handling - run() splits try-catch: failures throw doRun's original exception first (lock-release failure is not masked); if lock release fails after success, throw a RuntimeException (noting the rebuild completed and forceUnlock is needed) - releaseLock drops the \Throwable catch-all and swallows only 404 - forceUnlock is implemented independently and swallows 404 (idempotent success whether the index or doc is missing) - isLocked returns false only on 404; other ES errors propagate **BC:** forceUnlock/isLocked no longer swallow non-404 ES errors; run() throws on the success path when lock release fails (previously swallowed silently) Co-Authored-By: Claude --- phpstan-baseline.neon | 2 +- src/Index/Rebuild.php | 59 ++++++++++++++++--- tests/Index/RebuildTest.php | 110 ++++++++++++++++++++++++++++++++++++ 3 files changed, 161 insertions(+), 10 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 1321b33..f5e9a20 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -75,7 +75,7 @@ parameters: - message: '#^Call to an undefined method Elastic\\Elasticsearch\\ClientInterface\:\:delete\(\)\.$#' identifier: method.notFound - count: 1 + count: 2 path: src/Index/Rebuild.php - diff --git a/src/Index/Rebuild.php b/src/Index/Rebuild.php index 81f2be3..b032810 100644 --- a/src/Index/Rebuild.php +++ b/src/Index/Rebuild.php @@ -13,6 +13,8 @@ * * $name is always the app-facing name. After rebuild, $name becomes an alias * pointing to a backing index generated by rebuildName(). + * + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ class Rebuild { @@ -107,6 +109,10 @@ public function source(callable|iterable $source): static * - $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} */ @@ -116,8 +122,25 @@ public function run(array $context = []): array try { $result = $this->doRun($context); - } finally { + } catch (\Throwable $e) { + try { + $this->releaseLock(); + } catch (\Throwable) { + // Ignore release failure; the original exception propagates below. + } + throw $e; + } + + // Success: data is swapped, so a release failure means only the lock is stale. + try { $this->releaseLock(); + } catch (\Throwable $e) { + throw new RuntimeException( + "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 + ); } return $result; @@ -128,15 +151,29 @@ public function run(array $context = []): array * * 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 { - $this->releaseLock(); + 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 @@ -147,7 +184,10 @@ public function isLocked(): bool 'id' => $this->index->name(), ])->asBool(); } catch (ClientResponseException $e) { - return false; + if ($e->getResponse()->getStatusCode() === 404) { + return false; + } + throw $e; } } @@ -283,7 +323,10 @@ private function acquireLock(): void } /** - * Release the distributed lock. Idempotent — silently ignores 404. + * 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 { @@ -293,12 +336,10 @@ private function releaseLock(): void 'id' => $this->index->name(), ]); } catch (ClientResponseException $e) { - if ($e->getResponse()->getStatusCode() !== 404) { - throw $e; + if ($e->getResponse()->getStatusCode() === 404) { + return; // Lock already gone — idempotent } - // Lock already gone — idempotent - } catch (\Throwable $e) { - // Swallow transport / server errors in finally to avoid masking the original exception + throw $e; } } diff --git a/tests/Index/RebuildTest.php b/tests/Index/RebuildTest.php index 8d25dbb..5412571 100644 --- a/tests/Index/RebuildTest.php +++ b/tests/Index/RebuildTest.php @@ -716,4 +716,114 @@ public function source(array $context = []): iterable $this->assertStringStartsWith('products_', $result['newIndex']); } + + public function testRunThrowsWhenReleaseLockFailsAfterSuccess() + { + $indices = $this->createMock(TestIndices::class); + $indices->method('create')->willReturn(new ArrayResponse(['acknowledged' => true])); + $indices->method('exists')->willReturnCallback(function ($params) { + if (($params['index'] ?? '') === '.ek_locks') { + return new BoolResponse(true); + } + return new BoolResponse(false); + }); + $indices->method('existsAlias')->willReturn(new BoolResponse(false)); + $indices->method('putAlias')->willReturn(new ArrayResponse(['acknowledged' => true])); + + $response = $this->createMock(\Psr\Http\Message\ResponseInterface::class); + $response->method('getStatusCode')->willReturn(503); + $releaseException = new \Elastic\Elasticsearch\Exception\ClientResponseException(); + $releaseException->setResponse($response); + + $client = $this->createMock(TestClient::class); + $client->method('indices')->willReturn($indices); + $client->method('index')->willReturn(new ArrayResponse(['result' => 'created'])); + // 成功路径释放锁时抛 503 + $client->method('delete')->willThrowException($releaseException); + $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 + { + yield 1 => ['title' => 'A']; + } + }; + + try { + (new Rebuild($index))->run(); + $this->fail('Expected RuntimeException when lock release fails after success'); + } catch (\RuntimeException $e) { + $this->assertStringContainsString('succeeded but the lock could not be released', $e->getMessage()); + $this->assertStringContainsString('forceUnlock', $e->getMessage()); + $this->assertSame($releaseException, $e->getPrevious()); + } + } + + public function testRunPreservesOriginalExceptionWhenReleaseFails() + { + $indices = $this->createMock(TestIndices::class); + $indices->method('create')->willReturn(new ArrayResponse(['acknowledged' => true])); + $indices->method('exists')->willReturn(new BoolResponse(true)); + $indices->method('delete')->willReturn(new ArrayResponse(['acknowledged' => true])); + + $response = $this->createMock(\Psr\Http\Message\ResponseInterface::class); + $response->method('getStatusCode')->willReturn(503); + $releaseException = new \Elastic\Elasticsearch\Exception\ClientResponseException(); + $releaseException->setResponse($response); + + $client = $this->createMock(TestClient::class); + $client->method('indices')->willReturn($indices); + $client->method('index')->willReturn(new ArrayResponse(['result' => 'created'])); + // 释放锁也失败:503 + $client->method('delete')->willThrowException($releaseException); + // bulk 导入失败 → doRun 抛异常 + $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']; + } + }; + + try { + (new Rebuild($index))->run(); + $this->fail('Expected original import exception'); + } catch (\Throwable $e) { + // ⑤:必须抛出 doRun 的原始异常,而非释放锁的 503 ClientResponseException + $this->assertInstanceOf(\RuntimeException::class, $e); + $this->assertStringContainsString('Bulk request has errors', $e->getMessage()); + $this->assertStringNotContainsString('succeeded but the lock', $e->getMessage()); + $this->assertNotSame($releaseException, $e); + } + } + + public function testIsLockedThrowsOnServerError() + { + $response = $this->createMock(\Psr\Http\Message\ResponseInterface::class); + $response->method('getStatusCode')->willReturn(503); + $exception = new \Elastic\Elasticsearch\Exception\ClientResponseException(); + $exception->setResponse($response); + + $client = $this->createMock(TestClient::class); + $client->method('exists')->willThrowException($exception); + Index::setClient($client); + + $index = $this->createIndex('products'); + + $this->expectException(\Elastic\Elasticsearch\Exception\ClientResponseException::class); + (new Rebuild($index))->isLocked(); + } } From c0c020dd4308d8e174b02ab7b9ea0d72325a9840 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Tue, 23 Jun 2026 22:49:44 +0800 Subject: [PATCH 24/70] chore: upgrade phpunit.xml config, update TODO progress - phpunit.xml switches to the PHPUnit 10 schema declaration + cacheDirectory - .gitignore covers .phpunit.cache/ - CLAUDE.md: mark Rebuild exception handling and cursor/chunk API refactor as done Co-Authored-By: Claude --- .gitignore | 1 + CLAUDE.md | 4 ++-- phpunit.xml | 23 ++++++++++------------- 3 files changed, 13 insertions(+), 15 deletions(-) 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/CLAUDE.md b/CLAUDE.md index cec014b..4a3e1d2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,13 +50,13 @@ PSR-5 规范。 - [x] **命名参数一致性**:公开方法参数名是 API 的一部分,全库审查确保命名统一(如 connection/name/client 不混用) - [x] **PHP 8 现代化(Index 层)**:全 12 文件 strict_types + 构造器提升 + readonly + 属性/返回/参数类型 + 联合类型;callable 属性(resolver / errorHandler / dataSource)因 PHP 禁止 callable 作属性类型,保留 docblock - [x] **PHP 8 现代化(DSL 层)**:Node/Query/Agg + 122 leaf 类全部完成。strict_types 全 151 文件;4 原子属性类型同步全 leaf;leaf 参数类型对照 ES docblock 完成;`$_properties` 三模式统一为 `?array`(新增 `$_raw` 承接整体透传,null 保留为合法空态);`toJson` 加 `:string` + false 检查;`toArray` 因多态返回(array/stdClass/null)不强加 PHP 返回类型 -- [ ] **Rebuild 异常处理**:run() 改为 try-catch + releaseLock 分离,releaseLock 不吞任何异常,forceUnlock 单独处理 404(文档不存在 vs 索引不存在的 404 需区分);isLocked() 只吞 404 不吞其他 ClientResponseException;rebuild 失败优先抛原始异常;ensureLockIndex() replicas=0 在多节点集群有风险需注释说明 +- [x] **Rebuild 异常处理**:run() 分离 try-catch(失败优先抛原始异常,成功后释放锁失败抛 RuntimeException 说明 rebuild 已完成);releaseLock 只吞 404;forceUnlock 独立实现吞 404;isLocked 只在 404 返回 false、其他 ES 错误传播 - [x] **$client 抽到 Registry 类**:拆为 ClientManager / EventDispatcher / Pagination,Index 不再持有静态状态 - [x] **Node 构造函数重构**:拆分为 fromKeyValue/fromClosure/fromArrayField/fromScalar - [x] **Bulk/Rebuild onError 设计**:Bulk 加 onError(callback) 默认 throw,Rebuild 删 skipErrors 加 onError,删 rebuild.import.failed 事件 - [ ] **补核心路径的边界测试**:scroll、bulk 分批、rebuild 失败回滚 - [ ] **搭建集成测试基建**:`ELASTICKIT_TEST_HOST` 驱动,随机索引名隔离 -- [ ] **cursor/chunk API 重构**:现 `cursor()` 返回批次(Results)命名不准。拆为 `chunk($duration): Generator`(按批,即现 cursor 改名)+ `cursor($duration): Generator`(逐条 doc,内部扁平化 chunk、复用其 finally clear);保留 `scroll()/next()/clear()` 作低层原语。待定:单条 yield 完整 hit(带 _id/_score)还是只 _source;底层未来可换 PIT+search_after(上层签名不变) +- [x] **cursor/chunk API 重构**:`chunk($duration): Generator` 按批 yield Results;`cursor($duration): Generator` 逐条 yield 完整 hit(_id/_score/_source,yield from chunk 的 hits、复用其 finally clear);保留 `scroll()/next()/clear()` 作低层原语。底层未来可换 PIT+search_after(上层签名不变) ## 测试 diff --git a/phpunit.xml b/phpunit.xml index 944f10f..4166425 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,15 +1,12 @@ - - - - tests - tests/Index - - - tests/Index - - + + + + tests + tests/Index + + + tests/Index + + From 64f597cfa917b8553dd7dbea5bbbcb528be8aecb Mon Sep 17 00:00:00 2001 From: ykan821 Date: Wed, 24 Jun 2026 11:20:28 +0800 Subject: [PATCH 25/70] fix(dsl): fix 3 P0 silent data-loss / invalid-DSL issues - Query::buildQuery throws on same-key clauses instead of silently overwriting (extracted mergeClauses) - ScriptScore::minScore drops the Query::create wrapper so the float serializes correctly - Pipeline::bucketScript rejects the string shorthand (buckets_path must be a map) Co-Authored-By: Claude --- src/DSL/Aggs/Pipeline.php | 12 ++++++++-- src/DSL/Queries/Specialized/ScriptScore.php | 2 +- src/DSL/Query.php | 26 ++++++++++++++++++--- tests/AggsTest.php | 11 +++++++++ tests/CompoundQueriesTest.php | 4 ++-- tests/PolymorphicInputTest.php | 11 +++++++++ tests/SpecializedQueriesTest.php | 4 +++- 7 files changed, 61 insertions(+), 9 deletions(-) diff --git a/src/DSL/Aggs/Pipeline.php b/src/DSL/Aggs/Pipeline.php index 34d7a74..9a1230e 100644 --- a/src/DSL/Aggs/Pipeline.php +++ b/src/DSL/Aggs/Pipeline.php @@ -5,6 +5,7 @@ namespace ElasticKit\DSL\Aggs; use ElasticKit\DSL\Aggs\Pipeline\AvgBucket; +use InvalidArgumentException; use ElasticKit\DSL\Aggs\Pipeline\BucketScript; use ElasticKit\DSL\Aggs\Pipeline\CumulativeSum; use ElasticKit\DSL\Aggs\Pipeline\Derivative; @@ -95,11 +96,18 @@ public function derivative($value): static /** * Runs a custom script to compute values from multiple bucket metrics. * - * @param mixed $value + * 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($value): static { - return $this->node(BucketScript::create(is_string($value) ? ['buckets_path' => $value] : $value)); + 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/Queries/Specialized/ScriptScore.php b/src/DSL/Queries/Specialized/ScriptScore.php index bcee851..290f8f5 100644 --- a/src/DSL/Queries/Specialized/ScriptScore.php +++ b/src/DSL/Queries/Specialized/ScriptScore.php @@ -44,6 +44,6 @@ public function script($value): static */ public function minScore(float $value): static { - return $this->addProperty('min_score', Query::create($value)); + return $this->addProperty('min_score', $value); } } diff --git a/src/DSL/Query.php b/src/DSL/Query.php index 24eaf55..7dab986 100644 --- a/src/DSL/Query.php +++ b/src/DSL/Query.php @@ -6,6 +6,7 @@ use BadMethodCallException; use Closure; +use RuntimeException; use ElasticKit\DSL\Queries\Compound; use ElasticKit\DSL\Queries\FullText; use ElasticKit\DSL\Queries\Geo; @@ -269,10 +270,29 @@ private function buildQuery(): array|object if ($this->_multi) { return $clauses; } - if (empty($clauses)) { - return []; + + return $this->mergeClauses($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; } /** diff --git a/tests/AggsTest.php b/tests/AggsTest.php index 6de118f..70ed74d 100644 --- a/tests/AggsTest.php +++ b/tests/AggsTest.php @@ -1291,4 +1291,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/CompoundQueriesTest.php b/tests/CompoundQueriesTest.php index 65907fd..9d6132a 100644 --- a/tests/CompoundQueriesTest.php +++ b/tests/CompoundQueriesTest.php @@ -570,8 +570,8 @@ public function testWhenChaining() ->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() diff --git a/tests/PolymorphicInputTest.php b/tests/PolymorphicInputTest.php index 1c8e1a7..024a444 100644 --- a/tests/PolymorphicInputTest.php +++ b/tests/PolymorphicInputTest.php @@ -46,6 +46,17 @@ public function testMatchObject() $this->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/SpecializedQueriesTest.php b/tests/SpecializedQueriesTest.php index 5d5e9cb..6c6c5a2 100644 --- a/tests/SpecializedQueriesTest.php +++ b/tests/SpecializedQueriesTest.php @@ -202,7 +202,8 @@ public function testScriptScore() }, "script": { "source": "doc['my-int'].value / 10 " - } + }, + "min_score": 5.5 } } } @@ -215,6 +216,7 @@ public function testScriptScore() $scriptScore->script(function (\ElasticKit\DSL\Queries\Script $script) { $script->source('doc[\'my-int\'].value / 10 '); }); + $scriptScore->minScore(5.5); }); $this->assertQuery($exampleJson, $query); } From 7a6e9b59867d41a6e07f476c59f7ee2779b3c781 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Wed, 24 Jun 2026 17:43:53 +0800 Subject: [PATCH 26/70] refactor(dsl): unify Node polymorphic input, array-clause containers support 4 forms, fix empty-bool bug - remove Node::routeKeyValueClause; fromKeyValue returns to a pure leaf (object values throw) - ClausesSupport routes via method_exists (no clauseKeys declaration needed) - array-clause containers (Boolean/DisjunctionMax/SpanOr/SpanNear) use ClausesSupport, supporting closure/array/two-arg/instance - single-value compounds (Boosting/ConstantScore) use the Node default, no ClausesSupport - Compound/Span trait methods use create($field, $value) to unify single/two-arg - empty nodes serialize to a valid shape (bool:{}, must:[]) instead of being omitted, fixing {bool:null}/{must:{}} Co-Authored-By: Claude --- README.md | 20 ++++++ src/DSL/Aggs/Pipeline.php | 2 +- src/DSL/Node.php | 35 ++++++++-- src/DSL/Queries/Compound.php | 83 ++++++---------------- src/DSL/Queries/Span.php | 10 +-- src/DSL/Query.php | 45 ++++++++---- src/DSL/Support/ClausesSupport.php | 57 +++++++++++++++ tests/ClosureReturnTest.php | 4 +- tests/CompoundQueriesTest.php | 107 +++++++++++++++++++++++++++++ tests/SpanQueriesTest.php | 22 ++++++ 10 files changed, 298 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index d972849..35c5dc0 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,26 @@ $results = ProductIndex::query() } ``` +### 子句追加(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 diff --git a/src/DSL/Aggs/Pipeline.php b/src/DSL/Aggs/Pipeline.php index 9a1230e..09ec673 100644 --- a/src/DSL/Aggs/Pipeline.php +++ b/src/DSL/Aggs/Pipeline.php @@ -99,7 +99,7 @@ public function derivative($value): static * 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 + * @param array|callable|BucketScript $value * @return static */ public function bucketScript($value): static diff --git a/src/DSL/Node.php b/src/DSL/Node.php index e47e4af..41b96f9 100644 --- a/src/DSL/Node.php +++ b/src/DSL/Node.php @@ -5,6 +5,8 @@ namespace ElasticKit\DSL; use Closure; +use InvalidArgumentException; +use stdClass; /** * Abstract base class for DSL nodes (query types, params). @@ -93,7 +95,7 @@ public function __construct($field = null, $value = null) } elseif (is_scalar($field)) { $this->fromScalar($field); } elseif (is_array($field)) { - $this->_properties = $field; + $this->fromArrayProperties($field); } } @@ -112,6 +114,12 @@ protected function fromKeyValue($field, $value): void $this->_properties = []; } elseif (is_array($value)) { $this->_properties = $value; + } else { + 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); @@ -147,6 +155,19 @@ protected function fromArrayField(array $field): void } } + /** + * Initialize from an array of properties. + * + * 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. * @@ -256,16 +277,16 @@ 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 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); } /** @@ -289,7 +310,11 @@ public function toArray() } } } else { - $properties = $this->_properties === null ? null : $this->resolveProperties($this->_properties); + if (empty($this->_properties)) { + $properties = $this->_fieldKeyed ? null : new stdClass(); + } else { + $properties = $this->resolveProperties($this->_properties); + } } if ($this->_fieldKeyed) { diff --git a/src/DSL/Queries/Compound.php b/src/DSL/Queries/Compound.php index 90e92d8..ebc338d 100644 --- a/src/DSL/Queries/Compound.php +++ b/src/DSL/Queries/Compound.php @@ -4,7 +4,6 @@ namespace ElasticKit\DSL\Queries; -use ElasticKit\DSL\Query; use ElasticKit\DSL\Queries\Compound\Boolean; use ElasticKit\DSL\Queries\Compound\Boosting; use ElasticKit\DSL\Queries\Compound\ConstantScore; @@ -19,97 +18,57 @@ trait Compound /** * Add a bool query. * - * Supports three forms: - * bool(closure|Boolean) — full control over the bool query - * bool(['must' => 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 $value + * @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($value): static + public function bool($field = null, $value = null): static { - if (is_array($value)) { - $boolean = new Boolean(); - foreach ($value 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($value)); + return $this->addQuery(Boolean::create($field, $value)); } /** * Add a boosting query. * - * @param callable|Boosting|array $value + * @param mixed $field + * @param mixed $value * @return $this */ - public function boosting($value): static + public function boosting($field = null, $value = null): static { - if (is_array($value)) { - $b = new Boosting(); - foreach ($value 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($value)); + return $this->addQuery(Boosting::create($field, $value)); } /** * Add a constant_score query. * - * @param callable|ConstantScore|array $value + * @param mixed $field + * @param mixed $value * @return $this */ - public function constantScore($value): static + public function constantScore($field = null, $value = null): static { - if (is_array($value)) { - $cs = new ConstantScore(); - foreach ($value 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($value)); + return $this->addQuery(ConstantScore::create($field, $value)); } /** * Add a dis_max query. * - * @param callable|DisjunctionMax|array $value + * @param mixed $field + * @param mixed $value * @return $this */ - public function disMax($value): static + public function disMax($field = null, $value = null): static { - if (is_array($value)) { - $dm = new DisjunctionMax(); - foreach ($value 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($value)); + return $this->addQuery(DisjunctionMax::create($field, $value)); } /** diff --git a/src/DSL/Queries/Span.php b/src/DSL/Queries/Span.php index d20e19a..8a14d5f 100644 --- a/src/DSL/Queries/Span.php +++ b/src/DSL/Queries/Span.php @@ -67,12 +67,13 @@ public function spanMulti($value): static /** * Add a span_near query. * + * @param mixed $field * @param mixed $value * @return $this */ - public function spanNear($value): static + public function spanNear($field = null, $value = null): static { - return $this->addQuery(SpanNear::create($value)); + return $this->addQuery(SpanNear::create($field, $value)); } /** @@ -89,12 +90,13 @@ public function spanNot($value): static /** * Add a span_or query. * + * @param mixed $field * @param mixed $value * @return $this */ - public function spanOr($value): static + public function spanOr($field = null, $value = null): static { - return $this->addQuery(SpanOr::create($value)); + return $this->addQuery(SpanOr::create($field, $value)); } /** diff --git a/src/DSL/Query.php b/src/DSL/Query.php index 7dab986..3bb9aa2 100644 --- a/src/DSL/Query.php +++ b/src/DSL/Query.php @@ -218,27 +218,25 @@ public function toArray(): array $dsl = $this->_properties !== null ? $this->resolveProperties($this->_properties) : []; $query = $this->buildQuery(); - if (!empty($query)) { + if ($this->_multi || !empty($query)) { $dsl['query'] = $query; } $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(): array|object + private function buildQuery(): array { if (empty($this->_queries)) { - return $this->_multi ? (object)[] : []; + return []; } // Flatten nested Query instances @@ -253,25 +251,46 @@ private function buildQuery(): array|object } } + $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 ($flat as $query) { if ($query instanceof Node) { - $clauses[] = [$query->key() => $query->toArray()]; + $body = $query->toArray(); + if ($body === null) { + continue; + } + $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]; } } } - if ($this->_multi) { - return $clauses; - } - - return $this->mergeClauses($clauses); + return $clauses; } /** diff --git a/src/DSL/Support/ClausesSupport.php b/src/DSL/Support/ClausesSupport.php index 7c28048..303c0e5 100644 --- a/src/DSL/Support/ClausesSupport.php +++ b/src/DSL/Support/ClausesSupport.php @@ -30,9 +30,66 @@ protected function addClause(string $key, $clause): static $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/tests/ClosureReturnTest.php b/tests/ClosureReturnTest.php index f6b9a0d..dd7e537 100644 --- a/tests/ClosureReturnTest.php +++ b/tests/ClosureReturnTest.php @@ -58,13 +58,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 9d6132a..6f3c11d 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 = <<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' From dccd15e7854b06e2870f80237548903a40e0a939 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Wed, 24 Jun 2026 22:26:05 +0800 Subject: [PATCH 27/70] fix(dsl): Intervals::toArray() merges $_properties, throws on duplicate rule keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - toArray previously bypassed Node serialization entirely; inherited methods like boost() and array-constructed $_properties were silently dropped — now merged into the field node - when merging rules/properties, array_intersect_key detects conflicts; duplicate keys throw RuntimeException instead of being silently overwritten (aligned with Query::mergeClauses) - private mergeUnique within the class, not polluting Node Co-Authored-By: Claude --- src/DSL/Queries/FullText/Intervals.php | 28 ++++++++++++++++-- tests/FullTextQueriesTest.php | 41 +++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/src/DSL/Queries/FullText/Intervals.php b/src/DSL/Queries/FullText/Intervals.php index 198f6ec..f16d3a4 100644 --- a/src/DSL/Queries/FullText/Intervals.php +++ b/src/DSL/Queries/FullText/Intervals.php @@ -5,6 +5,7 @@ namespace ElasticKit\DSL\Queries\FullText; use ElasticKit\DSL\Node; +use RuntimeException; /** * Returns documents based on the order and proximity of matching terms. @@ -135,9 +136,10 @@ 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; } @@ -147,4 +149,24 @@ public function toArray() } return $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 $merged; + } } diff --git a/tests/FullTextQueriesTest.php b/tests/FullTextQueriesTest.php index 5d07a84..f083563 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() + { + // 继承自 Node 的 boost() 等方法此前被 Intervals::toArray() 静默丢弃, + // toArray 不应再绕过 $_properties。 + $exampleJson = <<intervals('my_text', function (Intervals $intervals) { + $intervals->match(['query' => 'salty']); + $intervals->boost(2.0); + }); + $this->assertQuery($exampleJson, $query); + } + + public function testIntervalsThrowsOnDuplicateRuleKey() + { + // 字段节点只接受单个 rule,两个 match 不应静默合并(后者覆盖前者),应抛异常。 + $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 = << Date: Wed, 24 Jun 2026 22:32:54 +0800 Subject: [PATCH 28/70] refactor(index)!: Manager::delete() refuses to delete alias backing indices by default - delete(bool $resolveAlias = false): when name is an alias, throw RuntimeException by default; resolveAlias=true is required to delete the backing indices - with resolveAlias=true, delete all backing indices the alias points to (comma-joined), fixing the former array_key_first pitfall of deleting only the first - removing an alias relationship uses removeAlias(); deleting a real index name is unchanged **BC:** delete() with no args and an alias name now throws instead of silently deleting backing indices Co-Authored-By: Claude --- src/Index/Manager.php | 37 +++++++++++++++++++++------ tests/Index/ManagerTest.php | 51 +++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 8 deletions(-) diff --git a/src/Index/Manager.php b/src/Index/Manager.php index 1d6054e..275fdfe 100644 --- a/src/Index/Manager.php +++ b/src/Index/Manager.php @@ -4,6 +4,7 @@ namespace ElasticKit\Index; +use RuntimeException; use stdClass; /** @@ -51,22 +52,42 @@ public function create(): array } /** - * 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(): array + public function delete(bool $resolveAlias = false): array { - $indexName = $this->resolveIndexName(); + $name = $this->index->name(); + $indices = $this->index->getClient()->indices(); + + $isAlias = $indices->existsAlias(['name' => $name])->asBool(); + + 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.before', $indexName); + $e = new Event('manager.delete.before', $target); EventDispatcher::dispatch($e); - $response = $this->index->getClient()->indices()->delete([ - 'index' => $indexName, - ])->asArray(); + $response = $indices->delete(['index' => $target])->asArray(); - $e = new Event('manager.delete.after', $indexName); + $e = new Event('manager.delete.after', $target); $e->response = $response; EventDispatcher::dispatch($e); diff --git a/tests/Index/ManagerTest.php b/tests/Index/ManagerTest.php index 5039b72..f7f8975 100644 --- a/tests/Index/ManagerTest.php +++ b/tests/Index/ManagerTest.php @@ -84,6 +84,57 @@ public function testDelete() $this->assertTrue($result['acknowledged']); } + public function testDeleteThrowsOnAliasWithoutResolve() + { + $indices = $this->createMock(TestIndices::class); + $indices->method('existsAlias')->willReturn(new BoolResponse(true)); + $client = $this->createMock(TestClient::class); + $client->method('indices')->willReturn($indices); + Index::setClient($client); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('is an alias'); + (new Manager($this->createIndex('products')))->delete(); + } + + public function testDeleteResolvesAliasToBackingIndex() + { + $indices = $this->createMock(TestIndices::class); + $indices->method('existsAlias')->willReturn(new BoolResponse(true)); + $indices->method('getAlias')->willReturn(new ArrayResponse([ + 'products_v1' => ['aliases' => ['products' => []]], + ])); + $indices->expects($this->once())->method('delete') + ->with(['index' => 'products_v1']) + ->willReturn(new ArrayResponse(['acknowledged' => true])); + $client = $this->createMock(TestClient::class); + $client->method('indices')->willReturn($indices); + Index::setClient($client); + + $result = (new Manager($this->createIndex('products')))->delete(resolveAlias: true); + + $this->assertTrue($result['acknowledged']); + } + + public function testDeleteResolvesAliasToAllBackingIndices() + { + // 别名指向多个 backing index 时一次性删除,而非只删第一个(原 array_key_first 隐患) + $indices = $this->createMock(TestIndices::class); + $indices->method('existsAlias')->willReturn(new BoolResponse(true)); + $indices->method('getAlias')->willReturn(new ArrayResponse([ + 'products_v1' => ['aliases' => ['products' => []]], + 'products_v2' => ['aliases' => ['products' => []]], + ])); + $indices->expects($this->once())->method('delete') + ->with(['index' => 'products_v1,products_v2']) + ->willReturn(new ArrayResponse(['acknowledged' => true])); + $client = $this->createMock(TestClient::class); + $client->method('indices')->willReturn($indices); + Index::setClient($client); + + (new Manager($this->createIndex('products')))->delete(resolveAlias: true); + } + public function testExistsReturnsTrue() { $this->mockIndices('exists', ['index' => 'products'], true); From 4d03114813098359ed2c383ecf6a77728c7ebfdd Mon Sep 17 00:00:00 2001 From: ykan821 Date: Thu, 25 Jun 2026 10:46:08 +0800 Subject: [PATCH 29/70] fix(index)!: Bulk execute() keeps the queue on error, onError switches to a raw-tools contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Data-loss fix: execute() previously cleared body before checking response['errors'], so callers couldn't retry after a partial auto-flush failure. It now keeps the queue on error — preserved whether there is no handler (throws) or the handler throws; cleared only on success or when the handler returns. The onError handler contract expands from fn($response) to fn($response, $body, $newbulk): the library does no retry logic, it only hands over three raw materials — the ES response, the full batch (including successful items, native format), and a fresh Bulk on the same index inheriting targetIndex (no handler; throws on its own errors without recursion). Users extract failed items from $body per $response and re-submit them on $newbulk. Rebuild::onError forwarding updated accordingly. **BC:** onError handler signature changes (old single-arg handlers still work, extra args ignored); execute() error-path behavior changes (keeps the queue instead of clearing). Co-Authored-By: Claude --- src/Index/Bulk.php | 35 ++++++-- src/Index/Rebuild.php | 10 ++- tests/Index/BulkTest.php | 189 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 221 insertions(+), 13 deletions(-) diff --git a/src/Index/Bulk.php b/src/Index/Bulk.php index f5c2ed3..dd82bdc 100644 --- a/src/Index/Bulk.php +++ b/src/Index/Bulk.php @@ -81,11 +81,16 @@ public function batchSize(int $size): static /** * Set a callback to handle bulk errors. * - * The callback receives the raw ES response. To continue execution, simply - * return without throwing. To abort, throw an exception from the callback. - * Without an error handler, execute() throws RuntimeException on 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. * - * @param callable $handler function (array $response): void + * Extract the failures from $body using $response (items[k] matches the k-th + * action), re-enqueue them on $newbulk, and call $newbulk->execute() 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 @@ -197,8 +202,14 @@ public function delete(string|int $id): static /** * Execute all queued actions and return the raw ES 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. + * * @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 = []): array { @@ -219,10 +230,6 @@ public function execute(array $options = []): array )->asArray(); $duration = microtime(true) - $start; - $this->body = []; - $this->docCount = 0; - $this->retryOnConflict = 0; - $e = new Event('bulk.execute.after', $indexName); $e->actions = $actions; $e->response = $response; @@ -231,7 +238,12 @@ public function execute(array $options = []): array if (!empty($response['errors'])) { if ($this->errorHandler) { - ($this->errorHandler)($response); + // 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 @@ -246,6 +258,11 @@ public function execute(array $options = []): array } } + // Success, or the handler consumed the batch. + $this->body = []; + $this->docCount = 0; + $this->retryOnConflict = 0; + return $response; } diff --git a/src/Index/Rebuild.php b/src/Index/Rebuild.php index b032810..8edc67b 100644 --- a/src/Index/Rebuild.php +++ b/src/Index/Rebuild.php @@ -63,11 +63,13 @@ public function batchSize(int $size): static /** * Set a callback to handle bulk import errors. * - * The callback receives the raw ES response. To continue the rebuild, simply - * return without throwing. To abort, throw an exception from the callback. - * Without an error handler, import errors cause the rebuild to abort. + * 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): void + * @param callable $handler function (array $response, array $body, Bulk $newbulk): void * @return $this */ public function onError(callable $handler): static diff --git a/tests/Index/BulkTest.php b/tests/Index/BulkTest.php index 82dee70..2a9a40c 100644 --- a/tests/Index/BulkTest.php +++ b/tests/Index/BulkTest.php @@ -285,6 +285,195 @@ public function testExecuteThrowsOnErrors() (new Bulk($index))->index('1', ['title' => 'foo'])->execute(); } + public function testExecutePreservesBodyForRetryOnError() + { + $callCount = 0; + $client = $this->createMock(TestClient::class); + $client->method('bulk')->willReturnCallback(function () use (&$callCount) { + $callCount++; + if ($callCount === 1) { + return new ArrayResponse(['errors' => true, 'items' => [ + ['index' => ['_id' => '1', 'status' => 400, 'error' => ['type' => 'mapper_parsing_exception']]], + ]]); + } + return new ArrayResponse(['errors' => false, 'items' => []]); + }); + Index::setClient($client); + + $index = $this->createIndex('products'); + $bulk = (new Bulk($index))->index('1', ['title' => 'foo']); + + try { + $bulk->execute(); + $this->fail('Expected RuntimeException'); + } catch (\RuntimeException $e) { + // body must survive the failure so the caller can retry + } + + $result = $bulk->execute(); + $this->assertFalse($result['errors']); + $this->assertEquals(2, $callCount); + } + + public function testOnErrorReceivesResponseBodyAndFreshBulk() + { + $client = $this->createMock(TestClient::class); + $client->method('bulk')->willReturn(new ArrayResponse([ + 'errors' => true, + 'items' => [ + ['index' => ['_id' => '1', 'status' => 201]], + ['index' => ['_id' => '2', 'status' => 400, 'error' => ['type' => 'mapper_parsing_exception']]], + ], + ])); + Index::setClient($client); + + $captured = []; + $outer = (new Bulk($this->createIndex('products'))) + ->target('products_new') + ->onError(function ($response, $body, $newbulk) use (&$captured) { + $captured['response'] = $response; + $captured['body'] = $body; + $captured['newbulk'] = $newbulk; + }); + $outer->index('1', ['title' => 'A'])->index('2', ['title' => 'B'])->execute(); + + $this->assertTrue($captured['response']['errors']); // raw ES response + $this->assertSame('1', $captured['body'][0]['index']['_id']); // full body, successes included + $this->assertSame(['title' => 'A'], $captured['body'][1]); + $this->assertSame('2', $captured['body'][2]['index']['_id']); + $this->assertInstanceOf(Bulk::class, $captured['newbulk']); // fresh Bulk + $this->assertNotSame($outer, $captured['newbulk']); // independent instance + } + + public function testOnErrorRetriesFailuresViaFreshBulkOnSameTarget() + { + // Outer targets 'products_new'. Batch of 2: id=1 succeeds, id=2 fails. + $calls = []; + $client = $this->createMock(TestClient::class); + $client->method('bulk')->willReturnCallback(function ($params) use (&$calls) { + $calls[] = $params['body']; + if (count($calls) === 1) { + return new ArrayResponse([ + 'errors' => true, + 'items' => [ + ['index' => ['_id' => '1', 'status' => 201]], + ['index' => ['_id' => '2', 'status' => 400, 'error' => ['type' => 'mapper_parsing_exception']]], + ], + ]); + } + return new ArrayResponse(['errors' => false, 'items' => []]); + }); + Index::setClient($client); + + $index = $this->createIndex('products'); + (new Bulk($index)) + ->target('products_new') + ->onError(function ($response, $body, $newbulk) { + // user extracts the failure (id=2) and re-sends it on the fresh bulk. + // items[k] ↔ k-th action; for an all-index batch, action k's data is body[2k+1]. + foreach ($response['items'] as $i => $item) { + if (($item['index']['status'] ?? 200) >= 400) { + $newbulk->index($item['index']['_id'], $body[$i * 2 + 1]); + } + } + $newbulk->execute(); + }) + ->index('1', ['title' => 'A']) + ->index('2', ['title' => 'B']) + ->execute(); + + $this->assertCount(2, $calls); // original + retry + $this->assertSame('products_new', $calls[1][0]['index']['_index']); // fresh bulk inherited target + $this->assertSame('2', $calls[1][0]['index']['_id']); // only the failure + $this->assertSame(['title' => 'B'], $calls[1][1]); // its data + } + + public function testOnErrorFreshBulkHasNoHandlerSoItsErrorsThrow() + { + // The fresh Bulk is bare (no handler): its own execute() throws on error + // rather than recursing back into the handler. + $calls = 0; + $client = $this->createMock(TestClient::class); + $client->method('bulk')->willReturnCallback(function () use (&$calls) { + $calls++; + return new ArrayResponse(['errors' => true, 'items' => [ + ['index' => ['_id' => '1', 'status' => 400, 'error' => ['type' => 'mapper_parsing_exception']]], + ]]); + }); + Index::setClient($client); + + $threw = false; + $index = $this->createIndex('products'); + (new Bulk($index)) + ->onError(function ($response, $body, $newbulk) use (&$threw) { + $newbulk->index('1', ['title' => 'retry']); + try { + $newbulk->execute(); + } catch (\RuntimeException $e) { + $threw = true; // surfaced as exception, no recursion + } + }) + ->index('1', ['title' => 'foo']) + ->execute(); + + $this->assertTrue($threw); + $this->assertEquals(2, $calls); // original + one retry attempt, no recursion + } + + public function testOnErrorClearsBatchWhenHandlerReturns() + { + $callCount = 0; + $client = $this->createMock(TestClient::class); + $client->method('bulk')->willReturnCallback(function () use (&$callCount) { + $callCount++; + return new ArrayResponse(['errors' => true, 'items' => []]); + }); + Index::setClient($client); + + $index = $this->createIndex('products'); + $bulk = (new Bulk($index)) + ->onError(function ($response, $body, $newbulk) { + // accept and drop + }) + ->index('1', ['title' => 'foo']); + $bulk->execute(); + + $this->assertEquals(1, $callCount); + $this->assertEquals([], $bulk->execute()); // batch consumed by handler + } + + public function testOnErrorPreservesBatchWhenHandlerThrows() + { + $callCount = 0; + $client = $this->createMock(TestClient::class); + $client->method('bulk')->willReturnCallback(function () use (&$callCount) { + $callCount++; + return new ArrayResponse(['errors' => true, 'items' => []]); + }); + Index::setClient($client); + + $index = $this->createIndex('products'); + $bulk = (new Bulk($index)) + ->onError(function ($response, $body, $newbulk) { + throw new \RuntimeException('handler aborted'); + }) + ->index('1', ['title' => 'foo']); + + try { + $bulk->execute(); + $this->fail('Expected exception'); + } catch (\RuntimeException $e) { + $this->assertSame('handler aborted', $e->getMessage()); + } + + try { + $bulk->execute(); // batch preserved → re-sent + } catch (\RuntimeException $e) { + // still failing, still preserved + } + $this->assertEquals(2, $callCount); + } + public function testExecuteCallsOnError() { $client = $this->createMock(TestClient::class); From f2358d57ab4df96c2ce74ce5b0b29e3dd15f06b0 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Thu, 25 Jun 2026 14:37:26 +0800 Subject: [PATCH 30/70] fix(index): Bulk::create() supports null id for auto-generation create() signature becomes nullable; the body writes _id conditionally like index() (omitted when empty so ES auto-generates it), aligned with Doc::create. Co-Authored-By: Claude --- src/Index/Bulk.php | 10 +++++++--- tests/Index/BulkTest.php | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/Index/Bulk.php b/src/Index/Bulk.php index dd82bdc..5f9ffc7 100644 --- a/src/Index/Bulk.php +++ b/src/Index/Bulk.php @@ -147,13 +147,17 @@ public function save(string|int|null $id, array $data): static /** * Queue a create action (fail if document already exists). * - * @param string|int $id + * @param string|int|null $id document ID, or null/'' to let ES auto-generate * @param array $data * @return $this */ - public function create(string|int $id, array $data): static + public function create(string|int|null $id, array $data): static { - $this->body[] = ['create' => ['_index' => $this->resolveIndex(), '_id' => $id]]; + $action = ['create' => ['_index' => $this->resolveIndex()]]; + if ($id !== null && $id !== '') { + $action['create']['_id'] = $id; + } + $this->body[] = $action; $this->body[] = $data; $this->afterPush(); diff --git a/tests/Index/BulkTest.php b/tests/Index/BulkTest.php index 2a9a40c..68acbb3 100644 --- a/tests/Index/BulkTest.php +++ b/tests/Index/BulkTest.php @@ -65,6 +65,24 @@ public function testCreateAction() (new Bulk($index))->create('1', ['title' => 'foo'])->execute(); } + public function testCreateActionWithoutIdOmitsId() + { + $client = $this->createMock(TestClient::class); + $client->expects($this->once()) + ->method('bulk') + ->with([ + 'body' => [ + ['create' => ['_index' => 'products']], // no _id → ES auto-generates + ['title' => 'foo'], + ], + ]) + ->willReturn(new ArrayResponse(['errors' => false, 'items' => []])); + Index::setClient($client); + + $index = $this->createIndex('products'); + (new Bulk($index))->create(null, ['title' => 'foo'])->execute(); + } + public function testUpdateAction() { $client = $this->createMock(TestClient::class); From 3a1a690fceeceaec12d450532a78318192f8ecef Mon Sep 17 00:00:00 2001 From: ykan821 Date: Thu, 25 Jun 2026 14:37:27 +0800 Subject: [PATCH 31/70] feat(dsl): DeepClone trait for reflective deep-copy of Query/Agg/Node Add a DeepClone trait: __clone() reflectively walks all non-static, initialized properties, deep-cloning objects/arrays (skipping Closure and scalars). New properties are covered automatically with no per-class boilerplate, eliminating the footgun of forgetting __clone. Node and Agg use DeepClone; Query and all Node subclasses are covered via inheritance. Reflection guards uninitialized typed properties ($_field/$_key) with isInitialized() and skips them. tests/CloneTest.php adds 4 cases verifying clone isolation. Co-Authored-By: Claude --- src/DSL/Agg.php | 1 + src/DSL/DeepClone.php | 55 ++++++++++++++++++++++++++++++++++ src/DSL/Node.php | 2 ++ tests/CloneTest.php | 70 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 128 insertions(+) create mode 100644 src/DSL/DeepClone.php create mode 100644 tests/CloneTest.php diff --git a/src/DSL/Agg.php b/src/DSL/Agg.php index fd27e51..0608abd 100644 --- a/src/DSL/Agg.php +++ b/src/DSL/Agg.php @@ -19,6 +19,7 @@ class Agg use Bucket; use Metric; use Pipeline; + use DeepClone; /** * The aggregation type node. diff --git a/src/DSL/DeepClone.php b/src/DSL/DeepClone.php new file mode 100644 index 0000000..d7f8796 --- /dev/null +++ b/src/DSL/DeepClone.php @@ -0,0 +1,55 @@ +getProperties() as $property) { + if ($property->isStatic() || !$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 41b96f9..2440bab 100644 --- a/src/DSL/Node.php +++ b/src/DSL/Node.php @@ -15,6 +15,8 @@ */ abstract class Node { + use DeepClone; + /** * Properties owned by a node. Either an array of attributes, or null when * the node carries no properties (empty construction / empty closure, 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); + } +} From 649c2fae7b57d400373cc913f0847a7d9095ee0c Mon Sep 17 00:00:00 2001 From: ykan821 Date: Thu, 25 Jun 2026 15:48:48 +0800 Subject: [PATCH 32/70] refactor(index)!: rename Bulk execute() to flush(), sync event names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clarify buffer semantics: execute() -> flush() (the buffer verb); batchSize() stays as the auto-flush threshold (opt-in — without it, pure buffering; with it, a full batch auto-sends and the tail still needs flush()). Events bulk.execute.before/after -> bulk.flush.before/after. Rebuild internal calls synced. Docs: rewrite the bulk section in docs/index.md (buffer+flush mechanism, batchSize auto-flush + tail flush, onError raw-tools contract); guide.md / README.md execute -> flush. **BC:** Bulk::execute() -> flush(); event names bulk.execute.* -> bulk.flush.*. Co-Authored-By: Claude --- README.md | 2 +- docs/guide.md | 2 +- docs/index.md | 55 +++++++++++++++++++++++++------------- src/Index/Bulk.php | 23 +++++++++------- src/Index/Event.php | 2 +- src/Index/Rebuild.php | 2 +- tests/Index/BulkTest.php | 56 +++++++++++++++++++-------------------- tests/Index/EventTest.php | 4 +-- 8 files changed, 85 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 35c5dc0..8af7b41 100644 --- a/README.md +++ b/README.md @@ -236,7 +236,7 @@ $bulk->batchSize(500) ->index(2, ['title' => 'B', 'price' => 20]) ->update(3, ['price' => 15]) ->delete(4) - ->execute(); + ->flush(); ``` ### 索引管理 diff --git a/docs/guide.md b/docs/guide.md index 3ca10a5..313f9f0 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -245,7 +245,7 @@ class SyncEsJob $bulk->index($id, $doc); } - $bulk->execute(); + $bulk->flush(); $job->delete(); } } diff --git a/docs/index.md b/docs/index.md index c1811bd..ce52de8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -163,38 +163,56 @@ $doc->update(['price' => 39.99], true); // 文档不存在时自动创建 ## 批量操作 +Bulk 是一个缓冲区:`index()/create()/update()/delete()` 只入队,**`flush()` 才发送**。 + ```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(); // 发送并清空缓冲 +``` + +`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 那批) ``` ### 错误处理 -`execute()` 默认在响应包含错误时抛出 `RuntimeException`。使用 `onError()` 自定义处理。回调接收 ES 原始响应,不抛出则继续,抛出则中断: +`flush()` 默认在响应包含错误时抛出 `RuntimeException`。用 `onError()` 自定义处理——回调收到三样原材料,自行决定: -```php -$bulk = new Bulk(new ProductIndex()); +- `$response` — ES 原始响应(`items[]` 带逐条 status/error) +- `$body` — 完整原始批次(含成功项,native ES 格式) +- `$newbulk` — 一个新的、绑定同索引+目标 的 Bulk,用于重投失败项 -// 不设 onError → 有错误就抛异常 -$bulk->execute(); +回调内**不抛(返回)→ 视为已处理,本批清空、继续;抛出 → 中断、本批保留**给调用方。 -// 设 onError → 回调内不抛则继续,抛则中断 -$bulk->onError(function (array $response) { - $failures = count(array_filter($response['items'], fn($i) => isset($i['index']['error']))); - if ($failures > 100) { - throw new RuntimeException("失败超过阈值: {$failures}"); +```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]); + } } - Log::warning("部分失败: {$failures} 条"); -})->execute(); + $newbulk->flush(); +})->flush(); ``` -> `batchSize` 自动 flush 时的错误同样走 `onError`,不会丢失。 +> `batchSize` 自动 flush 触发的错误同样走 `onError`。 ## 零停机重建 @@ -277,8 +295,9 @@ $rebuild->run(['after' => '2024-01-01']); Rebuild 内部使用 Bulk 执行导入,`onError()` 用法与 [批量操作 > 错误处理](#错误处理) 一致: ```php -$rebuild->onError(function (array $response) { +$rebuild->onError(function (array $response, array $body, Bulk $newbulk) { Log::warning("重建导入错误", $response); + // 需要重投失败项时用 $body + $newbulk,见「批量操作 > 错误处理」 })->run(); ``` @@ -371,8 +390,8 @@ Index::setClient($client); | `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` | | diff --git a/src/Index/Bulk.php b/src/Index/Bulk.php index 5f9ffc7..a5ad77e 100644 --- a/src/Index/Bulk.php +++ b/src/Index/Bulk.php @@ -28,6 +28,9 @@ class Bulk 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 int $batchSize = 0; @@ -56,7 +59,7 @@ public function __construct( */ public function target(string $indexName): static { - if (strpos($indexName, '.') === 0) { + if (str_starts_with($indexName, '.')) { throw new InvalidArgumentException("System index names (starting with '.') are not allowed: {$indexName}"); } @@ -66,7 +69,8 @@ public function target(string $indexName): static } /** - * 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 @@ -87,7 +91,7 @@ public function batchSize(int $size): static * - $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->execute() to retry. + * 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 @@ -204,18 +208,19 @@ public function delete(string|int $id): static } /** - * 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. + * 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 = []): array + public function flush(array $options = []): array { if (empty($this->body)) { return []; @@ -224,7 +229,7 @@ public function execute(array $options = []): array $indexName = $this->resolveIndex(); $actions = $this->body; - $e = new Event('bulk.execute.before', $indexName); + $e = new Event('bulk.flush.before', $indexName); $e->actions = $actions; EventDispatcher::dispatch($e); @@ -234,7 +239,7 @@ public function execute(array $options = []): array )->asArray(); $duration = microtime(true) - $start; - $e = new Event('bulk.execute.after', $indexName); + $e = new Event('bulk.flush.after', $indexName); $e->actions = $actions; $e->response = $response; $e->duration = $duration; @@ -288,7 +293,7 @@ private function afterPush(): void $this->docCount++; if ($this->batchSize > 0 && $this->docCount >= $this->batchSize) { - $this->execute(); + $this->flush(); } } } diff --git a/src/Index/Event.php b/src/Index/Event.php index bd8edd1..a027152 100644 --- a/src/Index/Event.php +++ b/src/Index/Event.php @@ -14,7 +14,7 @@ * @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 array|null $actions Bulk action lines (bulk.flush events) * @property string|null $newIndex New backing index name (rebuild.run.after) * @property string|null $oldIndex Previous backing index name (rebuild.run.after) */ diff --git a/src/Index/Rebuild.php b/src/Index/Rebuild.php index 8edc67b..582b67d 100644 --- a/src/Index/Rebuild.php +++ b/src/Index/Rebuild.php @@ -436,6 +436,6 @@ protected function import(string $newIndex, array $context): void ); } - $bulk->execute(); + $bulk->flush(); } } diff --git a/tests/Index/BulkTest.php b/tests/Index/BulkTest.php index 68acbb3..d63f4e6 100644 --- a/tests/Index/BulkTest.php +++ b/tests/Index/BulkTest.php @@ -42,7 +42,7 @@ public function testIndexAction() Index::setClient($client); $index = $this->createIndex('products'); - $result = (new Bulk($index))->index('1', ['title' => 'foo'])->execute(); + $result = (new Bulk($index))->index('1', ['title' => 'foo'])->flush(); $this->assertFalse($result['errors']); } @@ -62,7 +62,7 @@ public function testCreateAction() Index::setClient($client); $index = $this->createIndex('products'); - (new Bulk($index))->create('1', ['title' => 'foo'])->execute(); + (new Bulk($index))->create('1', ['title' => 'foo'])->flush(); } public function testCreateActionWithoutIdOmitsId() @@ -80,7 +80,7 @@ public function testCreateActionWithoutIdOmitsId() Index::setClient($client); $index = $this->createIndex('products'); - (new Bulk($index))->create(null, ['title' => 'foo'])->execute(); + (new Bulk($index))->create(null, ['title' => 'foo'])->flush(); } public function testUpdateAction() @@ -98,7 +98,7 @@ public function testUpdateAction() Index::setClient($client); $index = $this->createIndex('products'); - (new Bulk($index))->update('1', ['title' => 'updated'])->execute(); + (new Bulk($index))->update('1', ['title' => 'updated'])->flush(); } public function testUpdateWithoutUpsert() @@ -116,7 +116,7 @@ public function testUpdateWithoutUpsert() Index::setClient($client); $index = $this->createIndex('products'); - (new Bulk($index))->update('1', ['title' => 'updated'], false)->execute(); + (new Bulk($index))->update('1', ['title' => 'updated'], false)->flush(); } public function testUpdateWithRetryOnConflict() @@ -140,7 +140,7 @@ public function testUpdateWithRetryOnConflict() ->retryOnConflict(3) ->update('1', ['title' => 'updated']) ->update('2', ['title' => 'bar']) - ->execute(); + ->flush(); } public function testDeleteAction() @@ -157,7 +157,7 @@ public function testDeleteAction() Index::setClient($client); $index = $this->createIndex('products'); - (new Bulk($index))->delete('1')->execute(); + (new Bulk($index))->delete('1')->flush(); } public function testMixedActions() @@ -182,7 +182,7 @@ public function testMixedActions() ->index('1', ['title' => 'foo']) ->update('2', ['title' => 'bar']) ->delete('3') - ->execute(); + ->flush(); } public function testExecuteWithOptions() @@ -204,7 +204,7 @@ public function testExecuteWithOptions() $index = $this->createIndex('products'); (new Bulk($index)) ->index('1', ['title' => 'foo']) - ->execute(['refresh' => 'wait_for', 'timeout' => '5s']); + ->flush(['refresh' => 'wait_for', 'timeout' => '5s']); } public function testExecuteClearsBodyAndRetryOnConflict() @@ -227,8 +227,8 @@ public function testExecuteClearsBodyAndRetryOnConflict() $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(); + $bulk->retryOnConflict(3)->update('1', ['title' => 'first'])->flush(['refresh' => 'wait_for']); + $bulk->update('1', ['title' => 'second'])->flush(); $this->assertEquals(2, $callCount); } @@ -248,7 +248,7 @@ public function testTargetOverridesIndexName() Index::setClient($client); $index = $this->createIndex('products'); - (new Bulk($index))->target('products_new')->index('1', ['title' => 'foo'])->execute(); + (new Bulk($index))->target('products_new')->index('1', ['title' => 'foo'])->flush(); } public function testAutoFlushTriggersExecute() @@ -271,7 +271,7 @@ public function testAutoFlushTriggersExecute() $this->assertEquals(1, $callCount); $bulk->index('3', ['title' => 'c']); - $bulk->execute(); + $bulk->flush(); $this->assertEquals(2, $callCount); } @@ -282,7 +282,7 @@ public function testExecuteReturnsEmptyWhenBodyIsEmpty() Index::setClient($client); $index = $this->createIndex('products'); - $result = (new Bulk($index))->execute(); + $result = (new Bulk($index))->flush(); $this->assertEquals([], $result); } @@ -300,7 +300,7 @@ public function testExecuteThrowsOnErrors() $index = $this->createIndex('products'); $this->expectException(\RuntimeException::class); - (new Bulk($index))->index('1', ['title' => 'foo'])->execute(); + (new Bulk($index))->index('1', ['title' => 'foo'])->flush(); } public function testExecutePreservesBodyForRetryOnError() @@ -322,13 +322,13 @@ public function testExecutePreservesBodyForRetryOnError() $bulk = (new Bulk($index))->index('1', ['title' => 'foo']); try { - $bulk->execute(); + $bulk->flush(); $this->fail('Expected RuntimeException'); } catch (\RuntimeException $e) { // body must survive the failure so the caller can retry } - $result = $bulk->execute(); + $result = $bulk->flush(); $this->assertFalse($result['errors']); $this->assertEquals(2, $callCount); } @@ -353,7 +353,7 @@ public function testOnErrorReceivesResponseBodyAndFreshBulk() $captured['body'] = $body; $captured['newbulk'] = $newbulk; }); - $outer->index('1', ['title' => 'A'])->index('2', ['title' => 'B'])->execute(); + $outer->index('1', ['title' => 'A'])->index('2', ['title' => 'B'])->flush(); $this->assertTrue($captured['response']['errors']); // raw ES response $this->assertSame('1', $captured['body'][0]['index']['_id']); // full body, successes included @@ -394,11 +394,11 @@ public function testOnErrorRetriesFailuresViaFreshBulkOnSameTarget() $newbulk->index($item['index']['_id'], $body[$i * 2 + 1]); } } - $newbulk->execute(); + $newbulk->flush(); }) ->index('1', ['title' => 'A']) ->index('2', ['title' => 'B']) - ->execute(); + ->flush(); $this->assertCount(2, $calls); // original + retry $this->assertSame('products_new', $calls[1][0]['index']['_index']); // fresh bulk inherited target @@ -408,7 +408,7 @@ public function testOnErrorRetriesFailuresViaFreshBulkOnSameTarget() public function testOnErrorFreshBulkHasNoHandlerSoItsErrorsThrow() { - // The fresh Bulk is bare (no handler): its own execute() throws on error + // The fresh Bulk is bare (no handler): its own flush() throws on error // rather than recursing back into the handler. $calls = 0; $client = $this->createMock(TestClient::class); @@ -426,13 +426,13 @@ public function testOnErrorFreshBulkHasNoHandlerSoItsErrorsThrow() ->onError(function ($response, $body, $newbulk) use (&$threw) { $newbulk->index('1', ['title' => 'retry']); try { - $newbulk->execute(); + $newbulk->flush(); } catch (\RuntimeException $e) { $threw = true; // surfaced as exception, no recursion } }) ->index('1', ['title' => 'foo']) - ->execute(); + ->flush(); $this->assertTrue($threw); $this->assertEquals(2, $calls); // original + one retry attempt, no recursion @@ -454,10 +454,10 @@ public function testOnErrorClearsBatchWhenHandlerReturns() // accept and drop }) ->index('1', ['title' => 'foo']); - $bulk->execute(); + $bulk->flush(); $this->assertEquals(1, $callCount); - $this->assertEquals([], $bulk->execute()); // batch consumed by handler + $this->assertEquals([], $bulk->flush()); // batch consumed by handler } public function testOnErrorPreservesBatchWhenHandlerThrows() @@ -478,14 +478,14 @@ public function testOnErrorPreservesBatchWhenHandlerThrows() ->index('1', ['title' => 'foo']); try { - $bulk->execute(); + $bulk->flush(); $this->fail('Expected exception'); } catch (\RuntimeException $e) { $this->assertSame('handler aborted', $e->getMessage()); } try { - $bulk->execute(); // batch preserved → re-sent + $bulk->flush(); // batch preserved → re-sent } catch (\RuntimeException $e) { // still failing, still preserved } @@ -511,7 +511,7 @@ public function testExecuteCallsOnError() $received = $response; }) ->index('1', ['title' => 'foo']) - ->execute(); + ->flush(); $this->assertEquals($errorResponse, $received); } diff --git a/tests/Index/EventTest.php b/tests/Index/EventTest.php index 8d01c92..04a1339 100644 --- a/tests/Index/EventTest.php +++ b/tests/Index/EventTest.php @@ -204,7 +204,7 @@ public function testNoListenersDoesNotError() public function testBulkExecutePassesActions() { $actions = null; - EventDispatcher::listen('bulk.execute.before', function (Event $e) use (&$actions) { + EventDispatcher::listen('bulk.flush.before', function (Event $e) use (&$actions) { $actions = $e->actions; }); @@ -215,7 +215,7 @@ public function testBulkExecutePassesActions() $index = $this->createIndex(); $bulk = new \ElasticKit\Index\Bulk($index); $bulk->index(1, ['title' => 'test']); - $bulk->execute(); + $bulk->flush(); $this->assertIsArray($actions); $this->assertCount(2, $actions); From 37a118e015fa5400297d4b64ee229ac5a092053f Mon Sep 17 00:00:00 2001 From: ykan821 Date: Thu, 25 Jun 2026 16:23:55 +0800 Subject: [PATCH 33/70] fix: toJson preserves floats / lastPage perPage=0 guard / retryOnConflict persists across flushes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Node/Agg toJson adds JSON_PRESERVE_ZERO_FRACTION by default: integer-valued floats like boost=2.0 no longer serialize as integers (only affects user-facing JSON; ES requests go through toArray + client serialization, not toJson). - Results::lastPage() gains a perPage<1 guard to avoid a divide-by-zero crash. - Bulk flush() no longer resets retryOnConflict: it now persists across flushes (like target/batchSize — a setting, not per-batch); multi-batch updates no longer silently lose retry_on_conflict. retryOnConflict() docstring synced; tests updated accordingly. Co-Authored-By: Claude --- src/DSL/Agg.php | 2 +- src/DSL/Node.php | 2 +- src/Index/Bulk.php | 7 ++++--- src/Index/Results.php | 4 ++++ tests/Index/BulkTest.php | 8 ++++---- tests/Index/ResultsTest.php | 10 ++++++++++ 6 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/DSL/Agg.php b/src/DSL/Agg.php index 0608abd..226ebd8 100644 --- a/src/DSL/Agg.php +++ b/src/DSL/Agg.php @@ -235,7 +235,7 @@ public function toArray(): array * @param int $depth * @return string */ - public function toJson(int $flags = JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT, int $depth = 512): string + public function toJson(int $flags = JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_PRESERVE_ZERO_FRACTION, int $depth = 512): string { $json = json_encode($this->toArray(), $flags, $depth); diff --git a/src/DSL/Node.php b/src/DSL/Node.php index 2440bab..ced3871 100644 --- a/src/DSL/Node.php +++ b/src/DSL/Node.php @@ -332,7 +332,7 @@ public function toArray() * @param int $depth * @return string */ - public function toJson(int $flags = JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT, int $depth = 512): string + public function toJson(int $flags = JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_PRESERVE_ZERO_FRACTION, int $depth = 512): string { $json = json_encode($this->toArray(), $flags, $depth); diff --git a/src/Index/Bulk.php b/src/Index/Bulk.php index a5ad77e..f2c71b5 100644 --- a/src/Index/Bulk.php +++ b/src/Index/Bulk.php @@ -104,7 +104,8 @@ public function onError(callable $handler): static } /** - * Set retry_on_conflict for all update actions in this batch. + * 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 @@ -267,10 +268,10 @@ public function flush(array $options = []): array } } - // Success, or the handler consumed the batch. + // 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; - $this->retryOnConflict = 0; return $response; } diff --git a/src/Index/Results.php b/src/Index/Results.php index 18bc275..14de6e4 100644 --- a/src/Index/Results.php +++ b/src/Index/Results.php @@ -204,6 +204,10 @@ public function perPage(): int */ public function lastPage(): int { + if ($this->perPage < 1) { + return 1; + } + return (int) ceil($this->total() / $this->perPage) ?: 1; } diff --git a/tests/Index/BulkTest.php b/tests/Index/BulkTest.php index d63f4e6..114bca3 100644 --- a/tests/Index/BulkTest.php +++ b/tests/Index/BulkTest.php @@ -207,17 +207,17 @@ public function testExecuteWithOptions() ->flush(['refresh' => 'wait_for', 'timeout' => '5s']); } - public function testExecuteClearsBodyAndRetryOnConflict() + public function testFlushClearsBodyButPersistsRetryOnConflict() { $callCount = 0; $client = $this->createMock(TestClient::class); $client->method('bulk')->willReturnCallback(function ($params) use (&$callCount) { $callCount++; + // retryOnConflict persists across flushes (setting); refresh is per-call + $this->assertSame(3, $params['body'][0]['update']['retry_on_conflict']); if ($callCount === 1) { - $this->assertEquals(3, $params['body'][0]['update']['retry_on_conflict']); - $this->assertEquals('wait_for', $params['refresh']); + $this->assertSame('wait_for', $params['refresh']); } else { - $this->assertArrayNotHasKey('retry_on_conflict', $params['body'][0]['update']); $this->assertArrayNotHasKey('refresh', $params); } return new ArrayResponse(['errors' => false, 'items' => []]); diff --git a/tests/Index/ResultsTest.php b/tests/Index/ResultsTest.php index 853b0a4..fb00635 100644 --- a/tests/Index/ResultsTest.php +++ b/tests/Index/ResultsTest.php @@ -158,6 +158,16 @@ public function testLastPageMinimumIsOne() $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([ From f823cec57f3842a2f2369e4deb79cb15bae3743d Mon Sep 17 00:00:00 2001 From: ykan821 Date: Thu, 25 Jun 2026 17:39:00 +0800 Subject: [PATCH 34/70] refactor: input validation + dead code/symmetry + error message cleanup Input validation: Query/Agg aggs() throws BadMethodCallException on an empty alias (instead of producing {""} or silently dropping); Query::when() tightens its type to bool|\Closure, so a string is not treated as truthy (when('count',...) no longer calls count()). Mechanical fixes: flush() error message substr -> mb_strcut (UTF-8 truncation); Manager::putMapping() turns an empty mapping into stdClass. Dead code/symmetry: remove dead toArray() in Highlight/Knn/Rescore (resolveProperties already converts Query; they inherit Node::toArray); make Node/Agg resolveProperties symmetric (Node gains the Agg branch; Agg gains recursive array + null filtering). Error messages: Search count()/__call() exceptions now include the index name. Tests: fix 3 test names; add InputValidationTest to lock in input-validation behavior. **BC:** Query::when() type bool|callable -> bool|\Closure; aggs() with an empty alias changes from silent/invalid to throwing. Co-Authored-By: Claude --- src/DSL/Agg.php | 38 +++++++++++++++++------------- src/DSL/Node.php | 2 ++ src/DSL/Params/Highlight.php | 9 ------- src/DSL/Params/Knn.php | 10 -------- src/DSL/Params/Rescore.php | 10 -------- src/DSL/Query.php | 40 ++++++++++++++++++-------------- src/Index/Bulk.php | 2 +- src/Index/Manager.php | 4 +++- src/Index/Search.php | 6 +++-- tests/AggsTest.php | 2 +- tests/FullTextQueriesTest.php | 2 +- tests/InputValidationTest.php | 38 ++++++++++++++++++++++++++++++ tests/SpecializedQueriesTest.php | 2 +- 13 files changed, 96 insertions(+), 69 deletions(-) create mode 100644 tests/InputValidationTest.php diff --git a/src/DSL/Agg.php b/src/DSL/Agg.php index 226ebd8..7911a41 100644 --- a/src/DSL/Agg.php +++ b/src/DSL/Agg.php @@ -135,23 +135,29 @@ public function aggs($alias, $aggs = null): static } if ($aggs instanceof Agg) { - if ($alias !== null) { - $aggs->alias($alias); + $key = $alias ?? $aggs->getAlias(); + if ($key === null || $key === '') { + throw new BadMethodCallException('aggs() requires a non-empty alias.'); } - $this->_subAggs[$alias ?? $aggs->getAlias()] = $aggs; + $aggs->alias($key); + $this->_subAggs[$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); - if ($alias !== null) { - $childAgg->alias($alias); - } + $childAgg->alias($alias); $this->_subAggs[$alias] = $childAgg; return $this; } - if ($alias !== null && !isset($this->_subAggs[$alias])) { + if (!isset($this->_subAggs[$alias])) { $this->_subAggs[$alias] = new Agg(); $this->_subAggs[$alias]->alias($alias); } @@ -161,13 +167,9 @@ public function aggs($alias, $aggs = null): static 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; + throw new BadMethodCallException( + sprintf('aggs("%s", ...) requires a closure, array, or Agg instance as the definition.', $alias) + ); } /** @@ -180,14 +182,18 @@ 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); } /** diff --git a/src/DSL/Node.php b/src/DSL/Node.php index ced3871..f5dc296 100644 --- a/src/DSL/Node.php +++ b/src/DSL/Node.php @@ -280,6 +280,8 @@ protected function resolveProperties(array $properties): array foreach ($properties as $key => $property) { if ($property instanceof 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) { diff --git a/src/DSL/Params/Highlight.php b/src/DSL/Params/Highlight.php index 57e8180..37e3d6e 100644 --- a/src/DSL/Params/Highlight.php +++ b/src/DSL/Params/Highlight.php @@ -191,13 +191,4 @@ public function fragmenter(string $value): static { return $this->addProperty('fragmenter', $value); } - - public function toArray() - { - $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; - } } diff --git a/src/DSL/Params/Knn.php b/src/DSL/Params/Knn.php index c033394..d3e7140 100644 --- a/src/DSL/Params/Knn.php +++ b/src/DSL/Params/Knn.php @@ -6,7 +6,6 @@ use ElasticKit\DSL\Node; use ElasticKit\DSL\Query; -use stdClass; /** * Performs a k-nearest neighbor (kNN) search on a dense_vector field. @@ -97,13 +96,4 @@ public function rescoreVector(array $value): static { return $this->addProperty('rescore_vector', $value); } - - public function toArray() - { - $result = parent::toArray(); - if (isset($result['filter']) && $result['filter'] instanceof Query) { - $result['filter'] = $result['filter']->toArray()['query'] ?? new stdClass(); - } - return $result; - } } diff --git a/src/DSL/Params/Rescore.php b/src/DSL/Params/Rescore.php index 632ea45..a470616 100644 --- a/src/DSL/Params/Rescore.php +++ b/src/DSL/Params/Rescore.php @@ -6,7 +6,6 @@ use ElasticKit\DSL\Query; use ElasticKit\DSL\Node; -use stdClass; /** * Rescores the top documents returned by a query using a secondary query. @@ -76,13 +75,4 @@ public function scoreMode(string $value): static $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/Query.php b/src/DSL/Query.php index 3bb9aa2..6ff8dd1 100644 --- a/src/DSL/Query.php +++ b/src/DSL/Query.php @@ -131,14 +131,18 @@ public function addQuery($clause): static /** * Conditionally add a query clause. * - * @param bool|callable $condition + * $condition is a bool, or a Closure returning a bool. Bare values (e.g. + * strings) are treated as truthy values, NOT invoked — so when('count', …) + * does not call count(). + * + * @param bool|\Closure $condition * @param mixed $query * @param mixed $default * @return $this */ - public function when(bool|callable $condition, $query, $default = null): static + 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)); @@ -170,23 +174,29 @@ public function aggs($alias, $aggs = null): static } if ($aggs instanceof Agg) { - if ($alias !== null) { - $aggs->alias($alias); + $key = $alias ?? $aggs->getAlias(); + if ($key === null || $key === '') { + throw new BadMethodCallException('aggs() requires a non-empty alias.'); } - $this->_aggregations[$alias ?? $aggs->getAlias()] = $aggs; + $aggs->alias($key); + $this->_aggregations[$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); - if ($alias !== null) { - $childAgg->alias($alias); - } + $childAgg->alias($alias); $this->_aggregations[$alias] = $childAgg; return $this; } - if ($alias !== null && !isset($this->_aggregations[$alias])) { + if (!isset($this->_aggregations[$alias])) { $this->_aggregations[$alias] = new Agg(); $this->_aggregations[$alias]->alias($alias); } @@ -196,13 +206,9 @@ public function aggs($alias, $aggs = null): static 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; + throw new BadMethodCallException( + sprintf('aggs("%s", ...) requires a closure, array, or Agg instance as the definition.', $alias) + ); } /** diff --git a/src/Index/Bulk.php b/src/Index/Bulk.php index f2c71b5..d5454cc 100644 --- a/src/Index/Bulk.php +++ b/src/Index/Bulk.php @@ -262,7 +262,7 @@ public function flush(array $options = []): array $json = '(unable to encode bulk response)'; } if (strlen($json) > 4096) { - $json = substr($json, 0, 4096) . '... [truncated]'; + $json = mb_strcut($json, 0, 4096) . '... [truncated]'; } throw new RuntimeException("Bulk request has errors: {$json}"); } diff --git a/src/Index/Manager.php b/src/Index/Manager.php index 275fdfe..8bd77db 100644 --- a/src/Index/Manager.php +++ b/src/Index/Manager.php @@ -125,9 +125,11 @@ public function get(): array */ 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(); } diff --git a/src/Index/Search.php b/src/Index/Search.php index e5f9e7f..e2e815a 100644 --- a/src/Index/Search.php +++ b/src/Index/Search.php @@ -59,7 +59,7 @@ 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()) ); } @@ -112,7 +112,9 @@ public function count(): int $response = $this->doCount(); if (!isset($response['count'])) { - throw new RuntimeException('Missing "count" in Elasticsearch response.'); + throw new RuntimeException( + sprintf('Missing "count" in Elasticsearch response for index [%s].', $this->index->name()) + ); } return $response['count']; diff --git a/tests/AggsTest.php b/tests/AggsTest.php index 70ed74d..0e3b0ac 100644 --- a/tests/AggsTest.php +++ b/tests/AggsTest.php @@ -8,7 +8,7 @@ class AggsTest extends DslTestCase { - public function testTermsregation() + public function testTermsAggregation() { $expectedJson = <<assertQuery($exampleJson, $query); } - public function testA() + public function testQueryStringCrossFields() { $exampleJson = <<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/SpecializedQueriesTest.php b/tests/SpecializedQueriesTest.php index 6c6c5a2..e66d878 100644 --- a/tests/SpecializedQueriesTest.php +++ b/tests/SpecializedQueriesTest.php @@ -69,7 +69,7 @@ public function testMoreLikeThis() $this->assertQuery($exampleJson, $query); } - public function testMoreLikeThisWithQuery() + public function testPercolate() { $exampleJson = << Date: Thu, 25 Jun 2026 23:21:32 +0800 Subject: [PATCH 35/70] refactor(index)!: remove Results::hasMore(), use isEmpty() uniformly hasMore() (!empty hits) is the inverse of the existing isEmpty() and redundant, and its name misleads pagination users (reads as "there is a next page"). Remove it: scroll loops use !isEmpty(), pagination "next page" uses page() --- src/Index/Results.php | 19 ++++++------------- src/Index/Search.php | 2 +- tests/Index/ResultsTest.php | 17 ----------------- 3 files changed, 7 insertions(+), 31 deletions(-) diff --git a/src/Index/Results.php b/src/Index/Results.php index 14de6e4..f1a73be 100644 --- a/src/Index/Results.php +++ b/src/Index/Results.php @@ -138,13 +138,16 @@ public function totalRelation(): ?string } /** - * Return whether the current batch contains hits. + * Whether the current result set has no hits. + * + * For scroll loops: `while (! $results->isEmpty())`. For pagination + * "has a next page", use `page() < lastPage()` instead. * * @return bool */ - public function hasMore(): bool + public function isEmpty(): bool { - return !empty($this->response['hits']['hits']); + return empty($this->response['hits']['hits']); } /** @@ -221,16 +224,6 @@ public function items(): array return $this->docs(); } - /** - * Return whether the result set is empty. - * - * @return bool - */ - public function isEmpty(): bool - { - return empty($this->response['hits']['hits']); - } - /** * Convert to a framework paginator using the registered resolver. * diff --git a/src/Index/Search.php b/src/Index/Search.php index e2e815a..91f8d72 100644 --- a/src/Index/Search.php +++ b/src/Index/Search.php @@ -223,7 +223,7 @@ 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); } diff --git a/tests/Index/ResultsTest.php b/tests/Index/ResultsTest.php index fb00635..ead497d 100644 --- a/tests/Index/ResultsTest.php +++ b/tests/Index/ResultsTest.php @@ -89,23 +89,6 @@ public function testScrollIdReturnsNullWhenAbsent() $this->assertNull($results->scrollId()); } - public function testHasMoreScrollWithHits() - { - $results = new Results($this->makeResponse([ - 'hits' => [ - 'total' => ['value' => 1, 'relation' => 'eq'], - 'hits' => [['_source' => ['title' => 'foo']]], - ], - ])); - $this->assertTrue($results->hasMore()); - } - - public function testHasMoreScrollEmpty() - { - $results = new Results($this->makeResponse()); - $this->assertFalse($results->hasMore()); - } - public function testTotalRelationEq() { $results = new Results($this->makeResponse([ From cd7e9a998bcfec0da230a4ec5f61e31c02ed4cfc Mon Sep 17 00:00:00 2001 From: ykan821 Date: Thu, 25 Jun 2026 23:27:37 +0800 Subject: [PATCH 36/70] fix(dsl): AllOf::filter() wraps the value in a Filter node, aligned with AnyOf AllOf::filter() stored $value as-is; a closure was then converted by resolveProperties as a Query-closure (expecting Filter) -> TypeError crash. Now it wraps with Filter::create($value) like AnyOf, producing the correct filter-rule structure {after/before/...}. The two are now consistent. Co-Authored-By: Claude --- src/DSL/Queries/FullText/Intervals/AllOf.php | 2 +- tests/FullTextQueriesTest.php | 35 ++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/DSL/Queries/FullText/Intervals/AllOf.php b/src/DSL/Queries/FullText/Intervals/AllOf.php index c139ace..61d3fcb 100644 --- a/src/DSL/Queries/FullText/Intervals/AllOf.php +++ b/src/DSL/Queries/FullText/Intervals/AllOf.php @@ -81,6 +81,6 @@ public function ordered(bool $value): static */ public function filter($value): static { - return $this->addProperty('filter', $value); + return $this->addProperty('filter', Filter::create($value)); } } diff --git a/tests/FullTextQueriesTest.php b/tests/FullTextQueriesTest.php index f7645a9..1d5aa8e 100644 --- a/tests/FullTextQueriesTest.php +++ b/tests/FullTextQueriesTest.php @@ -654,4 +654,39 @@ public function testAnyOfAddInterval() }); $this->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); + } } From 8a77e28d7e7b4fcfb84304feee9a3f810d46cbdf Mon Sep 17 00:00:00 2001 From: ykan821 Date: Fri, 26 Jun 2026 00:30:34 +0800 Subject: [PATCH 37/70] refactor(dsl): Composite::sources() tightens to array (native source config only) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified against the ES spec: composite sources support only 4 types (terms/histogram/date_histogram/geotile_grid), all bucketing methods, not query types. Feeding Node/Query/Closure produces a query structure ({match/term:...}) that ES rejects as a source, and Node drops the type key. sources() is tightened to array, blocking wrong input at the type level. The original raw-array usage (['product'=>['terms'=>...]]) was already correct, no bug. The evaluated addSource (append-style) is removed — no conversion value; composite sources are a static set, sources() sets them all at once. Co-Authored-By: Claude --- src/DSL/Aggs/Bucket/Composite.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/DSL/Aggs/Bucket/Composite.php b/src/DSL/Aggs/Bucket/Composite.php index 8614888..c261f50 100644 --- a/src/DSL/Aggs/Bucket/Composite.php +++ b/src/DSL/Aggs/Bucket/Composite.php @@ -14,12 +14,13 @@ class Composite extends Node protected string $_key = 'composite'; /** - * List of source definitions used to build composite buckets. + * Set the whole sources list. Each element is {name: {type: config}} where + * type is one of: terms, histogram, date_histogram, geotile_grid. * - * @param mixed $value + * @param array> $value * @return static */ - public function sources($value): static + public function sources(array $value): static { return $this->addProperty('sources', $value); } From 30ff8ae6b81aa3380c5c157b8afd3e4f1f00bf2a Mon Sep 17 00:00:00 2001 From: ykan821 Date: Fri, 26 Jun 2026 01:04:00 +0800 Subject: [PATCH 38/70] refactor(index)!: Event uses real typed properties, drop magic __get/__set **BC:** A misspelled Event property changes from a silent null to a PHP Error; renamed properties now fail explicitly (no longer silently inert) Co-Authored-By: Claude --- src/Index/Event.php | 76 +++++++++++++++------------------------------ 1 file changed, 25 insertions(+), 51 deletions(-) diff --git a/src/Index/Event.php b/src/Index/Event.php index a027152..6cb3567 100644 --- a/src/Index/Event.php +++ b/src/Index/Event.php @@ -7,68 +7,42 @@ /** * Lightweight event object carrying event name, index, and contextual data. * - * @property string $name Event name (e.g. 'search.query.after') - * @property string $index Index name - * @property array|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.flush events) - * @property string|null $newIndex New backing index name (rebuild.run.after) - * @property string|null $oldIndex Previous backing index name (rebuild.run.after) + * Properties are real typed properties: typos throw an Error (not silent null), + * and IDEs/phpstan can validate them. Reading an unset property returns null. */ class Event { - /** - * @var string - */ public string $name; - /** - * @var string - */ public string $index; - /** - * @var array - */ - private array $data = []; + /** @var array|\stdClass|null Request body (search.query.before/after) */ + public array|\stdClass|null $dsl = null; - /** - * @param string $name - * @param string $index - */ - public function __construct(string $name, string $index) - { - $this->name = $name; - $this->index = $index; - } + /** @var string|null Calling method: get/first/count/scroll/paginate (search events) */ + public ?string $action = null; - /** - * @param string $key - * @return mixed - */ - public function __get(string $key): mixed - { - return $this->data[$key] ?? null; - } + /** @var array|null ES API response (all after events) */ + public ?array $response = null; - /** - * @param string $key - * @param mixed $value - */ - public function __set(string $key, mixed $value): void - { - $this->data[$key] = $value; - } + /** @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; - /** - * @param string $key - * @return bool - */ - public function __isset(string $key): bool + /** @var string|null Previous backing index name (rebuild.run.after) */ + public ?string $oldIndex = null; + + public function __construct(string $name, string $index) { - return isset($this->data[$key]); + $this->name = $name; + $this->index = $index; } } From 846d1f6af4be1e27ff3e5774aed304561d65526b Mon Sep 17 00:00:00 2001 From: ykan821 Date: Fri, 26 Jun 2026 15:01:14 +0800 Subject: [PATCH 39/70] docs: translate tests, CLAUDE.md, and CHANGELOG to English - tests: 8 Chinese comments to English (FullTextQueriesTest, ManagerTest, RebuildTest) - CLAUDE.md: English rewrite, switch commit/changelog conventions to English, drop completed TODOs - CHANGELOG.md: English Co-Authored-By: Claude --- CHANGELOG.md | 18 ++++---- CLAUDE.md | 83 ++++++++++++++--------------------- tests/FullTextQueriesTest.php | 6 +-- tests/Index/ManagerTest.php | 2 +- tests/Index/RebuildTest.php | 8 ++-- 5 files changed, 50 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62f9b3f..cb231cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,13 @@ ## [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 4a3e1d2..51874d4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,75 +1,58 @@ # 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 -- [x] **clone query 后 try-finally 恢复**:first() / paginate() 等方法 clone query 后修改状态,需 try-finally 确保恢复 -- [x] **ClausesSupport API 统一**:must()/should()/filter() 等改为追加语义,移除 addXXX() 方法 -- [x] **hasMore() 确认无 bug**:scroll 场景 !empty(hits) 正确,分页应用 page()` 按批 yield Results;`cursor($duration): Generator` 逐条 yield 完整 hit(_id/_score/_source,yield from chunk 的 hits、复用其 finally clear);保留 `scroll()/next()/clear()` 作低层原语。底层未来可换 PIT+search_after(上层签名不变) +- [ ] **Add boundary tests for core paths**: scroll, bulk batching, rebuild failure rollback +- [ ] **Set up integration test infrastructure**: driven by `ELASTICKIT_TEST_HOST`, with random index names for isolation -## 测试 +## Tests -测试在 Docker 容器中运行,需要设置以下环境变量: +Tests run inside a Docker container and require these environment variables: -| 变量 | 用途 | +| Variable | Purpose | |---|---| -| `PHP_CONTAINER` | Docker 容器名 | -| `PROJECT_PATH` | 项目在容器内的路径 | -| `PROXY_PORT` | HTTP 代理端口(推送用) | -| `ELASTICKIT_TEST_HOST` | ES 集成测试地址(如 `https://localhost:9200`),不设置则跳过集成测试 | +| `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 | -## 推送代码前需要执行4件套 +## Pre-push checklist ```bash docker exec $PHP_CONTAINER sh -c "cd $PROJECT_PATH && vendor/bin/phpunit --testsuite unit" @@ -80,6 +63,6 @@ docker exec $PHP_CONTAINER sh -c "cd $PROJECT_PATH && vendor/bin/phpstan analyse 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/tests/FullTextQueriesTest.php b/tests/FullTextQueriesTest.php index 1d5aa8e..eb9257d 100644 --- a/tests/FullTextQueriesTest.php +++ b/tests/FullTextQueriesTest.php @@ -68,8 +68,8 @@ public function testIntervals() public function testIntervalsPreservesInheritedProperties() { - // 继承自 Node 的 boost() 等方法此前被 Intervals::toArray() 静默丢弃, - // toArray 不应再绕过 $_properties。 + // 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' => 'foo']); diff --git a/tests/Index/ManagerTest.php b/tests/Index/ManagerTest.php index f7f8975..5fd99f9 100644 --- a/tests/Index/ManagerTest.php +++ b/tests/Index/ManagerTest.php @@ -118,7 +118,7 @@ public function testDeleteResolvesAliasToBackingIndex() public function testDeleteResolvesAliasToAllBackingIndices() { - // 别名指向多个 backing index 时一次性删除,而非只删第一个(原 array_key_first 隐患) + // An alias pointing at multiple backing indices must be deleted in one shot, not just the first (the former array_key_first pitfall). $indices = $this->createMock(TestIndices::class); $indices->method('existsAlias')->willReturn(new BoolResponse(true)); $indices->method('getAlias')->willReturn(new ArrayResponse([ diff --git a/tests/Index/RebuildTest.php b/tests/Index/RebuildTest.php index 5412571..d62511f 100644 --- a/tests/Index/RebuildTest.php +++ b/tests/Index/RebuildTest.php @@ -738,7 +738,7 @@ public function testRunThrowsWhenReleaseLockFailsAfterSuccess() $client = $this->createMock(TestClient::class); $client->method('indices')->willReturn($indices); $client->method('index')->willReturn(new ArrayResponse(['result' => 'created'])); - // 成功路径释放锁时抛 503 + // Lock release throws 503 on the success path. $client->method('delete')->willThrowException($releaseException); $client->method('bulk')->willReturn(new ArrayResponse(['items' => []])); Index::setClient($client); @@ -780,9 +780,9 @@ public function testRunPreservesOriginalExceptionWhenReleaseFails() $client = $this->createMock(TestClient::class); $client->method('indices')->willReturn($indices); $client->method('index')->willReturn(new ArrayResponse(['result' => 'created'])); - // 释放锁也失败:503 + // Lock release also fails: 503. $client->method('delete')->willThrowException($releaseException); - // bulk 导入失败 → doRun 抛异常 + // Bulk import fails → doRun throws. $client->method('bulk')->willReturn(new ArrayResponse(['items' => [], 'errors' => true])); Index::setClient($client); @@ -802,7 +802,7 @@ public function source(array $context = []): iterable (new Rebuild($index))->run(); $this->fail('Expected original import exception'); } catch (\Throwable $e) { - // ⑤:必须抛出 doRun 的原始异常,而非释放锁的 503 ClientResponseException + // ⑤: must throw doRun's original exception, not the 503 ClientResponseException from lock release. $this->assertInstanceOf(\RuntimeException::class, $e); $this->assertStringContainsString('Bulk request has errors', $e->getMessage()); $this->assertStringNotContainsString('succeeded but the lock', $e->getMessage()); From b35fa03050b84cfeaf2b312f3e0a29dae5607c5b Mon Sep 17 00:00:00 2001 From: ykan821 Date: Fri, 26 Jun 2026 15:57:55 +0800 Subject: [PATCH 40/70] fix: accept '0' index name, reject empty target, add gapPolicy/@deprecated - Index::name(): accept '0' (valid ES index name); empty() -> isset check for uninitialized typed property - Bulk::target(): reject empty string with InvalidArgumentException - GeoPolygon: add @deprecated tag (deprecated in ES 7.12) - CumulativeSum: add gapPolicy() to match other pipeline aggs Co-Authored-By: Claude --- src/DSL/Aggs/Pipeline/CumulativeSum.php | 11 +++++++++++ src/DSL/Queries/Geo/GeoPolygon.php | 2 +- src/Index/Bulk.php | 6 +++++- src/Index/Index.php | 2 +- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/DSL/Aggs/Pipeline/CumulativeSum.php b/src/DSL/Aggs/Pipeline/CumulativeSum.php index 3287ab2..5eac62b 100644 --- a/src/DSL/Aggs/Pipeline/CumulativeSum.php +++ b/src/DSL/Aggs/Pipeline/CumulativeSum.php @@ -24,6 +24,17 @@ public function bucketsPath(string $value): static return $this->addProperty('buckets_path', $value); } + /** + * Policy to apply when gaps are found in the data. + * + * @param string $value + * @return static + */ + public function gapPolicy(string $value): static + { + return $this->addProperty('gap_policy', $value); + } + /** * Format for the output value. * diff --git a/src/DSL/Queries/Geo/GeoPolygon.php b/src/DSL/Queries/Geo/GeoPolygon.php index bc5b05d..cd07c11 100644 --- a/src/DSL/Queries/Geo/GeoPolygon.php +++ b/src/DSL/Queries/Geo/GeoPolygon.php @@ -9,7 +9,7 @@ /** * Returns hits that only fall within a polygon of points. * - * Deprecated in 7.12. Use geo_shape instead where polygons are defined in GeoJSON or Well-Known Text (WKT). + * @deprecated Deprecated in ES 7.12. Use geo_shape instead, where polygons are defined in GeoJSON or Well-Known Text (WKT). */ class GeoPolygon extends Node { diff --git a/src/Index/Bulk.php b/src/Index/Bulk.php index d5454cc..34d16b7 100644 --- a/src/Index/Bulk.php +++ b/src/Index/Bulk.php @@ -55,10 +55,14 @@ public function __construct( * * @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(string $indexName): static { + 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}"); } diff --git a/src/Index/Index.php b/src/Index/Index.php index 35426e0..159a243 100644 --- a/src/Index/Index.php +++ b/src/Index/Index.php @@ -109,7 +109,7 @@ public static function on(string $connection): static */ public function name(): string { - if (empty($this->name)) { + if (!isset($this->name) || $this->name === '') { throw new RuntimeException( sprintf('Index $name is not set in %s', static::class) ); From 523498cd0132aca8d7d17291d444672f5626f22f Mon Sep 17 00:00:00 2001 From: ykan821 Date: Fri, 26 Jun 2026 17:03:31 +0800 Subject: [PATCH 41/70] feat(dsl): support _name (named query) on all nodes via Node::__call - Node::__call handles _name so it surfaces in matched_queries - missing argument throws ArgumentCountError; unknown methods throw BadMethodCallException - GeoDistance: drop the explicit _name, now provided by __call - Node: @SuppressWarnings for ExcessiveClassComplexity (DSL base accumulates accessors) Co-Authored-By: Claude --- src/DSL/Node.php | 21 +++++++++++++++++++++ src/DSL/Queries/Geo/GeoDistance.php | 11 ----------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/DSL/Node.php b/src/DSL/Node.php index f5dc296..25278d6 100644 --- a/src/DSL/Node.php +++ b/src/DSL/Node.php @@ -4,6 +4,8 @@ namespace ElasticKit\DSL; +use ArgumentCountError; +use BadMethodCallException; use Closure; use InvalidArgumentException; use stdClass; @@ -12,6 +14,7 @@ * Abstract base class for DSL nodes (query types, params). * * @phpstan-consistent-constructor + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) the DSL base accumulates many thin accessors */ abstract class Node { @@ -362,4 +365,22 @@ public function boost($value): static { 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/Queries/Geo/GeoDistance.php b/src/DSL/Queries/Geo/GeoDistance.php index e0ea3f4..4308765 100644 --- a/src/DSL/Queries/Geo/GeoDistance.php +++ b/src/DSL/Queries/Geo/GeoDistance.php @@ -58,17 +58,6 @@ public function distanceType(string $value): static return $this->addProperty('distance_type', $value); } - /** - * Optional name field to identify the query. - * - * @param string $value - * @return static - */ - public function _name(string $value): static - { - return $this->addProperty('_name', $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. From de3fdb7ec5dd1378cdfaa918bce3ec00b9cf1534 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Fri, 26 Jun 2026 22:55:49 +0800 Subject: [PATCH 42/70] test: split ES validation into integration layer - DslTestCase: JSON-only assertQuery; fix ensureIndex exists() asBool (ES validation never actually ran); expose createIndex/seedData/ensureSpecialFields as protected; drop ES connection from unit setUpBeforeClass - IntegrationTestCase: per-test random index isolation + assertQueryEs/makeIndex, skips without ELASTICKIT_TEST_HOST - phpunit.xml: integration testsuite; unit excludes tests/Integration - SmokeTest: verifies ES reachable + seed Co-Authored-By: Claude --- phpunit.xml | 4 + tests/DslTestCase.php | 66 +++------------ tests/Integration/IntegrationTestCase.php | 98 +++++++++++++++++++++++ tests/Integration/SmokeTest.php | 20 +++++ 4 files changed, 132 insertions(+), 56 deletions(-) create mode 100644 tests/Integration/IntegrationTestCase.php create mode 100644 tests/Integration/SmokeTest.php diff --git a/phpunit.xml b/phpunit.xml index 4166425..72ee5e3 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -4,9 +4,13 @@ tests tests/Index + tests/Integration tests/Index + + tests/Integration + diff --git a/tests/DslTestCase.php b/tests/DslTestCase.php index 548c82e..78ad43e 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 { @@ -25,70 +25,24 @@ abstract class DslTestCase extends TestCase 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; - } - } + // Unit tests assert JSON only; ES connection lives in IntegrationTestCase. } /** - * 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 +81,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 = [ [ @@ -222,7 +176,7 @@ private static function seedData(\Elastic\Elasticsearch\ClientInterface $client, /** * Ensure special field mappings (percolator, rank_feature, shape). */ - private static function ensureSpecialFields(\Elastic\Elasticsearch\ClientInterface $client, string $index): void + protected static function ensureSpecialFields(\Elastic\Elasticsearch\ClientInterface $client, string $index): void { $mapping = $client->indices()->getMapping(['index' => $index]); $properties = $mapping[$index]['mappings']['properties'] ?? []; diff --git a/tests/Integration/IntegrationTestCase.php b/tests/Integration/IntegrationTestCase.php new file mode 100644 index 0000000..c5f0800 --- /dev/null +++ b/tests/Integration/IntegrationTestCase.php @@ -0,0 +1,98 @@ +markTestSkipped('ELASTICKIT_TEST_HOST not set'); + return; + } + + if (static::$esClient === null) { + static::$esClient = ClientBuilder::create()->setHosts([$host])->build(); + } + + $this->indexName = 'ek_it_' . bin2hex(random_bytes(4)); + static::createIndex(static::$esClient, $this->indexName); + static::seedData(static::$esClient, $this->indexName); + + Index::setClient(static::$esClient); + } + + protected function tearDown(): void + { + if (static::$esClient !== null && isset($this->indexName)) { + try { + static::$esClient->indices()->delete(['index' => $this->indexName]); + } catch (\Throwable $e) { + // best-effort cleanup; ignore 404 if index already gone + } + } + ClientManager::reset(); + } + + /** + * Anonymous Index subclass bound to the random test index. + */ + protected function makeIndex(): Index + { + $name = $this->indexName; + return new class($name) extends Index { + public function __construct(string $name) + { + $this->name = $name; + } + }; + } + + /** + * 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 random 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); + } +} From 44587236ae76b890400e5049bf3fc9c1ba875dc4 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Fri, 26 Jun 2026 23:09:14 +0800 Subject: [PATCH 43/70] test(integration): add core DSL ES contract tests - TermLevel: term/terms/range/exists/prefix/wildcard/fuzzy/ids - FullText: match/matchPhrase/queryString/matchBoolPrefix - Compound: bool must/should/filter/mustNot, constantScore - Geo: geoDistance/geoBoundingBox - Joining: nested (match/term) Co-Authored-By: Claude --- .../Integration/Dsl/CompoundContractTest.php | 76 +++++++++++++++++++ .../Integration/Dsl/FullTextContractTest.php | 42 ++++++++++ tests/Integration/Dsl/GeoContractTest.php | 31 ++++++++ tests/Integration/Dsl/JoiningContractTest.php | 34 +++++++++ .../Integration/Dsl/TermLevelContractTest.php | 67 ++++++++++++++++ 5 files changed, 250 insertions(+) create mode 100644 tests/Integration/Dsl/CompoundContractTest.php create mode 100644 tests/Integration/Dsl/FullTextContractTest.php create mode 100644 tests/Integration/Dsl/GeoContractTest.php create mode 100644 tests/Integration/Dsl/JoiningContractTest.php create mode 100644 tests/Integration/Dsl/TermLevelContractTest.php 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/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); + } +} From 880815c9e605f2abb23b7bef30377e3d0a1b9a90 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Fri, 26 Jun 2026 23:18:12 +0800 Subject: [PATCH 44/70] test(integration): add specialized + aggregation ES contract tests - Specialized: script, scriptScore - Aggregation: terms/sum/avg/stats/cardinality/date_histogram Co-Authored-By: Claude --- .../Dsl/AggregationContractTest.php | 65 +++++++++++++++++++ .../Dsl/SpecializedContractTest.php | 39 +++++++++++ 2 files changed, 104 insertions(+) create mode 100644 tests/Integration/Dsl/AggregationContractTest.php create mode 100644 tests/Integration/Dsl/SpecializedContractTest.php 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/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']); + } +} From c9fe00b8063626a5a769a65df2fa416d89362a59 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Fri, 26 Jun 2026 23:26:24 +0800 Subject: [PATCH 45/70] test(integration): add Search + Doc contract tests, drop matching mocks - SearchContractTest: get/first/count/paginate/scroll/chunk/cursor (real ES) - DocContractTest: index/get/source/exists/update/upsert/create-conflict(409)/delete/auto-id (real ES) - Remove mock-based IndexTest and DocTest (replaced by integration) Co-Authored-By: Claude --- tests/Index/DocTest.php | 357 -------- tests/Index/IndexTest.php | 867 ------------------ tests/Integration/Index/DocContractTest.php | 81 ++ .../Integration/Index/SearchContractTest.php | 76 ++ 4 files changed, 157 insertions(+), 1224 deletions(-) delete mode 100644 tests/Index/DocTest.php delete mode 100644 tests/Index/IndexTest.php create mode 100644 tests/Integration/Index/DocContractTest.php create mode 100644 tests/Integration/Index/SearchContractTest.php diff --git a/tests/Index/DocTest.php b/tests/Index/DocTest.php deleted file mode 100644 index 949fac1..0000000 --- a/tests/Index/DocTest.php +++ /dev/null @@ -1,357 +0,0 @@ -createMock(TestClient::class)); - } - - protected function tearDown(): void - { - ClientManager::reset(); - } - - 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(); - } - - public function testIndexAutoGeneratesIdWhenEmpty() - { - $client = $this->createMock(TestClient::class); - $client->expects($this->once()) - ->method('index') - ->with(['index' => 'products', 'body' => ['title' => 'foo']]) - ->willReturn(new ArrayResponse(['result' => 'created', '_id' => 'auto'])); - Index::setClient($client); - - $index = $this->createIndex('products'); - $result = $index->newDoc(null)->index(['title' => 'foo']); - - $this->assertEquals('created', $result['result']); - } - - public function testUpdateRequiresExplicitId() - { - $index = $this->createIndex('products'); - - $this->expectException(\RuntimeException::class); - $index->newDoc(null)->update(['title' => 'foo']); - } -} diff --git a/tests/Index/IndexTest.php b/tests/Index/IndexTest.php deleted file mode 100644 index 2c2b325..0000000 --- a/tests/Index/IndexTest.php +++ /dev/null @@ -1,867 +0,0 @@ -createMock(TestClient::class)); - } - - protected function tearDown(): void - { - ClientManager::reset(); - Pagination::reset(); - } - - 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 testChunkYieldsResultsBatches() - { - $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()->chunk('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 testCursorYieldsIndividualHits() - { - $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'); - $hits = []; - foreach ($index->query()->matchAll()->cursor('1m') as $hit) { - $hits[] = $hit; - } - - // 3 docs flattened across 2 batches, each yielded as a raw hit - $this->assertCount(3, $hits); - $this->assertSame('1', $hits[0]['_id']); - $this->assertSame(['id' => 1], $hits[0]['_source']); - $this->assertSame('3', $hits[2]['_id']); - } - - 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() - { - Pagination::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() - { - Pagination::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 testGetClientThrowsForUnknownConnection() - { - $defaultClient = $this->createMock(TestClient::class); - Index::setClient($defaultClient); - - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('nonexistent'); - - $index = new class('unknown') extends Index { - public function __construct($name) - { - $this->name = $name; - $this->connection = 'nonexistent'; - } - }; - - $index->getClient(); - } - - public function testGetClientThrowsWhenNotRegistered() - { - ClientManager::reset(); - - $this->expectException(\RuntimeException::class); - - $index = $this->createIndex('test'); - $index->getClient(); - } - - public function testSetConnectionAndGetConnection() - { - $index = $this->createIndex('products'); - - $this->assertEquals('default', $index->getConnection()); - - $result = $index->setConnection('secondary'); - $this->assertSame($index, $result); - $this->assertEquals('secondary', $index->getConnection()); - } - - public function testOnCreatesInstanceWithConnection() - { - $defaultClient = $this->createMock(TestClient::class); - $secondaryClient = $this->createMock(TestClient::class); - Index::setClient($defaultClient); - Index::setClient($secondaryClient, 'secondary'); - - $index = TestConcreteIndex::on('secondary'); - - $this->assertInstanceOf(TestConcreteIndex::class, $index); - $this->assertEquals('secondary', $index->getConnection()); - $this->assertEquals('test', $index->name()); - $this->assertSame($secondaryClient, $index->getClient()); - } - - public function testNewQueryReturnsSearch() - { - $client = $this->createMock(TestClient::class); - $client->method('search')->willReturn(new ArrayResponse([ - 'hits' => ['total' => ['value' => 1], 'hits' => [['_source' => ['title' => 'test']]]], - ])); - Index::setClient($client); - - $index = $this->createIndex('products'); - $this->assertInstanceOf(Search::class, $index->newQuery()); - - $query = Query::create()->match('title', 'test'); - $results = $index->newQuery($query)->get(); - $this->assertEquals(1, $results->total()); - } - - public function testNewQueryUsesInstanceConnection() - { - $secondaryClient = $this->createMock(TestClient::class); - $secondaryClient->method('search')->willReturn(new ArrayResponse([ - 'hits' => ['total' => ['value' => 0], 'hits' => []], - ])); - Index::setClient($secondaryClient, 'secondary'); - - $index = $this->createIndex('products'); - $index->setConnection('secondary'); - $results = $index->newQuery()->matchAll()->get(); - - $this->assertEquals(0, $results->total()); - } - - public function testNewDocUsesInstanceConnection() - { - $secondaryClient = $this->createMock(TestClient::class); - $secondaryClient->method('getSource')->willReturn(new ArrayResponse([ - 'title' => 'test', - ])); - Index::setClient($secondaryClient, 'secondary'); - - $index = $this->createIndex('products'); - $index->setConnection('secondary'); - $doc = $index->newDoc(1); - - $this->assertInstanceOf(\ElasticKit\Index\Doc::class, $doc); - $this->assertEquals(['title' => 'test'], $doc->source()); - } - - public function testQueryDocDelegateToNew() - { - $this->assertInstanceOf(Search::class, TestConcreteIndex::query()); - $this->assertInstanceOf(\ElasticKit\Index\Doc::class, TestConcreteIndex::doc(1)); - } - - public function testOnNewQueryChain() - { - $secondaryClient = $this->createMock(TestClient::class); - $secondaryClient->method('search')->willReturn(new ArrayResponse([ - 'hits' => ['total' => ['value' => 1], 'hits' => [['_source' => ['title' => 'from_secondary']]]], - ])); - Index::setClient($secondaryClient, 'secondary'); - - $results = TestConcreteIndex::on('secondary')->newQuery()->matchAll()->get(); - - $this->assertEquals(1, $results->total()); - $this->assertEquals([['title' => 'from_secondary']], $results->docs()); - } - - public function testOnNewDocChain() - { - $secondaryClient = $this->createMock(TestClient::class); - $secondaryClient->method('getSource')->willReturn(new ArrayResponse([ - 'title' => 'secondary_doc', - ])); - Index::setClient($secondaryClient, 'secondary'); - - $doc = TestConcreteIndex::on('secondary')->newDoc(42); - - $this->assertEquals(['title' => 'secondary_doc'], $doc->source()); - } -} - -class TestConcreteIndex extends Index -{ - protected string $name = 'test'; -} diff --git a/tests/Integration/Index/DocContractTest.php b/tests/Integration/Index/DocContractTest.php new file mode 100644 index 0000000..8d197f0 --- /dev/null +++ b/tests/Integration/Index/DocContractTest.php @@ -0,0 +1,81 @@ +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']); + } +} diff --git a/tests/Integration/Index/SearchContractTest.php b/tests/Integration/Index/SearchContractTest.php new file mode 100644 index 0000000..bd90f20 --- /dev/null +++ b/tests/Integration/Index/SearchContractTest.php @@ -0,0 +1,76 @@ +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); + } + + 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->assertFalse($results->isEmpty()); + } + + 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); + } +} From 617df8e173b8a59bf080b78c059e0c44662b7672 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Fri, 26 Jun 2026 23:34:41 +0800 Subject: [PATCH 46/70] test(integration): add Bulk + Manager contract tests, drop matching mocks - BulkContractTest: index/create/update/delete, batchSize auto-flush, onError (real ES; error triggered by create-on-existing id) - ManagerContractTest: exists/putMapping/alias/refresh/delete (real ES) - Remove mock-based BulkTest and ManagerTest Co-Authored-By: Claude --- tests/Index/BulkTest.php | 559 ------------------ tests/Index/ManagerTest.php | 303 ---------- tests/Integration/Index/BulkContractTest.php | 71 +++ .../Integration/Index/ManagerContractTest.php | 59 ++ 4 files changed, 130 insertions(+), 862 deletions(-) delete mode 100644 tests/Index/BulkTest.php delete mode 100644 tests/Index/ManagerTest.php create mode 100644 tests/Integration/Index/BulkContractTest.php create mode 100644 tests/Integration/Index/ManagerContractTest.php diff --git a/tests/Index/BulkTest.php b/tests/Index/BulkTest.php deleted file mode 100644 index 114bca3..0000000 --- a/tests/Index/BulkTest.php +++ /dev/null @@ -1,559 +0,0 @@ -createMock(TestClient::class)); - } - - protected function tearDown(): void - { - ClientManager::reset(); - } - - 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'])->flush(); - - $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'])->flush(); - } - - public function testCreateActionWithoutIdOmitsId() - { - $client = $this->createMock(TestClient::class); - $client->expects($this->once()) - ->method('bulk') - ->with([ - 'body' => [ - ['create' => ['_index' => 'products']], // no _id → ES auto-generates - ['title' => 'foo'], - ], - ]) - ->willReturn(new ArrayResponse(['errors' => false, 'items' => []])); - Index::setClient($client); - - $index = $this->createIndex('products'); - (new Bulk($index))->create(null, ['title' => 'foo'])->flush(); - } - - 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'])->flush(); - } - - 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)->flush(); - } - - 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']) - ->flush(); - } - - 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')->flush(); - } - - 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') - ->flush(); - } - - 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']) - ->flush(['refresh' => 'wait_for', 'timeout' => '5s']); - } - - public function testFlushClearsBodyButPersistsRetryOnConflict() - { - $callCount = 0; - $client = $this->createMock(TestClient::class); - $client->method('bulk')->willReturnCallback(function ($params) use (&$callCount) { - $callCount++; - // retryOnConflict persists across flushes (setting); refresh is per-call - $this->assertSame(3, $params['body'][0]['update']['retry_on_conflict']); - if ($callCount === 1) { - $this->assertSame('wait_for', $params['refresh']); - } else { - $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'])->flush(['refresh' => 'wait_for']); - $bulk->update('1', ['title' => 'second'])->flush(); - - $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'])->flush(); - } - - 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->flush(); - $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))->flush(); - - $this->assertEquals([], $result); - } - - public function testExecuteThrowsOnErrors() - { - $client = $this->createMock(TestClient::class); - $client->method('bulk')->willReturn(new ArrayResponse([ - 'errors' => true, - 'items' => [ - ['index' => ['_id' => '1', 'status' => 400, 'error' => ['type' => 'mapper_parsing_exception']]], - ], - ])); - Index::setClient($client); - - $index = $this->createIndex('products'); - $this->expectException(\RuntimeException::class); - (new Bulk($index))->index('1', ['title' => 'foo'])->flush(); - } - - public function testExecutePreservesBodyForRetryOnError() - { - $callCount = 0; - $client = $this->createMock(TestClient::class); - $client->method('bulk')->willReturnCallback(function () use (&$callCount) { - $callCount++; - if ($callCount === 1) { - return new ArrayResponse(['errors' => true, 'items' => [ - ['index' => ['_id' => '1', 'status' => 400, 'error' => ['type' => 'mapper_parsing_exception']]], - ]]); - } - return new ArrayResponse(['errors' => false, 'items' => []]); - }); - Index::setClient($client); - - $index = $this->createIndex('products'); - $bulk = (new Bulk($index))->index('1', ['title' => 'foo']); - - try { - $bulk->flush(); - $this->fail('Expected RuntimeException'); - } catch (\RuntimeException $e) { - // body must survive the failure so the caller can retry - } - - $result = $bulk->flush(); - $this->assertFalse($result['errors']); - $this->assertEquals(2, $callCount); - } - - public function testOnErrorReceivesResponseBodyAndFreshBulk() - { - $client = $this->createMock(TestClient::class); - $client->method('bulk')->willReturn(new ArrayResponse([ - 'errors' => true, - 'items' => [ - ['index' => ['_id' => '1', 'status' => 201]], - ['index' => ['_id' => '2', 'status' => 400, 'error' => ['type' => 'mapper_parsing_exception']]], - ], - ])); - Index::setClient($client); - - $captured = []; - $outer = (new Bulk($this->createIndex('products'))) - ->target('products_new') - ->onError(function ($response, $body, $newbulk) use (&$captured) { - $captured['response'] = $response; - $captured['body'] = $body; - $captured['newbulk'] = $newbulk; - }); - $outer->index('1', ['title' => 'A'])->index('2', ['title' => 'B'])->flush(); - - $this->assertTrue($captured['response']['errors']); // raw ES response - $this->assertSame('1', $captured['body'][0]['index']['_id']); // full body, successes included - $this->assertSame(['title' => 'A'], $captured['body'][1]); - $this->assertSame('2', $captured['body'][2]['index']['_id']); - $this->assertInstanceOf(Bulk::class, $captured['newbulk']); // fresh Bulk - $this->assertNotSame($outer, $captured['newbulk']); // independent instance - } - - public function testOnErrorRetriesFailuresViaFreshBulkOnSameTarget() - { - // Outer targets 'products_new'. Batch of 2: id=1 succeeds, id=2 fails. - $calls = []; - $client = $this->createMock(TestClient::class); - $client->method('bulk')->willReturnCallback(function ($params) use (&$calls) { - $calls[] = $params['body']; - if (count($calls) === 1) { - return new ArrayResponse([ - 'errors' => true, - 'items' => [ - ['index' => ['_id' => '1', 'status' => 201]], - ['index' => ['_id' => '2', 'status' => 400, 'error' => ['type' => 'mapper_parsing_exception']]], - ], - ]); - } - return new ArrayResponse(['errors' => false, 'items' => []]); - }); - Index::setClient($client); - - $index = $this->createIndex('products'); - (new Bulk($index)) - ->target('products_new') - ->onError(function ($response, $body, $newbulk) { - // user extracts the failure (id=2) and re-sends it on the fresh bulk. - // items[k] ↔ k-th action; for an all-index batch, action k's data is body[2k+1]. - foreach ($response['items'] as $i => $item) { - if (($item['index']['status'] ?? 200) >= 400) { - $newbulk->index($item['index']['_id'], $body[$i * 2 + 1]); - } - } - $newbulk->flush(); - }) - ->index('1', ['title' => 'A']) - ->index('2', ['title' => 'B']) - ->flush(); - - $this->assertCount(2, $calls); // original + retry - $this->assertSame('products_new', $calls[1][0]['index']['_index']); // fresh bulk inherited target - $this->assertSame('2', $calls[1][0]['index']['_id']); // only the failure - $this->assertSame(['title' => 'B'], $calls[1][1]); // its data - } - - public function testOnErrorFreshBulkHasNoHandlerSoItsErrorsThrow() - { - // The fresh Bulk is bare (no handler): its own flush() throws on error - // rather than recursing back into the handler. - $calls = 0; - $client = $this->createMock(TestClient::class); - $client->method('bulk')->willReturnCallback(function () use (&$calls) { - $calls++; - return new ArrayResponse(['errors' => true, 'items' => [ - ['index' => ['_id' => '1', 'status' => 400, 'error' => ['type' => 'mapper_parsing_exception']]], - ]]); - }); - Index::setClient($client); - - $threw = false; - $index = $this->createIndex('products'); - (new Bulk($index)) - ->onError(function ($response, $body, $newbulk) use (&$threw) { - $newbulk->index('1', ['title' => 'retry']); - try { - $newbulk->flush(); - } catch (\RuntimeException $e) { - $threw = true; // surfaced as exception, no recursion - } - }) - ->index('1', ['title' => 'foo']) - ->flush(); - - $this->assertTrue($threw); - $this->assertEquals(2, $calls); // original + one retry attempt, no recursion - } - - public function testOnErrorClearsBatchWhenHandlerReturns() - { - $callCount = 0; - $client = $this->createMock(TestClient::class); - $client->method('bulk')->willReturnCallback(function () use (&$callCount) { - $callCount++; - return new ArrayResponse(['errors' => true, 'items' => []]); - }); - Index::setClient($client); - - $index = $this->createIndex('products'); - $bulk = (new Bulk($index)) - ->onError(function ($response, $body, $newbulk) { - // accept and drop - }) - ->index('1', ['title' => 'foo']); - $bulk->flush(); - - $this->assertEquals(1, $callCount); - $this->assertEquals([], $bulk->flush()); // batch consumed by handler - } - - public function testOnErrorPreservesBatchWhenHandlerThrows() - { - $callCount = 0; - $client = $this->createMock(TestClient::class); - $client->method('bulk')->willReturnCallback(function () use (&$callCount) { - $callCount++; - return new ArrayResponse(['errors' => true, 'items' => []]); - }); - Index::setClient($client); - - $index = $this->createIndex('products'); - $bulk = (new Bulk($index)) - ->onError(function ($response, $body, $newbulk) { - throw new \RuntimeException('handler aborted'); - }) - ->index('1', ['title' => 'foo']); - - try { - $bulk->flush(); - $this->fail('Expected exception'); - } catch (\RuntimeException $e) { - $this->assertSame('handler aborted', $e->getMessage()); - } - - try { - $bulk->flush(); // batch preserved → re-sent - } catch (\RuntimeException $e) { - // still failing, still preserved - } - $this->assertEquals(2, $callCount); - } - - public function testExecuteCallsOnError() - { - $client = $this->createMock(TestClient::class); - $errorResponse = [ - 'errors' => true, - 'items' => [ - ['index' => ['_id' => '1', 'status' => 400, 'error' => ['type' => 'mapper_parsing_exception']]], - ], - ]; - $client->method('bulk')->willReturn(new ArrayResponse($errorResponse)); - Index::setClient($client); - - $received = null; - $index = $this->createIndex('products'); - (new Bulk($index)) - ->onError(function ($response) use (&$received) { - $received = $response; - }) - ->index('1', ['title' => 'foo']) - ->flush(); - - $this->assertEquals($errorResponse, $received); - } - - public function testAutoFlushCallsOnError() - { - $errorResponse = [ - 'errors' => true, - 'items' => [ - ['index' => ['_id' => '1', 'status' => 400, 'error' => ['type' => 'mapper_parsing_exception']]], - ], - ]; - $client = $this->createMock(TestClient::class); - $client->method('bulk')->willReturn(new ArrayResponse($errorResponse)); - Index::setClient($client); - - $received = null; - $index = $this->createIndex('products'); - $bulk = (new Bulk($index)) - ->batchSize(1) - ->onError(function ($response) use (&$received) { - $received = $response; - }); - - $bulk->index('1', ['title' => 'foo']); // triggers auto-flush - - $this->assertEquals($errorResponse, $received); - } - - public function testAutoFlushThrowsOnErrorsWithoutHandler() - { - $client = $this->createMock(TestClient::class); - $client->method('bulk')->willReturn(new ArrayResponse([ - 'errors' => true, - 'items' => [], - ])); - Index::setClient($client); - - $index = $this->createIndex('products'); - $bulk = (new Bulk($index))->batchSize(1); - - $this->expectException(\RuntimeException::class); - $bulk->index('1', ['title' => 'foo']); // auto-flush throws - } -} diff --git a/tests/Index/ManagerTest.php b/tests/Index/ManagerTest.php deleted file mode 100644 index 5fd99f9..0000000 --- a/tests/Index/ManagerTest.php +++ /dev/null @@ -1,303 +0,0 @@ -createMock(TestClient::class)); - } - - protected function tearDown(): void - { - ClientManager::reset(); - } - - 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 testDeleteThrowsOnAliasWithoutResolve() - { - $indices = $this->createMock(TestIndices::class); - $indices->method('existsAlias')->willReturn(new BoolResponse(true)); - $client = $this->createMock(TestClient::class); - $client->method('indices')->willReturn($indices); - Index::setClient($client); - - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('is an alias'); - (new Manager($this->createIndex('products')))->delete(); - } - - public function testDeleteResolvesAliasToBackingIndex() - { - $indices = $this->createMock(TestIndices::class); - $indices->method('existsAlias')->willReturn(new BoolResponse(true)); - $indices->method('getAlias')->willReturn(new ArrayResponse([ - 'products_v1' => ['aliases' => ['products' => []]], - ])); - $indices->expects($this->once())->method('delete') - ->with(['index' => 'products_v1']) - ->willReturn(new ArrayResponse(['acknowledged' => true])); - $client = $this->createMock(TestClient::class); - $client->method('indices')->willReturn($indices); - Index::setClient($client); - - $result = (new Manager($this->createIndex('products')))->delete(resolveAlias: true); - - $this->assertTrue($result['acknowledged']); - } - - public function testDeleteResolvesAliasToAllBackingIndices() - { - // An alias pointing at multiple backing indices must be deleted in one shot, not just the first (the former array_key_first pitfall). - $indices = $this->createMock(TestIndices::class); - $indices->method('existsAlias')->willReturn(new BoolResponse(true)); - $indices->method('getAlias')->willReturn(new ArrayResponse([ - 'products_v1' => ['aliases' => ['products' => []]], - 'products_v2' => ['aliases' => ['products' => []]], - ])); - $indices->expects($this->once())->method('delete') - ->with(['index' => 'products_v1,products_v2']) - ->willReturn(new ArrayResponse(['acknowledged' => true])); - $client = $this->createMock(TestClient::class); - $client->method('indices')->willReturn($indices); - Index::setClient($client); - - (new Manager($this->createIndex('products')))->delete(resolveAlias: true); - } - - 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/Integration/Index/BulkContractTest.php b/tests/Integration/Index/BulkContractTest.php new file mode 100644 index 0000000..b59c7ed --- /dev/null +++ b/tests/Integration/Index/BulkContractTest.php @@ -0,0 +1,71 @@ +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); + } +} diff --git a/tests/Integration/Index/ManagerContractTest.php b/tests/Integration/Index/ManagerContractTest.php new file mode 100644 index 0000000..d33bf89 --- /dev/null +++ b/tests/Integration/Index/ManagerContractTest.php @@ -0,0 +1,59 @@ +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 + { + $manager = new Manager($this->makeIndex()); + $this->assertTrue($manager->exists()); + $manager->delete(); + $this->assertFalse($manager->exists()); + } +} From 0714ee96af7a0bb4cb0569f817f0370f26ec4ddf Mon Sep 17 00:00:00 2001 From: ykan821 Date: Fri, 26 Jun 2026 23:46:43 +0800 Subject: [PATCH 47/70] test(integration): add Rebuild contract tests, drop matching mock - RebuildContractTest: run creates alias + imports, swap (atomic alias exchange), rejects real index (real ES) - Remove mock-based RebuildTest Co-Authored-By: Claude --- tests/Index/RebuildTest.php | 829 ------------------ .../Integration/Index/RebuildContractTest.php | 85 ++ 2 files changed, 85 insertions(+), 829 deletions(-) delete mode 100644 tests/Index/RebuildTest.php create mode 100644 tests/Integration/Index/RebuildContractTest.php diff --git a/tests/Index/RebuildTest.php b/tests/Index/RebuildTest.php deleted file mode 100644 index d62511f..0000000 --- a/tests/Index/RebuildTest.php +++ /dev/null @@ -1,829 +0,0 @@ -createMock(TestClient::class)); - } - - protected function tearDown(): void - { - ClientManager::reset(); - } - - 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')->willReturnCallback(function ($params) { - if (($params['index'] ?? '') === '.ek_locks') { - return new BoolResponse(true); - } - return 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->method('index')->willReturn(new ArrayResponse(['result' => 'created'])); - $client->method('delete')->willReturn(new ArrayResponse(['result' => 'deleted'])); - $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('exists')->willReturn(new BoolResponse(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('index')->willReturn(new ArrayResponse(['result' => 'created'])); - $client->method('delete')->willReturn(new ArrayResponse(['result' => 'deleted'])); - $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('index')->willReturn(new ArrayResponse(['result' => 'created'])); - $client->method('delete')->willReturn(new ArrayResponse(['result' => 'deleted'])); - $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')->willReturnCallback(function ($params) { - if (($params['index'] ?? '') === '.ek_locks') { - return new BoolResponse(true); - } - return new BoolResponse(false); - }); - $indices->method('putAlias')->willReturn(new ArrayResponse(['acknowledged' => true])); - - $client = $this->createMock(TestClient::class); - $client->method('indices')->willReturn($indices); - $client->method('index')->willReturn(new ArrayResponse(['result' => 'created'])); - $client->method('delete')->willReturn(new ArrayResponse(['result' => 'deleted'])); - $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')->willReturnCallback(function ($params) { - if (($params['index'] ?? '') === '.ek_locks') { - return new BoolResponse(true); - } - return new BoolResponse(false); - }); - $indices->method('putAlias')->willReturn(new ArrayResponse(['acknowledged' => true])); - - $client = $this->createMock(TestClient::class); - $client->method('indices')->willReturn($indices); - $client->method('index')->willReturn(new ArrayResponse(['result' => 'created'])); - $client->method('delete')->willReturn(new ArrayResponse(['result' => 'deleted'])); - $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')->willReturnCallback(function ($params) { - if (($params['index'] ?? '') === '.ek_locks') { - return new BoolResponse(true); - } - return new BoolResponse(false); - }); - $indices->method('putAlias')->willReturn(new ArrayResponse(['acknowledged' => true])); - - $client = $this->createMock(TestClient::class); - $client->method('indices')->willReturn($indices); - $client->method('index')->willReturn(new ArrayResponse(['result' => 'created'])); - $client->method('delete')->willReturn(new ArrayResponse(['result' => 'deleted'])); - $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('exists')->willReturn(new BoolResponse(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); - $client->method('index')->willReturn(new ArrayResponse(['result' => 'created'])); - $client->method('delete')->willReturn(new ArrayResponse(['result' => 'deleted'])); - 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')->willReturnCallback(function ($params) { - if (($params['index'] ?? '') === '.ek_locks') { - return new BoolResponse(true); - } - return new BoolResponse(false); - }); - $indices->method('putAlias')->willReturn(new ArrayResponse(['acknowledged' => true])); - - $client = $this->createMock(TestClient::class); - $client->method('indices')->willReturn($indices); - $client->method('index')->willReturn(new ArrayResponse(['result' => 'created'])); - $client->method('delete')->willReturn(new ArrayResponse(['result' => 'deleted'])); - $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->method('exists')->willReturn(new BoolResponse(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('index')->willReturn(new ArrayResponse(['result' => 'created'])); - $client->method('delete')->willReturn(new ArrayResponse(['result' => 'deleted'])); - $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(); - } - - public function testRunAcquiresAndReleasesLock() - { - $indices = $this->createMock(TestIndices::class); - $indices->method('create')->willReturn(new ArrayResponse(['acknowledged' => true])); - $indices->method('exists')->willReturnCallback(function ($params) { - if (($params['index'] ?? '') === '.ek_locks') { - return new BoolResponse(true); - } - return new BoolResponse(false); - }); - $indices->method('existsAlias')->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' => []])); - $client->expects($this->once())->method('index')->with($this->callback(function ($params) { - return $params['index'] === '.ek_locks' - && $params['id'] === 'products' - && ($params['op_type'] ?? null) === 'create'; - }))->willReturn(new ArrayResponse(['result' => 'created'])); - $client->expects($this->once())->method('delete')->with($this->callback(function ($params) { - return $params['index'] === '.ek_locks' && $params['id'] === 'products'; - }))->willReturn(new ArrayResponse(['result' => 'deleted'])); - Index::setClient($client); - - $index = new class extends Index { - public function __construct() - { - $this->name = 'products'; - } - - public function source(array $context = []): iterable - { - return []; - } - }; - - (new Rebuild($index))->allowEmpty()->run(); - } - - public function testRunReleasesLockOnFailure() - { - $indices = $this->createMock(TestIndices::class); - $indices->method('create')->willReturn(new ArrayResponse(['acknowledged' => true])); - $indices->method('exists')->willReturn(new BoolResponse(true)); - $indices->method('delete')->willReturn(new ArrayResponse(['acknowledged' => true])); - - $lockReleased = false; - $client = $this->createMock(TestClient::class); - $client->method('indices')->willReturn($indices); - $client->method('index')->willReturn(new ArrayResponse(['result' => 'created'])); - $client->method('delete')->willReturnCallback(function () use (&$lockReleased) { - $lockReleased = true; - return new ArrayResponse(['result' => 'deleted']); - }); - $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']; - } - }; - - try { - (new Rebuild($index))->run(); - } catch (\RuntimeException $e) { - // Expected: bulk import error - } - - $this->assertTrue($lockReleased, 'Lock should be released even when run() fails'); - } - - public function testRunThrowsWhenLocked() - { - $indices = $this->createMock(TestIndices::class); - $indices->method('exists')->willReturn(new BoolResponse(true)); - - $response = $this->createMock(\Psr\Http\Message\ResponseInterface::class); - $response->method('getStatusCode')->willReturn(409); - - $exception = new \Elastic\Elasticsearch\Exception\ClientResponseException(); - $exception->setResponse($response); - - $client = $this->createMock(TestClient::class); - $client->method('indices')->willReturn($indices); - $client->method('index')->willThrowException($exception); - Index::setClient($client); - - $index = $this->createIndex('products'); - - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('already running'); - (new Rebuild($index))->source([1 => ['title' => 'A']])->run(); - } - - public function testForceUnlock() - { - $client = $this->createMock(TestClient::class); - $client->expects($this->once())->method('delete')->with($this->callback(function ($params) { - return $params['index'] === '.ek_locks' && $params['id'] === 'products'; - }))->willReturn(new ArrayResponse(['result' => 'deleted'])); - Index::setClient($client); - - $index = $this->createIndex('products'); - (new Rebuild($index))->forceUnlock(); - } - - public function testForceUnlockIsIdempotent() - { - $response = $this->createMock(\Psr\Http\Message\ResponseInterface::class); - $response->method('getStatusCode')->willReturn(404); - - $exception = new \Elastic\Elasticsearch\Exception\ClientResponseException(); - $exception->setResponse($response); - - $client = $this->createMock(TestClient::class); - $client->method('delete')->willThrowException($exception); - Index::setClient($client); - - $index = $this->createIndex('products'); - - // Should not throw despite 404 - (new Rebuild($index))->forceUnlock(); - $this->assertTrue(true); // Reached without exception - } - - public function testIsLockedReturnsTrue() - { - $client = $this->createMock(TestClient::class); - $client->method('exists')->with($this->callback(function ($params) { - return $params['index'] === '.ek_locks' && $params['id'] === 'products'; - }))->willReturn(new BoolResponse(true)); - Index::setClient($client); - - $index = $this->createIndex('products'); - $this->assertTrue((new Rebuild($index))->isLocked()); - } - - public function testIsLockedReturnsFalse() - { - $client = $this->createMock(TestClient::class); - $client->method('exists')->willReturn(new BoolResponse(false)); - Index::setClient($client); - - $index = $this->createIndex('products'); - $this->assertFalse((new Rebuild($index))->isLocked()); - } - - public function testEnsureLockIndexCreatesWhenNotExists() - { - $indices = $this->createMock(TestIndices::class); - $indices->method('exists')->willReturnCallback(function ($params) { - if (($params['index'] ?? '') === '.ek_locks') { - return new BoolResponse(false); - } - return new BoolResponse(false); - }); - $indices->method('create')->willReturnCallback(function ($params) { - return new ArrayResponse(['acknowledged' => true]); - }); - $indices->method('existsAlias')->willReturn(new BoolResponse(false)); - $indices->method('putAlias')->willReturn(new ArrayResponse(['acknowledged' => true])); - - $lockIndexCreated = false; - $indices->expects($this->exactly(2))->method('create')->willReturnCallback(function ($params) use (&$lockIndexCreated) { - if (($params['index'] ?? '') === '.ek_locks') { - $lockIndexCreated = true; - $this->assertEquals(1, $params['body']['settings']['number_of_shards'] ?? 0); - $this->assertEquals(0, $params['body']['settings']['number_of_replicas'] ?? 1); - } - return new ArrayResponse(['acknowledged' => true]); - }); - - $client = $this->createMock(TestClient::class); - $client->method('indices')->willReturn($indices); - $client->method('index')->willReturn(new ArrayResponse(['result' => 'created'])); - $client->method('delete')->willReturn(new ArrayResponse(['result' => 'deleted'])); - $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 []; - } - }; - - (new Rebuild($index))->allowEmpty()->run(); - } - - public function testOnErrorReceivesResponse() - { - $indices = $this->createMock(TestIndices::class); - $indices->method('create')->willReturn(new ArrayResponse(['acknowledged' => true])); - $indices->method('exists')->willReturn(new BoolResponse(false)); - $indices->method('existsAlias')->willReturn(new BoolResponse(false)); - $indices->method('putAlias')->willReturn(new ArrayResponse(['acknowledged' => true])); - - $errorResponse = [ - 'errors' => true, - 'items' => [ - ['index' => ['_id' => '1', 'status' => 400, 'error' => ['type' => 'mapper_parsing_exception']]], - ], - ]; - $client = $this->createMock(TestClient::class); - $client->method('indices')->willReturn($indices); - $client->method('index')->willReturn(new ArrayResponse(['result' => 'created'])); - $client->method('delete')->willReturn(new ArrayResponse(['result' => 'deleted'])); - $client->method('bulk')->willReturn(new ArrayResponse($errorResponse)); - Index::setClient($client); - - $index = new class extends Index { - public function __construct() - { - $this->name = 'products'; - } - - public function source(array $context = []): iterable - { - yield 1 => ['title' => 'A']; - } - }; - - $received = null; - (new Rebuild($index)) - ->onError(function ($response) use (&$received) { - $received = $response; - }) - ->run(); - - $this->assertEquals($errorResponse, $received); - } - - public function testOnErrorPreventsException() - { - $indices = $this->createMock(TestIndices::class); - $indices->method('create')->willReturn(new ArrayResponse(['acknowledged' => true])); - $indices->method('exists')->willReturn(new BoolResponse(false)); - $indices->method('existsAlias')->willReturn(new BoolResponse(false)); - $indices->method('putAlias')->willReturn(new ArrayResponse(['acknowledged' => true])); - - $client = $this->createMock(TestClient::class); - $client->method('indices')->willReturn($indices); - $client->method('index')->willReturn(new ArrayResponse(['result' => 'created'])); - $client->method('delete')->willReturn(new ArrayResponse(['result' => 'deleted'])); - $client->method('bulk')->willReturn(new ArrayResponse([ - 'errors' => true, - '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']; - } - }; - - // Without onError, would throw. With onError (empty callback), completes successfully. - $result = (new Rebuild($index)) - ->onError(function () {}) - ->run(); - - $this->assertStringStartsWith('products_', $result['newIndex']); - } - - public function testRunThrowsWhenReleaseLockFailsAfterSuccess() - { - $indices = $this->createMock(TestIndices::class); - $indices->method('create')->willReturn(new ArrayResponse(['acknowledged' => true])); - $indices->method('exists')->willReturnCallback(function ($params) { - if (($params['index'] ?? '') === '.ek_locks') { - return new BoolResponse(true); - } - return new BoolResponse(false); - }); - $indices->method('existsAlias')->willReturn(new BoolResponse(false)); - $indices->method('putAlias')->willReturn(new ArrayResponse(['acknowledged' => true])); - - $response = $this->createMock(\Psr\Http\Message\ResponseInterface::class); - $response->method('getStatusCode')->willReturn(503); - $releaseException = new \Elastic\Elasticsearch\Exception\ClientResponseException(); - $releaseException->setResponse($response); - - $client = $this->createMock(TestClient::class); - $client->method('indices')->willReturn($indices); - $client->method('index')->willReturn(new ArrayResponse(['result' => 'created'])); - // Lock release throws 503 on the success path. - $client->method('delete')->willThrowException($releaseException); - $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 - { - yield 1 => ['title' => 'A']; - } - }; - - try { - (new Rebuild($index))->run(); - $this->fail('Expected RuntimeException when lock release fails after success'); - } catch (\RuntimeException $e) { - $this->assertStringContainsString('succeeded but the lock could not be released', $e->getMessage()); - $this->assertStringContainsString('forceUnlock', $e->getMessage()); - $this->assertSame($releaseException, $e->getPrevious()); - } - } - - public function testRunPreservesOriginalExceptionWhenReleaseFails() - { - $indices = $this->createMock(TestIndices::class); - $indices->method('create')->willReturn(new ArrayResponse(['acknowledged' => true])); - $indices->method('exists')->willReturn(new BoolResponse(true)); - $indices->method('delete')->willReturn(new ArrayResponse(['acknowledged' => true])); - - $response = $this->createMock(\Psr\Http\Message\ResponseInterface::class); - $response->method('getStatusCode')->willReturn(503); - $releaseException = new \Elastic\Elasticsearch\Exception\ClientResponseException(); - $releaseException->setResponse($response); - - $client = $this->createMock(TestClient::class); - $client->method('indices')->willReturn($indices); - $client->method('index')->willReturn(new ArrayResponse(['result' => 'created'])); - // Lock release also fails: 503. - $client->method('delete')->willThrowException($releaseException); - // Bulk import fails → doRun throws. - $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']; - } - }; - - try { - (new Rebuild($index))->run(); - $this->fail('Expected original import exception'); - } catch (\Throwable $e) { - // ⑤: must throw doRun's original exception, not the 503 ClientResponseException from lock release. - $this->assertInstanceOf(\RuntimeException::class, $e); - $this->assertStringContainsString('Bulk request has errors', $e->getMessage()); - $this->assertStringNotContainsString('succeeded but the lock', $e->getMessage()); - $this->assertNotSame($releaseException, $e); - } - } - - public function testIsLockedThrowsOnServerError() - { - $response = $this->createMock(\Psr\Http\Message\ResponseInterface::class); - $response->method('getStatusCode')->willReturn(503); - $exception = new \Elastic\Elasticsearch\Exception\ClientResponseException(); - $exception->setResponse($response); - - $client = $this->createMock(TestClient::class); - $client->method('exists')->willThrowException($exception); - Index::setClient($client); - - $index = $this->createIndex('products'); - - $this->expectException(\Elastic\Elasticsearch\Exception\ClientResponseException::class); - (new Rebuild($index))->isLocked(); - } -} diff --git a/tests/Integration/Index/RebuildContractTest.php b/tests/Integration/Index/RebuildContractTest.php new file mode 100644 index 0000000..5e1107a --- /dev/null +++ b/tests/Integration/Index/RebuildContractTest.php @@ -0,0 +1,85 @@ +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(); + } +} From 8373c84bf3935261c936f3cee6899fc82f3b2501 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Fri, 26 Jun 2026 23:50:33 +0800 Subject: [PATCH 48/70] test(integration): add StatsSupport contract tests, drop matching mock - StatsSupportContractTest: max/min/sum/avg/stats (real ES aggregations) - Remove mock-based StatsSupportTest - tests/Index/ now keeps only EventTest + ResultsTest (library behavior: event dispatch, response parsing) Co-Authored-By: Claude --- tests/Index/StatsSupportTest.php | 196 ------------------ .../Index/StatsSupportContractTest.php | 39 ++++ 2 files changed, 39 insertions(+), 196 deletions(-) delete mode 100644 tests/Index/StatsSupportTest.php create mode 100644 tests/Integration/Index/StatsSupportContractTest.php diff --git a/tests/Index/StatsSupportTest.php b/tests/Index/StatsSupportTest.php deleted file mode 100644 index 715c084..0000000 --- a/tests/Index/StatsSupportTest.php +++ /dev/null @@ -1,196 +0,0 @@ -createMock(TestClient::class)); - } - - protected function tearDown(): void - { - ClientManager::reset(); - } - - 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 testStatsReturnsAllMetrics() - { - $client = $this->createMock(TestClient::class); - $client->method('search')->willReturn(new ArrayResponse([ - 'hits' => ['total' => ['value' => 0], 'hits' => []], - 'aggregations' => [ - '__stats' => [ - 'count' => 100, - 'min' => 9.99, - 'max' => 199.99, - 'avg' => 49.5, - 'sum' => 4950.0, - ], - ], - ])); - Index::setClient($client); - - $index = $this->createIndex(); - $result = $index->query()->stats('price'); - - $this->assertEquals([ - 'count' => 100, - 'min' => 9.99, - 'max' => 199.99, - 'avg' => 49.5, - 'sum' => 4950.0, - ], $result); - } - - public function testStatsReturnsNullWhenNoAggregation() - { - $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()->stats('nonexistent'); - - $this->assertNull($result); - } - - public function testStatsSendsCorrectBody() - { - $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']['__stats']['stats']['field']) - && $body['aggs']['__stats']['stats']['field'] === 'price'; - }))->willReturn(new ArrayResponse([ - 'hits' => ['total' => ['value' => 0], 'hits' => []], - 'aggregations' => [ - '__stats' => ['count' => 0, 'min' => null, 'max' => null, 'avg' => null, 'sum' => 0], - ], - ])); - Index::setClient($client); - - $index = $this->createIndex(); - $index->query()->stats('price'); - } - - public function testScalarSendsCorrectBody() - { - $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 testScalarDoesNotMutateQuery() - { - $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 testScalarReturnsNullWhenNoValue() - { - $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/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']); + } +} From 1708adf39f1ea1fd1364b26ed2d7a4628bb09fe8 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Sat, 27 Jun 2026 00:10:34 +0800 Subject: [PATCH 49/70] test: drop mock layer, Event to integration, Results alongside search - EventContractTest: real ES triggers event dispatch (search/bulk + multiple listeners) - ResultsTest moved to tests/Integration/ (pure parsing, constructed response data) - Remove tests/Index/, TestClient stubs, index testsuite, bootstrap require - Two-layer architecture: unit (DSL build + pure logic) + integration (real ES) - CLAUDE.md: drop --testsuite index from pre-push checklist Co-Authored-By: Claude --- CLAUDE.md | 1 - phpunit.xml | 4 - tests/Index/EventTest.php | 319 ------------------ tests/Integration/Index/EventContractTest.php | 60 ++++ tests/{Index => Integration}/ResultsTest.php | 10 +- tests/TestClient.php | 247 -------------- tests/bootstrap.php | 1 - 7 files changed, 69 insertions(+), 573 deletions(-) delete mode 100644 tests/Index/EventTest.php create mode 100644 tests/Integration/Index/EventContractTest.php rename tests/{Index => Integration}/ResultsTest.php (96%) delete mode 100644 tests/TestClient.php diff --git a/CLAUDE.md b/CLAUDE.md index 51874d4..7f36633 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,7 +56,6 @@ Tests run inside a Docker container and require these environment variables: ```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" diff --git a/phpunit.xml b/phpunit.xml index 72ee5e3..a4f6eaf 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -3,12 +3,8 @@ tests - tests/Index tests/Integration - - tests/Index - tests/Integration diff --git a/tests/Index/EventTest.php b/tests/Index/EventTest.php deleted file mode 100644 index 04a1339..0000000 --- a/tests/Index/EventTest.php +++ /dev/null @@ -1,319 +0,0 @@ -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; - EventDispatcher::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; - EventDispatcher::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; - EventDispatcher::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; - EventDispatcher::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; - EventDispatcher::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; - EventDispatcher::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 = []; - EventDispatcher::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 = []; - EventDispatcher::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 = []; - EventDispatcher::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; - EventDispatcher::listen('search.query.before', function (Event $e) use (&$count) { - $count++; - }); - EventDispatcher::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; - EventDispatcher::listen('bulk.flush.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->flush(); - - $this->assertIsArray($actions); - $this->assertCount(2, $actions); - } - - public function testManagerCreateEvents() - { - $events = []; - EventDispatcher::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 = []; - EventDispatcher::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 = []; - EventDispatcher::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 = []; - EventDispatcher::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/Integration/Index/EventContractTest.php b/tests/Integration/Index/EventContractTest.php new file mode 100644 index 0000000..6c8a20b --- /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/Index/ResultsTest.php b/tests/Integration/ResultsTest.php similarity index 96% rename from tests/Index/ResultsTest.php rename to tests/Integration/ResultsTest.php index ead497d..36930ab 100644 --- a/tests/Index/ResultsTest.php +++ b/tests/Integration/ResultsTest.php @@ -1,11 +1,19 @@ 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 @@ Date: Sat, 27 Jun 2026 00:40:15 +0800 Subject: [PATCH 50/70] test: class-level index sharing for integration, drop dead code - IntegrationTestCase: one index per test class (static map), reset between tests via deleteByQuery + re-seed instead of recreate; ~48% faster (132s -> 69s) - DslTestCase: remove dead $esIndex and empty setUpBeforeClass Co-Authored-By: Claude --- tests/DslTestCase.php | 10 ----- tests/Integration/IntegrationTestCase.php | 47 +++++++++++++++++------ 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/tests/DslTestCase.php b/tests/DslTestCase.php index 78ad43e..7de8124 100644 --- a/tests/DslTestCase.php +++ b/tests/DslTestCase.php @@ -18,16 +18,6 @@ abstract class DslTestCase extends TestCase */ protected static $esClient; - /** - * @var string - */ - protected static $esIndex = 'elastickit_test'; - - public static function setUpBeforeClass(): void - { - // Unit tests assert JSON only; ES connection lives in IntegrationTestCase. - } - /** * Assert Query produces the expected JSON structure. * diff --git a/tests/Integration/IntegrationTestCase.php b/tests/Integration/IntegrationTestCase.php index c5f0800..537f0d7 100644 --- a/tests/Integration/IntegrationTestCase.php +++ b/tests/Integration/IntegrationTestCase.php @@ -11,13 +11,15 @@ use Tests\DslTestCase; /** - * Base for integration tests: each test gets an isolated random ES index. - * - * Skipped unless ELASTICKIT_TEST_HOST is set. Reuses DslTestCase's - * createIndex/seedData helpers for a shared mapping/seed contract. + * Base for integration tests: one random ES index per test CLASS (shared by + * its tests), reset between tests via deleteByQuery + re-seed. Skipped unless + * ELASTICKIT_TEST_HOST is set. Reuses DslTestCase's createIndex/seedData. */ abstract class IntegrationTestCase extends DslTestCase { + /** @var array test class -> its shared index name */ + private static array $indices = []; + protected string $indexName; protected function setUp(): void @@ -32,8 +34,23 @@ protected function setUp(): void static::$esClient = ClientBuilder::create()->setHosts([$host])->build(); } - $this->indexName = 'ek_it_' . bin2hex(random_bytes(4)); - static::createIndex(static::$esClient, $this->indexName); + $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); @@ -41,18 +58,24 @@ protected function setUp(): void protected function tearDown(): void { - if (static::$esClient !== null && isset($this->indexName)) { + ClientManager::reset(); + } + + public static function tearDownAfterClass(): void + { + $class = static::class; + if (isset(self::$indices[$class]) && static::$esClient !== null) { try { - static::$esClient->indices()->delete(['index' => $this->indexName]); + static::$esClient->indices()->delete(['index' => self::$indices[$class]]); } catch (\Throwable $e) { - // best-effort cleanup; ignore 404 if index already gone + // best-effort cleanup } + unset(self::$indices[$class]); } - ClientManager::reset(); } /** - * Anonymous Index subclass bound to the random test index. + * Anonymous Index subclass bound to the shared test index. */ protected function makeIndex(): Index { @@ -89,7 +112,7 @@ protected function assertQueryEs(Query $query, ?int $expectedHits = null): array } /** - * Refresh the random index so writes are immediately searchable. + * Refresh the shared index so writes are immediately searchable. */ protected function refreshIndex(): void { From 6fd146ef2add46801fb3e6fd94b6d6b1996a1ea4 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Sat, 27 Jun 2026 00:44:37 +0800 Subject: [PATCH 51/70] test: pin Node::toArray invariants; fix when() doc/signature mismatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NodeInvariantsTest: lock down toArray branches (value shorthand, value+property promotion, valueKey conflict, empty fieldKeyed->null, properties-only, float survives) — the P0 data-loss boundaries - Query::when(): drop doc claim that bare strings are treated as truthy; signature is bool|Closure (strict_types rejects strings), so the documented behavior can't occur Co-Authored-By: Claude --- src/DSL/Query.php | 4 +-- tests/NodeInvariantsTest.php | 61 ++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 tests/NodeInvariantsTest.php diff --git a/src/DSL/Query.php b/src/DSL/Query.php index 6ff8dd1..020db9f 100644 --- a/src/DSL/Query.php +++ b/src/DSL/Query.php @@ -131,9 +131,7 @@ public function addQuery($clause): static /** * Conditionally add a query clause. * - * $condition is a bool, or a Closure returning a bool. Bare values (e.g. - * strings) are treated as truthy values, NOT invoked — so when('count', …) - * does not call count(). + * $condition is a bool, or a Closure returning a bool. * * @param bool|\Closure $condition * @param mixed $query diff --git a/tests/NodeInvariantsTest.php b/tests/NodeInvariantsTest.php new file mode 100644 index 0000000..8967ff8 --- /dev/null +++ b/tests/NodeInvariantsTest.php @@ -0,0 +1,61 @@ + 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()); + } +} From c7516fd070b7d7502e3b4773fb364b1bf5ebd9ce Mon Sep 17 00:00:00 2001 From: ykan821 Date: Sat, 27 Jun 2026 00:58:00 +0800 Subject: [PATCH 52/70] refactor(index): move infra classes to Support/ namespace - Move ClientManager, Event, EventDispatcher, StatsSupport, Pagination under src/Index/Support/ - Domain classes (Index/Search/Results/Doc/Bulk/Manager/Rebuild) stay at the root - Update all use-declarations and cross-namespace references Co-Authored-By: Claude --- README.md | 2 +- docs/index.md | 2 +- src/Index/Bulk.php | 2 ++ src/Index/Index.php | 1 + src/Index/Manager.php | 2 ++ src/Index/Rebuild.php | 2 ++ src/Index/Results.php | 1 + src/Index/Search.php | 4 ++++ src/Index/{ => Support}/ClientManager.php | 2 +- src/Index/{ => Support}/Event.php | 2 +- src/Index/{ => Support}/EventDispatcher.php | 2 +- src/Index/{ => Support}/Pagination.php | 2 +- src/Index/{ => Support}/StatsSupport.php | 2 +- tests/Integration/Index/EventContractTest.php | 4 ++-- tests/Integration/IntegrationTestCase.php | 2 +- tests/Integration/ResultsTest.php | 4 ++-- 16 files changed, 24 insertions(+), 12 deletions(-) rename src/Index/{ => Support}/ClientManager.php (97%) rename src/Index/{ => Support}/Event.php (97%) rename src/Index/{ => Support}/EventDispatcher.php (97%) rename src/Index/{ => Support}/Pagination.php (97%) rename src/Index/{ => Support}/StatsSupport.php (98%) diff --git a/README.md b/README.md index 8af7b41..c55caba 100644 --- a/README.md +++ b/README.md @@ -283,7 +283,7 @@ $result = (new Rebuild(new ProductIndex())) ### 事件监听 ```php -use ElasticKit\Index\Event; +use ElasticKit\Index\Support\Event; ProductIndex::listen('search.query.after', function (Event $e) { Log::info("Search on {$e->index}", [ diff --git a/docs/index.md b/docs/index.md index ce52de8..e6ea3b1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -320,7 +320,7 @@ $manager = new Manager(new ProductIndex()); ### 事件 ```php -use ElasticKit\Index\Event; +use ElasticKit\Index\Support\Event; Index::listen('search.query.after', function (Event $e) { Log::info("{$e->name} on {$e->index}", ['duration' => $e->duration]); diff --git a/src/Index/Bulk.php b/src/Index/Bulk.php index 34d16b7..b373647 100644 --- a/src/Index/Bulk.php +++ b/src/Index/Bulk.php @@ -4,6 +4,8 @@ namespace ElasticKit\Index; +use ElasticKit\Index\Support\Event; +use ElasticKit\Index\Support\EventDispatcher; use InvalidArgumentException; use RuntimeException; diff --git a/src/Index/Index.php b/src/Index/Index.php index 159a243..32bb82d 100644 --- a/src/Index/Index.php +++ b/src/Index/Index.php @@ -4,6 +4,7 @@ namespace ElasticKit\Index; +use ElasticKit\Index\Support\ClientManager; use BadMethodCallException; use Elastic\Elasticsearch\ClientInterface; use ElasticKit\DSL\Query; diff --git a/src/Index/Manager.php b/src/Index/Manager.php index 8bd77db..d09d369 100644 --- a/src/Index/Manager.php +++ b/src/Index/Manager.php @@ -4,6 +4,8 @@ namespace ElasticKit\Index; +use ElasticKit\Index\Support\Event; +use ElasticKit\Index\Support\EventDispatcher; use RuntimeException; use stdClass; diff --git a/src/Index/Rebuild.php b/src/Index/Rebuild.php index 582b67d..789972f 100644 --- a/src/Index/Rebuild.php +++ b/src/Index/Rebuild.php @@ -4,6 +4,8 @@ namespace ElasticKit\Index; +use ElasticKit\Index\Support\Event; +use ElasticKit\Index\Support\EventDispatcher; use Elastic\Elasticsearch\Exception\ClientResponseException; use RuntimeException; use stdClass; diff --git a/src/Index/Results.php b/src/Index/Results.php index f1a73be..ac534ef 100644 --- a/src/Index/Results.php +++ b/src/Index/Results.php @@ -4,6 +4,7 @@ namespace ElasticKit\Index; +use ElasticKit\Index\Support\Pagination; use RuntimeException; /** diff --git a/src/Index/Search.php b/src/Index/Search.php index 91f8d72..cab5043 100644 --- a/src/Index/Search.php +++ b/src/Index/Search.php @@ -4,6 +4,10 @@ namespace ElasticKit\Index; +use ElasticKit\Index\Support\Event; +use ElasticKit\Index\Support\EventDispatcher; +use ElasticKit\Index\Support\StatsSupport; +use ElasticKit\Index\Support\Pagination; use BadMethodCallException; use ElasticKit\DSL\Query; use RuntimeException; diff --git a/src/Index/ClientManager.php b/src/Index/Support/ClientManager.php similarity index 97% rename from src/Index/ClientManager.php rename to src/Index/Support/ClientManager.php index 443ef51..5893af8 100644 --- a/src/Index/ClientManager.php +++ b/src/Index/Support/ClientManager.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ElasticKit\Index; +namespace ElasticKit\Index\Support; use Elastic\Elasticsearch\ClientInterface; use RuntimeException; diff --git a/src/Index/Event.php b/src/Index/Support/Event.php similarity index 97% rename from src/Index/Event.php rename to src/Index/Support/Event.php index 6cb3567..8fe28c4 100644 --- a/src/Index/Event.php +++ b/src/Index/Support/Event.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ElasticKit\Index; +namespace ElasticKit\Index\Support; /** * Lightweight event object carrying event name, index, and contextual data. diff --git a/src/Index/EventDispatcher.php b/src/Index/Support/EventDispatcher.php similarity index 97% rename from src/Index/EventDispatcher.php rename to src/Index/Support/EventDispatcher.php index 830611b..d1ea382 100644 --- a/src/Index/EventDispatcher.php +++ b/src/Index/Support/EventDispatcher.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ElasticKit\Index; +namespace ElasticKit\Index\Support; /** * Event dispatcher for index operations. diff --git a/src/Index/Pagination.php b/src/Index/Support/Pagination.php similarity index 97% rename from src/Index/Pagination.php rename to src/Index/Support/Pagination.php index 7474381..01d470b 100644 --- a/src/Index/Pagination.php +++ b/src/Index/Support/Pagination.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ElasticKit\Index; +namespace ElasticKit\Index\Support; /** * Manages pagination resolvers. diff --git a/src/Index/StatsSupport.php b/src/Index/Support/StatsSupport.php similarity index 98% rename from src/Index/StatsSupport.php rename to src/Index/Support/StatsSupport.php index f25627e..fde766e 100644 --- a/src/Index/StatsSupport.php +++ b/src/Index/Support/StatsSupport.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ElasticKit\Index; +namespace ElasticKit\Index\Support; /** * Shortcut methods for common metric aggregations on Search. diff --git a/tests/Integration/Index/EventContractTest.php b/tests/Integration/Index/EventContractTest.php index 6c8a20b..8c0f1c6 100644 --- a/tests/Integration/Index/EventContractTest.php +++ b/tests/Integration/Index/EventContractTest.php @@ -5,8 +5,8 @@ namespace Tests\Integration\Index; use ElasticKit\Index\Bulk; -use ElasticKit\Index\Event; -use ElasticKit\Index\EventDispatcher; +use ElasticKit\Index\Support\Event; +use ElasticKit\Index\Support\EventDispatcher; use Tests\Integration\IntegrationTestCase; class EventContractTest extends IntegrationTestCase diff --git a/tests/Integration/IntegrationTestCase.php b/tests/Integration/IntegrationTestCase.php index 537f0d7..00f3119 100644 --- a/tests/Integration/IntegrationTestCase.php +++ b/tests/Integration/IntegrationTestCase.php @@ -6,7 +6,7 @@ use Elastic\Elasticsearch\ClientBuilder; use ElasticKit\DSL\Query; -use ElasticKit\Index\ClientManager; +use ElasticKit\Index\Support\ClientManager; use ElasticKit\Index\Index; use Tests\DslTestCase; diff --git a/tests/Integration/ResultsTest.php b/tests/Integration/ResultsTest.php index 36930ab..e3e56f8 100644 --- a/tests/Integration/ResultsTest.php +++ b/tests/Integration/ResultsTest.php @@ -5,8 +5,8 @@ namespace Tests\Integration; use PHPUnit\Framework\TestCase; -use ElasticKit\Index\ClientManager; -use ElasticKit\Index\Pagination; +use ElasticKit\Index\Support\ClientManager; +use ElasticKit\Index\Support\Pagination; use ElasticKit\Index\Results; /** From ed946daa3552af1a3feada1e1fa929c68acf9f92 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Sat, 27 Jun 2026 18:47:36 +0800 Subject: [PATCH 53/70] docs: make English the primary README, Chinese at README.zh.md - README.md: English (primary) - README.zh.md: Chinese translation - cross-links between the two Co-Authored-By: Claude --- README.md | 115 +++++++------- README.zh.md | 318 +++++++++++++++++++++++++++++++++++++ docs/guide.md | 95 +++++------ docs/guide.zh.md | 287 +++++++++++++++++++++++++++++++++ docs/index.md | 251 ++++++++++++++--------------- docs/index.zh.md | 405 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 1244 insertions(+), 227 deletions(-) create mode 100644 README.zh.md create mode 100644 docs/guide.zh.md create mode 100644 docs/index.zh.md diff --git a/README.md b/README.md index c55caba..ea59859 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,32 @@ # ElasticKit +> [中文](README.zh.md) | English + [![Latest Version](https://img.shields.io/packagist/v/ykan/elastickit)](https://packagist.org/packages/ykan/elastickit) [![Total Downloads](https://img.shields.io/packagist/dt/ykan/elastickit)](https://packagist.org/packages/ykan/elastickit) [![License](https://img.shields.io/packagist/l/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` 自动安装。 +> 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 string $name = 'products'; @@ -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(); // total hits ``` -## 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,7 +79,7 @@ $bool = Boolean::create() ->must(Match_::create('title', 'elasticsearch')) ->filter(Term::create('status', 'published')->boost(1.5)); -// 增量构建 +// incremental build if ($filterByPrice) { $bool->filter(Range::create('price', [10, 100])); } @@ -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,27 +124,27 @@ $results = ProductIndex::query() } ``` -### 子句追加(ClausesSupport) +### Clause appending (ClausesSupport) -`bool` 查询的子句(must / should / filter / must_not)**累加追加**,并接受与叶子查询相同的 4 种输入形式: +The clauses of a `bool` query (must / should / filter / must_not) **append**, and accept the same four input forms as leaf queries: ```php -// 4 种输入形式等价,都产出一条 must +// 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]]); // 同上 +$q->bool(['must' => [$q1, $q2]]); // same -// 对比:minimum_should_match 是单值属性,后调覆盖而非追加 +// 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` 等其他数组子句容器同理(queries / clauses 累加)。 +> `dis_max`, `span_or`, `span_near` and other array-clause containers behave the same way (queries / clauses append). -### 聚合 +### Aggregations ```php $results = ProductIndex::query() @@ -155,7 +157,7 @@ $results = ProductIndex::query() $aggs = $results->aggregations(); ``` -### 嵌套查询 +### Nested query ```php $results = ProductIndex::query() @@ -163,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' => [ @@ -181,38 +183,38 @@ $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) -// 分批遍历(大批量导出/批处理,每次 yield 一个 Results) +// batch iteration (large exports / batch processing; yields a Results per batch) foreach (ProductIndex::query()->chunk() as $results) { foreach ($results->docs() as $doc) { // ... } } -// 逐条遍历(导出/逐条加工,每次 yield 一个 hit:_id/_score/_source) +// per-hit iteration (exports / per-row processing; yields one hit: _id/_score/_source) foreach (ProductIndex::query()->cursor() as $hit) { $doc = $hit['_source']; // ... } ``` -### 文档 CRUD +### Document CRUD ```php ProductIndex::doc(1)->save(['title' => 'Hello', 'price' => 99.9]); @@ -224,7 +226,7 @@ $doc->update(['price' => 89.9]); $doc->delete(); ``` -### 批量操作 +### Bulk operations ```php use ElasticKit\Index\Bulk; @@ -239,25 +241,25 @@ $bulk->batchSize(500) ->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 @@ -268,47 +270,48 @@ 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\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); }); ```
-## 文档 +## Documentation -- [实践指南](docs/guide.md)——电商订单场景,从安装到上线的完整流程 -- [Index 文档](docs/index.md)——搜索、CRUD、批量操作、零停机重建、事件 -- [更新日志](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..fe4600c --- /dev/null +++ b/README.zh.md @@ -0,0 +1,318 @@ +# ElasticKit + +> 中文 | [English](README.md) + +[![Latest Version](https://img.shields.io/packagist/v/ykan/elastickit)](https://packagist.org/packages/ykan/elastickit) +[![Total Downloads](https://img.shields.io/packagist/dt/ykan/elastickit)](https://packagist.org/packages/ykan/elastickit) +[![License](https://img.shields.io/packagist/l/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(); // 命中总数 +``` + +## 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']; + // ... +} +``` + +### 文档 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); +}); +``` + +
+ +## 文档 + +- [实践指南](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/docs/guide.md b/docs/guide.md index 313f9f0..fbdb4e5 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; @@ -44,10 +44,10 @@ class OrderIndex extends Index 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,13 +120,13 @@ 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->filter(Term::create('status', $filters['status'])); } @@ -135,7 +135,7 @@ class OrderIndex extends Index $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->should(Wildcard::create('order_no', "*{$filters['keyword']}*")); $bool->should(Wildcard::create('merchant_name', "*{$filters['keyword']}*")); @@ -146,7 +146,7 @@ class OrderIndex extends Index } ``` -控制器调用: +Controller: ```php // app/Http/Controllers/OrderController.php @@ -160,9 +160,9 @@ public function index(Request $request) } ``` -> 条件用 `if` 逐个判断,只有传了值才加查询。`should()` 实现 OR 搜索。深分页场景用 `chunk()`(按批)或 `cursor()`(逐条)替代 `paginate()`。 +> 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()`. -运营还要导出筛选结果到 Excel。ES 默认 `max_result_window = 10000`,`from/size` 翻不到后面的数据,用 `cursor()` 基于 scroll 遍历: +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) @@ -171,23 +171,23 @@ public function export(array $filters) foreach ($search->chunk() as $results) { foreach ($results->docs() as $doc) { - // 写入 Excel + // 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 +198,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 +209,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 +224,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; @@ -251,19 +251,19 @@ class SyncEsJob } ``` -## 阶段 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 +272,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 +281,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..d7d1160 --- /dev/null +++ b/docs/guide.zh.md @@ -0,0 +1,287 @@ +# 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(); +} +``` + +> 条件用 `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 e6ea3b1..4535ce2 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 string $name = 'products'; // 索引名(必填) - protected array $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 array $settings = [ // 索引 settings + protected array $settings = [ // index settings 'number_of_shards' => 1, ]; - protected string $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,40 +56,42 @@ $results = ProductIndex::query() ->size(20) ->get(); -$results->total(); // 命中数 -$results->docs(); // _source 数组 -$results->hits(); // 完整 hit 数组 -$results->aggregations(); // 聚合结果 +$results->total(); // hit count +$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(); @@ -97,40 +99,40 @@ $results->toPaginator(); ## 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); ``` ## Chunk / Cursor -把 scroll 封装成 PHP 生成器,scroll 自动清理。 +Wraps scroll into a PHP generator; the scroll is cleared automatically. -**chunk** 按批遍历,每次 yield 一个 Results(含 docs/hits/total 等): +**chunk** iterates by batch, yielding a Results each time (with docs/hits/total etc.): ```php foreach (ProductIndex::query()->chunk() as $results) { foreach ($results->docs() as $doc) { - // 处理 + // process } } ``` -**cursor** 逐条遍历,每次 yield 一个完整 hit(_id/_score/_source): +**cursor** iterates per hit, yielding one full hit each time (_id/_score/_source): ```php foreach (ProductIndex::query()->cursor() as $hit) { @@ -139,31 +141,31 @@ foreach (ProductIndex::query()->cursor() as $hit) { } ``` -## 文档 CRUD +## 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 是一个缓冲区:`index()/create()/update()/delete()` 只入队,**`flush()` 才发送**。 +Bulk is a buffer: `index()/create()/update()/delete()` only enqueue; **`flush()` is what sends**. ```php use ElasticKit\Index\Bulk; @@ -172,36 +174,36 @@ $bulk = new Bulk(new ProductIndex()); $bulk->index(1, ['title' => 'Product A']); $bulk->index(2, ['title' => 'Product B']); $bulk->delete(3); -$bulk->flush(); // 发送并清空缓冲 +$bulk->flush(); // send and clear the buffer ``` -`batchSize(N)` 开启**自动 flush**:缓冲达到 N 时自动发送(默认 0 = 关闭,纯缓冲)。大导入用它避免一次性堆积内存;循环结束后**仍需 `flush()` 发送尾部**: +`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); // 满 500 自动 flush + $bulk->index($id, $doc); // auto-flushes at 500 } -$bulk->flush(); // 尾部(< 500 那批) +$bulk->flush(); // the tail (< 500) ``` -### 错误处理 +### Error handling -`flush()` 默认在响应包含错误时抛出 `RuntimeException`。用 `onError()` 自定义处理——回调收到三样原材料,自行决定: +`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` — ES 原始响应(`items[]` 带逐条 status/error) -- `$body` — 完整原始批次(含成功项,native ES 格式) -- `$newbulk` — 一个新的、绑定同索引+目标 的 Bulk,用于重投失败项 +- `$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 -// 不设 onError → 有错误就抛 RuntimeException +// no onError → throws RuntimeException on error $bulk->flush(); -// 设 onError → 自行处理失败项(可重投) +// with onError → handle failures yourself (you can re-send) $bulk->onError(function (array $response, array $body, Bulk $newbulk) { - // items[k] ↔ 第 k 个 action;把失败的挑出来重投(此处为纯 index 批次的简易对齐) + // 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) { @@ -212,43 +214,43 @@ $bulk->onError(function (array $response, array $body, Bulk $newbulk) { })->flush(); ``` -> `batchSize` 自动 flush 触发的错误同样走 `onError`。 +> Errors from a `batchSize` auto-flush also go through `onError`. -## 零停机重建 +## Zero-downtime rebuild -创建新索引 → 导入数据 → 切换别名。 +Create a new index → import data → swap the alias. -`$name` 始终是应用面向的名称。应用不需要更改使用的名称——所有 CRUD、搜索、批量操作始终指向 `$name`。重建后,`$name` 变成别名,指向由 `rebuildName()` 生成的新索引。 +`$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 @@ -260,9 +262,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 @@ -276,7 +278,7 @@ class ProductIndex extends Index } ``` -也可以在调用时传入自定义数据源: +You can also pass a custom data source at call time: ```php $rebuild->source(function () { @@ -284,32 +286,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 内部使用 Bulk 执行导入,`onError()` 用法与 [批量操作 > 错误处理](#错误处理) 一致: +Rebuild uses Bulk internally for the import; `onError()` works the same as in [Bulk operations > Error handling](#error-handling): ```php $rebuild->onError(function (array $response, array $body, Bulk $newbulk) { - Log::warning("重建导入错误", $response); - // 需要重投失败项时用 $body + $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; @@ -317,25 +319,26 @@ use ElasticKit\Index\Manager; $manager = new Manager(new ProductIndex()); ``` -### 事件 +### Events ```php 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() @@ -346,45 +349,45 @@ $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` | diff --git a/docs/index.zh.md b/docs/index.zh.md new file mode 100644 index 0000000..1bc1fd2 --- /dev/null +++ b/docs/index.zh.md @@ -0,0 +1,405 @@ +# 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(); // 命中数 +$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(); +``` + +## 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` | From 6dd3b121c7dce20d4b00d599fc0d094a24dfedc8 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Sat, 27 Jun 2026 19:30:46 +0800 Subject: [PATCH 54/70] fix(index): correct class name in toPaginator() error message Index::setPaginatorResolver -> Pagination::setPaginatorResolver (where the resolver actually lives after the Support/ reorg) Co-Authored-By: Claude --- src/Index/Results.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Index/Results.php b/src/Index/Results.php index ac534ef..0fe9743 100644 --- a/src/Index/Results.php +++ b/src/Index/Results.php @@ -242,7 +242,7 @@ public function toPaginator() $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.' ); } From 797a024ec14a9c0e569bf401d9c277e74bea62e9 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Sat, 27 Jun 2026 19:50:31 +0800 Subject: [PATCH 55/70] test: strengthen assertions, move ResultsTest to unit, drop dead ensureSpecialFields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SearchContractTest: testFirst asserts the hit title (was only assertIsArray); testScroll asserts hit count (was only isEmpty) - ResultsTest moved to tests/ (unit) — pure parsing, belongs in the unit suite - DslTestCase: remove dead ensureSpecialFields() (integration never called it) Co-Authored-By: Claude --- tests/DslTestCase.php | 39 ------------------- .../Integration/Index/SearchContractTest.php | 3 +- tests/{Integration => }/ResultsTest.php | 2 +- 3 files changed, 3 insertions(+), 41 deletions(-) rename tests/{Integration => }/ResultsTest.php (99%) diff --git a/tests/DslTestCase.php b/tests/DslTestCase.php index 7de8124..a7757d1 100644 --- a/tests/DslTestCase.php +++ b/tests/DslTestCase.php @@ -162,43 +162,4 @@ protected static function seedData(\Elastic\Elasticsearch\ClientInterface $clien $client->indices()->refresh(['index' => $index]); } - - /** - * Ensure special field mappings (percolator, rank_feature, shape). - */ - protected 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/Integration/Index/SearchContractTest.php b/tests/Integration/Index/SearchContractTest.php index bd90f20..b008896 100644 --- a/tests/Integration/Index/SearchContractTest.php +++ b/tests/Integration/Index/SearchContractTest.php @@ -19,6 +19,7 @@ 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 @@ -53,7 +54,7 @@ public function testScroll(): void { $results = $this->makeIndex()->newQuery()->matchAll()->scroll(null, '1m'); $this->assertNotEmpty($results->scrollId()); - $this->assertFalse($results->isEmpty()); + $this->assertGreaterThanOrEqual(1, count($results->hits())); } public function testChunk(): void diff --git a/tests/Integration/ResultsTest.php b/tests/ResultsTest.php similarity index 99% rename from tests/Integration/ResultsTest.php rename to tests/ResultsTest.php index e3e56f8..713cf57 100644 --- a/tests/Integration/ResultsTest.php +++ b/tests/ResultsTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Tests\Integration; +namespace Tests; use PHPUnit\Framework\TestCase; use ElasticKit\Index\Support\ClientManager; From e8a8a26fe3bcef126b8f306a422ffd5fe1945853 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Sat, 27 Jun 2026 19:57:28 +0800 Subject: [PATCH 56/70] test(integration): cover Rebuild lifecycle (rollback/clean/forceUnlock/isLocked/allowEmpty) - testRollback: alias swaps then rolls back to the previous backing index - testClean: backing index is deleted - testForceUnlockIsIdempotent: forceUnlock tolerates a missing lock (404) - testIsLockedFalseAfterRun: lock released after a successful run - testEmptySourceThrowsWithoutAllowEmpty: empty source without allowEmpty throws - testAllowEmpty: allowEmpty permits an empty source - shared rebuildIndex() helper to cut the anonymous-class boilerplate Co-Authored-By: Claude --- tests/Integration/Index/BulkContractTest.php | 45 +++++++++++ tests/Integration/Index/DocContractTest.php | 32 ++++++++ .../Integration/Index/ManagerContractTest.php | 58 +++++++++++++- .../Integration/Index/RebuildContractTest.php | 79 +++++++++++++++++++ 4 files changed, 213 insertions(+), 1 deletion(-) diff --git a/tests/Integration/Index/BulkContractTest.php b/tests/Integration/Index/BulkContractTest.php index b59c7ed..a3540d4 100644 --- a/tests/Integration/Index/BulkContractTest.php +++ b/tests/Integration/Index/BulkContractTest.php @@ -68,4 +68,49 @@ public function testOnErrorReceivesFailures(): void ->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 index 8d197f0..67a5205 100644 --- a/tests/Integration/Index/DocContractTest.php +++ b/tests/Integration/Index/DocContractTest.php @@ -78,4 +78,36 @@ 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/ManagerContractTest.php b/tests/Integration/Index/ManagerContractTest.php index d33bf89..51d5eed 100644 --- a/tests/Integration/Index/ManagerContractTest.php +++ b/tests/Integration/Index/ManagerContractTest.php @@ -51,9 +51,65 @@ public function testRefresh(): void public function testDelete(): void { - $manager = new Manager($this->makeIndex()); + $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 index 5e1107a..a30e3ee 100644 --- a/tests/Integration/Index/RebuildContractTest.php +++ b/tests/Integration/Index/RebuildContractTest.php @@ -82,4 +82,83 @@ public function source(array $context = []): iterable $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']); + } } From 6437323b8b54bee53fe620caa44b5aa31d863d58 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Sat, 27 Jun 2026 20:39:17 +0800 Subject: [PATCH 57/70] refactor(dsl): precise PHPDoc unions for core builder params + hasParam signature - Boolean must/should/filter/mustNot: @param mixed -> @param Query|\Closure|array (4) - Query::addQuery: @param mixed -> Query|\Closure|array|Node - Query::when $query/$default: @param mixed -> Query|\Closure|array - Param::hasParam: add string signature type (was only in docblock) - Boolean.php: add use ElasticKit\DSL\Query for PHPDoc resolution - Follows mainstream framework convention: polymorphic builder params keep no signature type, rely on detailed PHPDoc Co-Authored-By: Claude --- src/DSL/Param.php | 2 +- src/DSL/Queries/Compound/Boolean.php | 11 ++++++----- src/DSL/Query.php | 6 +++--- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/DSL/Param.php b/src/DSL/Param.php index 44eee44..ec95d2d 100644 --- a/src/DSL/Param.php +++ b/src/DSL/Param.php @@ -22,7 +22,7 @@ trait Param * @param string $key * @return bool */ - public function hasParam($key): bool + public function hasParam(string $key): bool { return array_key_exists($key, $this->_params); } diff --git a/src/DSL/Queries/Compound/Boolean.php b/src/DSL/Queries/Compound/Boolean.php index a1bae57..e073c0f 100644 --- a/src/DSL/Queries/Compound/Boolean.php +++ b/src/DSL/Queries/Compound/Boolean.php @@ -4,8 +4,9 @@ namespace ElasticKit\DSL\Queries\Compound; -use ElasticKit\DSL\Support\ClausesSupport; use ElasticKit\DSL\Node; +use ElasticKit\DSL\Query; +use ElasticKit\DSL\Support\ClausesSupport; /** * A query that matches documents matching boolean combinations of other queries. The bool query maps to Lucene BooleanQuery. It is built using one or more boolean clauses, each clause with a typed occurrence. The occurrence types are: @@ -20,7 +21,7 @@ class Boolean extends Node * The clause (query) must appear in matching documents and will contribute to the score. * Supports multiple calls to incrementally build the bool query. * - * @param mixed $value + * @param Query|\Closure|array $value * @return static */ public function must($value): static @@ -32,7 +33,7 @@ public function must($value): static * The clause (query) should appear in the matching document. * Supports multiple calls to incrementally build the bool query. * - * @param mixed $value + * @param Query|\Closure|array $value * @return static */ public function should($value): static @@ -44,7 +45,7 @@ public function should($value): static * 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 $value + * @param Query|\Closure|array $value * @return static */ public function filter($value): static @@ -56,7 +57,7 @@ public function filter($value): static * 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 $value + * @param Query|\Closure|array $value * @return static */ public function mustNot($value): static diff --git a/src/DSL/Query.php b/src/DSL/Query.php index 020db9f..d24ab24 100644 --- a/src/DSL/Query.php +++ b/src/DSL/Query.php @@ -119,7 +119,7 @@ public function getQueries(): array /** * Add a query clause to the query container. * - * @param mixed $clause + * @param Query|\Closure|array|Node $clause * @return $this */ public function addQuery($clause): static @@ -134,8 +134,8 @@ public function addQuery($clause): static * $condition is a bool, or a Closure returning a bool. * * @param bool|\Closure $condition - * @param mixed $query - * @param mixed $default + * @param Query|\Closure|array $query + * @param Query|\Closure|array|null $default * @return $this */ public function when(bool|\Closure $condition, $query, $default = null): static From 6c970b622132a2ba7191c7f7f375650a43255751 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Sat, 27 Jun 2026 20:46:23 +0800 Subject: [PATCH 58/70] ci: run integration suite in ES job; drop composer audit block-insecure - ci.yml: integration job was running --testsuite unit (ES container spinning idle); now --testsuite integration so the 89 real-ES contract tests actually run in CI - composer.json: remove audit.block-insecure=false to restore composer audit's insecure-package blocking Co-Authored-By: Claude --- .github/workflows/ci.yml | 2 +- composer.json | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 22331f7..f7df152 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 diff --git a/composer.json b/composer.json index ec1cc09..bac5509 100644 --- a/composer.json +++ b/composer.json @@ -35,10 +35,5 @@ "analyse": "phpstan analyse", "cs-check": "php-cs-fixer fix --dry-run --diff", "cs-fix": "php-cs-fixer fix" - }, - "config": { - "audit": { - "block-insecure": false - } } } From 07f8341cda49cd721cadc1a9ac5658a3a5133a52 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Sat, 27 Jun 2026 20:56:19 +0800 Subject: [PATCH 59/70] fix: CI integration suite, scalar clause validation, agg dup, rescore accumulation, first from, audit - ci.yml: integration job now runs --testsuite integration (was unit; ES container was idle) - Query::buildClauses: throw on scalar clauses (was silently producing empty must/should/etc) - Query::aggs: throw on duplicate alias for Agg-instance/array branches (was silently overwriting; closure branch keeps accumulating) - Param::rescore: accumulate on repeated calls (was last-write-wins; single call still produces a single object for BC) - Search::first(): reset from=0 (was returning the Nth doc if from() was set) - composer.json: drop audit.block-insecure=false Co-Authored-By: Claude --- src/DSL/Param.php | 9 ++++++++- src/DSL/Query.php | 11 +++++++++++ src/Index/Search.php | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/DSL/Param.php b/src/DSL/Param.php index ec95d2d..4469276 100644 --- a/src/DSL/Param.php +++ b/src/DSL/Param.php @@ -317,7 +317,14 @@ public function collapse($value): static */ public function rescore($value): static { - $this->_params['rescore'] = Params\Rescore::create($value); + $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; } diff --git a/src/DSL/Query.php b/src/DSL/Query.php index d24ab24..4128d72 100644 --- a/src/DSL/Query.php +++ b/src/DSL/Query.php @@ -177,6 +177,9 @@ public function aggs($alias, $aggs = null): static throw new BadMethodCallException('aggs() requires a non-empty alias.'); } $aggs->alias($key); + if (isset($this->_aggregations[$key])) { + throw new RuntimeException(sprintf('Duplicate aggregation alias "%s".', $key)); + } $this->_aggregations[$key] = $aggs; return $this; } @@ -190,6 +193,9 @@ public function aggs($alias, $aggs = null): static if (is_array($aggs)) { $childAgg = Agg::create($aggs); $childAgg->alias($alias); + if (isset($this->_aggregations[$alias])) { + throw new RuntimeException(sprintf('Duplicate aggregation alias "%s".', $alias)); + } $this->_aggregations[$alias] = $childAgg; return $this; } @@ -291,6 +297,11 @@ private function buildClauses(array $flat): array } $clauses[] = [$field => $item]; } + } else { + throw new RuntimeException(sprintf( + 'Unsupported clause type %s; use a Node, array, or closure.', + get_debug_type($query) + )); } } diff --git a/src/Index/Search.php b/src/Index/Search.php index cab5043..17ea7ff 100644 --- a/src/Index/Search.php +++ b/src/Index/Search.php @@ -95,7 +95,7 @@ public function first(): ?array { $saved = $this->query; $this->query = clone $this->query; - $this->query->size(1); + $this->query->size(1)->from(0); try { $response = $this->doSearch('first'); } finally { From e85092e1dfa0da5ecfbf481adde6e414061787e7 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Sat, 27 Jun 2026 21:00:45 +0800 Subject: [PATCH 60/70] chore: cs-fixer now covers tests/ (was src-only) - .php-cs-fixer.php Finder adds tests/ - apply fixes to 14 test files (anonymous class spacing etc.) Co-Authored-By: Claude --- .php-cs-fixer.php | 2 +- tests/AggsTest.php | 94 +++++++++---------- tests/ClosureReturnTest.php | 1 - tests/CompoundQueriesTest.php | 8 +- tests/GeoQueriesTest.php | 8 +- .../Integration/Index/ManagerContractTest.php | 8 +- .../Integration/Index/RebuildContractTest.php | 8 +- tests/Integration/IntegrationTestCase.php | 2 +- tests/JoiningQueriesTest.php | 12 +-- tests/MatchAllTest.php | 4 +- tests/ParamsTest.php | 56 +++++------ tests/ShapeQueriesTest.php | 2 +- tests/SpanQueriesTest.php | 2 +- tests/SpecializedQueriesTest.php | 18 ++-- tests/TermLevelTest.php | 30 +++--- 15 files changed, 126 insertions(+), 129 deletions(-) diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 6bc47da..2a924d8 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -1,7 +1,7 @@ in(__DIR__ . '/src'); + ->in([__DIR__ . '/src', __DIR__ . '/tests']); return (new PhpCsFixer\Config()) ->setRules([ diff --git a/tests/AggsTest.php b/tests/AggsTest.php index 0e3b0ac..70468e7 100644 --- a/tests/AggsTest.php +++ b/tests/AggsTest.php @@ -10,7 +10,7 @@ class AggsTest extends DslTestCase { public function testTermsAggregation() { -$expectedJson = <<indexName; - $index = new class($name) extends Index { + $index = new class ($name) extends Index { public function __construct(string $name) { $this->name = $name; @@ -52,7 +52,7 @@ public function testRefresh(): void public function testDelete(): void { $name = 'ek_mgr_' . bin2hex(random_bytes(4)); - $index = new class($name) extends Index { + $index = new class ($name) extends Index { public function __construct(string $name) { $this->name = $name; @@ -68,7 +68,7 @@ public function __construct(string $name) public function testCreate(): void { $name = 'ek_mgr_' . bin2hex(random_bytes(4)); - $index = new class($name) extends Index { + $index = new class ($name) extends Index { public function __construct(string $name) { $this->name = $name; @@ -100,7 +100,7 @@ public function testPutSettings(): void public function testCloseAndOpen(): void { $name = 'ek_mgr_' . bin2hex(random_bytes(4)); - $index = new class($name) extends Index { + $index = new class ($name) extends Index { public function __construct(string $name) { $this->name = $name; diff --git a/tests/Integration/Index/RebuildContractTest.php b/tests/Integration/Index/RebuildContractTest.php index a30e3ee..53ac043 100644 --- a/tests/Integration/Index/RebuildContractTest.php +++ b/tests/Integration/Index/RebuildContractTest.php @@ -13,7 +13,7 @@ class RebuildContractTest extends IntegrationTestCase public function testRunCreatesAlias(): void { $alias = 'ek_rebuild_' . bin2hex(random_bytes(4)); - $index = new class($alias) extends Index { + $index = new class ($alias) extends Index { public function __construct(string $alias) { $this->name = $alias; @@ -38,7 +38,7 @@ public function source(array $context = []): iterable public function testRunSwapsAlias(): void { $alias = 'ek_rebuild_' . bin2hex(random_bytes(4)); - $index = new class($alias) extends Index { + $index = new class ($alias) extends Index { public function __construct(string $alias) { $this->name = $alias; @@ -66,7 +66,7 @@ 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 { + $index = new class ($name) extends Index { public function __construct(string $name) { $this->name = $name; @@ -85,7 +85,7 @@ public function source(array $context = []): iterable private function rebuildIndex(string $alias, bool $empty = false): Index { - return new class($alias, $empty) extends Index { + return new class ($alias, $empty) extends Index { private bool $empty; public function __construct(string $alias, bool $empty) diff --git a/tests/Integration/IntegrationTestCase.php b/tests/Integration/IntegrationTestCase.php index 00f3119..713a31d 100644 --- a/tests/Integration/IntegrationTestCase.php +++ b/tests/Integration/IntegrationTestCase.php @@ -80,7 +80,7 @@ public static function tearDownAfterClass(): void protected function makeIndex(): Index { $name = $this->indexName; - return new class($name) extends Index { + return new class ($name) extends Index { public function __construct(string $name) { $this->name = $name; 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/ParamsTest.php b/tests/ParamsTest.php index bb22f33..1d26cc6 100644 --- a/tests/ParamsTest.php +++ b/tests/ParamsTest.php @@ -7,7 +7,7 @@ class ParamsTest extends DslTestCase { public function testSizeWithQuery() { -$expectedJson = <<assertQuery($exampleJson, $query); } -} \ No newline at end of file +} diff --git a/tests/TermLevelTest.php b/tests/TermLevelTest.php index 01c0e9d..fe85d00 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); } -} \ No newline at end of file +} From 3549e6f7a29779f28c45cf67d876c83cc678312e Mon Sep 17 00:00:00 2001 From: ykan821 Date: Sat, 27 Jun 2026 21:43:21 +0800 Subject: [PATCH 61/70] fix(dsl): aggs duplicate alias throws for all input forms (was closure accumulated) - Query::aggs + Agg::aggs closure branch: was reusing existing Agg (silent accumulate); now throws like instance/array branches - Incremental aggregation building goes through closure-internal or Agg object, not repeated top-level aggs() calls - Aligns with map semantics: alias is a key (not a list index); duplicate key = error Co-Authored-By: Claude --- src/DSL/Agg.php | 15 ++++++++++++--- src/DSL/Query.php | 8 +++++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/DSL/Agg.php b/src/DSL/Agg.php index 7911a41..92760d8 100644 --- a/src/DSL/Agg.php +++ b/src/DSL/Agg.php @@ -6,6 +6,7 @@ use BadMethodCallException; use ElasticKit\DSL\Aggs\Bucket; +use RuntimeException; use ElasticKit\DSL\Aggs\Metric; use ElasticKit\DSL\Aggs\Pipeline; @@ -140,6 +141,9 @@ public function aggs($alias, $aggs = null): static throw new BadMethodCallException('aggs() requires a non-empty alias.'); } $aggs->alias($key); + if (isset($this->_subAggs[$key])) { + throw new RuntimeException(sprintf('Duplicate aggregation alias "%s".', $key)); + } $this->_subAggs[$key] = $aggs; return $this; } @@ -153,15 +157,20 @@ public function aggs($alias, $aggs = null): static if (is_array($aggs)) { $childAgg = Agg::create($aggs); $childAgg->alias($alias); + if (isset($this->_subAggs[$alias])) { + throw new RuntimeException(sprintf('Duplicate aggregation alias "%s".', $alias)); + } $this->_subAggs[$alias] = $childAgg; return $this; } - if (!isset($this->_subAggs[$alias])) { - $this->_subAggs[$alias] = new Agg(); - $this->_subAggs[$alias]->alias($alias); + if (isset($this->_subAggs[$alias])) { + throw new RuntimeException(sprintf('Duplicate aggregation alias "%s".', $alias)); } + $this->_subAggs[$alias] = new Agg(); + $this->_subAggs[$alias]->alias($alias); + if ($aggs instanceof \Closure) { $aggs($this->_subAggs[$alias]); return $this; diff --git a/src/DSL/Query.php b/src/DSL/Query.php index 4128d72..119baff 100644 --- a/src/DSL/Query.php +++ b/src/DSL/Query.php @@ -200,11 +200,13 @@ public function aggs($alias, $aggs = null): static return $this; } - if (!isset($this->_aggregations[$alias])) { - $this->_aggregations[$alias] = new Agg(); - $this->_aggregations[$alias]->alias($alias); + if (isset($this->_aggregations[$alias])) { + throw new RuntimeException(sprintf('Duplicate aggregation alias "%s".', $alias)); } + $this->_aggregations[$alias] = new Agg(); + $this->_aggregations[$alias]->alias($alias); + if ($aggs instanceof \Closure) { $aggs($this->_aggregations[$alias]); return $this; From 9ba5bfd28f7959c43bf18833bc69f38fab4c2fe3 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Sat, 27 Jun 2026 22:18:30 +0800 Subject: [PATCH 62/70] perf: cache DeepClone reflection + avoid clone in Search hot paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DeepClone: cache ReflectionProperty[] per class (static), avoid new ReflectionClass on every clone - Search::doSearch: $extra['body'] merges into query body (not replaces) - first()/paginate()/scroll(): pass size/from override via $extra['body'] instead of clone+restore — all three hot paths are now clone-free Co-Authored-By: Claude --- src/DSL/DeepClone.php | 15 +++++++++++++-- src/Index/Search.php | 43 +++++++++++++++---------------------------- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/src/DSL/DeepClone.php b/src/DSL/DeepClone.php index d7f8796..7500d52 100644 --- a/src/DSL/DeepClone.php +++ b/src/DSL/DeepClone.php @@ -17,10 +17,21 @@ */ trait DeepClone { + /** @var array> */ + private static array $cloneProperties = []; + public function __clone(): void { - foreach ((new ReflectionClass($this))->getProperties() as $property) { - if ($property->isStatic() || !$property->isInitialized($this)) { + $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; } diff --git a/src/Index/Search.php b/src/Index/Search.php index 17ea7ff..5cdfcd3 100644 --- a/src/Index/Search.php +++ b/src/Index/Search.php @@ -93,14 +93,7 @@ public function get(): Results */ public function first(): ?array { - $saved = $this->query; - $this->query = clone $this->query; - $this->query->size(1)->from(0); - try { - $response = $this->doSearch('first'); - } finally { - $this->query = $saved; - } + $response = $this->doSearch('first', ['body' => ['size' => 1, 'from' => 0]]); $docs = (new Results($response))->docs(); return $docs[0] ?? null; @@ -137,18 +130,12 @@ public function scroll(?string $scrollId = null, string $duration = '5m'): Resul 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]; } - try { - $response = $this->doSearch('scroll', ['scroll' => $duration]); - } finally { - $this->query = $saved; - } + $response = $this->doSearch('scroll', $extra); return new Results($response); } @@ -276,15 +263,10 @@ public function paginate(?int $page = null, ?int $perPage = null): Results $perPage = $maxPerPage; } - $saved = $this->query; - $this->query = clone $this->query; - $this->query->from(($page - 1) * $perPage); - $this->query->size($perPage); - try { - $response = $this->doSearch('paginate'); - } finally { - $this->query = $saved; - } + $response = $this->doSearch('paginate', ['body' => [ + 'from' => ($page - 1) * $perPage, + 'size' => $perPage, + ]]); return (new Results($response))->paginate($page, $perPage); } @@ -326,13 +308,18 @@ protected function doCount(): array * 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(string $action, array $extra = []): array { $indexName = $this->index->name(); - $body = $this->query->toArray() ?: new stdClass(); + $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; From 9f1cb94d21f88eb2f3bab2b68cab04bf60896ea4 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Sat, 27 Jun 2026 23:22:14 +0800 Subject: [PATCH 63/70] refactor: extract RegistersAgg trait to Support/ - New RegistersAgg trait in src/DSL/Support/: registerAgg($alias, $aggs, &$store) consolidates the ~50-line aggs() body shared between Query and Agg - Query::aggs() and Agg::aggs() now one-liner delegates - @SuppressWarnings(PHPMD.NPathComplexity) on registerAgg (flat polymorphic dispatch) - phpstan memory limit via --memory-limit=256M CLI flag (neon/CI/CLAUDE.md/composer.json) Co-Authored-By: Claude --- .github/workflows/ci.yml | 2 +- CLAUDE.md | 2 +- composer.json | 2 +- src/DSL/Agg.php | 57 ++-------------------- src/DSL/Query.php | 56 ++------------------- src/DSL/Support/RegistersAgg.php | 84 ++++++++++++++++++++++++++++++++ tests/AggsTest.php | 12 +++++ 7 files changed, 107 insertions(+), 108 deletions(-) create mode 100644 src/DSL/Support/RegistersAgg.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7df152..f03c592 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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/CLAUDE.md b/CLAUDE.md index 7f36633..b2afd5f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,7 +58,7 @@ Tests run inside a Docker container and require these environment variables: docker exec $PHP_CONTAINER sh -c "cd $PROJECT_PATH && vendor/bin/phpunit --testsuite unit" 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" diff --git a/composer.json b/composer.json index bac5509..d7bb4b1 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ } ], "scripts": { - "analyse": "phpstan analyse", + "analyse": "phpstan analyse --memory-limit=256M", "cs-check": "php-cs-fixer fix --dry-run --diff", "cs-fix": "php-cs-fixer fix" } diff --git a/src/DSL/Agg.php b/src/DSL/Agg.php index 92760d8..135d433 100644 --- a/src/DSL/Agg.php +++ b/src/DSL/Agg.php @@ -4,11 +4,10 @@ namespace ElasticKit\DSL; -use BadMethodCallException; use ElasticKit\DSL\Aggs\Bucket; -use RuntimeException; use ElasticKit\DSL\Aggs\Metric; use ElasticKit\DSL\Aggs\Pipeline; +use ElasticKit\DSL\Support\RegistersAgg; /** * Aggregation container (independent, does not extend Node). @@ -21,6 +20,7 @@ class Agg use Metric; use Pipeline; use DeepClone; + use RegistersAgg; /** * The aggregation type node. @@ -125,60 +125,11 @@ public function getAlias(): ?string * * @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): 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($this->_subAggs[$key])) { - throw new RuntimeException(sprintf('Duplicate aggregation alias "%s".', $key)); - } - $this->_subAggs[$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($this->_subAggs[$alias])) { - throw new RuntimeException(sprintf('Duplicate aggregation alias "%s".', $alias)); - } - $this->_subAggs[$alias] = $childAgg; - return $this; - } - - if (isset($this->_subAggs[$alias])) { - throw new RuntimeException(sprintf('Duplicate aggregation alias "%s".', $alias)); - } - - $this->_subAggs[$alias] = new Agg(); - $this->_subAggs[$alias]->alias($alias); - - if ($aggs instanceof \Closure) { - $aggs($this->_subAggs[$alias]); - return $this; - } - - throw new BadMethodCallException( - sprintf('aggs("%s", ...) requires a closure, array, or Agg instance as the definition.', $alias) - ); + return $this->registerAgg($alias, $aggs, $this->_subAggs); } /** diff --git a/src/DSL/Query.php b/src/DSL/Query.php index 119baff..664c0c8 100644 --- a/src/DSL/Query.php +++ b/src/DSL/Query.php @@ -4,7 +4,6 @@ namespace ElasticKit\DSL; -use BadMethodCallException; use Closure; use RuntimeException; use ElasticKit\DSL\Queries\Compound; @@ -14,6 +13,7 @@ use ElasticKit\DSL\Queries\MatchAll; use ElasticKit\DSL\Queries\Shape; use ElasticKit\DSL\Queries\Span; +use ElasticKit\DSL\Support\RegistersAgg; use ElasticKit\DSL\Queries\Specialized; use ElasticKit\DSL\Queries\TermLevel; use stdClass; @@ -27,6 +27,7 @@ class Query extends Node { use Compound; + use RegistersAgg; use FullText; use Geo; use Shape; @@ -161,60 +162,11 @@ public function when(bool|\Closure $condition, $query, $default = null): static * * @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): 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($this->_aggregations[$key])) { - throw new RuntimeException(sprintf('Duplicate aggregation alias "%s".', $key)); - } - $this->_aggregations[$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($this->_aggregations[$alias])) { - throw new RuntimeException(sprintf('Duplicate aggregation alias "%s".', $alias)); - } - $this->_aggregations[$alias] = $childAgg; - return $this; - } - - if (isset($this->_aggregations[$alias])) { - throw new RuntimeException(sprintf('Duplicate aggregation alias "%s".', $alias)); - } - - $this->_aggregations[$alias] = new Agg(); - $this->_aggregations[$alias]->alias($alias); - - if ($aggs instanceof \Closure) { - $aggs($this->_aggregations[$alias]); - return $this; - } - - throw new BadMethodCallException( - sprintf('aggs("%s", ...) requires a closure, array, or Agg instance as the definition.', $alias) - ); + return $this->registerAgg($alias, $aggs, $this->_aggregations); } /** 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/tests/AggsTest.php b/tests/AggsTest.php index 70468e7..0aec2f5 100644 --- a/tests/AggsTest.php +++ b/tests/AggsTest.php @@ -2,12 +2,24 @@ 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 testAggsAcceptsAggInstanceAsOnlyArgument() + { + $agg = (new Agg())->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 = << Date: Sun, 28 Jun 2026 00:28:05 +0800 Subject: [PATCH 64/70] fix(index): Doc retryOnConflict/refresh persist across writes (was reset after each) - Removed resetOptions() that silently cleared retryOnConflict and refresh after each write - Options now persist until explicitly changed, consistent with Bulk and builder conventions - Updated docblock: 'next write operation' -> 'subsequent write operations' Co-Authored-By: Claude --- src/Index/Doc.php | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/src/Index/Doc.php b/src/Index/Doc.php index 3ab8178..421e5de 100644 --- a/src/Index/Doc.php +++ b/src/Index/Doc.php @@ -44,7 +44,7 @@ public function id(): string|int|null } /** - * Set retry_on_conflict for the next write operation. + * Set retry_on_conflict for subsequent write operations. * * @param int $count * @return $this @@ -57,7 +57,7 @@ public function retryOnConflict(int $count): static } /** - * 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 @@ -144,7 +144,6 @@ public function update(array $data, bool $upsert = false): array $params['refresh'] = $this->refresh; } - $this->resetOptions(); return $this->index->getClient()->update($params)->asArray(); } @@ -173,7 +172,6 @@ public function index(array $data): array $params['refresh'] = $this->refresh; } - $this->resetOptions(); return $this->index->getClient()->index($params)->asArray(); } @@ -215,7 +213,6 @@ public function create(array $data): array $params['refresh'] = $this->refresh; } - $this->resetOptions(); return $this->index->getClient()->index($params)->asArray(); } @@ -238,7 +235,6 @@ public function delete(): array $params['refresh'] = $this->refresh; } - $this->resetOptions(); return $this->index->getClient()->delete($params)->asArray(); } @@ -264,13 +260,4 @@ private function requireId(string $operation): string|int return $this->id; } - - /** - * Reset pending options after a write operation. - */ - private function resetOptions(): void - { - $this->retryOnConflict = 0; - $this->refresh = null; - } } From 1a16f9fcc2efcdbc669a3d266a5e60676416e5a1 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Sun, 28 Jun 2026 13:26:36 +0800 Subject: [PATCH 65/70] fix(dsl): serialization correctness across field-keyed/agg/range/span - Node: toArray() throws LogicException when a field-keyed node has no field set, replacing an uncatchable typed-property Error - Agg: empty aggregation body serializes to {} (stdClass), not [] which Elasticsearch rejects; aligns with Global_/ReverseNested - RangeSupport: reject positional elements beyond the [start, end] shorthand instead of leaking them as numeric string keys - SpanTerm: term() emits ES's {value} key (was {term}); delegates to value() - DateHistogram: mark interval() @deprecated (ES deprecated the key) Baseline pre-existing RegistersAgg by-ref phpstan errors (proper fix deferred to the phpstan cleanup). Co-Authored-By: Claude --- phpstan-baseline.neon | 29 +++++++++++++++++---------- src/DSL/Agg.php | 5 +++-- src/DSL/Aggs/Bucket/DateHistogram.php | 1 + src/DSL/Node.php | 7 +++++++ src/DSL/Queries/Span/SpanTerm.php | 4 +++- src/DSL/Support/RangeSupport.php | 12 ++++++++++- tests/AggsTest.php | 8 ++++++++ tests/NodeInvariantsTest.php | 7 +++++++ tests/SpanQueriesTest.php | 2 +- tests/TermLevelTest.php | 7 +++++++ 10 files changed, 66 insertions(+), 16 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index f5e9a20..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 @@ -42,12 +54,6 @@ parameters: count: 1 path: src/Index/Doc.php - - - message: '#^Call to an undefined method Elastic\\Elasticsearch\\ClientInterface\:\:index\(\)\.$#' - identifier: method.notFound - count: 1 - path: src/Index/Index.php - - message: '#^Call to an undefined method Elastic\\Elasticsearch\\ClientInterface\:\:indices\(\)\.$#' identifier: method.notFound @@ -55,9 +61,9 @@ parameters: path: src/Index/Manager.php - - message: '#^Call to an undefined method Elastic\\Elasticsearch\\ClientInterface\:\:indices\(\)\.$#' + message: '#^Call to an undefined method Elastic\\Elasticsearch\\ClientInterface\:\:delete\(\)\.$#' identifier: method.notFound - count: 5 + count: 2 path: src/Index/Rebuild.php - @@ -73,14 +79,15 @@ parameters: path: src/Index/Rebuild.php - - message: '#^Call to an undefined method Elastic\\Elasticsearch\\ClientInterface\:\:delete\(\)\.$#' + message: '#^Call to an undefined method Elastic\\Elasticsearch\\ClientInterface\:\:indices\(\)\.$#' identifier: method.notFound - count: 2 + count: 5 path: src/Index/Rebuild.php - message: '#^If condition is always false\.$#' identifier: if.alwaysFalse + count: 1 path: src/Index/Rebuild.php - @@ -98,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/src/DSL/Agg.php b/src/DSL/Agg.php index 135d433..6696c4e 100644 --- a/src/DSL/Agg.php +++ b/src/DSL/Agg.php @@ -8,6 +8,7 @@ use ElasticKit\DSL\Aggs\Metric; use ElasticKit\DSL\Aggs\Pipeline; use ElasticKit\DSL\Support\RegistersAgg; +use stdClass; /** * Aggregation container (independent, does not extend Node). @@ -169,7 +170,7 @@ 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; } @@ -188,7 +189,7 @@ public function toArray(): array } if ($this->_alias !== null) { - return [$this->_alias => $inner]; + return [$this->_alias => ($inner === [] ? new stdClass() : $inner)]; } return $inner; diff --git a/src/DSL/Aggs/Bucket/DateHistogram.php b/src/DSL/Aggs/Bucket/DateHistogram.php index 82110b2..8aeab30 100644 --- a/src/DSL/Aggs/Bucket/DateHistogram.php +++ b/src/DSL/Aggs/Bucket/DateHistogram.php @@ -27,6 +27,7 @@ public function calendarInterval(string $value): static /** * Interval for bucketing. Deprecated in favor of calendar_interval or fixed_interval. * + * @deprecated ES deprecated the bare `interval` key; use calendarInterval() or fixedInterval() instead. * @param string $value * @return static */ diff --git a/src/DSL/Node.php b/src/DSL/Node.php index 25278d6..64f89ef 100644 --- a/src/DSL/Node.php +++ b/src/DSL/Node.php @@ -8,6 +8,7 @@ use BadMethodCallException; use Closure; use InvalidArgumentException; +use LogicException; use stdClass; /** @@ -325,6 +326,12 @@ public function toArray() } if ($this->_fieldKeyed) { + 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]; } return $properties; diff --git a/src/DSL/Queries/Span/SpanTerm.php b/src/DSL/Queries/Span/SpanTerm.php index 1a468e8..71958a9 100644 --- a/src/DSL/Queries/Span/SpanTerm.php +++ b/src/DSL/Queries/Span/SpanTerm.php @@ -19,12 +19,14 @@ class SpanTerm extends Node /** * The value of the term to match. * + * ES span_term uses the {value} key (not {term}); delegates to value(). + * * @param string|int|float|bool $value * @return static */ public function term(string|int|float|bool $value): static { - return $this->addProperty('term', $value); + return $this->value($value); } /** diff --git a/src/DSL/Support/RangeSupport.php b/src/DSL/Support/RangeSupport.php index 6a35899..bfc23e6 100644 --- a/src/DSL/Support/RangeSupport.php +++ b/src/DSL/Support/RangeSupport.php @@ -4,6 +4,8 @@ namespace ElasticKit\DSL\Support; +use InvalidArgumentException; + /** * Maps operator shorthands (>=, >, <=, <) and [start, end] to ES range keys. */ @@ -26,7 +28,7 @@ public function __construct($field = null, $value = null) } /** - * @param array $props + * @param array $props * @return array */ private static function normalizeKeys(array $props): array @@ -39,6 +41,14 @@ private static function normalizeKeys(array $props): array 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/tests/AggsTest.php b/tests/AggsTest.php index 0aec2f5..3afc5b8 100644 --- a/tests/AggsTest.php +++ b/tests/AggsTest.php @@ -1150,6 +1150,14 @@ public function testTermsStringShorthand() $this->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(); diff --git a/tests/NodeInvariantsTest.php b/tests/NodeInvariantsTest.php index 8967ff8..69fc6d4 100644 --- a/tests/NodeInvariantsTest.php +++ b/tests/NodeInvariantsTest.php @@ -58,4 +58,11 @@ public function testFloatValueSurvivesToJson() // 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(); + } } diff --git a/tests/SpanQueriesTest.php b/tests/SpanQueriesTest.php index 5084523..b1152fc 100644 --- a/tests/SpanQueriesTest.php +++ b/tests/SpanQueriesTest.php @@ -267,7 +267,7 @@ 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; diff --git a/tests/TermLevelTest.php b/tests/TermLevelTest.php index fe85d00..f18cb7a 100644 --- a/tests/TermLevelTest.php +++ b/tests/TermLevelTest.php @@ -146,6 +146,13 @@ public function testRangeShorthand() $this->assertQuery($expectedJson, $query); } + public function testRangeShorthandRejectsExtraPositionalElements() + { + $this->expectException(\InvalidArgumentException::class); + $query = new Query(); + $query->range('price', [10, 20, 30]); + } + public function testRangeOperators() { $expectedJson = << Date: Sun, 28 Jun 2026 13:41:41 +0800 Subject: [PATCH 66/70] feat(index)!: opt-in track_total_hits, rebuild swap rollback Pagination total tracking is now opt-in per index: - Index: add $trackTotalHits (default false); Search applies it to non-scroll requests (ES forbids disabling it in a scroll context) - Results: total()/lastPage() return ?int, null when the total is unavailable (track_total_hits=false); add hasMorePages() (full-page heuristic when no total) - Results: toPaginator() throws PaginationTotalUnavailableException when the total is unavailable; isEmpty() docblock points to hasMorePages() - max_result_window overflow is left to Elasticsearch's own 400 Rebuild: wrap the alias-swap step (refresh + updateAliases/putAlias) in try/catch and delete the orphaned new index on failure. **BC:** Index now defaults track_total_hits to false, so total()/lastPage() return null and toPaginator() throws unless the index sets trackTotalHits=true. Co-Authored-By: Claude --- .../PaginationTotalUnavailableException.php | 19 +++++++ src/Index/Index.php | 20 ++++++++ src/Index/Rebuild.php | 41 +++++++++------- src/Index/Results.php | 49 ++++++++++++++++--- src/Index/Search.php | 7 +++ tests/Integration/IntegrationTestCase.php | 1 + tests/ResultsTest.php | 41 ++++++++++++++++ 7 files changed, 152 insertions(+), 26 deletions(-) create mode 100644 src/Index/Exception/PaginationTotalUnavailableException.php 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 @@ +maxPerPage; } + /** + * Return whether total hit tracking is enabled for this index. + * + * @return bool + */ + public function trackTotalHits(): bool + { + return $this->trackTotalHits; + } + /** * Yield documents as [id => doc] pairs. Override to provide a default data source for rebuild. * diff --git a/src/Index/Rebuild.php b/src/Index/Rebuild.php index 789972f..4256b4a 100644 --- a/src/Index/Rebuild.php +++ b/src/Index/Rebuild.php @@ -265,28 +265,33 @@ private function doRun(array $context): array throw $e; } - $client->refresh(['index' => $newIndex]); - $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]]; + 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]); } - $actions[] = ['add' => ['index' => $newIndex, 'alias' => $name]]; - $client->updateAliases(['body' => ['actions' => $actions]]); - } elseif ($client->exists(['index' => $name])->asBool()) { + } catch (\Throwable $e) { + // Swap failed (or precondition unmet) — remove the orphaned new index. $client->delete(['index' => $newIndex]); - 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]); + throw $e; } $e = new Event('rebuild.run.after', $name); diff --git a/src/Index/Results.php b/src/Index/Results.php index 0fe9743..3b4d947 100644 --- a/src/Index/Results.php +++ b/src/Index/Results.php @@ -6,6 +6,7 @@ use ElasticKit\Index\Support\Pagination; use RuntimeException; +use ElasticKit\Index\Exception\PaginationTotalUnavailableException; /** * Lightweight wrapper for Elasticsearch search response. @@ -56,13 +57,15 @@ public function paginate(int $page, int $perPage): static } /** - * 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(): int + public function total(): ?int { - return $this->response['hits']['total']['value'] ?? 0; + return $this->response['hits']['total']['value'] ?? null; } /** @@ -142,7 +145,7 @@ public function totalRelation(): ?string * Whether the current result set has no hits. * * For scroll loops: `while (! $results->isEmpty())`. For pagination - * "has a next page", use `page() < lastPage()` instead. + * "has a next page", use hasMorePages() (works with or without a total). * * @return bool */ @@ -202,12 +205,16 @@ public function perPage(): int } /** - * 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(): int + public function lastPage(): ?int { + if ($this->total() === null) { + return null; + } + if ($this->perPage < 1) { return 1; } @@ -215,6 +222,25 @@ public function lastPage(): int return (int) ceil($this->total() / $this->perPage) ?: 1; } + /** + * Whether there is a page after the current one. + * + * 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 hasMorePages(): bool + { + $lastPage = $this->lastPage(); + + if ($lastPage !== null) { + return $this->page < $lastPage; + } + + return count($this->hits()) === $this->perPage; + } + /** * Alias for docs(), aligned with paginator semantics. * @@ -239,6 +265,13 @@ public function toPaginator() ); } + if ($this->totalRelation() === 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( diff --git a/src/Index/Search.php b/src/Index/Search.php index 5cdfcd3..6cfe677 100644 --- a/src/Index/Search.php +++ b/src/Index/Search.php @@ -314,6 +314,13 @@ protected function doCount(): array protected function doSearch(string $action, array $extra = []): array { $indexName = $this->index->name(); + + // 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']); diff --git a/tests/Integration/IntegrationTestCase.php b/tests/Integration/IntegrationTestCase.php index 713a31d..30c5fbc 100644 --- a/tests/Integration/IntegrationTestCase.php +++ b/tests/Integration/IntegrationTestCase.php @@ -84,6 +84,7 @@ protected function makeIndex(): Index public function __construct(string $name) { $this->name = $name; + $this->trackTotalHits = true; } }; } diff --git a/tests/ResultsTest.php b/tests/ResultsTest.php index 713cf57..438277d 100644 --- a/tests/ResultsTest.php +++ b/tests/ResultsTest.php @@ -5,6 +5,7 @@ namespace Tests; use PHPUnit\Framework\TestCase; +use ElasticKit\Index\Exception\PaginationTotalUnavailableException; use ElasticKit\Index\Support\ClientManager; use ElasticKit\Index\Support\Pagination; use ElasticKit\Index\Results; @@ -229,4 +230,44 @@ 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(); + } } From 9713576bc11706a4abdbdb7c5592a747f2c60317 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Sun, 28 Jun 2026 13:48:38 +0800 Subject: [PATCH 67/70] docs: refresh stale TODO, static-state warning, phpstan stub note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CLAUDE.md: replace stale TODO (scroll/bulk/rebuild now tested, infra built) with real gaps — Span/Shape integration contracts, Rebuild import-failure rollback - README.md / README.zh.md: warn that Index::setClient / ClientManager / EventDispatcher / Pagination hold static state that leaks across requests in long-lived workers (Swoole/RoadRunner/Octane); show reset() - stubs/ClientInterface.stub: document that phpstan does not merge interface-stub methods over the vendored interface, so endpoint calls stay baseline-suppressed (the Indices class stub works; this one does not) Co-Authored-By: Claude --- CLAUDE.md | 4 ++-- README.md | 18 ++++++++++++++++++ README.zh.md | 18 ++++++++++++++++++ stubs/ClientInterface.stub | 6 ++++++ 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b2afd5f..6049386 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,8 +38,8 @@ Follow PSR-5. ## TODO -- [ ] **Add boundary tests for core paths**: scroll, bulk batching, rebuild failure rollback -- [ ] **Set up integration test infrastructure**: driven by `ELASTICKIT_TEST_HOST`, with random index names for isolation +- [ ] **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 diff --git a/README.md b/README.md index ea59859..09b64c3 100644 --- a/README.md +++ b/README.md @@ -302,6 +302,24 @@ EventDispatcher::listen('search.*', function (Event $e) { +## 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 - [Guide](docs/guide.md) — an e-commerce order scenario, the full flow from install to production diff --git a/README.zh.md b/README.zh.md index fe4600c..3712b22 100644 --- a/README.zh.md +++ b/README.zh.md @@ -302,6 +302,24 @@ EventDispatcher::listen('search.*', function (Event $e) { +## 常驻进程 + +`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)——电商订单场景,从安装到上线的完整流程 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 From d78eba11c57d238bab9e4067ab53b5e79d5c8c6d Mon Sep 17 00:00:00 2001 From: ykan821 Date: Sun, 28 Jun 2026 14:22:09 +0800 Subject: [PATCH 68/70] =?UTF-8?q?fix:=20review=20findings=20=E2=80=94=20fi?= =?UTF-8?q?eld-keyed=20toArray=20guard,=20hasMorePages,=20toPaginator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Intervals::toArray() read \$this->_field without the field-keyed guard that Node gained, so (new Intervals())->toArray() still threw an uncatchable typed-property Error. Extract a shared Node::wrapFieldKeyed() helper and use it in Node, Intervals, and FunctionScore so every field-keyed toArray() path is guarded (LogicException). - Results::hasMorePages(): guard perPage <= 0 to avoid the 0 === 0 false positive. - Results::toPaginator(): guard on total() === null (the value it needs) rather than totalRelation(). Co-Authored-By: Claude --- src/DSL/Node.php | 32 +++++++++++++++------- src/DSL/Queries/Compound/FunctionScore.php | 5 +--- src/DSL/Queries/FullText/Intervals.php | 5 +--- src/Index/Results.php | 4 +-- tests/NodeInvariantsTest.php | 7 +++++ 5 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/DSL/Node.php b/src/DSL/Node.php index 64f89ef..5e663cb 100644 --- a/src/DSL/Node.php +++ b/src/DSL/Node.php @@ -297,6 +297,27 @@ protected function resolveProperties(array $properties): array 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. * @@ -325,16 +346,7 @@ public function toArray() } } - if ($this->_fieldKeyed) { - 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]; - } - return $properties; + return $this->wrapFieldKeyed($properties); } /** diff --git a/src/DSL/Queries/Compound/FunctionScore.php b/src/DSL/Queries/Compound/FunctionScore.php index 52338e6..23b7ea8 100644 --- a/src/DSL/Queries/Compound/FunctionScore.php +++ b/src/DSL/Queries/Compound/FunctionScore.php @@ -116,9 +116,6 @@ public function toArray() $properties = $this->resolveProperties($properties); - if ($this->_fieldKeyed) { - return [$this->_field => $properties]; - } - return $properties; + return $this->wrapFieldKeyed($properties); } } diff --git a/src/DSL/Queries/FullText/Intervals.php b/src/DSL/Queries/FullText/Intervals.php index f16d3a4..6d9fa6a 100644 --- a/src/DSL/Queries/FullText/Intervals.php +++ b/src/DSL/Queries/FullText/Intervals.php @@ -144,10 +144,7 @@ public function toArray() $properties = $resolved; } - if ($this->_fieldKeyed) { - return [$this->_field => $properties]; - } - return $properties; + return $this->wrapFieldKeyed($properties); } /** diff --git a/src/Index/Results.php b/src/Index/Results.php index 3b4d947..b17ab3a 100644 --- a/src/Index/Results.php +++ b/src/Index/Results.php @@ -238,7 +238,7 @@ public function hasMorePages(): bool return $this->page < $lastPage; } - return count($this->hits()) === $this->perPage; + return $this->perPage > 0 && count($this->hits()) === $this->perPage; } /** @@ -265,7 +265,7 @@ public function toPaginator() ); } - if ($this->totalRelation() === null) { + 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.' diff --git a/tests/NodeInvariantsTest.php b/tests/NodeInvariantsTest.php index 69fc6d4..571fd1b 100644 --- a/tests/NodeInvariantsTest.php +++ b/tests/NodeInvariantsTest.php @@ -65,4 +65,11 @@ 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(); + } } From 4981cdee62230cd9c3944d9ae52600571e432015 Mon Sep 17 00:00:00 2001 From: ykan821 Date: Sun, 28 Jun 2026 17:37:51 +0800 Subject: [PATCH 69/70] docs: track_total_hits defaults to false; property is int|bool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit $trackTotalHits is int|bool (true counts every hit, false omits the total, an int caps the count) — was typed bool, which rejected a custom cap. Document the default-false behavior across README/docs/guide (EN + zh): total()/lastPage() return null and toPaginator() throws unless the index sets $trackTotalHits = true (or a cap); use hasMorePages/chunk/cursor for total-less iteration. Co-Authored-By: Claude --- README.md | 4 +++- README.zh.md | 4 +++- docs/guide.md | 2 ++ docs/guide.zh.md | 2 ++ docs/index.md | 13 ++++++++++++- docs/index.zh.md | 13 ++++++++++++- src/Index/Index.php | 12 ++++++------ 7 files changed, 40 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 09b64c3..6f442f9 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ $results = ProductIndex::query() ->get(); $hits = $results->docs(); // [['title' => '...'], ...] -$total = $results->total(); // total hits +$total = $results->total(); // null unless $trackTotalHits = true (see Pagination & cursor) ``` ## DSL Examples @@ -214,6 +214,8 @@ foreach (ProductIndex::query()->cursor() as $hit) { } ``` +> **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 diff --git a/README.zh.md b/README.zh.md index 3712b22..ffb7583 100644 --- a/README.zh.md +++ b/README.zh.md @@ -45,7 +45,7 @@ $results = ProductIndex::query() ->get(); $hits = $results->docs(); // [['title' => '...'], ...] -$total = $results->total(); // 命中总数 +$total = $results->total(); // 索引未设 $trackTotalHits = true 时为 null(见分页与游标) ``` ## DSL 示例 @@ -214,6 +214,8 @@ foreach (ProductIndex::query()->cursor() as $hit) { } ``` +> **分页要总数,而总数默认关闭。** `Index` 默认 `$trackTotalHits = false`,`total()`/`lastPage()` 返 `null`、`toPaginator()` 抛异常。需要页码分页时在索引上设 `protected int|bool $trackTotalHits = true;`(或计数上限);否则用 `hasMorePages()` / `chunk()` / `cursor()` 做无总数遍历。 + ### 文档 CRUD ```php diff --git a/docs/guide.md b/docs/guide.md index fbdb4e5..1d8b541 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -160,6 +160,8 @@ public function index(Request $request) } ``` +> `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. + > 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): diff --git a/docs/guide.zh.md b/docs/guide.zh.md index d7d1160..747ef7d 100644 --- a/docs/guide.zh.md +++ b/docs/guide.zh.md @@ -160,6 +160,8 @@ public function index(Request $request) } ``` +> `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 遍历: diff --git a/docs/index.md b/docs/index.md index 4535ce2..868e4fc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -56,7 +56,7 @@ $results = ProductIndex::query() ->size(20) ->get(); -$results->total(); // hit count +$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 @@ -97,6 +97,17 @@ Pagination::setPaginatorResolver(function ($results, $page, $perPage) { $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 For large datasets, use scroll to fetch in batches: diff --git a/docs/index.zh.md b/docs/index.zh.md index 1bc1fd2..2a40120 100644 --- a/docs/index.zh.md +++ b/docs/index.zh.md @@ -56,7 +56,7 @@ $results = ProductIndex::query() ->size(20) ->get(); -$results->total(); // 命中数 +$results->total(); // 命中数(索引未设 $trackTotalHits = true 时为 null) $results->docs(); // _source 数组 $results->hits(); // 完整 hit 数组 $results->aggregations(); // 聚合结果 @@ -97,6 +97,17 @@ Pagination::setPaginatorResolver(function ($results, $page, $perPage) { $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 分批获取: diff --git a/src/Index/Index.php b/src/Index/Index.php index a0cdc11..20e6c39 100644 --- a/src/Index/Index.php +++ b/src/Index/Index.php @@ -51,11 +51,11 @@ abstract class Index * Whether searches on this index track the total hit count. * * false (default) leaves the total unset (Elasticsearch omits hits.total); - * set true on subclasses that need accurate pagination totals. + * true counts every hit; an int caps the count at that many hits. * - * @var bool + * @var int|bool */ - protected bool $trackTotalHits = false; + protected int|bool $trackTotalHits = false; /** * Register an Elasticsearch client. Optionally name the connection. @@ -224,11 +224,11 @@ public function maxPerPage(): int } /** - * Return whether total hit tracking is enabled for this index. + * Return the track_total_hits setting: true, false, or a count cap. * - * @return bool + * @return int|bool */ - public function trackTotalHits(): bool + public function trackTotalHits(): int|bool { return $this->trackTotalHits; } From 9e6619d9d8e19d8bd248f3be2555afb274ffab7c Mon Sep 17 00:00:00 2001 From: ykan821 Date: Sun, 28 Jun 2026 17:48:02 +0800 Subject: [PATCH 70/70] docs(changelog): 8.0.0-beta.5 Co-Authored-By: Claude --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ composer.json | 7 +------ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb231cc..02769c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ # 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 diff --git a/composer.json b/composer.json index d7bb4b1..003b25d 100644 --- a/composer.json +++ b/composer.json @@ -30,10 +30,5 @@ "name": "ykan", "email": "smykggi@gmail.com" } - ], - "scripts": { - "analyse": "phpstan analyse --memory-limit=256M", - "cs-check": "php-cs-fixer fix --dry-run --diff", - "cs-fix": "php-cs-fixer fix" - } + ] }