From ffd8dd70bbcab7e168eb296d393ca611d7684cac Mon Sep 17 00:00:00 2001 From: Fabrizio Cau Date: Fri, 18 Jul 2025 16:44:14 +0200 Subject: [PATCH 1/4] Add withTrackTotalHits method to Builder class to add track_total_hits in elasticsearch request body --- src/Query/Builder.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Query/Builder.php b/src/Query/Builder.php index 7b07981..31847b8 100644 --- a/src/Query/Builder.php +++ b/src/Query/Builder.php @@ -2414,6 +2414,13 @@ public function withAnalyzer(string $analyzer): self return $this; } + public function withTrackTotalHits(bool $val): self + { + $this->bodyParameters['track_total_hits'] = $val; + + return $this; + } + // ---------------------------------------------------------------------- // Internal Operations // ---------------------------------------------------------------------- From 73531ac8f45b744dea5b625d7cdd594e1711065d Mon Sep 17 00:00:00 2001 From: David Philip Date: Wed, 20 Aug 2025 10:57:06 +0200 Subject: [PATCH 2/4] withTrackTotalHits parameter handling --- src/Query/Builder.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Query/Builder.php b/src/Query/Builder.php index 31847b8..910f0a8 100644 --- a/src/Query/Builder.php +++ b/src/Query/Builder.php @@ -2414,8 +2414,11 @@ public function withAnalyzer(string $analyzer): self return $this; } - public function withTrackTotalHits(bool $val): self + public function withTrackTotalHits(bool|int|null $val = true): self { + if ($val === null) { + return $this; + } $this->bodyParameters['track_total_hits'] = $val; return $this; From 5d69ff4d714203e9590c25707cf17f033903414c Mon Sep 17 00:00:00 2001 From: David Philip Date: Wed, 20 Aug 2025 11:49:13 +0200 Subject: [PATCH 3/4] 'track_total_hits' option in the Connection for default Query Builder --- src/Connection.php | 3 +++ src/Query/Builder.php | 27 +++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/Connection.php b/src/Connection.php index 9a581fe..4c5fabf 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -123,6 +123,7 @@ private function sanitizeConfig(): void 'cert_password' => null, ], 'options' => [ + 'track_total_hits' => null, // null -> skips - max 10k by default; true -> full values; false -> no hit tracking, returns -1; int -> max hit tracking val, ex 20000 'bypass_map_validation' => false, // This skips the safety checks for Elastic Specific queries. 'logging' => false, 'ssl_verification' => true, @@ -162,6 +163,8 @@ public function setOptions(): void { $this->allowIdSort = $this->config['options']['allow_id_sort'] ?? false; + $this->options()->add('track_total_hits', $this->config['options']['track_total_hits'] ?? null); + $this->options()->add('bypass_map_validation', $this->config['options']['bypass_map_validation'] ?? null); if (isset($this->config['options']['ssl_verification'])) { diff --git a/src/Query/Builder.php b/src/Query/Builder.php index 910f0a8..01ac2ec 100644 --- a/src/Query/Builder.php +++ b/src/Query/Builder.php @@ -13,6 +13,8 @@ use Illuminate\Contracts\Support\Arrayable; use Illuminate\Database\Query\Builder as BaseBuilder; use Illuminate\Database\Query\Expression; +use Illuminate\Database\Query\Grammars\Grammar; +use Illuminate\Database\Query\Processors\Processor; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Str; @@ -108,6 +110,12 @@ class Builder extends BaseBuilder protected ?MetaDTO $metaTransfer = null; + public function __construct(...$args) + { + parent::__construct(...$args); + $this->applyConnectionOptions(); + } + public function __call($method, $parameters) { if (Str::startsWith($method, 'filterWhere')) { @@ -117,7 +125,7 @@ public function __call($method, $parameters) return parent::__call($method, $parameters); } - public function toDsl(): array + public function toDsl(): array|string { $this->applyBeforeQueryCallbacks(); @@ -129,6 +137,14 @@ public function toSql(): array|string return $this->toDsl(); } + private function applyConnectionOptions() + { + $trackTotalHits = $this->connection->options()->get('track_total_hits'); + if ($trackTotalHits !== null) { + $this->bodyParameters['track_total_hits'] = $trackTotalHits; + } + } + // ====================================================================== // Inherited Methods // ====================================================================== @@ -2417,13 +2433,20 @@ public function withAnalyzer(string $analyzer): self public function withTrackTotalHits(bool|int|null $val = true): self { if ($val === null) { - return $this; + return $this->withoutTrackTotalHits(); } $this->bodyParameters['track_total_hits'] = $val; return $this; } + public function withoutTrackTotalHits(): self + { + unset($this->bodyParameters['track_total_hits']); + + return $this; + } + // ---------------------------------------------------------------------- // Internal Operations // ---------------------------------------------------------------------- From 84cf79553a5b357c365c98622baaf2d4cf90dc47 Mon Sep 17 00:00:00 2001 From: David Philip Date: Wed, 20 Aug 2025 12:36:10 +0200 Subject: [PATCH 4/4] Tests for track_total_hits --- tests/LargeRecordsTest.php | 42 ++++++++++++++++++++++++++++++++++++++ tests/Models/Product.php | 13 ++++++++++++ tests/TestCase.php | 10 +++++++++ 3 files changed, 65 insertions(+) create mode 100644 tests/LargeRecordsTest.php diff --git a/tests/LargeRecordsTest.php b/tests/LargeRecordsTest.php new file mode 100644 index 0000000..a3c39d6 --- /dev/null +++ b/tests/LargeRecordsTest.php @@ -0,0 +1,42 @@ +get(); + expect($products->getQueryMeta()->getTotalHits())->toBe(10000); + + $products = Product::limit(1)->withTrackTotalHits()->get(); + expect($products->getQueryMeta()->getTotalHits())->toBe(11000); + + $products = Product::limit(1)->withTrackTotalHits(false)->get(); + expect($products->getQueryMeta()->getTotalHits())->toBe(-1); + + $products = Product::limit(1)->withTrackTotalHits(300)->get(); + expect($products->getQueryMeta()->getTotalHits())->toBe(300); + + $products = ProductWithDefaultTrackTotalHits::limit(1)->get(); + expect($products->getQueryMeta()->getTotalHits())->toBe(11000); + + $products = ProductWithDefaultTrackTotalHits::limit(1)->withoutTrackTotalHits()->get(); + expect($products->getQueryMeta()->getTotalHits())->toBe(10000); + + $products = ProductWithDefaultTrackTotalHits::limit(1)->withTrackTotalHits(300)->get(); + expect($products->getQueryMeta()->getTotalHits())->toBe(300); + +}); diff --git a/tests/Models/Product.php b/tests/Models/Product.php index 4eb79c9..3579558 100644 --- a/tests/Models/Product.php +++ b/tests/Models/Product.php @@ -29,4 +29,17 @@ public static function executeSchema() $table->date('updated_at'); }); } + + public static function buildRecords($limit = 100) + { + $records = []; + while ($limit) { + $records[] = [ + 'state' => rand(1, 100), + ]; + $limit--; + } + Product::insert($records); + // Product::withoutRefresh()->insert($records); + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 6727b0f..974a911 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -56,5 +56,15 @@ protected function getEnvironmentSetUp($app): void 'logging' => true, ], ]); + + $app['config']->set('database.connections.elasticsearch_with_default_track_total_hits', [ + 'driver' => 'elasticsearch', + 'auth_type' => 'http', + 'hosts' => ['http://localhost:9200'], + 'options' => [ + 'track_total_hits' => true, + 'logging' => true, + ], + ]); } }